Repository: StackStorm/st2

Issue:
It fails to install pack when st2stream is behind of HTTP Proxy
##### SUMMARY

At the `st2client` as following,
```
[st2client] --> [HTTP Proxy (Nginx)] --> [st2stream]
```

the `st2 pack install(remove)` command fails because of 'SSL Certification Error' even through the `silence_ssl_warnings` is True at that `.st2/config` when I use self-signed certifications.
```
(virtualenv) ubuntu@ubuntu0:~$ cat .st2/config
[general]
silence_ssl_warnings = True

[stream]
url = https://ubuntu0/stream/v1

[credentials]
username = st2admin
(virtualenv) ubuntu@ubuntu0:~$ st2 pack install zabbix

For the "zabbix" pack, the following content will be registered:

rules     |  0
sensors   |  0
triggers  |  1
actions   |  10
aliases   |  0

Installation may take a while for packs with many items.
ERROR: ("bad handshake: Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')],)",)

(virtualenv) ubuntu@ubuntu0:~$ 
```
(c.f. [Here](https://gist.github.com/userlocalhost/dcb73ec134b2fad74f2ee8f9e92d6e1f) is execution output with '--debug' option)

##### ISSUE TYPE
- Bug Report
- Other

##### STACKSTORM VERSION
st2 2.9dev, on Python 2.7.12

##### WORKAROUND
We can get around this problem by setting environment variable `REQUESTS_CA_BUNDLE` as below.
```
$ export REQUESTS_CA_BUNDLE=/etc/ssl/st2/st2.crt
$ st2 pack install zabbix
```
(c.f. [for more detail](https://gist.github.com/userlocalhost/a5313e36695d84df165f2431bb618c44))

Please tell me if there is a better way.

##### DISCUSSION
###### 1. Adding a document at Troubleshooting
I couldn't find out a document that treats this problem at the [Troubleshooting page](https://docs.stackstorm.com/troubleshooting/index.html). So you may want to write a document that describes workaround.

###### 2. Adding a configuration parameter in 'cli' section
If there were a configuration parameter to pass `verify` argument of SSEClient (at [here](https://github.com/StackStorm/st2/blob/master/st2client/st2client/models/core.py#L614)) that passes to [requests](http://docs.python-requests.org/en/master/api/#requests.request), we could do it and also disable this SSL verification.
So how about adding this parameter to the `cli` section.



Discussion:
Looks like we have some automatic handling in place for non-stream connections - e.g. see this in [st2/httpclient.py](https://github.com/StackStorm/st2/blob/master/st2client/st2client/utils/httpclient.py):

```python
def add_ssl_verify_to_kwargs(func):
    def decorate(*args, **kwargs):
        if isinstance(args[0], HTTPClient) and 'https' in getattr(args[0], 'root', ''):
            cacert = getattr(args[0], 'cacert', None)
            kwargs['verify'] = cacert if cacert is not None else False
        return func(*args, **kwargs)
    return decorate
```

But we don't do that for StreamManager connections. Probably needs some similar handling there.
Thank you for giving advice for modification.
I understood we don't have to add a parameter that indicates `cacert` in the configuration because that has been already existed.
So, I'd like to make a small change in st2client to pass it to the StreamManager.

Resolving patch:
diff --git a/st2client/st2client/models/core.py b/st2client/st2client/models/core.py
--- a/st2client/st2client/models/core.py
+++ b/st2client/st2client/models/core.py
@@ -582,7 +582,7 @@ def match(self, instance, **kwargs):
 
 
 class StreamManager(object):
-    def __init__(self, endpoint, cacert, debug):
+    def __init__(self, endpoint, cacert=None, debug=False):
         self._url = httpclient.get_url_without_trailing_slash(endpoint) + '/stream'
         self.debug = debug
         self.cacert = cacert
@@ -595,6 +595,7 @@ def listen(self, events=None, **kwargs):
 
         url = self._url
         query_params = {}
+        request_params = {}
 
         if events and isinstance(events, six.string_types):
             events = [events]
@@ -608,10 +609,13 @@ def listen(self, events=None, **kwargs):
         if events:
             query_params['events'] = ','.join(events)
 
+        if self.cacert is not None:
+            request_params['verify'] = self.cacert
+
         query_string = '?' + urllib.parse.urlencode(query_params)
         url = url + query_string
 
-        for message in SSEClient(url):
+        for message in SSEClient(url, **request_params):
 
             # If the execution on the API server takes too long, the message
             # can be empty. In this case, rerun the query.


Test patch:
diff --git a/st2client/tests/unit/test_models.py b/st2client/tests/unit/test_models.py
--- a/st2client/tests/unit/test_models.py
+++ b/st2client/tests/unit/test_models.py
@@ -256,3 +256,33 @@ def test_resource_delete_failed(self):
         mgr = models.ResourceManager(base.FakeResource, base.FAKE_ENDPOINT)
         instance = mgr.get_by_name('abc')
         self.assertRaises(Exception, mgr.delete, instance)
+
+    @mock.patch('sseclient.SSEClient')
+    def test_stream_resource_listen(self, mock):
+        mock_msg = mock.Mock()
+        mock_msg.data = json.dumps(base.RESOURCES)
+
+        # checking the case to specify valid 'cacert' parameter to the StreamManager
+        def side_effect_checking_verify_parameter_is(endpoint_url, **kwargs):
+            self.assertEqual(endpoint_url, 'https://example.com/stream?events=foo%2Cbar')
+            self.assertEqual(kwargs['verify'], '/path/ca.crt')
+            return [mock_msg]
+
+        mock.side_effect = side_effect_checking_verify_parameter_is
+        mgr = models.StreamManager('https://example.com', cacert='/path/ca.crt')
+
+        resp = mgr.listen(events=['foo', 'bar'])
+        self.assertEqual(list(resp), [base.RESOURCES])
+
+        # checking the case not to specify valid 'cacert' parameter to the StreamManager
+        def side_effect_checking_verify_parameter_is_not(endpoint_url, **kwargs):
+            # checking endpoint_url in case of no event specification
+            self.assertEqual(endpoint_url, 'https://example.com/stream?')
+            self.assertFalse('verify' in kwargs)
+            return [mock_msg]
+
+        mock.side_effect = side_effect_checking_verify_parameter_is_not
+        mgr = models.StreamManager('https://example.com')
+
+        resp = mgr.listen()
+        self.assertEqual(list(resp), [base.RESOURCES])