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
|
|||
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) |
||
0 ignored issues
–
show
It seems like
_raw_get_command_output was declared protected and should not be accessed from this context.
Prefixing a member variable class MyParent:
def __init__(self):
self._x = 1;
self.y = 2;
class MyChild(MyParent):
def some_method(self):
return self._x # Ok, since accessed from a child class
class AnotherClass:
def some_method(self, instance_of_my_child):
return instance_of_my_child._x # Would be flagged as AnotherClass is not
# a child class of MyParent
Loading history...
|
|||
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) |
||
0 ignored issues
–
show
It seems like
_clean_error_msg was declared protected and should not be accessed from this context.
Prefixing a member variable class MyParent:
def __init__(self):
self._x = 1;
self.y = 2;
class MyChild(MyParent):
def some_method(self):
return self._x # Ok, since accessed from a child class
class AnotherClass:
def some_method(self, instance_of_my_child):
return instance_of_my_child._x # Would be flagged as AnotherClass is not
# a child class of MyParent
Loading history...
|
|||
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 |
||
0 ignored issues
–
show
|
|||
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 |
It is generally advisable to initialize the super-class by calling its
__init__
method: