Test Failed
Push — master ( e380d0...f5671d )
by W
02:58
created

runners/winrm_runner/winrm_runner/winrm_base.py (1 issue)

1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
from __future__ import absolute_import
17
18
import re
19
import six
20
import time
21
22
from base64 import b64encode
23
from st2common import log as logging
24
from st2common.constants import action as action_constants
25
from st2common.constants import exit_codes as exit_code_constants
26
from st2common.runners.base import ActionRunner
27
from st2common.util import jsonify
28
from winrm import Session, Response
29
from winrm.exceptions import WinRMOperationTimeoutError
30
31
__all__ = [
32
    'WinRmBaseRunner',
33
]
34
35
LOG = logging.getLogger(__name__)
36
37
RUNNER_CWD = "cwd"
38
RUNNER_ENV = "env"
39
RUNNER_HOST = "host"
40
RUNNER_KWARG_OP = "kwarg_op"
41
RUNNER_PASSWORD = "password"
42
RUNNER_PORT = "port"
43
RUNNER_SCHEME = "scheme"
44
RUNNER_TIMEOUT = "timeout"
45
RUNNER_TRANSPORT = "transport"
46
RUNNER_USERNAME = "username"
47
RUNNER_VERIFY_SSL = "verify_ssl_cert"
48
49
WINRM_HTTPS_PORT = 5986
50
WINRM_HTTP_PORT = 5985
51
# explicity made so that it does not equal SUCCESS so a failure is returned
52
WINRM_TIMEOUT_EXIT_CODE = exit_code_constants.SUCCESS_EXIT_CODE - 1
53
54
DEFAULT_KWARG_OP = "-"
55
DEFAULT_PORT = WINRM_HTTPS_PORT
56
DEFAULT_SCHEME = "https"
57
DEFAULT_TIMEOUT = 60
58
DEFAULT_TRANSPORT = "ntlm"
59
DEFAULT_VERIFY_SSL = True
60
61
RESULT_KEYS_TO_TRANSFORM = ["stdout", "stderr"]
62
63
# key = value in linux/bash to escape
64
# value = powershell escaped equivalent
65
#
66
# Compiled list from the following sources:
67
# https://ss64.com/ps/syntax-esc.html
68
# https://www.techotopia.com/index.php/Windows_PowerShell_1.0_String_Quoting_and_Escape_Sequences#PowerShell_Special_Escape_Sequences
69
PS_ESCAPE_SEQUENCES = {'\n': '`n',
70
                       '\r': '`r',
71
                       '\t': '`t',
72
                       '\a': '`a',
73
                       '\b': '`b',
74
                       '\f': '`f',
75
                       '\v': '`v',
76
                       '"': '`"',
77
                       '\'': '`\'',
78
                       '`': '``',
79
                       '\0': '`0',
80
                       '$': '`$'}
81
82
83
class WinRmRunnerTimoutError(Exception):
84
85
    def __init__(self, response):
0 ignored issues
show
The __init__ method of the super-class Exception is not called.

It is generally advisable to initialize the super-class by calling its __init__ method:

class SomeParent:
    def __init__(self):
        self.x = 1

class SomeChild(SomeParent):
    def __init__(self):
        # Initialize the super class
        SomeParent.__init__(self)
Loading history...
86
        self.response = response
87
88
89
class WinRmBaseRunner(ActionRunner):
90
91
    def pre_run(self):
92
        super(WinRmBaseRunner, self).pre_run()
93
94
        # common connection parameters
95
        self._host = self.runner_parameters[RUNNER_HOST]
96
        self._username = self.runner_parameters[RUNNER_USERNAME]
97
        self._password = self.runner_parameters[RUNNER_PASSWORD]
98
        self._timeout = self.runner_parameters.get(RUNNER_TIMEOUT, DEFAULT_TIMEOUT)
99
        self._read_timeout = self._timeout + 1  # read_timeout must be > operation_timeout
100
101
        # default to https port 5986 over ntlm
102
        self._port = self.runner_parameters.get(RUNNER_PORT, DEFAULT_PORT)
103
        self._scheme = self.runner_parameters.get(RUNNER_SCHEME, DEFAULT_SCHEME)
104
        self._transport = self.runner_parameters.get(RUNNER_TRANSPORT, DEFAULT_TRANSPORT)
105
106
        # if connecting to the HTTP port then we must use "http" as the scheme
107
        # in the URL
108
        if self._port == WINRM_HTTP_PORT:
109
            self._scheme = "http"
110
111
        # construct the URL for connecting to WinRM on the host
112
        self._winrm_url = "{}://{}:{}/wsman".format(self._scheme, self._host, self._port)
113
114
        # default to verifying SSL certs
115
        self._verify_ssl = self.runner_parameters.get(RUNNER_VERIFY_SSL, DEFAULT_VERIFY_SSL)
116
        self._server_cert_validation = "validate" if self._verify_ssl else "ignore"
117
118
        # additional parameters
119
        self._cwd = self.runner_parameters.get(RUNNER_CWD, None)
120
        self._env = self.runner_parameters.get(RUNNER_ENV, {})
121
        self._env = self._env or {}
122
        self._kwarg_op = self.runner_parameters.get(RUNNER_KWARG_OP, DEFAULT_KWARG_OP)
123
124
    def _create_session(self):
125
        # create the session
126
        LOG.info("Connecting via WinRM to url: {}".format(self._winrm_url))
127
        session = Session(self._winrm_url,
128
                          auth=(self._username, self._password),
129
                          transport=self._transport,
130
                          server_cert_validation=self._server_cert_validation,
131
                          operation_timeout_sec=self._timeout,
132
                          read_timeout_sec=self._read_timeout)
133
        return session
134
135
    def _winrm_get_command_output(self, protocol, shell_id, command_id):
136
        # NOTE: this is copied from pywinrm because it doesn't support
137
        # timeouts
138
        stdout_buffer, stderr_buffer = [], []
139
        return_code = 0
140
        command_done = False
141
        start_time = time.time()
142
        while not command_done:
143
            # check if we need to timeout (StackStorm custom)
144
            current_time = time.time()
145
            elapsed_time = (current_time - start_time)
146
            if self._timeout and (elapsed_time > self._timeout):
147
                raise WinRmRunnerTimoutError(Response((b''.join(stdout_buffer),
148
                                                       b''.join(stderr_buffer),
149
                                                       WINRM_TIMEOUT_EXIT_CODE)))
150
            # end stackstorm custom
151
152
            try:
153
                stdout, stderr, return_code, command_done = \
154
                    protocol._raw_get_command_output(shell_id, command_id)
155
                stdout_buffer.append(stdout)
156
                stderr_buffer.append(stderr)
157
            except WinRMOperationTimeoutError:
158
                # this is an expected error when waiting for a long-running process,
159
                # just silently retry
160
                pass
161
        return b''.join(stdout_buffer), b''.join(stderr_buffer), return_code
162
163
    def _winrm_run_cmd(self, session, command, args=(), env=None, cwd=None):
164
        # NOTE: this is copied from pywinrm because it doesn't support
165
        # passing env and working_directory from the Session.run_cmd.
166
        # It also doesn't support timeouts. All of these things have been
167
        # added
168
        shell_id = session.protocol.open_shell(env_vars=env,
169
                                               working_directory=cwd)
170
        command_id = session.protocol.run_command(shell_id, command, args)
171
        # try/catch is for custom timeout handing (StackStorm custom)
172
        try:
173
            rs = Response(self._winrm_get_command_output(session.protocol,
174
                                                         shell_id,
175
                                                         command_id))
176
            rs.timeout = False
177
        except WinRmRunnerTimoutError as e:
178
            rs = e.response
179
            rs.timeout = True
180
        # end stackstorm custom
181
        session.protocol.cleanup_command(shell_id, command_id)
182
        session.protocol.close_shell(shell_id)
183
        return rs
184
185
    def _winrm_run_ps(self, session, script, env=None, cwd=None):
186
        # NOTE: this is copied from pywinrm because it doesn't support
187
        # passing env and working_directory from the Session.run_ps
188
        encoded_ps = b64encode(script.encode('utf_16_le')).decode('ascii')
189
        rs = self._winrm_run_cmd(session,
190
                                 'powershell -encodedcommand {0}'.format(encoded_ps),
191
                                 env=env,
192
                                 cwd=cwd)
193
        if len(rs.std_err):
194
            # if there was an error message, clean it it up and make it human
195
            # readable
196
            if isinstance(rs.std_err, bytes):
197
                # decode bytes into utf-8 because of a bug in pywinrm
198
                # real fix is here: https://github.com/diyan/pywinrm/pull/222/files
199
                rs.std_err = rs.std_err.decode('utf-8')
200
            rs.std_err = session._clean_error_msg(rs.std_err)
201
        return rs
202
203
    def _translate_response(self, response):
204
        # check exit status for errors
205
        succeeded = (response.status_code == exit_code_constants.SUCCESS_EXIT_CODE)
206
        status = action_constants.LIVEACTION_STATUS_SUCCEEDED
207
        status_code = response.status_code
208
        if response.timeout:
209
            status = action_constants.LIVEACTION_STATUS_TIMED_OUT
210
            status_code = WINRM_TIMEOUT_EXIT_CODE
211
        elif not succeeded:
212
            status = action_constants.LIVEACTION_STATUS_FAILED
213
214
        # create result
215
        result = {
216
            'failed': not succeeded,
217
            'succeeded': succeeded,
218
            'return_code': status_code,
219
            'stdout': response.std_out,
220
            'stderr': response.std_err
221
        }
222
223
        # automatically convert result stdout/stderr from JSON strings to
224
        # objects so they can be used natively
225
        return (status, jsonify.json_loads(result, RESULT_KEYS_TO_TRANSFORM), None)
226
227
    def run_cmd(self, cmd):
228
        # connect
229
        session = self._create_session()
230
        # execute
231
        response = self._winrm_run_cmd(session, cmd, env=self._env, cwd=self._cwd)
232
        # create triplet from WinRM response
233
        return self._translate_response(response)
234
235
    def run_ps(self, powershell):
236
        # connect
237
        session = self._create_session()
238
        # execute
239
        response = self._winrm_run_ps(session, powershell, env=self._env, cwd=self._cwd)
240
        # create triplet from WinRM response
241
        return self._translate_response(response)
242
243
    def _multireplace(self, string, replacements):
244
        """
245
        Given a string and a replacement map, it returns the replaced string.
246
        Source = https://gist.github.com/bgusach/a967e0587d6e01e889fd1d776c5f3729
247
        Reference = https://stackoverflow.com/questions/6116978/how-to-replace-multiple-substrings-of-a-string  # noqa
248
        :param str string: string to execute replacements on
249
        :param dict replacements: replacement dictionary {value to find: value to replace}
250
        :rtype: str
251
        """
252
        # Place longer ones first to keep shorter substrings from matching where
253
        # the longer ones should take place
254
        # For instance given the replacements {'ab': 'AB', 'abc': 'ABC'} against
255
        # the string 'hey abc', it should produce 'hey ABC' and not 'hey ABc'
256
        substrs = sorted(replacements, key=len, reverse=True)
257
258
        # Create a big OR regex that matches any of the substrings to replace
259
        regexp = re.compile('|'.join([re.escape(s) for s in substrs]))
260
261
        # For each match, look up the new string in the replacements
262
        return regexp.sub(lambda match: replacements[match.group(0)], string)
263
264
    def _param_to_ps(self, param):
265
        ps_str = ""
266
        if param is None:
267
            ps_str = "$null"
268
        elif isinstance(param, six.string_types):
269
            ps_str = '"' + self._multireplace(param, PS_ESCAPE_SEQUENCES) + '"'
270
        elif isinstance(param, bool):
271
            ps_str = "$true" if param else "$false"
272
        elif isinstance(param, list):
273
            ps_str = "@("
274
            ps_str += ", ".join([self._param_to_ps(p) for p in param])
275
            ps_str += ")"
276
        elif isinstance(param, dict):
277
            ps_str = "@{"
278
            ps_str += "; ".join([(self._param_to_ps(k) + ' = ' + self._param_to_ps(v))
279
                                 for k, v in six.iteritems(param)])
280
            ps_str += "}"
281
        else:
282
            ps_str = str(param)
283
        return ps_str
284
285
    def _transform_params_to_ps(self, positional_args, named_args):
286
        if positional_args:
287
            for i, arg in enumerate(positional_args):
288
                positional_args[i] = self._param_to_ps(arg)
289
290
        if named_args:
291
            for key, value in six.iteritems(named_args):
292
                named_args[key] = self._param_to_ps(value)
293
294
        return positional_args, named_args
295
296
    def create_ps_params_string(self, positional_args, named_args):
297
        # convert the script parameters into powershell strings
298
        positional_args, named_args = self._transform_params_to_ps(positional_args,
299
                                                                   named_args)
300
        # concatenate them into a long string
301
        ps_params_str = ""
302
        if named_args:
303
            ps_params_str += " " .join([(k + " " + v) for k, v in six.iteritems(named_args)])
304
            ps_params_str += " "
305
        if positional_args:
306
            ps_params_str += " ".join(positional_args)
307
        return ps_params_str
308