From 1d13de5c0513837c2e27f17c5beb5133b2d55677 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Thu, 7 Jun 2018 16:35:51 -0400 Subject: [PATCH 01/31] Initial work on winrm_runner --- contrib/runners/winrm_runner/MANIFEST.in | 11 + contrib/runners/winrm_runner/dist_utils.py | 107 ++++++++++ contrib/runners/winrm_runner/requirements.txt | 1 + contrib/runners/winrm_runner/runner.yaml | 1 + contrib/runners/winrm_runner/setup.py | 55 +++++ .../tests/unit/.#test_winrm_runner.py | 1 + .../winrm_runner/tests/unit/__init__.py | 0 .../winrm_runner/winrm_runner/__init__.py | 0 .../winrm_runner/winrm_runner/runner.yaml | 125 +++++++++++ .../winrm_runner/winrm_runner/winrm_base.py | 194 ++++++++++++++++++ .../winrm_runner/winrm_ps_command_runner.py | 50 +++++ .../winrm_runner/winrm_ps_script_runner.py | 56 +++++ 12 files changed, 601 insertions(+) create mode 100644 contrib/runners/winrm_runner/MANIFEST.in create mode 100644 contrib/runners/winrm_runner/dist_utils.py create mode 100644 contrib/runners/winrm_runner/requirements.txt create mode 120000 contrib/runners/winrm_runner/runner.yaml create mode 100644 contrib/runners/winrm_runner/setup.py create mode 120000 contrib/runners/winrm_runner/tests/unit/.#test_winrm_runner.py create mode 100644 contrib/runners/winrm_runner/tests/unit/__init__.py create mode 100644 contrib/runners/winrm_runner/winrm_runner/__init__.py create mode 100644 contrib/runners/winrm_runner/winrm_runner/runner.yaml create mode 100644 contrib/runners/winrm_runner/winrm_runner/winrm_base.py create mode 100644 contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py create mode 100644 contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py diff --git a/contrib/runners/winrm_runner/MANIFEST.in b/contrib/runners/winrm_runner/MANIFEST.in new file mode 100644 index 0000000000..25ce80c091 --- /dev/null +++ b/contrib/runners/winrm_runner/MANIFEST.in @@ -0,0 +1,11 @@ +# https://docs.python.org/2/distutils/sourcedist.html#commands +# Include all files under the source tree by default. +# Another behaviour can be used in the future though. +include __init__.py +include runner.yaml +include dist_utils.py +include requirements.txt +include README.rst +include CHANGELOG.rst +include LICENSE +global-exclude *.pyc diff --git a/contrib/runners/winrm_runner/dist_utils.py b/contrib/runners/winrm_runner/dist_utils.py new file mode 100644 index 0000000000..ede9e4a358 --- /dev/null +++ b/contrib/runners/winrm_runner/dist_utils.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +import os +import re +import sys + +from distutils.version import StrictVersion + +GET_PIP = 'curl https://bootstrap.pypa.io/get-pip.py | python' + +try: + import pip + from pip import __version__ as pip_version +except ImportError as e: + print('Failed to import pip: %s' % (str(e))) + print('') + print('Download pip:\n%s' % (GET_PIP)) + sys.exit(1) + +try: + # pip < 10.0 + from pip.req import parse_requirements +except ImportError: + # pip >= 10.0 + + try: + from pip._internal.req.req_file import parse_requirements + except ImportError as e: + print('Failed to import parse_requirements from pip: %s' % (str(e))) + print('Using pip: %s' % (str(pip_version))) + sys.exit(1) + +__all__ = [ + 'check_pip_version', + 'fetch_requirements', + 'apply_vagrant_workaround', + 'get_version_string', + 'parse_version_string' +] + + +def check_pip_version(): + """ + Ensure that a minimum supported version of pip is installed. + """ + if StrictVersion(pip.__version__) < StrictVersion('6.0.0'): + print("Upgrade pip, your version `{0}' " + "is outdated:\n{1}".format(pip.__version__, GET_PIP)) + sys.exit(1) + + +def fetch_requirements(requirements_file_path): + """ + Return a list of requirements and links by parsing the provided requirements file. + """ + links = [] + reqs = [] + for req in parse_requirements(requirements_file_path, session=False): + if req.link: + links.append(str(req.link)) + reqs.append(str(req.req)) + return (reqs, links) + + +def apply_vagrant_workaround(): + """ + Function which detects if the script is being executed inside vagrant and if it is, it deletes + "os.link" attribute. + Note: Without this workaround, setup.py sdist will fail when running inside a shared directory + (nfs / virtualbox shared folders). + """ + if os.environ.get('USER', None) == 'vagrant': + del os.link + + +def get_version_string(init_file): + """ + Read __version__ string for an init file. + """ + + with open(init_file, 'r') as fp: + content = fp.read() + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + content, re.M) + if version_match: + return version_match.group(1) + + raise RuntimeError('Unable to find version string in %s.' % (init_file)) + + +# alias for get_version_string +parse_version_string = get_version_string diff --git a/contrib/runners/winrm_runner/requirements.txt b/contrib/runners/winrm_runner/requirements.txt new file mode 100644 index 0000000000..17a16ff26d --- /dev/null +++ b/contrib/runners/winrm_runner/requirements.txt @@ -0,0 +1 @@ +pywinrm diff --git a/contrib/runners/winrm_runner/runner.yaml b/contrib/runners/winrm_runner/runner.yaml new file mode 120000 index 0000000000..1fa11a1342 --- /dev/null +++ b/contrib/runners/winrm_runner/runner.yaml @@ -0,0 +1 @@ +winrm_runner/runner.yaml \ No newline at end of file diff --git a/contrib/runners/winrm_runner/setup.py b/contrib/runners/winrm_runner/setup.py new file mode 100644 index 0000000000..30542a3d05 --- /dev/null +++ b/contrib/runners/winrm_runner/setup.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +import os.path + +from setuptools import setup +from setuptools import find_packages + +from dist_utils import fetch_requirements +from dist_utils import apply_vagrant_workaround + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +REQUIREMENTS_FILE = os.path.join(BASE_DIR, 'requirements.txt') + +install_reqs, dep_links = fetch_requirements(REQUIREMENTS_FILE) + +apply_vagrant_workaround() +setup( + name='stackstorm-runner-winrm', + version='1.0.0', + description=('WinRM shell command and PowerShell script action runner for' + ' the StackStorm event-driven automation platform'), + author='StackStorm', + author_email='info@stackstorm.com', + license='Apache License (2.0)', + url='https://stackstorm.com/', + install_requires=install_reqs, + dependency_links=dep_links, + test_suite='tests', + zip_safe=False, + include_package_data=True, + packages=find_packages(exclude=['setuptools', 'tests']), + package_data={'winrm_ps_command_runner': ['runner.yaml']}, + scripts=[], + entry_points={ + 'st2common.runners.runner': [ + 'winrm-ps-cmd = winrm_runner.winrm_ps_command_runner', + 'winrm-ps-script = winrm_runner.winrm_ps_script_runner', + ], + } +) diff --git a/contrib/runners/winrm_runner/tests/unit/.#test_winrm_runner.py b/contrib/runners/winrm_runner/tests/unit/.#test_winrm_runner.py new file mode 120000 index 0000000000..dd5e6bc2f4 --- /dev/null +++ b/contrib/runners/winrm_runner/tests/unit/.#test_winrm_runner.py @@ -0,0 +1 @@ +nmaludy@cerberus.home.17871:1528281446 \ No newline at end of file diff --git a/contrib/runners/winrm_runner/tests/unit/__init__.py b/contrib/runners/winrm_runner/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/winrm_runner/winrm_runner/__init__.py b/contrib/runners/winrm_runner/winrm_runner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/winrm_runner/winrm_runner/runner.yaml b/contrib/runners/winrm_runner/winrm_runner/runner.yaml new file mode 100644 index 0000000000..2540cc098b --- /dev/null +++ b/contrib/runners/winrm_runner/winrm_runner/runner.yaml @@ -0,0 +1,125 @@ +- description: A remote execution runner that executes PowerShell commands via WinRM on a set of remote hosts + enabled: true + name: winrm-ps-cmd + runner_package: winrm_runner + runner_module: winrm_ps_command_runner + runner_parameters: + cmd: + description: Arbitrary PowerShell command to be executed on the remote host(s). + type: string + cwd: + description: Working directory where the command will be executed in + type: string + env: + description: Environment variables which will be available to the command (e.g. + key1=val1,key2=val2) + type: object + host: + description: A host where the command will be run + required: true + type: string + password: + description: Password used to log in. + required: true + secret: true + type: string + port: + default: 5986 + description: 'WinRM port to connect on. If using port 5985 scheme must be "http"' + required: false + type: integer + scheme: + default: "https" + description: 'Scheme to use in the WinRM URL. If using scheme "http" port must be 5985' + required: false + type: string + timeout: + default: 60 + description: Action timeout in seconds. Action will get killed if it doesn't + finish in timeout seconds. + type: integer + transport: + default: "ntlm" + description: The type of transport that WinRM will use to communicate. + See https://github.com/diyan/pywinrm#valid-transport-options + required: false + type: string + enum: + - "basic" + - "certificate" + - "credssp" + - "kerberos" + - "ntlm" + - "plaintext" + - "ssl" + username: + description: Username used to log-in. + required: true + type: string + verify_ssl_cert: + default: true + description: Certificate for HTTPS request is verified by default using requests + CA bundle which comes from Mozilla. Verification using a custom CA bundle + is not yet supported. Set to False to skip verification. + type: boolean +- description: A remote execution runner that executes PowerShell script via WinRM on a set of remote hosts + enabled: true + name: winrm-ps-script + runner_package: winrm_runner + runner_module: winrm_ps_script_runner + runner_parameters: + cwd: + description: Working directory where the command will be executed in + type: string + env: + description: Environment variables which will be available to the command (e.g. + key1=val1,key2=val2) + type: object + host: + description: A host where the command will be run + required: true + type: string + password: + description: Password used to log in. + required: true + secret: true + type: string + port: + default: 5986 + description: 'WinRM port to connect on. If using port 5985 scheme must be "http"' + required: false + type: integer + scheme: + default: "https" + description: 'Scheme to use in the WinRM URL. If using scheme "http" port must be 5985' + required: false + type: string + timeout: + default: 60 + description: Action timeout in seconds. Action will get killed if it doesn't + finish in timeout seconds. + type: integer + transport: + default: "ntlm" + description: The type of transport that WinRM will use to communicate. + See https://github.com/diyan/pywinrm#valid-transport-options + required: false + type: string + enum: + - "basic" + - "certificate" + - "credssp" + - "kerberos" + - "ntlm" + - "plaintext" + - "ssl" + username: + description: Username used to log-in. + required: true + type: string + verify_ssl_cert: + default: true + description: Certificate for HTTPS request is verified by default using requests + CA bundle which comes from Mozilla. Verification using a custom CA bundle + is not yet supported. Set to False to skip verification. + type: boolean diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py new file mode 100644 index 0000000000..865c3622eb --- /dev/null +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -0,0 +1,194 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +from base64 import b64encode +from st2common import log as logging +from st2common.constants import action as action_constants +from st2common.constants import exit_codes as exit_code_constants +from st2common.runners.base import ActionRunner +from st2common.util import jsonify +from winrm import Session, Response +from winrm.exceptions import WinRMOperationTimeoutError +import time + +__all__ = [ + 'WinRmBaseRunner', +] + +LOG = logging.getLogger(__name__) + +RUNNER_CWD = 'cwd' +RUNNER_ENV = 'env' +RUNNER_HOST = "host" +RUNNER_PASSWORD = "password" +RUNNER_PORT = "port" +RUNNER_SCHEME = "scheme" +RUNNER_TIMEOUT = "timeout" +RUNNER_TRANSPORT = "transport" +RUNNER_USERNAME = "username" +RUNNER_VERIFY_SSL = "verify_ssl_cert" + +WINRM_HTTPS_PORT = 5986 +WINRM_HTTP_PORT = 5985 +# explicity made so that it does not equal SUCCESS so a failure is returned +WINRM_TIMEOUT_EXIT_CODE = exit_code_constants.SUCCESS_EXIT_CODE - 1 + +DEFAULT_PORT = WINRM_HTTPS_PORT +DEFAULT_SCHEME = "https" +DEFAULT_TIMEOUT = 60 +DEFAULT_TRANSPORT = "ntlm" +DEFAULT_VERIFY_SSL = True + + +class WinRMRunnerTimoutError(Exception): + + def __init__(self, response): + self.response = response + + +class WinRmBaseRunner(ActionRunner): + KEYS_TO_TRANSFORM = ['stdout', 'stderr'] + + def _create_session(self, action_parameters): + # common connection parameters + host = self.runner_parameters[RUNNER_HOST] + username = self.runner_parameters[RUNNER_USERNAME] + password = self.runner_parameters[RUNNER_PASSWORD] + timeout = self.runner_parameters.get(RUNNER_TIMEOUT, DEFAULT_TIMEOUT) + read_timeout = timeout + 1 # read_timeout must be > operation_timeout + + # default to https port 5986 over ntlm + port = self.runner_parameters.get(RUNNER_PORT, DEFAULT_PORT) + scheme = self.runner_parameters.get(RUNNER_SCHEME, DEFAULT_SCHEME) + transport = self.runner_parameters.get(RUNNER_TRANSPORT, DEFAULT_TRANSPORT) + + # if connecting to the HTTP port then we must use "http" as the scheme + # in the URL + if port == WINRM_HTTP_PORT: + scheme = "http" + + # default to verifying SSL certs + verify_ssl = self.runner_parameters.get(RUNNER_VERIFY_SSL, DEFAULT_VERIFY_SSL) + winrm_cert_validate = "validate" if verify_ssl else "ignore" + + # create the session + winrm_url = '{}://{}:{}/wsman'.format(scheme, host, port) + LOG.info("Connecting via WinRM to url: {}".format(winrm_url)) + session = Session(winrm_url, + auth=(username, password), + transport=transport, + server_cert_validation=winrm_cert_validate, + operation_timeout_sec=timeout, + read_timeout_sec=read_timeout) + return session + + def _winrm_get_command_output(self, protocol, shell_id, command_id): + # NOTE: this is copied from pywinrm because it doesn't support + # timeouts + stdout_buffer, stderr_buffer = [], [] + return_code = 0 + command_done = False + timeout = self.runner_parameters[RUNNER_TIMEOUT] + start_time = time.time() + while not command_done: + # check if we need to timeout (StackStorm custom) + current_time = time.time() + elapsed_time = (current_time - start_time) + if timeout and (elapsed_time > timeout): + raise WinRMRunnerTimoutError(Response((b''.join(stdout_buffer), + b''.join(stderr_buffer), + WINRM_TIMEOUT_EXIT_CODE))) + # end stackstorm custom + + try: + stdout, stderr, return_code, command_done = \ + protocol._raw_get_command_output(shell_id, command_id) + stdout_buffer.append(stdout) + stderr_buffer.append(stderr) + except WinRMOperationTimeoutError as e: + # this is an expected error when waiting for a long-running process, + # just silently retry + pass + return b''.join(stdout_buffer), b''.join(stderr_buffer), return_code + + def _winrm_run_cmd(self, session, command, args=(), env=None, cwd=None): + # NOTE: this is copied from pywinrm because it doesn't support + # passing env and working_directory from the Session.run_cmd + shell_id = session.protocol.open_shell(env_vars=env, + working_directory=cwd) + command_id = session.protocol.run_command(shell_id, command, args) + # try/catch is for custom timeout handing (StackStorm custom) + try: + stdout, stderr, return_code = self._winrm_get_command_output(session.protocol, + shell_id, + command_id) + rs = Response(stdout, stderr, return_code) + rs.timeout = False + except WinRMRunnerTimoutError as e: + rs = e.response + rs.timeout = True + # end stackstorm custom + session.protocol.cleanup_command(shell_id, command_id) + session.protocol.close_shell(shell_id) + return rs + + def _winrm_run_ps(self, session, script, env=None, cwd=None): + # NOTE: this is copied from pywinrm because it doesn't support + # passing env and working_directory from the Session.run_ps + encoded_ps = b64encode(script.encode('utf_16_le')).decode('ascii') + rs = self._winrm_run_cmd(session, + 'powershell -encodedcommand {0}'.format(encoded_ps), + env=env, + cwd=cwd) + if len(rs.std_err): + # if there was an error message, clean it it up and make it human + # readable + rs.std_err = session._clean_error_msg(rs.std_err) + return rs + + def _run_ps(self, action_parameters, powershell): + env = self.runner_parameters.get(RUNNER_ENV, None) + cwd = self.runner_parameters.get(RUNNER_CWD, None) + + # connect + session = self._create_session(action_parameters) + # execute + response = self._winrm_run_ps(session, powershell, env=env, cwd=cwd) + # create triplet from WinRM response + return self._translate_response(response) + + def _translate_response(self, response): + # check exit status for errors + succeeded = (response.status_code == exit_code_constants.SUCCESS_EXIT_CODE) + status = action_constants.LIVEACTION_STATUS_SUCCEEDED + if response.timeout: + status = action_constants.LIVEACTION_STATUS_TIMED_OUT + elif not succeeded: + status = action_constants.LIVEACTION_STATUS_FAILED + + # create result + result = { + 'failed': not succeeded, + 'succeeded': succeeded, + 'return_code': response.status_code, + 'stdout': response.std_out, + 'stderr': response.std_err + } + + # automatically convert result stdout/stderr from JSON strings to + # objects so they can be used natively + return (status, jsonify.json_loads(result, WinRmBaseRunner.KEYS_TO_TRANSFORM), None) diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py new file mode 100644 index 0000000000..eee0ef7937 --- /dev/null +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py @@ -0,0 +1,50 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +import uuid + +from st2common import log as logging +from st2common.runners.base import ActionRunner +from st2common.runners.base import get_metadata as get_runner_metadata +from winrm_runner.winrm_base import WinRmBaseRunner + + +__all__ = [ + 'WinRmPsCommandRunner', + 'get_runner', + 'get_metadata' +] + +LOG = logging.getLogger(__name__) + +RUNNER_COMMAND = 'cmd' + + +class WinRmPsCommandRunner(WinRmBaseRunner): + + def run(self, action_parameters): + powershell_command = self.runner_parameters[RUNNER_COMMAND] + + # execute + return self._run_ps(action_parameters, powershell_command) + + +def get_runner(): + return WinRmPsCommandRunner(str(uuid.uuid4())) + + +def get_metadata(): + return get_runner_metadata('winrm_ps_command_runner') diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py new file mode 100644 index 0000000000..011c20e7b6 --- /dev/null +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py @@ -0,0 +1,56 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +import uuid + +from st2common import log as logging +from st2common.runners.base import ActionRunner +from st2common.runners.base import get_metadata as get_runner_metadata +from winrm_runner.winrm_base import WinRmBaseRunner + + +__all__ = [ + 'WinRmPsScriptRunner', + 'get_runner', + 'get_metadata' +] + +LOG = logging.getLogger(__name__) + +RUNNER_CWD = 'cwd' +RUNNER_ENV = 'env' + + +class WinRmPsScriptRunner(WinRmBaseRunner): + + def run(self, action_parameters): + if not self.entry_point: + raise ValueError('Missing entry_point action metadata attribute') + + # read in the script contents from the local file + with open(self.entry_point, 'r') as script_file: + powershell_script = script_file.read() + + # execute + return self._run_ps(action_parameters, powershell_script) + + +def get_runner(): + return WinRmPsScriptRunner(str(uuid.uuid4())) + + +def get_metadata(): + return get_runner_metadata('winrm_ps_script_runner') From c816e3298667f5fe3d6433675b6e98c017de658b Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Fri, 8 Jun 2018 08:53:47 -0400 Subject: [PATCH 02/31] Removed temp file and added integration test stub for winrm_runner --- contrib/runners/winrm_runner/tests/integration/__init__.py | 0 contrib/runners/winrm_runner/tests/unit/.#test_winrm_runner.py | 1 - 2 files changed, 1 deletion(-) create mode 100644 contrib/runners/winrm_runner/tests/integration/__init__.py delete mode 120000 contrib/runners/winrm_runner/tests/unit/.#test_winrm_runner.py diff --git a/contrib/runners/winrm_runner/tests/integration/__init__.py b/contrib/runners/winrm_runner/tests/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/winrm_runner/tests/unit/.#test_winrm_runner.py b/contrib/runners/winrm_runner/tests/unit/.#test_winrm_runner.py deleted file mode 120000 index dd5e6bc2f4..0000000000 --- a/contrib/runners/winrm_runner/tests/unit/.#test_winrm_runner.py +++ /dev/null @@ -1 +0,0 @@ -nmaludy@cerberus.home.17871:1528281446 \ No newline at end of file From 8086ec17646e463435bee51d861a2bc7003437f3 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Fri, 8 Jun 2018 14:33:29 -0400 Subject: [PATCH 03/31] Added support for passing in parameters to the winrm script runner --- .../winrm_runner/winrm_runner/runner.yaml | 8 + .../winrm_runner/winrm_runner/winrm_base.py | 148 ++++++++++++++---- .../winrm_runner/winrm_ps_script_runner.py | 28 +++- 3 files changed, 147 insertions(+), 37 deletions(-) diff --git a/contrib/runners/winrm_runner/winrm_runner/runner.yaml b/contrib/runners/winrm_runner/winrm_runner/runner.yaml index 2540cc098b..2eaf81f45d 100644 --- a/contrib/runners/winrm_runner/winrm_runner/runner.yaml +++ b/contrib/runners/winrm_runner/winrm_runner/runner.yaml @@ -18,6 +18,10 @@ description: A host where the command will be run required: true type: string + kwarg_op: + default: - + description: Operator to use in front of keyword args i.e. "-" or "/". + type: string password: description: Password used to log in. required: true @@ -79,6 +83,10 @@ description: A host where the command will be run required: true type: string + kwarg_op: + default: - + description: Operator to use in front of keyword args i.e. "-" or "/". + type: string password: description: Password used to log in. required: true diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index 865c3622eb..653d514605 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -23,6 +23,8 @@ from st2common.util import jsonify from winrm import Session, Response from winrm.exceptions import WinRMOperationTimeoutError +import re +import six import time __all__ = [ @@ -34,6 +36,7 @@ RUNNER_CWD = 'cwd' RUNNER_ENV = 'env' RUNNER_HOST = "host" +RUNNER_KWARG_OP = 'kwarg_op' RUNNER_PASSWORD = "password" RUNNER_PORT = "port" RUNNER_SCHEME = "scheme" @@ -47,12 +50,32 @@ # explicity made so that it does not equal SUCCESS so a failure is returned WINRM_TIMEOUT_EXIT_CODE = exit_code_constants.SUCCESS_EXIT_CODE - 1 +DEFAULT_KWARG_OP = "-" DEFAULT_PORT = WINRM_HTTPS_PORT DEFAULT_SCHEME = "https" DEFAULT_TIMEOUT = 60 DEFAULT_TRANSPORT = "ntlm" DEFAULT_VERIFY_SSL = True +# key = value in linux/bash to escape +# value = powershell escaped equivalent +# +# Compiled list from the following sources: +# https://ss64.com/ps/syntax-esc.html +# https://www.techotopia.com/index.php/Windows_PowerShell_1.0_String_Quoting_and_Escape_Sequences#PowerShell_Special_Escape_Sequences +PS_ESCAPE_SEQUENCES = {'\n': '`n', + '\r': '`r', + '\t': '`t', + '\a': '`a', + '\b': '`b', + '\f': '`f', + '\v': '`v', + '"': '`"', + '\'': '`\'', + '`': '``', + '\0': '`0', + '$': '`$'} + class WinRMRunnerTimoutError(Exception): @@ -63,37 +86,49 @@ def __init__(self, response): class WinRmBaseRunner(ActionRunner): KEYS_TO_TRANSFORM = ['stdout', 'stderr'] - def _create_session(self, action_parameters): + def pre_run(self): + super(WinRmBaseRunner, self).pre_run() + # common connection parameters - host = self.runner_parameters[RUNNER_HOST] - username = self.runner_parameters[RUNNER_USERNAME] - password = self.runner_parameters[RUNNER_PASSWORD] - timeout = self.runner_parameters.get(RUNNER_TIMEOUT, DEFAULT_TIMEOUT) - read_timeout = timeout + 1 # read_timeout must be > operation_timeout + self._host = self.runner_parameters[RUNNER_HOST] + self._username = self.runner_parameters[RUNNER_USERNAME] + self._password = self.runner_parameters[RUNNER_PASSWORD] + self._timeout = self.runner_parameters.get(RUNNER_TIMEOUT, DEFAULT_TIMEOUT) + self._read_timeout = self._timeout + 1 # read_timeout must be > operation_timeout # default to https port 5986 over ntlm - port = self.runner_parameters.get(RUNNER_PORT, DEFAULT_PORT) - scheme = self.runner_parameters.get(RUNNER_SCHEME, DEFAULT_SCHEME) - transport = self.runner_parameters.get(RUNNER_TRANSPORT, DEFAULT_TRANSPORT) + self._port = self.runner_parameters.get(RUNNER_PORT, DEFAULT_PORT) + self._scheme = self.runner_parameters.get(RUNNER_SCHEME, DEFAULT_SCHEME) + self._transport = self.runner_parameters.get(RUNNER_TRANSPORT, DEFAULT_TRANSPORT) # if connecting to the HTTP port then we must use "http" as the scheme # in the URL - if port == WINRM_HTTP_PORT: - scheme = "http" + if self._port == WINRM_HTTP_PORT: + self._scheme = "http" + + # construct the URL for connecting to WinRM on the host + self._winrm_url = '{}://{}:{}/wsman'.format(self._scheme, self._host, self._port) # default to verifying SSL certs - verify_ssl = self.runner_parameters.get(RUNNER_VERIFY_SSL, DEFAULT_VERIFY_SSL) - winrm_cert_validate = "validate" if verify_ssl else "ignore" + self._verify_ssl = self.runner_parameters.get(RUNNER_VERIFY_SSL, DEFAULT_VERIFY_SSL) + self._server_cert_validation = "validate" if self._verify_ssl else "ignore" + + self._cwd = self.runner_parameters.get(RUNNER_CWD, None) + self._env = self.runner_parameters.get(RUNNER_ENV, {}) + self._env = self._env or {} + self._kwarg_op = self.runner_parameters.get(RUNNER_KWARG_OP, DEFAULT_KWARG_OP) + self._timeout = self.runner_parameters.get(RUNNER_TIMEOUT, DEFAULT_TIMEOUT) + + def _create_session(self, action_parameters): # create the session - winrm_url = '{}://{}:{}/wsman'.format(scheme, host, port) - LOG.info("Connecting via WinRM to url: {}".format(winrm_url)) - session = Session(winrm_url, - auth=(username, password), - transport=transport, - server_cert_validation=winrm_cert_validate, - operation_timeout_sec=timeout, - read_timeout_sec=read_timeout) + LOG.info("Connecting via WinRM to url: {}".format(self._winrm_url)) + session = Session(self._winrm_url, + auth=(self._username, self._password), + transport=self._transport, + server_cert_validation=self._server_cert_validation, + operation_timeout_sec=self._timeout, + read_timeout_sec=self._read_timeout) return session def _winrm_get_command_output(self, protocol, shell_id, command_id): @@ -102,13 +137,12 @@ def _winrm_get_command_output(self, protocol, shell_id, command_id): stdout_buffer, stderr_buffer = [], [] return_code = 0 command_done = False - timeout = self.runner_parameters[RUNNER_TIMEOUT] start_time = time.time() while not command_done: # check if we need to timeout (StackStorm custom) current_time = time.time() elapsed_time = (current_time - start_time) - if timeout and (elapsed_time > timeout): + if self._timeout and (elapsed_time > self._timeout): raise WinRMRunnerTimoutError(Response((b''.join(stdout_buffer), b''.join(stderr_buffer), WINRM_TIMEOUT_EXIT_CODE))) @@ -133,10 +167,9 @@ def _winrm_run_cmd(self, session, command, args=(), env=None, cwd=None): command_id = session.protocol.run_command(shell_id, command, args) # try/catch is for custom timeout handing (StackStorm custom) try: - stdout, stderr, return_code = self._winrm_get_command_output(session.protocol, - shell_id, - command_id) - rs = Response(stdout, stderr, return_code) + rs = Response(self._winrm_get_command_output(session.protocol, + shell_id, + command_id)) rs.timeout = False except WinRMRunnerTimoutError as e: rs = e.response @@ -161,13 +194,10 @@ def _winrm_run_ps(self, session, script, env=None, cwd=None): return rs def _run_ps(self, action_parameters, powershell): - env = self.runner_parameters.get(RUNNER_ENV, None) - cwd = self.runner_parameters.get(RUNNER_CWD, None) - # connect session = self._create_session(action_parameters) # execute - response = self._winrm_run_ps(session, powershell, env=env, cwd=cwd) + response = self._winrm_run_ps(session, powershell, env=self._env, cwd=self._cwd) # create triplet from WinRM response return self._translate_response(response) @@ -192,3 +222,59 @@ def _translate_response(self, response): # automatically convert result stdout/stderr from JSON strings to # objects so they can be used natively return (status, jsonify.json_loads(result, WinRmBaseRunner.KEYS_TO_TRANSFORM), None) + + def transform_params_to_ps(self, positional_args, named_args): + for i, arg in enumerate(positional_args): + positional_args[i] = self._param_to_ps(arg) + + for key, value in six.iteritems(named_args): + named_args[key] = self._param_to_ps(value) + + return positional_args, named_args + + def _param_to_ps(self, param): + ps_str = "" + if isinstance(param, six.string_types): + ps_str = '"' + self._multireplace(param, PS_ESCAPE_SEQUENCES) + '"' + elif isinstance(param, bool): + ps_str = "$true" if param else "$false" + elif isinstance(param, list): + ps_str = "@(" + ps_str += ", ".join([self._param_to_ps(p) for p in param]) + ps_str += ")" + elif isinstance(param, dict): + ps_str = "@{" + ps_str += "; ".join([(self._param_to_ps(k) + ' = ' + self._param_to_ps(v)) + for k, v in six.iteritems(param)]) + ps_str += "}" + else: + ps_str = str(param) + return ps_str + + def _multireplace(self, string, replacements): + """ + Given a string and a replacement map, it returns the replaced string. + Source = https://gist.github.com/bgusach/a967e0587d6e01e889fd1d776c5f3729 + Reference = https://stackoverflow.com/questions/6116978/how-to-replace-multiple-substrings-of-a-string + :param str string: string to execute replacements on + :param dict replacements: replacement dictionary {value to find: value to replace} + :rtype: str + """ + # Place longer ones first to keep shorter substrings from matching where + # the longer ones should take place + # For instance given the replacements {'ab': 'AB', 'abc': 'ABC'} against + # the string 'hey abc', it should produce 'hey ABC' and not 'hey ABc' + substrs = sorted(replacements, key=len, reverse=True) + + # Create a big OR regex that matches any of the substrings to replace + regexp = re.compile('|'.join([re.escape(s) for s in substrs])) + + # For each match, look up the new string in the replacements + return regexp.sub(lambda match: replacements[match.group(0)], string) + + def create_ps_params_string(self, positional_args, named_args): + ps_params_str = "" + ps_params_str += " " .join([(k + " " + v) for k, v in six.iteritems(named_args)]) + ps_params_str +=" " + ps_params_str += " ".join(positional_args) + return ps_params_str diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py index 011c20e7b6..96aad2f380 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py @@ -18,9 +18,11 @@ from st2common import log as logging from st2common.runners.base import ActionRunner +from st2common.runners.base import ShellRunnerMixin from st2common.runners.base import get_metadata as get_runner_metadata from winrm_runner.winrm_base import WinRmBaseRunner +import json # todo: debug __all__ = [ 'WinRmPsScriptRunner', @@ -30,11 +32,8 @@ LOG = logging.getLogger(__name__) -RUNNER_CWD = 'cwd' -RUNNER_ENV = 'env' - -class WinRmPsScriptRunner(WinRmBaseRunner): +class WinRmPsScriptRunner(WinRmBaseRunner, ShellRunnerMixin): def run(self, action_parameters): if not self.entry_point: @@ -42,10 +41,27 @@ def run(self, action_parameters): # read in the script contents from the local file with open(self.entry_point, 'r') as script_file: - powershell_script = script_file.read() + ps_script = script_file.read() + + # extract script parameters specified in the action metadata file + positional_args, named_args = self._get_script_args(action_parameters) + named_args = self._transform_named_args(named_args) + + # convert the script parameters into powershell strings + positional_args, named_args = self.transform_params_to_ps(positional_args, + named_args) + # build a string from all of the named and positional arguments + # this will be our full parameter list when executing the script + ps_params = self.create_ps_params_string(positional_args, named_args) + + + # the following wraps the script (from the file) in a script block ( {} ) + # executes it, passing in the parameters built above + # https://docs.microsoft.com/en-us/powershell/scripting/core-powershell/console/powershell.exe-command-line-help + ps_script_and_params = "& { %s } %s" % (ps_script, ps_params) # execute - return self._run_ps(action_parameters, powershell_script) + return self._run_ps(action_parameters, ps_script_and_params) def get_runner(): From 1fce590ec728aaa9c821af2de8c091dd97ced4aa Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Fri, 8 Jun 2018 15:29:39 -0400 Subject: [PATCH 04/31] Fix flake8 errors --- .../runners/winrm_runner/winrm_runner/winrm_base.py | 11 +++++------ .../winrm_runner/winrm_ps_command_runner.py | 2 -- .../winrm_runner/winrm_ps_script_runner.py | 4 ---- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index 653d514605..84e484bb57 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -113,12 +113,11 @@ def pre_run(self): self._verify_ssl = self.runner_parameters.get(RUNNER_VERIFY_SSL, DEFAULT_VERIFY_SSL) self._server_cert_validation = "validate" if self._verify_ssl else "ignore" - + # additional parameters self._cwd = self.runner_parameters.get(RUNNER_CWD, None) self._env = self.runner_parameters.get(RUNNER_ENV, {}) self._env = self._env or {} self._kwarg_op = self.runner_parameters.get(RUNNER_KWARG_OP, DEFAULT_KWARG_OP) - self._timeout = self.runner_parameters.get(RUNNER_TIMEOUT, DEFAULT_TIMEOUT) def _create_session(self, action_parameters): # create the session @@ -153,7 +152,7 @@ def _winrm_get_command_output(self, protocol, shell_id, command_id): protocol._raw_get_command_output(shell_id, command_id) stdout_buffer.append(stdout) stderr_buffer.append(stderr) - except WinRMOperationTimeoutError as e: + except WinRMOperationTimeoutError: # this is an expected error when waiting for a long-running process, # just silently retry pass @@ -173,7 +172,7 @@ def _winrm_run_cmd(self, session, command, args=(), env=None, cwd=None): rs.timeout = False except WinRMRunnerTimoutError as e: rs = e.response - rs.timeout = True + rs.timeout = True # end stackstorm custom session.protocol.cleanup_command(shell_id, command_id) session.protocol.close_shell(shell_id) @@ -255,7 +254,7 @@ def _multireplace(self, string, replacements): """ Given a string and a replacement map, it returns the replaced string. Source = https://gist.github.com/bgusach/a967e0587d6e01e889fd1d776c5f3729 - Reference = https://stackoverflow.com/questions/6116978/how-to-replace-multiple-substrings-of-a-string + Reference = https://stackoverflow.com/questions/6116978/how-to-replace-multiple-substrings-of-a-string # noqa :param str string: string to execute replacements on :param dict replacements: replacement dictionary {value to find: value to replace} :rtype: str @@ -275,6 +274,6 @@ def _multireplace(self, string, replacements): def create_ps_params_string(self, positional_args, named_args): ps_params_str = "" ps_params_str += " " .join([(k + " " + v) for k, v in six.iteritems(named_args)]) - ps_params_str +=" " + ps_params_str += " " ps_params_str += " ".join(positional_args) return ps_params_str diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py index eee0ef7937..4a2a01b709 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py @@ -17,11 +17,9 @@ import uuid from st2common import log as logging -from st2common.runners.base import ActionRunner from st2common.runners.base import get_metadata as get_runner_metadata from winrm_runner.winrm_base import WinRmBaseRunner - __all__ = [ 'WinRmPsCommandRunner', 'get_runner', diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py index 96aad2f380..b08d249599 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py @@ -17,13 +17,10 @@ import uuid from st2common import log as logging -from st2common.runners.base import ActionRunner from st2common.runners.base import ShellRunnerMixin from st2common.runners.base import get_metadata as get_runner_metadata from winrm_runner.winrm_base import WinRmBaseRunner -import json # todo: debug - __all__ = [ 'WinRmPsScriptRunner', 'get_runner', @@ -54,7 +51,6 @@ def run(self, action_parameters): # this will be our full parameter list when executing the script ps_params = self.create_ps_params_string(positional_args, named_args) - # the following wraps the script (from the file) in a script block ( {} ) # executes it, passing in the parameters built above # https://docs.microsoft.com/en-us/powershell/scripting/core-powershell/console/powershell.exe-command-line-help From e50f329f71533fa6b8093b570eedc78e3ba16e40 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Mon, 11 Jun 2018 07:13:22 -0400 Subject: [PATCH 05/31] Initial work on WinRM runner unit testing --- .../tests/unit/test_winrm_base.py | 35 +++++++++++++++++++ .../winrm_runner/winrm_runner/runner.yaml | 4 +-- .../winrm_runner/winrm_runner/winrm_base.py | 18 +++++----- 3 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 contrib/runners/winrm_runner/tests/unit/test_winrm_base.py diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py new file mode 100644 index 0000000000..d6ef6a41df --- /dev/null +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -0,0 +1,35 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +from st2common.runners.base import ActionRunner +from unittest2 import TestCase +from winrm_runner.winrm_base import WinRmBaseRunner, WinRmRunnerTimoutError + + +class WinRmBaseTestCase(TestCase): + + def test_win_rm_runner_timout_error(self): + error = WinRmRunnerTimoutError('test_response') + self.assertIsInstance(error, Exception) + self.assertEquals(error.response, 'test_response') + with self.assertRaises(WinRmRunnerTimoutError): + raise WinRmRunnerTimoutError('test raising') + + def test_init(self) + runner = WinRmBaseRunner('abcdef'): + self.assertIsInstance(ActionRunner) + self.assertEquals(runner.runner_id, "abcdef") diff --git a/contrib/runners/winrm_runner/winrm_runner/runner.yaml b/contrib/runners/winrm_runner/winrm_runner/runner.yaml index 2eaf81f45d..8883724644 100644 --- a/contrib/runners/winrm_runner/winrm_runner/runner.yaml +++ b/contrib/runners/winrm_runner/winrm_runner/runner.yaml @@ -19,7 +19,7 @@ required: true type: string kwarg_op: - default: - + default: "-" description: Operator to use in front of keyword args i.e. "-" or "/". type: string password: @@ -84,7 +84,7 @@ required: true type: string kwarg_op: - default: - + default: "-" description: Operator to use in front of keyword args i.e. "-" or "/". type: string password: diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index 84e484bb57..1ae46472e4 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -15,6 +15,10 @@ from __future__ import absolute_import +import re +import six +import time + from base64 import b64encode from st2common import log as logging from st2common.constants import action as action_constants @@ -23,9 +27,6 @@ from st2common.util import jsonify from winrm import Session, Response from winrm.exceptions import WinRMOperationTimeoutError -import re -import six -import time __all__ = [ 'WinRmBaseRunner', @@ -57,6 +58,8 @@ DEFAULT_TRANSPORT = "ntlm" DEFAULT_VERIFY_SSL = True +RESULT_KEYS_TO_TRANSFORM = ['stdout', 'stderr'] + # key = value in linux/bash to escape # value = powershell escaped equivalent # @@ -77,14 +80,13 @@ '$': '`$'} -class WinRMRunnerTimoutError(Exception): +class WinRmRunnerTimoutError(Exception): def __init__(self, response): self.response = response class WinRmBaseRunner(ActionRunner): - KEYS_TO_TRANSFORM = ['stdout', 'stderr'] def pre_run(self): super(WinRmBaseRunner, self).pre_run() @@ -142,7 +144,7 @@ def _winrm_get_command_output(self, protocol, shell_id, command_id): current_time = time.time() elapsed_time = (current_time - start_time) if self._timeout and (elapsed_time > self._timeout): - raise WinRMRunnerTimoutError(Response((b''.join(stdout_buffer), + raise WinRmRunnerTimoutError(Response((b''.join(stdout_buffer), b''.join(stderr_buffer), WINRM_TIMEOUT_EXIT_CODE))) # end stackstorm custom @@ -170,7 +172,7 @@ def _winrm_run_cmd(self, session, command, args=(), env=None, cwd=None): shell_id, command_id)) rs.timeout = False - except WinRMRunnerTimoutError as e: + except WinRmRunnerTimoutError as e: rs = e.response rs.timeout = True # end stackstorm custom @@ -220,7 +222,7 @@ def _translate_response(self, response): # automatically convert result stdout/stderr from JSON strings to # objects so they can be used natively - return (status, jsonify.json_loads(result, WinRmBaseRunner.KEYS_TO_TRANSFORM), None) + return (status, jsonify.json_loads(result, RESULT_KEYS_TO_TRANSFORM), None) def transform_params_to_ps(self, positional_args, named_args): for i, arg in enumerate(positional_args): From 26d773eb2f6bca4de1bd36ccb5ebac2f60e8d341 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Mon, 11 Jun 2018 12:46:06 -0400 Subject: [PATCH 06/31] WinRM runner unit tests --- .../tests/unit/test_winrm_base.py | 305 +++++++++++++++++- .../winrm_runner/winrm_runner/winrm_base.py | 20 +- .../winrm_runner/winrm_ps_command_runner.py | 2 +- .../winrm_runner/winrm_ps_script_runner.py | 2 +- 4 files changed, 313 insertions(+), 16 deletions(-) diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py index d6ef6a41df..686f1213ef 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -15,12 +15,30 @@ from __future__ import absolute_import +import time +import mock +from base64 import b64encode +from winrm import Response +from winrm.exceptions import WinRMOperationTimeoutError + from st2common.runners.base import ActionRunner -from unittest2 import TestCase +from st2tests.base import RunnerTestCase from winrm_runner.winrm_base import WinRmBaseRunner, WinRmRunnerTimoutError +from winrm_runner.winrm_base import WINRM_TIMEOUT_EXIT_CODE +from winrm_runner import winrm_ps_command_runner + +class WinRmBaseTestCase(RunnerTestCase): + def setUp(self): + super(WinRmBaseTestCase, self).setUpClass() + self._runner = winrm_ps_command_runner.get_runner() -class WinRmBaseTestCase(TestCase): + def _init_runner(self): + runner_parameters = {'host': 'host@domain.tld', + 'username': 'user@domain.tld', + 'password': 'xyz987'} + self._runner.runner_parameters = runner_parameters + self._runner.pre_run() def test_win_rm_runner_timout_error(self): error = WinRmRunnerTimoutError('test_response') @@ -29,7 +47,284 @@ def test_win_rm_runner_timout_error(self): with self.assertRaises(WinRmRunnerTimoutError): raise WinRmRunnerTimoutError('test raising') - def test_init(self) - runner = WinRmBaseRunner('abcdef'): - self.assertIsInstance(ActionRunner) + def test_init(self): + runner = winrm_ps_command_runner.WinRmPsCommandRunner('abcdef') + self.assertIsInstance(runner, WinRmBaseRunner) + self.assertIsInstance(runner, ActionRunner) self.assertEquals(runner.runner_id, "abcdef") + + @mock.patch('winrm_runner.winrm_base.ActionRunner.pre_run') + def test_pre_run(self, mock_pre_run): + runner_parameters = {'host': 'host@domain.tld', + 'username': 'user@domain.tld', + 'password': 'abc123', + 'timeout': 99, + 'port': 1234, + 'scheme': 'http', + 'transport': 'ntlm', + 'verify_ssl_cert': False, + 'cwd': 'C:\\Test', + 'env': {'TEST_VAR': 'TEST_VALUE'}, + 'kwarg_op': '/'} + self._runner.runner_parameters = runner_parameters + self._runner.pre_run() + mock_pre_run.assert_called_with() + self.assertEquals(self._runner._host, 'host@domain.tld') + self.assertEquals(self._runner._username, 'user@domain.tld') + self.assertEquals(self._runner._password, 'abc123') + self.assertEquals(self._runner._timeout, 99) + self.assertEquals(self._runner._read_timeout, 100) + self.assertEquals(self._runner._port, 1234) + self.assertEquals(self._runner._scheme, 'http') + self.assertEquals(self._runner._transport, 'ntlm') + self.assertEquals(self._runner._winrm_url, 'http://host@domain.tld:1234/wsman') + self.assertEquals(self._runner._verify_ssl, False) + self.assertEquals(self._runner._server_cert_validation, 'ignore') + self.assertEquals(self._runner._cwd, 'C:\\Test') + self.assertEquals(self._runner._env, {'TEST_VAR': 'TEST_VALUE'}) + self.assertEquals(self._runner._kwarg_op, '/') + + @mock.patch('winrm_runner.winrm_base.ActionRunner.pre_run') + def test_pre_run_defaults(self, mock_pre_run): + runner_parameters = {'host': 'host@domain.tld', + 'username': 'user@domain.tld', + 'password': 'abc123'} + self._runner.runner_parameters = runner_parameters + self._runner.pre_run() + mock_pre_run.assert_called_with() + self.assertEquals(self._runner._host, 'host@domain.tld') + self.assertEquals(self._runner._username, 'user@domain.tld') + self.assertEquals(self._runner._password, 'abc123') + self.assertEquals(self._runner._timeout, 60) + self.assertEquals(self._runner._read_timeout, 61) + self.assertEquals(self._runner._port, 5986) + self.assertEquals(self._runner._scheme, 'https') + self.assertEquals(self._runner._transport, 'ntlm') + self.assertEquals(self._runner._winrm_url, 'https://host@domain.tld:5986/wsman') + self.assertEquals(self._runner._verify_ssl, True) + self.assertEquals(self._runner._server_cert_validation, 'validate') + self.assertEquals(self._runner._cwd, None) + self.assertEquals(self._runner._env, {}) + self.assertEquals(self._runner._kwarg_op, '-') + + @mock.patch('winrm_runner.winrm_base.ActionRunner.pre_run') + def test_pre_run_5985_force_http(self, mock_pre_run): + runner_parameters = {'host': 'host@domain.tld', + 'username': 'user@domain.tld', + 'password': 'abc123', + 'port': 5985, + 'scheme': 'https'} + self._runner.runner_parameters = runner_parameters + self._runner.pre_run() + mock_pre_run.assert_called_with() + self.assertEquals(self._runner._host, 'host@domain.tld') + self.assertEquals(self._runner._username, 'user@domain.tld') + self.assertEquals(self._runner._password, 'abc123') + self.assertEquals(self._runner._timeout, 60) + self.assertEquals(self._runner._read_timeout, 61) + # ensure port is still 5985 + self.assertEquals(self._runner._port, 5985) + # ensure scheme is set back to http + self.assertEquals(self._runner._scheme, 'http') + self.assertEquals(self._runner._transport, 'ntlm') + self.assertEquals(self._runner._winrm_url, 'http://host@domain.tld:5985/wsman') + self.assertEquals(self._runner._verify_ssl, True) + self.assertEquals(self._runner._server_cert_validation, 'validate') + self.assertEquals(self._runner._cwd, None) + self.assertEquals(self._runner._env, {}) + self.assertEquals(self._runner._kwarg_op, '-') + + @mock.patch('winrm_runner.winrm_base.ActionRunner.pre_run') + def test_pre_run_none_env(self, mock_pre_run): + runner_parameters = {'host': 'host@domain.tld', + 'username': 'user@domain.tld', + 'password': 'abc123', + 'env': None} + self._runner.runner_parameters = runner_parameters + self._runner.pre_run() + mock_pre_run.assert_called_with() + # ensure that env is set to {} even though we passed in None + self.assertEquals(self._runner._env, {}) + + @mock.patch('winrm_runner.winrm_base.ActionRunner.pre_run') + def test_pre_run_ssl_verify_true(self, mock_pre_run): + runner_parameters = {'host': 'host@domain.tld', + 'username': 'user@domain.tld', + 'password': 'abc123', + 'verify_ssl_cert': True} + self._runner.runner_parameters = runner_parameters + self._runner.pre_run() + mock_pre_run.assert_called_with() + self.assertEquals(self._runner._verify_ssl, True) + self.assertEquals(self._runner._server_cert_validation, 'validate') + + @mock.patch('winrm_runner.winrm_base.ActionRunner.pre_run') + def test_pre_run_ssl_verify_false(self, mock_pre_run): + runner_parameters = {'host': 'host@domain.tld', + 'username': 'user@domain.tld', + 'password': 'abc123', + 'verify_ssl_cert': False} + self._runner.runner_parameters = runner_parameters + self._runner.pre_run() + mock_pre_run.assert_called_with() + self.assertEquals(self._runner._verify_ssl, False) + self.assertEquals(self._runner._server_cert_validation, 'ignore') + + @mock.patch('winrm_runner.winrm_base.Session') + def test_create_session(self, mock_session): + self._runner._winrm_url = 'https://host@domain.tld:5986/wsman' + self._runner._username = 'user@domain.tld' + self._runner._password = 'abc123' + self._runner._transport = 'ntlm' + self._runner._server_cert_validation = 'validate' + self._runner._timeout = 60 + self._runner._read_timeout = 61 + mock_session.return_value = "session" + + result = self._runner._create_session() + self.assertEquals(result, "session") + mock_session.assert_called_with('https://host@domain.tld:5986/wsman', + auth=('user@domain.tld', 'abc123'), + transport='ntlm', + server_cert_validation='validate', + operation_timeout_sec=60, + read_timeout_sec=61) + + def test_get_command_output(self): + self._runner._timeout = 0 + mock_protocol = mock.MagicMock() + mock_protocol._raw_get_command_output.side_effect = [ + ('output1', 'error1', 123, False), + ('output2', 'error2', 456, False), + ('output3', 'error3', 789, True) + ] + + result = self._runner._winrm_get_command_output(mock_protocol, 567, 890) + + self.assertEquals(result, ('output1output2output3', 'error1error2error3', 789)) + mock_protocol._raw_get_command_output.assert_has_calls = [ + mock.call(567, 890), + mock.call(567, 890), + mock.call(567, 890) + ] + + def test_get_command_output_timeout(self): + self._runner._timeout = 1 + + mock_protocol = mock.MagicMock() + def sleep_for_timeout(*args, **kwargs): + time.sleep(2) + return ('output1', 'error1', 123, False) + mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout + + with self.assertRaises(WinRmRunnerTimoutError) as cm: + self._runner._winrm_get_command_output(mock_protocol, 567, 890) + + timeout_exception = cm.exception + self.assertEqual(timeout_exception.response.std_out, 'output1') + self.assertEqual(timeout_exception.response.std_err, 'error1') + self.assertEqual(timeout_exception.response.status_code, WINRM_TIMEOUT_EXIT_CODE) + mock_protocol._raw_get_command_output.assert_called_with(567, 890) + + def test_get_command_output_operation_timeout(self): + self._runner._timeout = 1 + + mock_protocol = mock.MagicMock() + def sleep_for_timeout_then_raise(*args, **kwargs): + time.sleep(2) + raise WinRMOperationTimeoutError() + mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise + + with self.assertRaises(WinRmRunnerTimoutError) as cm: + self._runner._winrm_get_command_output(mock_protocol, 567, 890) + + timeout_exception = cm.exception + self.assertEqual(timeout_exception.response.std_out, '') + self.assertEqual(timeout_exception.response.std_err, '') + self.assertEqual(timeout_exception.response.status_code, WINRM_TIMEOUT_EXIT_CODE) + mock_protocol._raw_get_command_output.assert_called_with(567, 890) + + def test_winrm_run_cmd(self): + mock_protocol = mock.MagicMock() + mock_protocol.open_shell.return_value = 123 + mock_protocol.run_command.return_value = 456 + mock_protocol._raw_get_command_output.return_value = ('output', 'error', 9, True) + mock_session = mock.MagicMock(protocol=mock_protocol) + + self._init_runner() + result = self._runner._winrm_run_cmd(mock_session, "fake-command", + args=['arg1', 'arg2'], + env={'PATH': 'C:\\st2\\bin'}, + cwd='C:\\st2') + expected_response = Response(('output', 'error', 9)) + expected_response.timeout = False + + self.assertEquals(result.__dict__, expected_response.__dict__) + mock_protocol.open_shell.assert_called_with(env_vars={'PATH': 'C:\\st2\\bin'}, + working_directory='C:\\st2') + mock_protocol.run_command.assert_called_with(123, 'fake-command', ['arg1', 'arg2']) + mock_protocol._raw_get_command_output.assert_called_with(123, 456) + mock_protocol.cleanup_command.assert_called_with(123, 456) + mock_protocol.close_shell.assert_called_with(123) + + @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._winrm_get_command_output') + def test_winrm_run_cmd_timeout(self, mock_get_command_output): + mock_protocol = mock.MagicMock() + mock_protocol.open_shell.return_value = 123 + mock_protocol.run_command.return_value = 456 + mock_session = mock.MagicMock(protocol=mock_protocol) + mock_get_command_output.side_effect = WinRmRunnerTimoutError(Response(('', '', 5))) + + self._init_runner() + result = self._runner._winrm_run_cmd(mock_session, "fake-command", + args=['arg1', 'arg2'], + env={'PATH': 'C:\\st2\\bin'}, + cwd='C:\\st2') + expected_response = Response(('', '', 5)) + expected_response.timeout = True + + self.assertEquals(result.__dict__, expected_response.__dict__) + mock_protocol.open_shell.assert_called_with(env_vars={'PATH': 'C:\\st2\\bin'}, + working_directory='C:\\st2') + mock_protocol.run_command.assert_called_with(123, 'fake-command', ['arg1', 'arg2']) + mock_protocol.cleanup_command.assert_called_with(123, 456) + mock_protocol.close_shell.assert_called_with(123) + + @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._winrm_run_cmd') + def test_winrm_run_ps(self, mock_run_cmd): + mock_run_cmd.return_value = Response(('output', '', 3)) + script = "Get-ADUser stanley" + + result = self._runner._winrm_run_ps("session", script, + env={'PATH': 'C:\\st2\\bin'}, + cwd='C:\\st2') + + self.assertEquals(result.__dict__, + Response(('output', '', 3)).__dict__) + expected_ps = ('powershell -encodedcommand ' + + b64encode("Get-ADUser stanley".encode('utf_16_le')).decode('ascii')) + mock_run_cmd.assert_called_with("session", + expected_ps, + env={'PATH': 'C:\\st2\\bin'}, + cwd='C:\\st2') + + @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._winrm_run_cmd') + def test_winrm_run_ps_clean_stderr(self, mock_run_cmd): + mock_run_cmd.return_value = Response(('output', 'error', 3)) + mock_session = mock.MagicMock() + mock_session._clean_error_msg.return_value = 'e' + script = "Get-ADUser stanley" + + result = self._runner._winrm_run_ps(mock_session, script, + env={'PATH': 'C:\\st2\\bin'}, + cwd='C:\\st2') + + self.assertEquals(result.__dict__, + Response(('output', 'e', 3)).__dict__) + expected_ps = ('powershell -encodedcommand ' + + b64encode("Get-ADUser stanley".encode('utf_16_le')).decode('ascii')) + mock_run_cmd.assert_called_with(mock_session, + expected_ps, + env={'PATH': 'C:\\st2\\bin'}, + cwd='C:\\st2') + mock_session._clean_error_msg.assert_called_with('error') diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index 1ae46472e4..f51b071647 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -34,10 +34,10 @@ LOG = logging.getLogger(__name__) -RUNNER_CWD = 'cwd' -RUNNER_ENV = 'env' +RUNNER_CWD = "cwd" +RUNNER_ENV = "env" RUNNER_HOST = "host" -RUNNER_KWARG_OP = 'kwarg_op' +RUNNER_KWARG_OP = "kwarg_op" RUNNER_PASSWORD = "password" RUNNER_PORT = "port" RUNNER_SCHEME = "scheme" @@ -58,7 +58,7 @@ DEFAULT_TRANSPORT = "ntlm" DEFAULT_VERIFY_SSL = True -RESULT_KEYS_TO_TRANSFORM = ['stdout', 'stderr'] +RESULT_KEYS_TO_TRANSFORM = ["stdout", "stderr"] # key = value in linux/bash to escape # value = powershell escaped equivalent @@ -109,7 +109,7 @@ def pre_run(self): self._scheme = "http" # construct the URL for connecting to WinRM on the host - self._winrm_url = '{}://{}:{}/wsman'.format(self._scheme, self._host, self._port) + self._winrm_url = "{}://{}:{}/wsman".format(self._scheme, self._host, self._port) # default to verifying SSL certs self._verify_ssl = self.runner_parameters.get(RUNNER_VERIFY_SSL, DEFAULT_VERIFY_SSL) @@ -121,7 +121,7 @@ def pre_run(self): self._env = self._env or {} self._kwarg_op = self.runner_parameters.get(RUNNER_KWARG_OP, DEFAULT_KWARG_OP) - def _create_session(self, action_parameters): + def _create_session(self): # create the session LOG.info("Connecting via WinRM to url: {}".format(self._winrm_url)) session = Session(self._winrm_url, @@ -162,7 +162,9 @@ def _winrm_get_command_output(self, protocol, shell_id, command_id): def _winrm_run_cmd(self, session, command, args=(), env=None, cwd=None): # NOTE: this is copied from pywinrm because it doesn't support - # passing env and working_directory from the Session.run_cmd + # passing env and working_directory from the Session.run_cmd. + # It also doesn't support timeouts. All of these things have been + # added shell_id = session.protocol.open_shell(env_vars=env, working_directory=cwd) command_id = session.protocol.run_command(shell_id, command, args) @@ -194,9 +196,9 @@ def _winrm_run_ps(self, session, script, env=None, cwd=None): rs.std_err = session._clean_error_msg(rs.std_err) return rs - def _run_ps(self, action_parameters, powershell): + def _run_ps(self, powershell): # connect - session = self._create_session(action_parameters) + session = self._create_session() # execute response = self._winrm_run_ps(session, powershell, env=self._env, cwd=self._cwd) # create triplet from WinRM response diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py index 4a2a01b709..ab4630ab09 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py @@ -37,7 +37,7 @@ def run(self, action_parameters): powershell_command = self.runner_parameters[RUNNER_COMMAND] # execute - return self._run_ps(action_parameters, powershell_command) + return self._run_ps(powershell_command) def get_runner(): diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py index b08d249599..398288f8f1 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py @@ -57,7 +57,7 @@ def run(self, action_parameters): ps_script_and_params = "& { %s } %s" % (ps_script, ps_params) # execute - return self._run_ps(action_parameters, ps_script_and_params) + return self._run_ps(ps_script_and_params) def get_runner(): From a9363942e89d00cdf81eb243861c98847c1fb18b Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Mon, 11 Jun 2018 14:54:28 -0400 Subject: [PATCH 07/31] Added runner requirements.txt files to virtualenv for testing --- Makefile | 4 +- .../action_chain_runner/in-requirements.txt | 0 .../action_chain_runner/requirements.txt | 2 + .../announcement_runner/in-requirements.txt | 0 .../announcement_runner/requirements.txt | 2 + .../cloudslang_runner/in-requirements.txt | 0 .../cloudslang_runner/requirements.txt | 2 + .../runners/http_runner/in-requirements.txt | 0 contrib/runners/http_runner/requirements.txt | 2 + .../inquirer_runner/in-requirements.txt | 0 .../runners/inquirer_runner/requirements.txt | 2 + .../runners/local_runner/in-requirements.txt | 0 contrib/runners/local_runner/requirements.txt | 2 + .../runners/mistral_v2/in-requirements.txt | 0 contrib/runners/mistral_v2/requirements.txt | 2 + .../runners/noop_runner/in-requirements.txt | 0 contrib/runners/noop_runner/requirements.txt | 2 + .../runners/python_runner/in-requirements.txt | 0 .../runners/python_runner/requirements.txt | 2 + .../runners/remote_runner/in-requirements.txt | 0 .../runners/remote_runner/requirements.txt | 2 + .../windows_runner/in-requirements.txt | 0 .../runners/windows_runner/requirements.txt | 2 + .../runners/winrm_runner/in-requirements.txt | 1 + contrib/runners/winrm_runner/requirements.txt | 3 +- .../tests/unit/test_winrm_base.py | 99 +++++++++++++++++++ .../winrm_runner/winrm_runner/winrm_base.py | 4 +- fixed-requirements.txt | 1 + requirements.txt | 1 + 29 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 contrib/runners/action_chain_runner/in-requirements.txt create mode 100644 contrib/runners/announcement_runner/in-requirements.txt create mode 100644 contrib/runners/cloudslang_runner/in-requirements.txt create mode 100644 contrib/runners/http_runner/in-requirements.txt create mode 100644 contrib/runners/inquirer_runner/in-requirements.txt create mode 100644 contrib/runners/local_runner/in-requirements.txt create mode 100644 contrib/runners/mistral_v2/in-requirements.txt create mode 100644 contrib/runners/noop_runner/in-requirements.txt create mode 100644 contrib/runners/python_runner/in-requirements.txt create mode 100644 contrib/runners/remote_runner/in-requirements.txt create mode 100644 contrib/runners/windows_runner/in-requirements.txt create mode 100644 contrib/runners/winrm_runner/in-requirements.txt diff --git a/Makefile b/Makefile index cf130e9042..144e36f62b 100644 --- a/Makefile +++ b/Makefile @@ -258,10 +258,10 @@ requirements: virtualenv .sdist-requirements $(VIRTUALENV_DIR)/bin/pip install --upgrade "virtualenv==15.1.0" # Required for packs.install in dev envs. # Generate all requirements to support current CI pipeline. - $(VIRTUALENV_DIR)/bin/python scripts/fixate-requirements.py --skip=virtualenv -s st2*/in-requirements.txt -f fixed-requirements.txt -o requirements.txt + $(VIRTUALENV_DIR)/bin/python scripts/fixate-requirements.py --skip=virtualenv -s st2*/in-requirements.txt contrib/runners/*/in-requirements.txt -f fixed-requirements.txt -o requirements.txt # Generate finall requirements.txt file for each component - @for component in $(COMPONENTS); do\ + @for component in $(COMPONENTS_WITH_RUNNERS); do\ echo "==========================================================="; \ echo "Generating requirements.txt for" $$component; \ echo "==========================================================="; \ diff --git a/contrib/runners/action_chain_runner/in-requirements.txt b/contrib/runners/action_chain_runner/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/action_chain_runner/requirements.txt b/contrib/runners/action_chain_runner/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/action_chain_runner/requirements.txt +++ b/contrib/runners/action_chain_runner/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/announcement_runner/in-requirements.txt b/contrib/runners/announcement_runner/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/announcement_runner/requirements.txt b/contrib/runners/announcement_runner/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/announcement_runner/requirements.txt +++ b/contrib/runners/announcement_runner/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/cloudslang_runner/in-requirements.txt b/contrib/runners/cloudslang_runner/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/cloudslang_runner/requirements.txt b/contrib/runners/cloudslang_runner/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/cloudslang_runner/requirements.txt +++ b/contrib/runners/cloudslang_runner/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/http_runner/in-requirements.txt b/contrib/runners/http_runner/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/http_runner/requirements.txt b/contrib/runners/http_runner/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/http_runner/requirements.txt +++ b/contrib/runners/http_runner/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/inquirer_runner/in-requirements.txt b/contrib/runners/inquirer_runner/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/inquirer_runner/requirements.txt b/contrib/runners/inquirer_runner/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/inquirer_runner/requirements.txt +++ b/contrib/runners/inquirer_runner/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/local_runner/in-requirements.txt b/contrib/runners/local_runner/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/local_runner/requirements.txt b/contrib/runners/local_runner/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/local_runner/requirements.txt +++ b/contrib/runners/local_runner/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/mistral_v2/in-requirements.txt b/contrib/runners/mistral_v2/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/mistral_v2/requirements.txt b/contrib/runners/mistral_v2/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/mistral_v2/requirements.txt +++ b/contrib/runners/mistral_v2/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/noop_runner/in-requirements.txt b/contrib/runners/noop_runner/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/noop_runner/requirements.txt b/contrib/runners/noop_runner/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/noop_runner/requirements.txt +++ b/contrib/runners/noop_runner/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/python_runner/in-requirements.txt b/contrib/runners/python_runner/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/python_runner/requirements.txt b/contrib/runners/python_runner/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/python_runner/requirements.txt +++ b/contrib/runners/python_runner/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/remote_runner/in-requirements.txt b/contrib/runners/remote_runner/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/remote_runner/requirements.txt b/contrib/runners/remote_runner/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/remote_runner/requirements.txt +++ b/contrib/runners/remote_runner/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/windows_runner/in-requirements.txt b/contrib/runners/windows_runner/in-requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/runners/windows_runner/requirements.txt b/contrib/runners/windows_runner/requirements.txt index e69de29bb2..b4be240ac2 100644 --- a/contrib/runners/windows_runner/requirements.txt +++ b/contrib/runners/windows_runner/requirements.txt @@ -0,0 +1,2 @@ +# Don't edit this file. It's generated automatically! + diff --git a/contrib/runners/winrm_runner/in-requirements.txt b/contrib/runners/winrm_runner/in-requirements.txt new file mode 100644 index 0000000000..17a16ff26d --- /dev/null +++ b/contrib/runners/winrm_runner/in-requirements.txt @@ -0,0 +1 @@ +pywinrm diff --git a/contrib/runners/winrm_runner/requirements.txt b/contrib/runners/winrm_runner/requirements.txt index 17a16ff26d..8fa65d3a82 100644 --- a/contrib/runners/winrm_runner/requirements.txt +++ b/contrib/runners/winrm_runner/requirements.txt @@ -1 +1,2 @@ -pywinrm +# Don't edit this file. It's generated automatically! +pywinrm==0.3.0 diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py index 686f1213ef..aac85d276c 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -328,3 +328,102 @@ def test_winrm_run_ps_clean_stderr(self, mock_run_cmd): env={'PATH': 'C:\\st2\\bin'}, cwd='C:\\st2') mock_session._clean_error_msg.assert_called_with('error') + + @mock.patch('winrm.Protocol') + def test_run_ps(self, mock_protocol_init): + mock_protocol = mock.MagicMock() + mock_protocol._raw_get_command_output.side_effect = [ + ('output1', 'error1', 0, False), + ('output2', 'error2', 0, False), + ('output3', 'error3', 0, True) + ] + mock_protocol_init.return_value = mock_protocol + + self._init_runner() + result = self._runner._run_ps("Get-Location") + self.assertEquals(result, ('succeeded', + {'failed': False, + 'succeeded': True, + 'return_code': 0, + 'stdout': 'output1output2output3', + 'stderr': 'error1error2error3'}, + None)) + + @mock.patch('winrm.Protocol') + def test_run_ps_failed(self, mock_protocol_init): + mock_protocol = mock.MagicMock() + mock_protocol._raw_get_command_output.side_effect = [ + ('output1', 'error1', 0, False), + ('output2', 'error2', 0, False), + ('output3', 'error3', 1, True) + ] + mock_protocol_init.return_value = mock_protocol + + self._init_runner() + result = self._runner._run_ps("Get-Location") + self.assertEquals(result, ('failed', + {'failed': True, + 'succeeded': False, + 'return_code': 1, + 'stdout': 'output1output2output3', + 'stderr': 'error1error2error3'}, + None)) + + @mock.patch('winrm.Protocol') + def test_run_ps_timeout(self, mock_protocol_init): + mock_protocol = mock.MagicMock() + def sleep_for_timeout_then_raise(*args, **kwargs): + time.sleep(2) + return ('output1', 'error1', 123, False) + mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise + mock_protocol_init.return_value = mock_protocol + + self._init_runner() + self._runner._timeout = 1 + result = self._runner._run_ps("Get-Location") + self.assertEquals(result, ('timeout', + {'failed': True, + 'succeeded': False, + 'return_code': -1, + 'stdout': 'output1', + 'stderr': 'error1'}, + None)) + + def test_translate_response_success(self): + response = Response(('output1', 'error1', 0)) + response.timeout = False + + result = self._runner._translate_response(response) + self.assertEquals(result, ('succeeded', + {'failed': False, + 'succeeded': True, + 'return_code': 0, + 'stdout': 'output1', + 'stderr': 'error1'}, + None)) + + def test_translate_response_failure(self): + response = Response(('output1', 'error1', 123)) + response.timeout = False + + result = self._runner._translate_response(response) + self.assertEquals(result, ('failed', + {'failed': True, + 'succeeded': False, + 'return_code': 123, + 'stdout': 'output1', + 'stderr': 'error1'}, + None)) + + def test_translate_response_timeout(self): + response = Response(('output1', 'error1', 123)) + response.timeout = True + + result = self._runner._translate_response(response) + self.assertEquals(result, ('timeout', + {'failed': True, + 'succeeded': False, + 'return_code': -1, + 'stdout': 'output1', + 'stderr': 'error1'}, + None)) diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index f51b071647..f8f4c87c3a 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -208,8 +208,10 @@ def _translate_response(self, response): # check exit status for errors succeeded = (response.status_code == exit_code_constants.SUCCESS_EXIT_CODE) status = action_constants.LIVEACTION_STATUS_SUCCEEDED + status_code = response.status_code if response.timeout: status = action_constants.LIVEACTION_STATUS_TIMED_OUT + status_code = WINRM_TIMEOUT_EXIT_CODE elif not succeeded: status = action_constants.LIVEACTION_STATUS_FAILED @@ -217,7 +219,7 @@ def _translate_response(self, response): result = { 'failed': not succeeded, 'succeeded': succeeded, - 'return_code': response.status_code, + 'return_code': status_code, 'stdout': response.std_out, 'stderr': response.std_err } diff --git a/fixed-requirements.txt b/fixed-requirements.txt index 0a6b7f7c5c..c36ad4afcf 100644 --- a/fixed-requirements.txt +++ b/fixed-requirements.txt @@ -46,6 +46,7 @@ routes==2.4.1 flex==6.13.1 webob==1.7.4 prance==0.9.0 +pywinrm==0.3.0 # test requirements below nose-timer>=0.7.2,<0.8 psutil==5.4.5 diff --git a/requirements.txt b/requirements.txt index 312856a703..d38efd9a13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,6 +40,7 @@ python-json-logger python-keyczar==0.716 python-statsd==2.1.0 pytz==2018.4 +pywinrm==0.3.0 pyyaml<4.0,>=3.12 rednose requests[security]<2.15,>=2.14.1 From f9e7b78d0c06309e99c407c073fabb37af024147 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Mon, 11 Jun 2018 15:22:18 -0400 Subject: [PATCH 08/31] Fix flake8 errors --- contrib/runners/winrm_runner/tests/unit/test_winrm_base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py index aac85d276c..6e7e797a7f 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -27,6 +27,7 @@ from winrm_runner.winrm_base import WINRM_TIMEOUT_EXIT_CODE from winrm_runner import winrm_ps_command_runner + class WinRmBaseTestCase(RunnerTestCase): def setUp(self): @@ -212,9 +213,11 @@ def test_get_command_output_timeout(self): self._runner._timeout = 1 mock_protocol = mock.MagicMock() + def sleep_for_timeout(*args, **kwargs): time.sleep(2) return ('output1', 'error1', 123, False) + mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout with self.assertRaises(WinRmRunnerTimoutError) as cm: @@ -230,9 +233,11 @@ def test_get_command_output_operation_timeout(self): self._runner._timeout = 1 mock_protocol = mock.MagicMock() + def sleep_for_timeout_then_raise(*args, **kwargs): time.sleep(2) raise WinRMOperationTimeoutError() + mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise with self.assertRaises(WinRmRunnerTimoutError) as cm: @@ -372,9 +377,11 @@ def test_run_ps_failed(self, mock_protocol_init): @mock.patch('winrm.Protocol') def test_run_ps_timeout(self, mock_protocol_init): mock_protocol = mock.MagicMock() + def sleep_for_timeout_then_raise(*args, **kwargs): time.sleep(2) return ('output1', 'error1', 123, False) + mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise mock_protocol_init.return_value = mock_protocol From d0f182a290e753b1cdff2773d1f7bbf967f34af6 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Mon, 11 Jun 2018 16:20:27 -0400 Subject: [PATCH 09/31] More unit tests for winrm runner --- .../tests/unit/test_winrm_base.py | 143 ++++++++++++++++++ .../winrm_runner/winrm_runner/winrm_base.py | 64 ++++---- 2 files changed, 175 insertions(+), 32 deletions(-) diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py index 6e7e797a7f..9619cc444f 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -15,6 +15,7 @@ from __future__ import absolute_import +import collections import time import mock from base64 import b64encode @@ -25,6 +26,7 @@ from st2tests.base import RunnerTestCase from winrm_runner.winrm_base import WinRmBaseRunner, WinRmRunnerTimoutError from winrm_runner.winrm_base import WINRM_TIMEOUT_EXIT_CODE +from winrm_runner.winrm_base import PS_ESCAPE_SEQUENCES from winrm_runner import winrm_ps_command_runner @@ -420,6 +422,7 @@ def test_translate_response_failure(self): 'return_code': 123, 'stdout': 'output1', 'stderr': 'error1'}, + None)) def test_translate_response_timeout(self): @@ -434,3 +437,143 @@ def test_translate_response_timeout(self): 'stdout': 'output1', 'stderr': 'error1'}, None)) + + def test_multireplace(self): + multireplace_map = {'a': 'x', + 'c': 'y', + 'aaa': 'z'} + result = self._runner._multireplace('aaaccaa', multireplace_map) + self.assertEquals(result, 'zyyxx') + + def test_multireplace_powershell(self): + param_str = ( + '\n' + '\r' + '\t' + '\a' + '\b' + '\f' + '\v' + '"' + '\'' + '`' + '\0' + '$' + ) + result = self._runner._multireplace(param_str, PS_ESCAPE_SEQUENCES) + self.assertEquals(result, ( + '`n' + '`r' + '`t' + '`a' + '`b' + '`f' + '`v' + '`"' + '`\'' + '``' + '`0' + '`$' + )) + + def test_param_to_ps_string(self): + # test ascii + param_str = 'StackStorm 1234' + result = self._runner._param_to_ps(param_str) + self.assertEquals(result, '"StackStorm 1234"') + + # test escaped + param_str = '\n\r\t' + result = self._runner._param_to_ps(param_str) + self.assertEquals(result, '"`n`r`t"') + + def test_param_to_ps_bool(self): + # test True + result = self._runner._param_to_ps(True) + self.assertEquals(result, '$true') + + # test False + result = self._runner._param_to_ps(False) + self.assertEquals(result, '$false') + + def test_param_to_ps_integer(self): + result = self._runner._param_to_ps(9876) + self.assertEquals(result, '9876') + + result = self._runner._param_to_ps(-765) + self.assertEquals(result, '-765') + + def test_param_to_ps_float(self): + result = self._runner._param_to_ps(98.76) + self.assertEquals(result, '98.76') + + result = self._runner._param_to_ps(-76.5) + self.assertEquals(result, '-76.5') + + def test_param_to_ps_list(self): + input_list = ['StackStorm Test String', + '`\0$', + True, + 99] + result = self._runner._param_to_ps(input_list) + self.assertEquals(result, '@("StackStorm Test String", "```0`$", $true, 99)') + + def test_param_to_ps_list_nested(self): + input_list = [['a'], ['b'], [['c']]] + result = self._runner._param_to_ps(input_list) + self.assertEquals(result, '@(@("a"), @("b"), @(@("c")))') + + def test_param_to_ps_dict(self): + input_list = collections.OrderedDict( + [('str key', 'Value String'), + ('esc str\n', '\b\f\v"'), + (False, True), + (11, 99), + (18.3, 12.34)]) + result = self._runner._param_to_ps(input_list) + expected_str = ( + '@{"str key" = "Value String"; ' + '"esc str`n" = "`b`f`v`\""; ' + '$false = $true; ' + '11 = 99; ' + '18.3 = 12.34}' + ) + self.assertEquals(result, expected_str) + + def test_param_to_ps_dict_nexted(self): + input_list = collections.OrderedDict( + [('a', {'deep_a': 'value'}), + ('b', {'deep_b': {'deep_deep_b': 'value'}})]) + result = self._runner._param_to_ps(input_list) + expected_str = ( + '@{"a" = @{"deep_a" = "value"}; ' + '"b" = @{"deep_b" = @{"deep_deep_b" = "value"}}}' + ) + self.assertEquals(result, expected_str) + + def test_param_to_ps_deep_nested_dict_outer(self): + #### + # dict as outer container + input_list = collections.OrderedDict( + [('a', [{'deep_a': 'value'}, + {'deep_b': ['a', 'b', 'c']}])]) + result = self._runner._param_to_ps(input_list) + expected_str = ( + '@{"a" = @(@{"deep_a" = "value"}, ' + '@{"deep_b" = @("a", "b", "c")})}' + ) + self.assertEquals(result, expected_str) + + def test_param_to_ps_deep_nested_list_outer(self): + #### + # list as outer container + input_list = [{'deep_a': 'value'}, + {'deep_b': ['a', 'b', 'c']}, + {'deep_c': [{'x': 'y'}]}] + result = self._runner._param_to_ps(input_list) + expected_str = ( + '@(@{"deep_a" = "value"}, ' + '@{"deep_b" = @("a", "b", "c")}, ' + '@{"deep_c" = @(@{"x" = "y"})})' + ) + self.assertEquals(result, expected_str) diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index f8f4c87c3a..d36b86e29c 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -196,14 +196,6 @@ def _winrm_run_ps(self, session, script, env=None, cwd=None): rs.std_err = session._clean_error_msg(rs.std_err) return rs - def _run_ps(self, powershell): - # connect - session = self._create_session() - # execute - response = self._winrm_run_ps(session, powershell, env=self._env, cwd=self._cwd) - # create triplet from WinRM response - return self._translate_response(response) - def _translate_response(self, response): # check exit status for errors succeeded = (response.status_code == exit_code_constants.SUCCESS_EXIT_CODE) @@ -228,14 +220,34 @@ def _translate_response(self, response): # objects so they can be used natively return (status, jsonify.json_loads(result, RESULT_KEYS_TO_TRANSFORM), None) - def transform_params_to_ps(self, positional_args, named_args): - for i, arg in enumerate(positional_args): - positional_args[i] = self._param_to_ps(arg) + def _run_ps(self, powershell): + # connect + session = self._create_session() + # execute + response = self._winrm_run_ps(session, powershell, env=self._env, cwd=self._cwd) + # create triplet from WinRM response + return self._translate_response(response) - for key, value in six.iteritems(named_args): - named_args[key] = self._param_to_ps(value) + def _multireplace(self, string, replacements): + """ + Given a string and a replacement map, it returns the replaced string. + Source = https://gist.github.com/bgusach/a967e0587d6e01e889fd1d776c5f3729 + Reference = https://stackoverflow.com/questions/6116978/how-to-replace-multiple-substrings-of-a-string # noqa + :param str string: string to execute replacements on + :param dict replacements: replacement dictionary {value to find: value to replace} + :rtype: str + """ + # Place longer ones first to keep shorter substrings from matching where + # the longer ones should take place + # For instance given the replacements {'ab': 'AB', 'abc': 'ABC'} against + # the string 'hey abc', it should produce 'hey ABC' and not 'hey ABc' + substrs = sorted(replacements, key=len, reverse=True) - return positional_args, named_args + # Create a big OR regex that matches any of the substrings to replace + regexp = re.compile('|'.join([re.escape(s) for s in substrs])) + + # For each match, look up the new string in the replacements + return regexp.sub(lambda match: replacements[match.group(0)], string) def _param_to_ps(self, param): ps_str = "" @@ -256,26 +268,14 @@ def _param_to_ps(self, param): ps_str = str(param) return ps_str - def _multireplace(self, string, replacements): - """ - Given a string and a replacement map, it returns the replaced string. - Source = https://gist.github.com/bgusach/a967e0587d6e01e889fd1d776c5f3729 - Reference = https://stackoverflow.com/questions/6116978/how-to-replace-multiple-substrings-of-a-string # noqa - :param str string: string to execute replacements on - :param dict replacements: replacement dictionary {value to find: value to replace} - :rtype: str - """ - # Place longer ones first to keep shorter substrings from matching where - # the longer ones should take place - # For instance given the replacements {'ab': 'AB', 'abc': 'ABC'} against - # the string 'hey abc', it should produce 'hey ABC' and not 'hey ABc' - substrs = sorted(replacements, key=len, reverse=True) + def transform_params_to_ps(self, positional_args, named_args): + for i, arg in enumerate(positional_args): + positional_args[i] = self._param_to_ps(arg) - # Create a big OR regex that matches any of the substrings to replace - regexp = re.compile('|'.join([re.escape(s) for s in substrs])) + for key, value in six.iteritems(named_args): + named_args[key] = self._param_to_ps(value) - # For each match, look up the new string in the replacements - return regexp.sub(lambda match: replacements[match.group(0)], string) + return positional_args, named_args def create_ps_params_string(self, positional_args, named_args): ps_params_str = "" From d466e14bb660a0cde9724c7d199a58c1ca476d98 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Mon, 11 Jun 2018 20:15:46 -0400 Subject: [PATCH 10/31] Work in WinRM unit tests --- .../tests/unit/fixtures/TestScript.ps1 | 23 +++++ .../tests/unit/test_winrm_base.py | 50 +++++++++-- .../unit/test_winrm_ps_command_runner.py | 45 ++++++++++ .../tests/unit/test_winrm_ps_script_runner.py | 84 +++++++++++++++++++ .../winrm_runner/winrm_runner/winrm_base.py | 6 +- .../winrm_runner/winrm_ps_script_runner.py | 3 - 6 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 contrib/runners/winrm_runner/tests/unit/fixtures/TestScript.ps1 create mode 100644 contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py create mode 100644 contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py diff --git a/contrib/runners/winrm_runner/tests/unit/fixtures/TestScript.ps1 b/contrib/runners/winrm_runner/tests/unit/fixtures/TestScript.ps1 new file mode 100644 index 0000000000..ed8bc4bad8 --- /dev/null +++ b/contrib/runners/winrm_runner/tests/unit/fixtures/TestScript.ps1 @@ -0,0 +1,23 @@ +[CmdletBinding()] +Param( + [bool]$p_bool, + [int]$p_integer, + [double]$p_number, + [string]$p_str, + [array]$p_array, + [hashtable]$p_obj, + [Parameter(Position=0)] + [string]$p_pos0, + [Parameter(Position=1)] + [string]$p_pos1 +) + + +Write-Output "p_bool = $p_bool" +Write-Output "p_integer = $p_integer" +Write-Output "p_number = $p_number" +Write-Output "p_str = $p_str" +Write-Output "p_array = $($p_array | ConvertTo-Json -Compress)" +Write-Output "p_obj = $($p_obj | ConvertTo-Json -Compress)" +Write-Output "p_pos0 = $p_pos0" +Write-Output "p_pos1 = $p_pos1" diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py index 9619cc444f..07cde49762 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -212,12 +212,12 @@ def test_get_command_output(self): ] def test_get_command_output_timeout(self): - self._runner._timeout = 1 + self._runner._timeout = 0.1 mock_protocol = mock.MagicMock() def sleep_for_timeout(*args, **kwargs): - time.sleep(2) + time.sleep(0.2) return ('output1', 'error1', 123, False) mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout @@ -232,12 +232,12 @@ def sleep_for_timeout(*args, **kwargs): mock_protocol._raw_get_command_output.assert_called_with(567, 890) def test_get_command_output_operation_timeout(self): - self._runner._timeout = 1 + self._runner._timeout = 0.1 mock_protocol = mock.MagicMock() def sleep_for_timeout_then_raise(*args, **kwargs): - time.sleep(2) + time.sleep(0.2) raise WinRMOperationTimeoutError() mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise @@ -379,16 +379,16 @@ def test_run_ps_failed(self, mock_protocol_init): @mock.patch('winrm.Protocol') def test_run_ps_timeout(self, mock_protocol_init): mock_protocol = mock.MagicMock() + self._init_runner() + self._runner._timeout = 0.1 def sleep_for_timeout_then_raise(*args, **kwargs): - time.sleep(2) + time.sleep(0.2) return ('output1', 'error1', 123, False) mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise mock_protocol_init.return_value = mock_protocol - self._init_runner() - self._runner._timeout = 1 result = self._runner._run_ps("Get-Location") self.assertEquals(result, ('timeout', {'failed': True, @@ -554,10 +554,10 @@ def test_param_to_ps_dict_nexted(self): def test_param_to_ps_deep_nested_dict_outer(self): #### # dict as outer container - input_list = collections.OrderedDict( + input_dict = collections.OrderedDict( [('a', [{'deep_a': 'value'}, {'deep_b': ['a', 'b', 'c']}])]) - result = self._runner._param_to_ps(input_list) + result = self._runner._param_to_ps(input_dict) expected_str = ( '@{"a" = @(@{"deep_a" = "value"}, ' '@{"deep_b" = @("a", "b", "c")})}' @@ -577,3 +577,35 @@ def test_param_to_ps_deep_nested_list_outer(self): '@{"deep_c" = @(@{"x" = "y"})})' ) self.assertEquals(result, expected_str) + + def test_transform_params_to_ps(self): + positional_args = [1, 'a', '\n'] + named_args = collections.OrderedDict( + [('a', 'value1'), + ('b', True), + ('c', ['x', 'y']), + ('d', {'z': 'w'})] + ) + + result_pos, result_named = self._runner._transform_params_to_ps(positional_args, + named_args) + self.assertEquals(result_pos, ['1', '"a"', '"`n"']) + self.assertEquals(result_named, collections.OrderedDict([ + ('a', '"value1"'), + ('b', '$true'), + ('c', '@("x", "y")'), + ('d', '@{"z" = "w"}')])) + + def test_create_ps_params_string(self): + positional_args = [1, 'a', '\n'] + named_args = collections.OrderedDict( + [('-a', 'value1'), + ('-b', True), + ('-c', ['x', 'y']), + ('-d', {'z': 'w'})] + ) + + result = self._runner.create_ps_params_string(positional_args, named_args) + + self.assertEquals(result, + '-a "value1" -b $true -c @("x", "y") -d @{"z" = "w"} 1 "a" "`n"') diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py new file mode 100644 index 0000000000..c3417128f0 --- /dev/null +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py @@ -0,0 +1,45 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the 'License'); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import mock +from st2common.runners.base import ActionRunner +from st2tests.base import RunnerTestCase +from winrm_runner import winrm_ps_command_runner +from winrm_runner.winrm_base import WinRmBaseRunner + + +class WinRmPsCommandRunnerTestCase(RunnerTestCase): + + def setUp(self): + super(WinRmPsCommandRunnerTestCase, self).setUpClass() + self._runner = winrm_ps_command_runner.get_runner() + + def test_init(self): + runner = winrm_ps_command_runner.WinRmPsCommandRunner('abcdef') + self.assertIsInstance(runner, WinRmBaseRunner) + self.assertIsInstance(runner, ActionRunner) + self.assertEquals(runner.runner_id, 'abcdef') + + @mock.patch('winrm_runner.winrm_ps_command_runner.WinRmPsCommandRunner._run_ps') + def test_run(self, mock_run_ps): + mock_run_ps.return_value = 'expected' + + self._runner.runner_parameters = {'cmd': 'Get-ADUser stanley'} + result = self._runner.run({}) + + self.assertEquals(result, 'expected') + mock_run_ps.assert_called_with('Get-ADUser stanley') diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py new file mode 100644 index 0000000000..aaf8cd368e --- /dev/null +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py @@ -0,0 +1,84 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the 'License'); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import collections +import mock +import os.path +from st2common.runners.base import ActionRunner +from st2tests.base import RunnerTestCase +from winrm_runner import winrm_ps_script_runner +from winrm_runner.winrm_base import WinRmBaseRunner + +FIXTURES_PATH = os.path.join(os.path.dirname(__file__), 'fixtures') + +POWERSHELL_SCRIPT_PATH = os.path.join(FIXTURES_PATH, "TestScript.ps1") + + +class WinRmPsScriptRunnerTestCase(RunnerTestCase): + + def setUp(self): + super(WinRmPsScriptRunnerTestCase, self).setUpClass() + self._runner = winrm_ps_script_runner.get_runner() + + def test_init(self): + runner = winrm_ps_script_runner.WinRmPsScriptRunner('abcdef') + self.assertIsInstance(runner, WinRmBaseRunner) + self.assertIsInstance(runner, ActionRunner) + self.assertEquals(runner.runner_id, 'abcdef') + + @mock.patch('winrm_runner.winrm_ps_script_runner.WinRmPsScriptRunner._get_script_args') + @mock.patch('winrm_runner.winrm_ps_script_runner.WinRmPsScriptRunner._run_ps') + def test_run(self, mock_run_ps, mock_get_script_args): + mock_run_ps.return_value = 'expected' + pos_args = [1, 'a'] + named_args = collections.OrderedDict([ + ("a", "z"), + ("b", True), + ("c", ["\r", "\n"]), + ("d", {"test": "value"})]) + mock_get_script_args.return_value = (pos_args, named_args) + + self._runner.entry_point = POWERSHELL_SCRIPT_PATH + self._runner.runner_parameters = {} + self._runner.kwarg_op = '-' + + result = self._runner.run({}) + + self.assertEquals(result, 'expected') + mock_run_ps.assert_called_with('''& {[CmdletBinding()] +Param( + [bool]$p_bool, + [int]$p_integer, + [double]$p_number, + [string]$p_str, + [array]$p_array, + [hashtable]$p_obj, + [Parameter(Position=0)] + [string]$p_pos0, + [Parameter(Position=1)] + [string]$p_pos1 +) + + +Write-Output "p_bool = $p_bool" +Write-Output "p_integer = $p_integer" +Write-Output "p_number = $p_number" +Write-Output "p_str = $p_str" +Write-Output "p_array = $($p_array | ConvertTo-Json -Compress)" +Write-Output "p_obj = $($p_obj | ConvertTo-Json -Compress)" +Write-Output "p_pos0 = $p_pos0" +Write-Output "p_pos1 = $p_pos1"} -a "z" -b $true -c @("`r", "`n") -d @{"test" = "value}''') diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index d36b86e29c..2297d1fcdf 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -268,7 +268,7 @@ def _param_to_ps(self, param): ps_str = str(param) return ps_str - def transform_params_to_ps(self, positional_args, named_args): + def _transform_params_to_ps(self, positional_args, named_args): for i, arg in enumerate(positional_args): positional_args[i] = self._param_to_ps(arg) @@ -278,6 +278,10 @@ def transform_params_to_ps(self, positional_args, named_args): return positional_args, named_args def create_ps_params_string(self, positional_args, named_args): + # convert the script parameters into powershell strings + positional_args, named_args = self._transform_params_to_ps(positional_args, + named_args) + # concatenate them into a long string ps_params_str = "" ps_params_str += " " .join([(k + " " + v) for k, v in six.iteritems(named_args)]) ps_params_str += " " diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py index 398288f8f1..9cd342acaf 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py @@ -44,9 +44,6 @@ def run(self, action_parameters): positional_args, named_args = self._get_script_args(action_parameters) named_args = self._transform_named_args(named_args) - # convert the script parameters into powershell strings - positional_args, named_args = self.transform_params_to_ps(positional_args, - named_args) # build a string from all of the named and positional arguments # this will be our full parameter list when executing the script ps_params = self.create_ps_params_string(positional_args, named_args) From ac730ec0c7ba0ea41333d599cc3ca625f5d0a681 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 12 Jun 2018 07:32:21 -0400 Subject: [PATCH 11/31] Fixed script winrm runner unit test --- .../tests/unit/test_winrm_ps_script_runner.py | 13 +++++-------- .../winrm_runner/winrm_ps_script_runner.py | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py index aaf8cd368e..b012cdaeda 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py @@ -44,17 +44,13 @@ def test_init(self): @mock.patch('winrm_runner.winrm_ps_script_runner.WinRmPsScriptRunner._run_ps') def test_run(self, mock_run_ps, mock_get_script_args): mock_run_ps.return_value = 'expected' - pos_args = [1, 'a'] - named_args = collections.OrderedDict([ - ("a", "z"), - ("b", True), - ("c", ["\r", "\n"]), - ("d", {"test": "value"})]) + pos_args = [1, 'abc'] + named_args = {"d": {"test": ["\r", True, 3]}} mock_get_script_args.return_value = (pos_args, named_args) self._runner.entry_point = POWERSHELL_SCRIPT_PATH self._runner.runner_parameters = {} - self._runner.kwarg_op = '-' + self._runner._kwarg_op = '-' result = self._runner.run({}) @@ -81,4 +77,5 @@ def test_run(self, mock_run_ps, mock_get_script_args): Write-Output "p_array = $($p_array | ConvertTo-Json -Compress)" Write-Output "p_obj = $($p_obj | ConvertTo-Json -Compress)" Write-Output "p_pos0 = $p_pos0" -Write-Output "p_pos1 = $p_pos1"} -a "z" -b $true -c @("`r", "`n") -d @{"test" = "value}''') +Write-Output "p_pos1 = $p_pos1" +} -d @{"test" = @("`r", $true, 3)} 1 "abc"''') diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py index 9cd342acaf..3c865b2e3b 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py @@ -51,7 +51,7 @@ def run(self, action_parameters): # the following wraps the script (from the file) in a script block ( {} ) # executes it, passing in the parameters built above # https://docs.microsoft.com/en-us/powershell/scripting/core-powershell/console/powershell.exe-command-line-help - ps_script_and_params = "& { %s } %s" % (ps_script, ps_params) + ps_script_and_params = "& {%s} %s" % (ps_script, ps_params) # execute return self._run_ps(ps_script_and_params) From b21823b758db88c7fac79044b04e9eed0d9e7b06 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 12 Jun 2018 08:05:04 -0400 Subject: [PATCH 12/31] Added new winrm-cmd runner and new actions core.winrm_cmd and winrm_ps_cmd --- CHANGELOG.rst | 15 ++++ contrib/core/actions/winrm_cmd.yaml | 12 ++++ contrib/core/actions/winrm_ps_cmd.yaml | 12 ++++ .../tests/unit/test_winrm_base.py | 62 ++++++++++++++++ .../tests/unit/test_winrm_ps_script_runner.py | 1 - .../winrm_runner/winrm_runner/runner.yaml | 72 ++++++++++++++++++- .../winrm_runner/winrm_runner/winrm_base.py | 8 +++ .../winrm_runner/winrm_command_runner.py | 48 +++++++++++++ 8 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 contrib/core/actions/winrm_cmd.yaml create mode 100644 contrib/core/actions/winrm_ps_cmd.yaml create mode 100644 contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0217fc8473..21cbecc9ce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,6 +36,15 @@ Added pack in question needs to support Python 3. Note 2: This feature is experimental and opt-in. (new feature) #4016 #3922 #4149 +* Added new runners: ``winrm-cmd``, ``winrm-ps-cmd`` and ``winrm-ps-script``. + The ``winrm-cmd`` runner executes Command Prompt commands remotely on Windows hosts using + the WinRM protocol. The ``winrm-ps-cmd`` and ``winrm-ps-script`` runners execute PowerShell + commands and scripts on remote Windows hosts using the WinRM protocol. + To accompany these new runners, there are two new actions ``core.winrm_cmd`` that + executes remote Command Prompt commands along with ``core.winrm_ps_cmd`` that + executes remote PowerShell commands. (new feature) #1636 + + Contributed by Nick Maludy (Encore Technologies). Changed ~~~~~~~ @@ -53,6 +62,12 @@ Changed to function correctly in some scenarios is used. (improvement) #4127 #4120 * Upgrade various internal Python library dependencies to the latest stable versions (kombu, amqp, gitpython, pytz, semver, oslo.utils). (improvement) #4162 +* Migrated runners to using the ``in-requirements.txt`` pattern for "components" in the build + system, so the ``Makefile`` correctly generates and installs runner dependencies during + testing and packaging. (improvement) (bugfix) #4169 + + Contributed by Nick Maludy (Encore Technologies). + Fixed ~~~~~ diff --git a/contrib/core/actions/winrm_cmd.yaml b/contrib/core/actions/winrm_cmd.yaml new file mode 100644 index 0000000000..11cf2c6ee5 --- /dev/null +++ b/contrib/core/actions/winrm_cmd.yaml @@ -0,0 +1,12 @@ + +--- + name: "winrm_cmd" + runner_type: "winrm-cmd" + description: "Action to execute arbitrary Windows Command Prompt command remotely via WinRM." + enabled: true + entry_point: "" + parameters: + cmd: + description: "Arbitrary Windows Command Prompt command to be executed on the remote host." + type: "string" + required: true diff --git a/contrib/core/actions/winrm_ps_cmd.yaml b/contrib/core/actions/winrm_ps_cmd.yaml new file mode 100644 index 0000000000..8415aec1fe --- /dev/null +++ b/contrib/core/actions/winrm_ps_cmd.yaml @@ -0,0 +1,12 @@ + +--- + name: "winrm_ps_cmd" + runner_type: "winrm-ps-cmd" + description: "Action to execute arbitrary Windows PowerShell command remotely via WinRM." + enabled: true + entry_point: "" + parameters: + cmd: + description: "Arbitrary Windows PowerShell command to be executed on the remote host." + type: "string" + required: true diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py index 07cde49762..ef32e17c72 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -336,6 +336,68 @@ def test_winrm_run_ps_clean_stderr(self, mock_run_cmd): cwd='C:\\st2') mock_session._clean_error_msg.assert_called_with('error') + @mock.patch('winrm.Protocol') + def test_run_cmd(self, mock_protocol_init): + mock_protocol = mock.MagicMock() + mock_protocol._raw_get_command_output.side_effect = [ + ('output1', 'error1', 0, False), + ('output2', 'error2', 0, False), + ('output3', 'error3', 0, True) + ] + mock_protocol_init.return_value = mock_protocol + + self._init_runner() + result = self._runner._run_cmd("ipconfig /all") + self.assertEquals(result, ('succeeded', + {'failed': False, + 'succeeded': True, + 'return_code': 0, + 'stdout': 'output1output2output3', + 'stderr': 'error1error2error3'}, + None)) + + @mock.patch('winrm.Protocol') + def test_run_cmd_failed(self, mock_protocol_init): + mock_protocol = mock.MagicMock() + mock_protocol._raw_get_command_output.side_effect = [ + ('output1', 'error1', 0, False), + ('output2', 'error2', 0, False), + ('output3', 'error3', 1, True) + ] + mock_protocol_init.return_value = mock_protocol + + self._init_runner() + result = self._runner._run_cmd("ipconfig /all") + self.assertEquals(result, ('failed', + {'failed': True, + 'succeeded': False, + 'return_code': 1, + 'stdout': 'output1output2output3', + 'stderr': 'error1error2error3'}, + None)) + + @mock.patch('winrm.Protocol') + def test_run_cmd_timeout(self, mock_protocol_init): + mock_protocol = mock.MagicMock() + self._init_runner() + self._runner._timeout = 0.1 + + def sleep_for_timeout_then_raise(*args, **kwargs): + time.sleep(0.2) + return ('output1', 'error1', 123, False) + + mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise + mock_protocol_init.return_value = mock_protocol + + result = self._runner._run_cmd("ipconfig /all") + self.assertEquals(result, ('timeout', + {'failed': True, + 'succeeded': False, + 'return_code': -1, + 'stdout': 'output1', + 'stderr': 'error1'}, + None)) + @mock.patch('winrm.Protocol') def test_run_ps(self, mock_protocol_init): mock_protocol = mock.MagicMock() diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py index b012cdaeda..25259d1df1 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py @@ -15,7 +15,6 @@ from __future__ import absolute_import -import collections import mock import os.path from st2common.runners.base import ActionRunner diff --git a/contrib/runners/winrm_runner/winrm_runner/runner.yaml b/contrib/runners/winrm_runner/winrm_runner/runner.yaml index 8883724644..88a938c37f 100644 --- a/contrib/runners/winrm_runner/winrm_runner/runner.yaml +++ b/contrib/runners/winrm_runner/winrm_runner/runner.yaml @@ -1,11 +1,79 @@ -- description: A remote execution runner that executes PowerShell commands via WinRM on a set of remote hosts +- description: A remote execution runner that executes a Command Prompt commands via WinRM on a remote host + enabled: true + name: winrm-cmd + runner_package: winrm_runner + runner_module: winrm_command_runner + runner_parameters: + cmd: + description: Arbitrary Command Prompt command to be executed on the remote host. + type: string + cwd: + description: Working directory where the command will be executed in + type: string + env: + description: Environment variables which will be available to the command (e.g. + key1=val1,key2=val2) + type: object + host: + description: A host where the command will be run + required: true + type: string + kwarg_op: + default: "-" + description: Operator to use in front of keyword args i.e. "-" or "/". + type: string + password: + description: Password used to log in. + required: true + secret: true + type: string + port: + default: 5986 + description: 'WinRM port to connect on. If using port 5985 scheme must be "http"' + required: false + type: integer + scheme: + default: "https" + description: 'Scheme to use in the WinRM URL. If using scheme "http" port must be 5985' + required: false + type: string + timeout: + default: 60 + description: Action timeout in seconds. Action will get killed if it doesn't + finish in timeout seconds. + type: integer + transport: + default: "ntlm" + description: The type of transport that WinRM will use to communicate. + See https://github.com/diyan/pywinrm#valid-transport-options + required: false + type: string + enum: + - "basic" + - "certificate" + - "credssp" + - "kerberos" + - "ntlm" + - "plaintext" + - "ssl" + username: + description: Username used to log-in. + required: true + type: string + verify_ssl_cert: + default: true + description: Certificate for HTTPS request is verified by default using requests + CA bundle which comes from Mozilla. Verification using a custom CA bundle + is not yet supported. Set to False to skip verification. + type: boolean +- description: A remote execution runner that executes PowerShell commands via WinRM on a remote host enabled: true name: winrm-ps-cmd runner_package: winrm_runner runner_module: winrm_ps_command_runner runner_parameters: cmd: - description: Arbitrary PowerShell command to be executed on the remote host(s). + description: Arbitrary PowerShell command to be executed on the remote host. type: string cwd: description: Working directory where the command will be executed in diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index 2297d1fcdf..3405810690 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -220,6 +220,14 @@ def _translate_response(self, response): # objects so they can be used natively return (status, jsonify.json_loads(result, RESULT_KEYS_TO_TRANSFORM), None) + def _run_cmd(self, cmd): + # connect + session = self._create_session() + # execute + response = self._winrm_run_cmd(session, cmd, env=self._env, cwd=self._cwd) + # create triplet from WinRM response + return self._translate_response(response) + def _run_ps(self, powershell): # connect session = self._create_session() diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py new file mode 100644 index 0000000000..db6e32a8e8 --- /dev/null +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py @@ -0,0 +1,48 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +import uuid + +from st2common import log as logging +from st2common.runners.base import get_metadata as get_runner_metadata +from winrm_runner.winrm_base import WinRmBaseRunner + +__all__ = [ + 'WinRmCommandRunner', + 'get_runner', + 'get_metadata' +] + +LOG = logging.getLogger(__name__) + +RUNNER_COMMAND = 'cmd' + + +class WinRmCommandRunner(WinRmBaseRunner): + + def run(self, action_parameters): + cmd_command = self.runner_parameters[RUNNER_COMMAND] + + # execute + return self._run_cmd(cmd_command) + + +def get_runner(): + return WinRmCommandRunner(str(uuid.uuid4())) + + +def get_metadata(): + return get_runner_metadata('winrm_command_runner') From 2b1fe5f367e29d3cea6b1e76f73fef6387fce2fd Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 12 Jun 2018 08:07:01 -0400 Subject: [PATCH 13/31] Added unit tests for winrm_command_runner --- .../tests/unit/test_winrm_command_runner.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py new file mode 100644 index 0000000000..447674de67 --- /dev/null +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py @@ -0,0 +1,45 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the 'License'); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import mock +from st2common.runners.base import ActionRunner +from st2tests.base import RunnerTestCase +from winrm_runner import winrm_command_runner +from winrm_runner.winrm_base import WinRmBaseRunner + + +class WinRmCommandRunnerTestCase(RunnerTestCase): + + def setUp(self): + super(WinRmCommandRunnerTestCase, self).setUpClass() + self._runner = winrm_command_runner.get_runner() + + def test_init(self): + runner = winrm_command_runner.WinRmCommandRunner('abcdef') + self.assertIsInstance(runner, WinRmBaseRunner) + self.assertIsInstance(runner, ActionRunner) + self.assertEquals(runner.runner_id, 'abcdef') + + @mock.patch('winrm_runner.winrm_command_runner.WinRmCommandRunner._run_cmd') + def test_run(self, mock_run_cmd): + mock_run_cmd.return_value = 'expected' + + self._runner.runner_parameters = {'cmd': 'ipconfig /all'} + result = self._runner.run({}) + + self.assertEquals(result, 'expected') + mock_run_cmd.assert_called_with('ipconfig /all') From e7dfc75efc81be20f2be7e75ca552c21ae07e2b1 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 12 Jun 2018 08:30:02 -0400 Subject: [PATCH 14/31] Added winrm-cmd to winrm runner entry points --- contrib/runners/winrm_runner/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/runners/winrm_runner/setup.py b/contrib/runners/winrm_runner/setup.py index 30542a3d05..133ce5421c 100644 --- a/contrib/runners/winrm_runner/setup.py +++ b/contrib/runners/winrm_runner/setup.py @@ -48,6 +48,7 @@ scripts=[], entry_points={ 'st2common.runners.runner': [ + 'winrm-cmd = winrm_runner.winrm_command_runner', 'winrm-ps-cmd = winrm_runner.winrm_ps_command_runner', 'winrm-ps-script = winrm_runner.winrm_ps_script_runner', ], From 11e6328ff34264918a2a38a4021b4be6967d0675 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 12 Jun 2018 10:05:32 -0400 Subject: [PATCH 15/31] Added winrm runner to tox --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6a3def8e9e..e40e79443f 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,7 @@ commands = [testenv:py36] basepython = python3.6 -setenv = PYTHONPATH = {toxinidir}/external:{toxinidir}/st2common:{toxinidir}/st2api:{toxinidir}/st2actions:{toxinidir}/st2reactor:{toxinidir}/st2tests:{toxinidir}/contrib/runners/action_chain_runner:{toxinidir}/contrib/runners/local_runner:{toxinidir}/contrib/runners/windows_runner:{toxinidir}/contrib/runners/python_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/noop_runner:{toxinidir}/contrib/runners/announcement_runner:{toxinidir}/contrib/runners/remote_script_runner:{toxinidir}/contrib/runners/remote_command_runner:{toxinidir}/contrib/runners/mistral_v2:{toxinidir}/contrib/runners/inquirer_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/cloudslang_runner +setenv = PYTHONPATH = {toxinidir}/external:{toxinidir}/st2common:{toxinidir}/st2api:{toxinidir}/st2actions:{toxinidir}/st2reactor:{toxinidir}/st2tests:{toxinidir}/contrib/runners/action_chain_runner:{toxinidir}/contrib/runners/local_runner:{toxinidir}/contrib/runners/windows_runner:{toxinidir}/contrib/runners/python_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/noop_runner:{toxinidir}/contrib/runners/announcement_runner:{toxinidir}/contrib/runners/remote_script_runner:{toxinidir}/contrib/runners/remote_command_runner:{toxinidir}/contrib/runners/mistral_v2:{toxinidir}/contrib/runners/inquirer_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/cloudslang_runner:{toxinidir}/contrib/runners/winrm_runner VIRTUALENV_DIR = {envdir} install_command = pip install -U --force-reinstall {opts} {packages} deps = virtualenv @@ -52,6 +52,7 @@ commands = nosetests --with-timer --rednose -sv contrib/runners/mistral_v2/tests/unit/ contrib/runners/mistral_v2/tests/integration/ nosetests --with-timer --rednose -sv contrib/runners/python_runner/tests/unit/ contrib/runners/python_runner/tests/integration/ nosetests --with-timer --rednose -sv contrib/runners/windows_runner/tests/unit/ + nosetests --with-timer --rednose -sv contrib/runners/winrm_runner/tests/unit/ [testenv:venv] commands = {posargs} From 1420e887da2374a6e51e45111ca0ccf3f3c90cc4 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 12 Jun 2018 10:17:00 -0400 Subject: [PATCH 16/31] Changed winrm_runner/runner.yaml symlink to include ./ --- contrib/runners/winrm_runner/runner.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/runners/winrm_runner/runner.yaml b/contrib/runners/winrm_runner/runner.yaml index 1fa11a1342..45f4fed513 120000 --- a/contrib/runners/winrm_runner/runner.yaml +++ b/contrib/runners/winrm_runner/runner.yaml @@ -1 +1 @@ -winrm_runner/runner.yaml \ No newline at end of file +./winrm_runner/runner.yaml \ No newline at end of file From 920c1be508422199249ede204f7ba64dca68cb66 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 12 Jun 2018 10:32:34 -0400 Subject: [PATCH 17/31] Added winrm_runner symlink to st2tests --- st2tests/st2tests/fixtures/packs/runners/winrm_runner | 1 + 1 file changed, 1 insertion(+) create mode 120000 st2tests/st2tests/fixtures/packs/runners/winrm_runner diff --git a/st2tests/st2tests/fixtures/packs/runners/winrm_runner b/st2tests/st2tests/fixtures/packs/runners/winrm_runner new file mode 120000 index 0000000000..5bb8e19cfe --- /dev/null +++ b/st2tests/st2tests/fixtures/packs/runners/winrm_runner @@ -0,0 +1 @@ +../../../../../contrib/runners/winrm_runner \ No newline at end of file From d11ba9f43574307ed2280f55d0800a1724c7d253 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 12 Jun 2018 10:47:09 -0400 Subject: [PATCH 18/31] Changed hard coded number of runners in st2api tests --- st2api/tests/unit/controllers/v1/test_packs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/st2api/tests/unit/controllers/v1/test_packs.py b/st2api/tests/unit/controllers/v1/test_packs.py index 30a319f036..939cadfd4d 100644 --- a/st2api/tests/unit/controllers/v1/test_packs.py +++ b/st2api/tests/unit/controllers/v1/test_packs.py @@ -516,14 +516,14 @@ def test_packs_register_endpoint(self, mock_get_packs): {'packs': ['dummy_pack_1'], 'types': ['action']}) self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.json, {'actions': 1, 'runners': 14}) + self.assertEqual(resp.json, {'actions': 1, 'runners': 17}) # Verify that plural name form also works resp = self.app.post_json('/v1/packs/register', {'packs': ['dummy_pack_1'], 'types': ['actions']}) self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.json, {'actions': 1, 'runners': 14}) + self.assertEqual(resp.json, {'actions': 1, 'runners': 17}) # Register single resource from a single pack specified multiple times - verify that # resources from the same pack are only registered once @@ -533,7 +533,7 @@ def test_packs_register_endpoint(self, mock_get_packs): 'fail_on_failure': False}) self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.json, {'actions': 1, 'runners': 14}) + self.assertEqual(resp.json, {'actions': 1, 'runners': 17}) # Register resources from a single (non-existent pack) resp = self.app.post_json('/v1/packs/register', {'packs': ['doesntexist']}, From 409fd6fe205cd7fdc35a6614761aeba5d938523d Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Tue, 12 Jun 2018 14:59:11 -0400 Subject: [PATCH 19/31] Fixed python3 byte/unicode problem --- .../tests/unit/test_winrm_base.py | 68 +++++++++---------- .../winrm_runner/winrm_runner/winrm_base.py | 4 ++ 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py index ef32e17c72..64534c9941 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -197,14 +197,14 @@ def test_get_command_output(self): self._runner._timeout = 0 mock_protocol = mock.MagicMock() mock_protocol._raw_get_command_output.side_effect = [ - ('output1', 'error1', 123, False), - ('output2', 'error2', 456, False), - ('output3', 'error3', 789, True) + (b'output1', b'error1', 123, False), + (b'output2', b'error2', 456, False), + (b'output3', b'error3', 789, True) ] result = self._runner._winrm_get_command_output(mock_protocol, 567, 890) - self.assertEquals(result, ('output1output2output3', 'error1error2error3', 789)) + self.assertEquals(result, (b'output1output2output3', b'error1error2error3', 789)) mock_protocol._raw_get_command_output.assert_has_calls = [ mock.call(567, 890), mock.call(567, 890), @@ -218,7 +218,7 @@ def test_get_command_output_timeout(self): def sleep_for_timeout(*args, **kwargs): time.sleep(0.2) - return ('output1', 'error1', 123, False) + return (b'output1', b'error1', 123, False) mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout @@ -226,8 +226,8 @@ def sleep_for_timeout(*args, **kwargs): self._runner._winrm_get_command_output(mock_protocol, 567, 890) timeout_exception = cm.exception - self.assertEqual(timeout_exception.response.std_out, 'output1') - self.assertEqual(timeout_exception.response.std_err, 'error1') + self.assertEqual(timeout_exception.response.std_out, b'output1') + self.assertEqual(timeout_exception.response.std_err, b'error1') self.assertEqual(timeout_exception.response.status_code, WINRM_TIMEOUT_EXIT_CODE) mock_protocol._raw_get_command_output.assert_called_with(567, 890) @@ -246,8 +246,8 @@ def sleep_for_timeout_then_raise(*args, **kwargs): self._runner._winrm_get_command_output(mock_protocol, 567, 890) timeout_exception = cm.exception - self.assertEqual(timeout_exception.response.std_out, '') - self.assertEqual(timeout_exception.response.std_err, '') + self.assertEqual(timeout_exception.response.std_out, b'') + self.assertEqual(timeout_exception.response.std_err, b'') self.assertEqual(timeout_exception.response.status_code, WINRM_TIMEOUT_EXIT_CODE) mock_protocol._raw_get_command_output.assert_called_with(567, 890) @@ -255,7 +255,7 @@ def test_winrm_run_cmd(self): mock_protocol = mock.MagicMock() mock_protocol.open_shell.return_value = 123 mock_protocol.run_command.return_value = 456 - mock_protocol._raw_get_command_output.return_value = ('output', 'error', 9, True) + mock_protocol._raw_get_command_output.return_value = (b'output', b'error', 9, True) mock_session = mock.MagicMock(protocol=mock_protocol) self._init_runner() @@ -263,7 +263,7 @@ def test_winrm_run_cmd(self): args=['arg1', 'arg2'], env={'PATH': 'C:\\st2\\bin'}, cwd='C:\\st2') - expected_response = Response(('output', 'error', 9)) + expected_response = Response((b'output', b'error', 9)) expected_response.timeout = False self.assertEquals(result.__dict__, expected_response.__dict__) @@ -340,9 +340,9 @@ def test_winrm_run_ps_clean_stderr(self, mock_run_cmd): def test_run_cmd(self, mock_protocol_init): mock_protocol = mock.MagicMock() mock_protocol._raw_get_command_output.side_effect = [ - ('output1', 'error1', 0, False), - ('output2', 'error2', 0, False), - ('output3', 'error3', 0, True) + (b'output1', b'error1', 0, False), + (b'output2', b'error2', 0, False), + (b'output3', b'error3', 0, True) ] mock_protocol_init.return_value = mock_protocol @@ -352,17 +352,17 @@ def test_run_cmd(self, mock_protocol_init): {'failed': False, 'succeeded': True, 'return_code': 0, - 'stdout': 'output1output2output3', - 'stderr': 'error1error2error3'}, + 'stdout': b'output1output2output3', + 'stderr': b'error1error2error3'}, None)) @mock.patch('winrm.Protocol') def test_run_cmd_failed(self, mock_protocol_init): mock_protocol = mock.MagicMock() mock_protocol._raw_get_command_output.side_effect = [ - ('output1', 'error1', 0, False), - ('output2', 'error2', 0, False), - ('output3', 'error3', 1, True) + (b'output1', b'error1', 0, False), + (b'output2', b'error2', 0, False), + (b'output3', b'error3', 1, True) ] mock_protocol_init.return_value = mock_protocol @@ -372,8 +372,8 @@ def test_run_cmd_failed(self, mock_protocol_init): {'failed': True, 'succeeded': False, 'return_code': 1, - 'stdout': 'output1output2output3', - 'stderr': 'error1error2error3'}, + 'stdout': b'output1output2output3', + 'stderr': b'error1error2error3'}, None)) @mock.patch('winrm.Protocol') @@ -384,7 +384,7 @@ def test_run_cmd_timeout(self, mock_protocol_init): def sleep_for_timeout_then_raise(*args, **kwargs): time.sleep(0.2) - return ('output1', 'error1', 123, False) + return (b'output1', b'error1', 123, False) mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise mock_protocol_init.return_value = mock_protocol @@ -394,17 +394,17 @@ def sleep_for_timeout_then_raise(*args, **kwargs): {'failed': True, 'succeeded': False, 'return_code': -1, - 'stdout': 'output1', - 'stderr': 'error1'}, + 'stdout': b'output1', + 'stderr': b'error1'}, None)) @mock.patch('winrm.Protocol') def test_run_ps(self, mock_protocol_init): mock_protocol = mock.MagicMock() mock_protocol._raw_get_command_output.side_effect = [ - ('output1', 'error1', 0, False), - ('output2', 'error2', 0, False), - ('output3', 'error3', 0, True) + (b'output1', b'error1', 0, False), + (b'output2', b'error2', 0, False), + (b'output3', b'error3', 0, True) ] mock_protocol_init.return_value = mock_protocol @@ -414,7 +414,7 @@ def test_run_ps(self, mock_protocol_init): {'failed': False, 'succeeded': True, 'return_code': 0, - 'stdout': 'output1output2output3', + 'stdout': b'output1output2output3', 'stderr': 'error1error2error3'}, None)) @@ -422,9 +422,9 @@ def test_run_ps(self, mock_protocol_init): def test_run_ps_failed(self, mock_protocol_init): mock_protocol = mock.MagicMock() mock_protocol._raw_get_command_output.side_effect = [ - ('output1', 'error1', 0, False), - ('output2', 'error2', 0, False), - ('output3', 'error3', 1, True) + (b'output1', b'error1', 0, False), + (b'output2', b'error2', 0, False), + (b'output3', b'error3', 1, True) ] mock_protocol_init.return_value = mock_protocol @@ -434,7 +434,7 @@ def test_run_ps_failed(self, mock_protocol_init): {'failed': True, 'succeeded': False, 'return_code': 1, - 'stdout': 'output1output2output3', + 'stdout': b'output1output2output3', 'stderr': 'error1error2error3'}, None)) @@ -446,7 +446,7 @@ def test_run_ps_timeout(self, mock_protocol_init): def sleep_for_timeout_then_raise(*args, **kwargs): time.sleep(0.2) - return ('output1', 'error1', 123, False) + return (b'output1', b'error1', 123, False) mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise mock_protocol_init.return_value = mock_protocol @@ -456,7 +456,7 @@ def sleep_for_timeout_then_raise(*args, **kwargs): {'failed': True, 'succeeded': False, 'return_code': -1, - 'stdout': 'output1', + 'stdout': b'output1', 'stderr': 'error1'}, None)) diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index 3405810690..3130266cbe 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -193,6 +193,10 @@ def _winrm_run_ps(self, session, script, env=None, cwd=None): if len(rs.std_err): # if there was an error message, clean it it up and make it human # readable + if isinstance(rs.std_err, bytes): + # decode bytes into utf-8 because of a bug in pywinrm + # real fix is here: https://github.com/diyan/pywinrm/pull/222/files + rs.std_err = rs.std_err.decode('utf-8') rs.std_err = session._clean_error_msg(rs.std_err) return rs From 08a681d1f33ff184ba5dc0213debd1c5f27441bb Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Thu, 14 Jun 2018 11:09:33 -0400 Subject: [PATCH 20/31] Added WinRM runner examples. Found a few more corner-cases in the WinRM runner parameter transformations --- .../examples/actions/winrm_get_uptime.yaml | 6 +++++ contrib/examples/actions/winrm_ipconfig.yaml | 10 +++++++++ .../actions/winrm_powershell_env.yaml | 17 ++++++++++++++ .../tests/unit/test_winrm_base.py | 22 +++++++++++++++++++ .../winrm_runner/winrm_runner/winrm_base.py | 22 ++++++++++++------- 5 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 contrib/examples/actions/winrm_get_uptime.yaml create mode 100644 contrib/examples/actions/winrm_ipconfig.yaml create mode 100644 contrib/examples/actions/winrm_powershell_env.yaml diff --git a/contrib/examples/actions/winrm_get_uptime.yaml b/contrib/examples/actions/winrm_get_uptime.yaml new file mode 100644 index 0000000000..412e0f8ea2 --- /dev/null +++ b/contrib/examples/actions/winrm_get_uptime.yaml @@ -0,0 +1,6 @@ +--- +name: "winrm_get_uptime" +description: "Action that returns Windows host's uptime using WinRM." +enabled: true +entry_point: "windows/get_uptime.ps1" +runner_type: "winrm-ps-script" diff --git a/contrib/examples/actions/winrm_ipconfig.yaml b/contrib/examples/actions/winrm_ipconfig.yaml new file mode 100644 index 0000000000..75410dd7d3 --- /dev/null +++ b/contrib/examples/actions/winrm_ipconfig.yaml @@ -0,0 +1,10 @@ +--- +name: "winrm_get_ipconfig" +description: "Action that returns Windows host's IP config information." +enabled: true +runner_type: "winrm-cmd" +parameters: + cmd: + type: string + default: "ipconfig /all" + immutable: true diff --git a/contrib/examples/actions/winrm_powershell_env.yaml b/contrib/examples/actions/winrm_powershell_env.yaml new file mode 100644 index 0000000000..5a63458e5a --- /dev/null +++ b/contrib/examples/actions/winrm_powershell_env.yaml @@ -0,0 +1,17 @@ +--- +name: "winrm_powershell_env" +description: "Action that sets an environment variable then prints it out. This also demonstrates returning JSON data from PowerShell and being able to utilize it as an object via stdout." +enabled: true +runner_type: "winrm-ps-cmd" +parameters: + env_var_name: + type: string + default: "ST2_WINRM_ENV_TEST" + cmd: + type: string + default: "Get-Item Env:{{ env_var_name }} | ConvertTo-Json -Depth 1 -Compress" + immutable: true + env: + type: object + default: + "{{ env_var_name }}": "This is a test" diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py index 64534c9941..43c5bf1539 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -538,6 +538,12 @@ def test_multireplace_powershell(self): '`$' )) + def test_param_to_ps_none(self): + # test None/null + param = None + result = self._runner._param_to_ps(param) + self.assertEquals(result, '$null') + def test_param_to_ps_string(self): # test ascii param_str = 'StackStorm 1234' @@ -658,6 +664,15 @@ def test_transform_params_to_ps(self): ('c', '@("x", "y")'), ('d', '@{"z" = "w"}')])) + def test_transform_params_to_ps_none(self): + positional_args = None + named_args = None + + result_pos, result_named = self._runner._transform_params_to_ps(positional_args, + named_args) + self.assertEquals(result_pos, None) + self.assertEquals(result_named, None) + def test_create_ps_params_string(self): positional_args = [1, 'a', '\n'] named_args = collections.OrderedDict( @@ -671,3 +686,10 @@ def test_create_ps_params_string(self): self.assertEquals(result, '-a "value1" -b $true -c @("x", "y") -d @{"z" = "w"} 1 "a" "`n"') + + def test_create_ps_params_string_none(self): + positional_args = None + named_args = None + + result = self._runner.create_ps_params_string(positional_args, named_args) + self.assertEquals(result, "") diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index 3130266cbe..ee0d7f0ce0 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -263,7 +263,9 @@ def _multireplace(self, string, replacements): def _param_to_ps(self, param): ps_str = "" - if isinstance(param, six.string_types): + if param is None: + ps_str = "$null" + elif isinstance(param, six.string_types): ps_str = '"' + self._multireplace(param, PS_ESCAPE_SEQUENCES) + '"' elif isinstance(param, bool): ps_str = "$true" if param else "$false" @@ -281,11 +283,13 @@ def _param_to_ps(self, param): return ps_str def _transform_params_to_ps(self, positional_args, named_args): - for i, arg in enumerate(positional_args): - positional_args[i] = self._param_to_ps(arg) + if positional_args: + for i, arg in enumerate(positional_args): + positional_args[i] = self._param_to_ps(arg) - for key, value in six.iteritems(named_args): - named_args[key] = self._param_to_ps(value) + if named_args: + for key, value in six.iteritems(named_args): + named_args[key] = self._param_to_ps(value) return positional_args, named_args @@ -295,7 +299,9 @@ def create_ps_params_string(self, positional_args, named_args): named_args) # concatenate them into a long string ps_params_str = "" - ps_params_str += " " .join([(k + " " + v) for k, v in six.iteritems(named_args)]) - ps_params_str += " " - ps_params_str += " ".join(positional_args) + if named_args: + ps_params_str += " " .join([(k + " " + v) for k, v in six.iteritems(named_args)]) + ps_params_str += " " + if positional_args: + ps_params_str += " ".join(positional_args) return ps_params_str From 3f6f9b30d4a5137974605563e42f09c97411cfe9 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Mon, 18 Jun 2018 19:31:14 -0400 Subject: [PATCH 21/31] Renamed _run_cmd and _run_ps functions in winrm base runner --- .../winrm_runner/tests/unit/test_winrm_base.py | 12 ++++++------ .../tests/unit/test_winrm_command_runner.py | 2 +- .../tests/unit/test_winrm_ps_command_runner.py | 2 +- .../tests/unit/test_winrm_ps_script_runner.py | 2 +- .../runners/winrm_runner/winrm_runner/winrm_base.py | 4 ++-- .../winrm_runner/winrm_command_runner.py | 2 +- .../winrm_runner/winrm_ps_command_runner.py | 2 +- .../winrm_runner/winrm_ps_script_runner.py | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py index 43c5bf1539..1ef787ac5f 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -347,7 +347,7 @@ def test_run_cmd(self, mock_protocol_init): mock_protocol_init.return_value = mock_protocol self._init_runner() - result = self._runner._run_cmd("ipconfig /all") + result = self._runner.run_cmd("ipconfig /all") self.assertEquals(result, ('succeeded', {'failed': False, 'succeeded': True, @@ -367,7 +367,7 @@ def test_run_cmd_failed(self, mock_protocol_init): mock_protocol_init.return_value = mock_protocol self._init_runner() - result = self._runner._run_cmd("ipconfig /all") + result = self._runner.run_cmd("ipconfig /all") self.assertEquals(result, ('failed', {'failed': True, 'succeeded': False, @@ -389,7 +389,7 @@ def sleep_for_timeout_then_raise(*args, **kwargs): mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise mock_protocol_init.return_value = mock_protocol - result = self._runner._run_cmd("ipconfig /all") + result = self._runner.run_cmd("ipconfig /all") self.assertEquals(result, ('timeout', {'failed': True, 'succeeded': False, @@ -409,7 +409,7 @@ def test_run_ps(self, mock_protocol_init): mock_protocol_init.return_value = mock_protocol self._init_runner() - result = self._runner._run_ps("Get-Location") + result = self._runner.run_ps("Get-Location") self.assertEquals(result, ('succeeded', {'failed': False, 'succeeded': True, @@ -429,7 +429,7 @@ def test_run_ps_failed(self, mock_protocol_init): mock_protocol_init.return_value = mock_protocol self._init_runner() - result = self._runner._run_ps("Get-Location") + result = self._runner.run_ps("Get-Location") self.assertEquals(result, ('failed', {'failed': True, 'succeeded': False, @@ -451,7 +451,7 @@ def sleep_for_timeout_then_raise(*args, **kwargs): mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise mock_protocol_init.return_value = mock_protocol - result = self._runner._run_ps("Get-Location") + result = self._runner.run_ps("Get-Location") self.assertEquals(result, ('timeout', {'failed': True, 'succeeded': False, diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py index 447674de67..e95b2baa9d 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py @@ -34,7 +34,7 @@ def test_init(self): self.assertIsInstance(runner, ActionRunner) self.assertEquals(runner.runner_id, 'abcdef') - @mock.patch('winrm_runner.winrm_command_runner.WinRmCommandRunner._run_cmd') + @mock.patch('winrm_runner.winrm_command_runner.WinRmCommandRunner.run_cmd') def test_run(self, mock_run_cmd): mock_run_cmd.return_value = 'expected' diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py index c3417128f0..8de0c348c4 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py @@ -34,7 +34,7 @@ def test_init(self): self.assertIsInstance(runner, ActionRunner) self.assertEquals(runner.runner_id, 'abcdef') - @mock.patch('winrm_runner.winrm_ps_command_runner.WinRmPsCommandRunner._run_ps') + @mock.patch('winrm_runner.winrm_ps_command_runner.WinRmPsCommandRunner.run_ps') def test_run(self, mock_run_ps): mock_run_ps.return_value = 'expected' diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py index 25259d1df1..3d0ebaf065 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py @@ -40,7 +40,7 @@ def test_init(self): self.assertEquals(runner.runner_id, 'abcdef') @mock.patch('winrm_runner.winrm_ps_script_runner.WinRmPsScriptRunner._get_script_args') - @mock.patch('winrm_runner.winrm_ps_script_runner.WinRmPsScriptRunner._run_ps') + @mock.patch('winrm_runner.winrm_ps_script_runner.WinRmPsScriptRunner.run_ps') def test_run(self, mock_run_ps, mock_get_script_args): mock_run_ps.return_value = 'expected' pos_args = [1, 'abc'] diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index ee0d7f0ce0..8cfbb6e174 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -224,7 +224,7 @@ def _translate_response(self, response): # objects so they can be used natively return (status, jsonify.json_loads(result, RESULT_KEYS_TO_TRANSFORM), None) - def _run_cmd(self, cmd): + def run_cmd(self, cmd): # connect session = self._create_session() # execute @@ -232,7 +232,7 @@ def _run_cmd(self, cmd): # create triplet from WinRM response return self._translate_response(response) - def _run_ps(self, powershell): + def run_ps(self, powershell): # connect session = self._create_session() # execute diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py index db6e32a8e8..8a15456f9b 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py @@ -37,7 +37,7 @@ def run(self, action_parameters): cmd_command = self.runner_parameters[RUNNER_COMMAND] # execute - return self._run_cmd(cmd_command) + return self.run_cmd(cmd_command) def get_runner(): diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py index ab4630ab09..4b1fb00c65 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py @@ -37,7 +37,7 @@ def run(self, action_parameters): powershell_command = self.runner_parameters[RUNNER_COMMAND] # execute - return self._run_ps(powershell_command) + return self.run_ps(powershell_command) def get_runner(): diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py index 3c865b2e3b..b48956d1e0 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py @@ -54,7 +54,7 @@ def run(self, action_parameters): ps_script_and_params = "& {%s} %s" % (ps_script, ps_params) # execute - return self._run_ps(ps_script_and_params) + return self.run_ps(ps_script_and_params) def get_runner(): From b05a241e86ece1e90f9f6a123991712352228af7 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Mon, 18 Jun 2018 19:31:14 -0400 Subject: [PATCH 22/31] Renamed _run_cmd and _run_ps functions in winrm base runner --- .../winrm_runner/tests/unit/test_winrm_base.py | 12 ++++++------ .../tests/unit/test_winrm_command_runner.py | 2 +- .../tests/unit/test_winrm_ps_command_runner.py | 2 +- .../tests/unit/test_winrm_ps_script_runner.py | 2 +- .../runners/winrm_runner/winrm_runner/winrm_base.py | 4 ++-- .../winrm_runner/winrm_command_runner.py | 2 +- .../winrm_runner/winrm_ps_command_runner.py | 2 +- .../winrm_runner/winrm_ps_script_runner.py | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py index 43c5bf1539..1ef787ac5f 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py @@ -347,7 +347,7 @@ def test_run_cmd(self, mock_protocol_init): mock_protocol_init.return_value = mock_protocol self._init_runner() - result = self._runner._run_cmd("ipconfig /all") + result = self._runner.run_cmd("ipconfig /all") self.assertEquals(result, ('succeeded', {'failed': False, 'succeeded': True, @@ -367,7 +367,7 @@ def test_run_cmd_failed(self, mock_protocol_init): mock_protocol_init.return_value = mock_protocol self._init_runner() - result = self._runner._run_cmd("ipconfig /all") + result = self._runner.run_cmd("ipconfig /all") self.assertEquals(result, ('failed', {'failed': True, 'succeeded': False, @@ -389,7 +389,7 @@ def sleep_for_timeout_then_raise(*args, **kwargs): mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise mock_protocol_init.return_value = mock_protocol - result = self._runner._run_cmd("ipconfig /all") + result = self._runner.run_cmd("ipconfig /all") self.assertEquals(result, ('timeout', {'failed': True, 'succeeded': False, @@ -409,7 +409,7 @@ def test_run_ps(self, mock_protocol_init): mock_protocol_init.return_value = mock_protocol self._init_runner() - result = self._runner._run_ps("Get-Location") + result = self._runner.run_ps("Get-Location") self.assertEquals(result, ('succeeded', {'failed': False, 'succeeded': True, @@ -429,7 +429,7 @@ def test_run_ps_failed(self, mock_protocol_init): mock_protocol_init.return_value = mock_protocol self._init_runner() - result = self._runner._run_ps("Get-Location") + result = self._runner.run_ps("Get-Location") self.assertEquals(result, ('failed', {'failed': True, 'succeeded': False, @@ -451,7 +451,7 @@ def sleep_for_timeout_then_raise(*args, **kwargs): mock_protocol._raw_get_command_output.side_effect = sleep_for_timeout_then_raise mock_protocol_init.return_value = mock_protocol - result = self._runner._run_ps("Get-Location") + result = self._runner.run_ps("Get-Location") self.assertEquals(result, ('timeout', {'failed': True, 'succeeded': False, diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py index 447674de67..e95b2baa9d 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_command_runner.py @@ -34,7 +34,7 @@ def test_init(self): self.assertIsInstance(runner, ActionRunner) self.assertEquals(runner.runner_id, 'abcdef') - @mock.patch('winrm_runner.winrm_command_runner.WinRmCommandRunner._run_cmd') + @mock.patch('winrm_runner.winrm_command_runner.WinRmCommandRunner.run_cmd') def test_run(self, mock_run_cmd): mock_run_cmd.return_value = 'expected' diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py index c3417128f0..8de0c348c4 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_command_runner.py @@ -34,7 +34,7 @@ def test_init(self): self.assertIsInstance(runner, ActionRunner) self.assertEquals(runner.runner_id, 'abcdef') - @mock.patch('winrm_runner.winrm_ps_command_runner.WinRmPsCommandRunner._run_ps') + @mock.patch('winrm_runner.winrm_ps_command_runner.WinRmPsCommandRunner.run_ps') def test_run(self, mock_run_ps): mock_run_ps.return_value = 'expected' diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py index 25259d1df1..3d0ebaf065 100644 --- a/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_ps_script_runner.py @@ -40,7 +40,7 @@ def test_init(self): self.assertEquals(runner.runner_id, 'abcdef') @mock.patch('winrm_runner.winrm_ps_script_runner.WinRmPsScriptRunner._get_script_args') - @mock.patch('winrm_runner.winrm_ps_script_runner.WinRmPsScriptRunner._run_ps') + @mock.patch('winrm_runner.winrm_ps_script_runner.WinRmPsScriptRunner.run_ps') def test_run(self, mock_run_ps, mock_get_script_args): mock_run_ps.return_value = 'expected' pos_args = [1, 'abc'] diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py index ee0d7f0ce0..8cfbb6e174 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py @@ -224,7 +224,7 @@ def _translate_response(self, response): # objects so they can be used natively return (status, jsonify.json_loads(result, RESULT_KEYS_TO_TRANSFORM), None) - def _run_cmd(self, cmd): + def run_cmd(self, cmd): # connect session = self._create_session() # execute @@ -232,7 +232,7 @@ def _run_cmd(self, cmd): # create triplet from WinRM response return self._translate_response(response) - def _run_ps(self, powershell): + def run_ps(self, powershell): # connect session = self._create_session() # execute diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py index db6e32a8e8..8a15456f9b 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_command_runner.py @@ -37,7 +37,7 @@ def run(self, action_parameters): cmd_command = self.runner_parameters[RUNNER_COMMAND] # execute - return self._run_cmd(cmd_command) + return self.run_cmd(cmd_command) def get_runner(): diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py index ab4630ab09..4b1fb00c65 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_command_runner.py @@ -37,7 +37,7 @@ def run(self, action_parameters): powershell_command = self.runner_parameters[RUNNER_COMMAND] # execute - return self._run_ps(powershell_command) + return self.run_ps(powershell_command) def get_runner(): diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py index 3c865b2e3b..b48956d1e0 100644 --- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py +++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py @@ -54,7 +54,7 @@ def run(self, action_parameters): ps_script_and_params = "& {%s} %s" % (ps_script, ps_params) # execute - return self._run_ps(ps_script_and_params) + return self.run_ps(ps_script_and_params) def get_runner(): From f01e359a9ab54b1c74cd28cc4deae7ab5ef0f682 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Fri, 22 Jun 2018 08:01:03 -0400 Subject: [PATCH 23/31] Fixed unit tests for WinRM runner --- st2api/tests/unit/controllers/v1/test_packs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2api/tests/unit/controllers/v1/test_packs.py b/st2api/tests/unit/controllers/v1/test_packs.py index 939cadfd4d..d0ba9d94dc 100644 --- a/st2api/tests/unit/controllers/v1/test_packs.py +++ b/st2api/tests/unit/controllers/v1/test_packs.py @@ -516,7 +516,7 @@ def test_packs_register_endpoint(self, mock_get_packs): {'packs': ['dummy_pack_1'], 'types': ['action']}) self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.json, {'actions': 1, 'runners': 17}) + self.assertEqual(resp.json, {'actions': 1, 'runners': 18}) # Verify that plural name form also works resp = self.app.post_json('/v1/packs/register', From 2d3eb18efa0106b206e089ed4eb3eb7fc808cd50 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Fri, 22 Jun 2018 08:11:30 -0400 Subject: [PATCH 24/31] Fixed some more places where the number of runners was hard coded --- st2api/tests/unit/controllers/v1/test_packs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/st2api/tests/unit/controllers/v1/test_packs.py b/st2api/tests/unit/controllers/v1/test_packs.py index d0ba9d94dc..23faf9775c 100644 --- a/st2api/tests/unit/controllers/v1/test_packs.py +++ b/st2api/tests/unit/controllers/v1/test_packs.py @@ -523,7 +523,7 @@ def test_packs_register_endpoint(self, mock_get_packs): {'packs': ['dummy_pack_1'], 'types': ['actions']}) self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.json, {'actions': 1, 'runners': 17}) + self.assertEqual(resp.json, {'actions': 1, 'runners': 18}) # Register single resource from a single pack specified multiple times - verify that # resources from the same pack are only registered once @@ -533,7 +533,7 @@ def test_packs_register_endpoint(self, mock_get_packs): 'fail_on_failure': False}) self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.json, {'actions': 1, 'runners': 17}) + self.assertEqual(resp.json, {'actions': 1, 'runners': 18}) # Register resources from a single (non-existent pack) resp = self.app.post_json('/v1/packs/register', {'packs': ['doesntexist']}, From deffff9f558220f3c0c4de9ce5d90cf927f8d566 Mon Sep 17 00:00:00 2001 From: Nick Maludy Date: Thu, 28 Jun 2018 08:59:13 -0400 Subject: [PATCH 25/31] Linked winrm_runner version to st2common to go along with #4211 --- contrib/runners/winrm_runner/setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/runners/winrm_runner/setup.py b/contrib/runners/winrm_runner/setup.py index 133ce5421c..34848e8e3e 100644 --- a/contrib/runners/winrm_runner/setup.py +++ b/contrib/runners/winrm_runner/setup.py @@ -23,6 +23,8 @@ from dist_utils import fetch_requirements from dist_utils import apply_vagrant_workaround +from st2common import __version__ + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) REQUIREMENTS_FILE = os.path.join(BASE_DIR, 'requirements.txt') @@ -31,7 +33,7 @@ apply_vagrant_workaround() setup( name='stackstorm-runner-winrm', - version='1.0.0', + version=__version__, description=('WinRM shell command and PowerShell script action runner for' ' the StackStorm event-driven automation platform'), author='StackStorm', From 67f45833f1bb1875835207402d4f7ef459a10310 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Mon, 9 Jul 2018 15:17:21 +0200 Subject: [PATCH 26/31] Update CHANGELOG.rst --- CHANGELOG.rst | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3df46d5de2..2ab7759d2d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,16 @@ in development Added ~~~~~ +* Add new runners: ``winrm-cmd``, ``winrm-ps-cmd`` and ``winrm-ps-script``. + The ``winrm-cmd`` runner executes Command Prompt commands remotely on Windows hosts using the + WinRM protocol. The ``winrm-ps-cmd`` and ``winrm-ps-script`` runners execute PowerShell commands + and scripts on remote Windows hosts using the WinRM protocol. + + To accompany these new runners, there are two new actions ``core.winrm_cmd`` that executes remote + Command Prompt commands along with ``core.winrm_ps_cmd`` that executes remote PowerShell commands. + (new feature) #1636 + + Contributed by Nick Maludy (Encore Technologies). * Add new ``?tags``, query param filter to the ``/v1/actions`` API endpoint. This query parameter allows users to filter out actions based on the tag name . By default, when no filter values are provided, all actions are returned. (new feature) #4219 @@ -58,15 +68,6 @@ Added (``os.path.dirname``). #4184 Contributed by Florian Reisinger (@reisingerf). -* Add new runners: ``winrm-cmd``, ``winrm-ps-cmd`` and ``winrm-ps-script``. - The ``winrm-cmd`` runner executes Command Prompt commands remotely on Windows hosts using - the WinRM protocol. The ``winrm-ps-cmd`` and ``winrm-ps-script`` runners execute PowerShell - commands and scripts on remote Windows hosts using the WinRM protocol. - To accompany these new runners, there are two new actions ``core.winrm_cmd`` that - executes remote Command Prompt commands along with ``core.winrm_ps_cmd`` that - executes remote PowerShell commands. (new feature) #1636 - - Contributed by Nick Maludy (Encore Technologies). Changed ~~~~~~~ From de155e6e57124d9e476fe6829f9db6660f115c38 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Mon, 9 Jul 2018 15:57:16 +0200 Subject: [PATCH 27/31] Update setup.py --- contrib/runners/winrm_runner/setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contrib/runners/winrm_runner/setup.py b/contrib/runners/winrm_runner/setup.py index 34848e8e3e..67b27dad10 100644 --- a/contrib/runners/winrm_runner/setup.py +++ b/contrib/runners/winrm_runner/setup.py @@ -23,8 +23,6 @@ from dist_utils import fetch_requirements from dist_utils import apply_vagrant_workaround -from st2common import __version__ - BASE_DIR = os.path.dirname(os.path.abspath(__file__)) REQUIREMENTS_FILE = os.path.join(BASE_DIR, 'requirements.txt') @@ -33,7 +31,7 @@ apply_vagrant_workaround() setup( name='stackstorm-runner-winrm', - version=__version__, + version='2.8.0', description=('WinRM shell command and PowerShell script action runner for' ' the StackStorm event-driven automation platform'), author='StackStorm', From 2ecf3a09655712c3875b5fe2f9a4582f48c451a9 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Mon, 9 Jul 2018 16:26:03 +0200 Subject: [PATCH 28/31] Update CHANGELOG.rst --- CHANGELOG.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2ab7759d2d..b6ae91de33 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,11 @@ Changed * Update ``st2client/setup.py`` file to dynamically load requirements from ``st2client/requirements.txt`` file. The code works with pip >= 6.0.0, although using pip 9.0.0 or higher is strongly recommended. (improvement) #4209 +* Migrated runners to using the ``in-requirements.txt`` pattern for "components" in the build + system, so the ``Makefile`` correctly generates and installs runner dependencies during + testing and packaging. (improvement) (bugfix) #4169 + + Contributed by Nick Maludy (Encore Technologies). 2.8.0 - July 10, 2018 --------------------- @@ -93,13 +98,7 @@ Changed Note: This change is fully backward compatible since it just changes the underlying backend and implementation details. The same underlying encryption algorithm is used (AES256 in CBC mode with HMAC signature). (improvement) #4165 -* Migrated runners to using the ``in-requirements.txt`` pattern for "components" in the build - system, so the ``Makefile`` correctly generates and installs runner dependencies during - testing and packaging. (improvement) (bugfix) #4169 - Contributed by Nick Maludy (Encore Technologies). - - Fixed ~~~~~ From 1783f0aa9592be8739a814bdb714cd1372f64384 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Mon, 9 Jul 2018 16:26:31 +0200 Subject: [PATCH 29/31] Update CHANGELOG.rst --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b6ae91de33..ff3f22e160 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -98,7 +98,7 @@ Changed Note: This change is fully backward compatible since it just changes the underlying backend and implementation details. The same underlying encryption algorithm is used (AES256 in CBC mode with HMAC signature). (improvement) #4165 - + Fixed ~~~~~ From dbecb3733ee672e0bc1fa37d9a0ad301c8bc222a Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Mon, 9 Jul 2018 17:14:03 +0200 Subject: [PATCH 30/31] Update tox.ini --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c7c1c0f355..fd3af0c1d4 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,8 @@ commands = [testenv:py36-integration] basepython = python3.6 -setenv = PYTHONPATH = {toxinidir}/external:{toxinidir}/st2common:{toxinidir}/st2api:{toxinidir}/st2actions:{toxinidir}/st2exporter:{toxinidir}/st2reactor:{toxinidir}/st2tests:{toxinidir}/contrib/runners/action_chain_runner:{toxinidir}/contrib/runners/local_runner:{toxinidir}/contrib/runners/windows_runner:{toxinidir}/contrib/runners/python_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/noop_runner:{toxinidir}/contrib/runners/announcement_runner:{toxinidir}/contrib/runners/remote_runner:{toxinidir}/contrib/runners/remote_runner:{toxinidir}/contrib/runners/mistral_v2:{toxinidir}/contrib/runners/orchestra:{toxinidir}/contrib/runners/inquirer_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/cloudslang_runner:{toxinidir}/contrib/runners/winrm_runner VIRTUALENV_DIR = {envdir} +setenv = PYTHONPATH = {toxinidir}/external:{toxinidir}/st2common:{toxinidir}/st2auth:{toxinidir}/st2api:{toxinidir}/st2actions:{toxinidir}/st2exporter:{toxinidir}/st2reactor:{toxinidir}/st2tests:{toxinidir}/contrib/runners/action_chain_runner:{toxinidir}/contrib/runners/local_runner:{toxinidir}/contrib/runners/windows_runner:{toxinidir}/contrib/runners/python_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/noop_runner:{toxinidir}/contrib/runners/announcement_runner:{toxinidir}/contrib/runners/remote_runner:{toxinidir}/contrib/runners/remote_runner:{toxinidir}/contrib/runners/mistral_v2:{toxinidir}/contrib/runners/orchestra:{toxinidir}/contrib/runners/inquirer_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/cloudslang_runner:{toxinidir}/contrib/runners/winrm_runner +VIRTUALENV_DIR = {envdir} install_command = pip install -U --force-reinstall {opts} {packages} deps = virtualenv -r{toxinidir}/requirements.txt From 3c78019fb237b68a0a48cf32e258021974987f8b Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Mon, 9 Jul 2018 17:15:27 +0200 Subject: [PATCH 31/31] Update tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fd3af0c1d4..a0c1973e6c 100644 --- a/tox.ini +++ b/tox.ini @@ -54,7 +54,7 @@ commands = [testenv:py36-integration] basepython = python3.6 setenv = PYTHONPATH = {toxinidir}/external:{toxinidir}/st2common:{toxinidir}/st2auth:{toxinidir}/st2api:{toxinidir}/st2actions:{toxinidir}/st2exporter:{toxinidir}/st2reactor:{toxinidir}/st2tests:{toxinidir}/contrib/runners/action_chain_runner:{toxinidir}/contrib/runners/local_runner:{toxinidir}/contrib/runners/windows_runner:{toxinidir}/contrib/runners/python_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/noop_runner:{toxinidir}/contrib/runners/announcement_runner:{toxinidir}/contrib/runners/remote_runner:{toxinidir}/contrib/runners/remote_runner:{toxinidir}/contrib/runners/mistral_v2:{toxinidir}/contrib/runners/orchestra:{toxinidir}/contrib/runners/inquirer_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/cloudslang_runner:{toxinidir}/contrib/runners/winrm_runner -VIRTUALENV_DIR = {envdir} + VIRTUALENV_DIR = {envdir} install_command = pip install -U --force-reinstall {opts} {packages} deps = virtualenv -r{toxinidir}/requirements.txt