Repository: StackStorm/st2

Issue:
Feature Request - Execute large WinRM scripts
##### SUMMARY

Provide the ability to execute large WinRM scripts using the `winrm-ps-script` runner.

##### ISSUE TYPE
 - Feature Idea

##### STEPS TO REPRODUCE
Execute a script larger than 8192 bytes: https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation

##### EXPECTED RESULTS
Script runs successfully

##### ACTUAL RESULTS
Exits with `Command is too long`.

##### INVESTIGATION

This is a limitation of WinRM and the current runner limitation. Currently the runner simply takes the script and executes it as one long command on the command line. It does this because WinRM does not support file transfers out of the box. 

To accomplish file transfers in WinRM you can check out this thread from the `pywinrm` library: https://github.com/diyan/pywinrm/issues/18



Discussion:
Here's the ruby library for doing file uploads. https://github.com/WinRb/winrm-fs

The ruby library looks great and seems highly optimized for large file uploads.

Since this is intended to be used only for script uploads, and not large file transfers, i'm planning on implementing the easier and more straightforward solution found in the pywinrm issue as a first-pass.

If further down the road we would like to implement large file transfer actions, then we can investigate adding those features.
Here's a new python library that has emerged for WinRM that supports file transfers: https://github.com/jborean93/pypsrp


Resolving patch:
diff --git a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py
--- a/contrib/runners/winrm_runner/winrm_runner/winrm_base.py
+++ b/contrib/runners/winrm_runner/winrm_runner/winrm_base.py
@@ -15,11 +15,14 @@
 
 from __future__ import absolute_import
 
+import base64
+import os
 import re
 import six
 import time
 
 from base64 import b64encode
+from contextlib import contextmanager
 from st2common import log as logging
 from st2common.constants import action as action_constants
 from st2common.constants import exit_codes as exit_code_constants
@@ -46,10 +49,17 @@
 RUNNER_USERNAME = "username"
 RUNNER_VERIFY_SSL = "verify_ssl_cert"
 
+WINRM_DEFAULT_TMP_DIR_PS = '[System.IO.Path]::GetTempPath()'
+# maximum cmdline length for systems >= Windows XP
+# https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation
+WINRM_MAX_CMD_LENGTH = 8191
 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
+# number of bytes in each chunk when uploading data via WinRM to a target host
+# this was chosen arbitrarily and could probably use some tuning
+WINRM_UPLOAD_CHUNK_SIZE_BYTES = 2048
 
 DEFAULT_KWARG_OP = "-"
 DEFAULT_PORT = WINRM_HTTPS_PORT
@@ -92,6 +102,7 @@ def pre_run(self):
         super(WinRmBaseRunner, self).pre_run()
 
         # common connection parameters
+        self._session = None
         self._host = self.runner_parameters[RUNNER_HOST]
         self._username = self.runner_parameters[RUNNER_USERNAME]
         self._password = self.runner_parameters[RUNNER_PASSWORD]
@@ -121,16 +132,17 @@ 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):
-        # create the session
-        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 _get_session(self):
+        # cache session (only create if it doesn't exist yet)
+        if not self._session:
+            LOG.debug("Connecting via WinRM to url: {}".format(self._winrm_url))
+            self._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 self._session
 
     def _winrm_get_command_output(self, protocol, shell_id, command_id):
         # NOTE: this is copied from pywinrm because it doesn't support
@@ -182,12 +194,23 @@ def _winrm_run_cmd(self, session, command, args=(), env=None, cwd=None):
         session.protocol.close_shell(shell_id)
         return rs
 
-    def _winrm_run_ps(self, session, script, env=None, cwd=None):
+    def _winrm_encode(self, script):
+        return b64encode(script.encode('utf_16_le')).decode('ascii')
+
+    def _winrm_ps_cmd(self, encoded_ps):
+        return 'powershell -encodedcommand {0}'.format(encoded_ps)
+
+    def _winrm_run_ps(self, session, script, env=None, cwd=None, is_b64=False):
         # 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')
+
+        # encode the script in UTF only if it isn't passed in encoded
+        LOG.debug('_winrm_run_ps() - script size = {}'.format(len(script)))
+        encoded_ps = script if is_b64 else self._winrm_encode(script)
+        ps_cmd = self._winrm_ps_cmd(encoded_ps)
+        LOG.debug('_winrm_run_ps() - ps cmd size = {}'.format(len(ps_cmd)))
         rs = self._winrm_run_cmd(session,
-                                 'powershell -encodedcommand {0}'.format(encoded_ps),
+                                 ps_cmd,
                                  env=env,
                                  cwd=cwd)
         if len(rs.std_err):
@@ -220,26 +243,154 @@ def _translate_response(self, response):
             'stderr': response.std_err
         }
 
+        # Ensure stdout and stderr is always a string
+        if isinstance(result['stdout'], six.binary_type):
+            result['stdout'] = result['stdout'].decode('utf-8')
+
+        if isinstance(result['stderr'], six.binary_type):
+            result['stderr'] = result['stderr'].decode('utf-8')
+
         # automatically convert result stdout/stderr from JSON strings to
         # objects so they can be used natively
         return (status, jsonify.json_loads(result, RESULT_KEYS_TO_TRANSFORM), None)
 
+    def _make_tmp_dir(self, parent):
+        LOG.debug("Creating temporary directory for WinRM script in parent: {}".format(parent))
+        ps = """$parent = {parent}
+$name = [System.IO.Path]::GetRandomFileName()
+$path = Join-Path $parent $name
+New-Item -ItemType Directory -Path $path | Out-Null
+$path""".format(parent=parent)
+        result = self._run_ps_or_raise(ps, ("Unable to make temporary directory for"
+                                            " powershell script"))
+        # strip to remove trailing newline and whitespace (if any)
+        return result['stdout'].strip()
+
+    def _rm_dir(self, directory):
+        ps = 'Remove-Item -Force -Recurse -Path "{}"'.format(directory)
+        self._run_ps_or_raise(ps, "Unable to remove temporary directory for powershell script")
+
+    def _upload(self, src_path_or_data, dst_path):
+        src_data = None
+        # detect if this is a path or a string containing data
+        # if this is a path, then read the data from the path
+        if os.path.exists(src_path_or_data):
+            LOG.debug("WinRM uploading local file: {}".format(src_path_or_data))
+            with open(src_path_or_data, 'r') as src_file:
+                src_data = src_file.read()
+        else:
+            LOG.debug("WinRM uploading data from a string")
+            src_data = src_path_or_data
+
+        # upload the data in chunks such that each chunk doesn't exceed the
+        # max command size of the windows command line
+        for i in range(0, len(src_data), WINRM_UPLOAD_CHUNK_SIZE_BYTES):
+            LOG.debug("WinRM uploading data bytes: {}-{}".
+                      format(i, (i + WINRM_UPLOAD_CHUNK_SIZE_BYTES)))
+            self._upload_chunk(dst_path, src_data[i:(i + WINRM_UPLOAD_CHUNK_SIZE_BYTES)])
+
+    def _upload_chunk(self, dst_path, src_data):
+        # adapted from https://github.com/diyan/pywinrm/issues/18
+        if not isinstance(src_data, six.binary_type):
+            src_data = src_data.encode('utf-8')
+
+        ps = """$filePath = "{dst_path}"
+$s = @"
+{b64_data}
+"@
+$data = [System.Convert]::FromBase64String($s)
+Add-Content -value $data -encoding byte -path $filePath
+""".format(dst_path=dst_path,
+           b64_data=base64.b64encode(src_data).decode('utf-8'))
+
+        LOG.debug('WinRM uploading chunk, size = {}'.format(len(ps)))
+        self._run_ps_or_raise(ps, "Failed to upload chunk of powershell script")
+
+    @contextmanager
+    def _tmp_script(self, parent, script):
+        tmp_dir = None
+        try:
+            LOG.info("WinRM Script - Making temporary directory")
+            tmp_dir = self._make_tmp_dir(parent)
+            LOG.debug("WinRM Script - Tmp directory created: {}".format(tmp_dir))
+            LOG.info("WinRM Script = Upload starting")
+            tmp_script = tmp_dir + "\\script.ps1"
+            LOG.debug("WinRM Uploading script to: {}".format(tmp_script))
+            self._upload(script, tmp_script)
+            LOG.info("WinRM Script - Upload complete")
+            yield tmp_script
+        finally:
+            if tmp_dir:
+                LOG.debug("WinRM Script - Removing script: {}".format(tmp_dir))
+                self._rm_dir(tmp_dir)
+
     def run_cmd(self, cmd):
         # connect
-        session = self._create_session()
+        session = self._get_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):
+    def run_ps(self, script, params=None):
+        # temporary directory for the powershell script
+        if params:
+            powershell = '& {%s} %s' % (script, params)
+        else:
+            powershell = script
+        encoded_ps = self._winrm_encode(powershell)
+        ps_cmd = self._winrm_ps_cmd(encoded_ps)
+
+        # if the powershell script is small enough to fit in one command
+        # then run it as a single command (faster)
+        # else we need to upload the script to a temporary file and execute it,
+        # then remove the temporary file
+        if len(ps_cmd) <= WINRM_MAX_CMD_LENGTH:
+            LOG.info(("WinRM powershell command size {} is > {}, the max size of a"
+                      " powershell command. Converting to a script execution.")
+                     .format(WINRM_MAX_CMD_LENGTH, len(ps_cmd)))
+            return self._run_ps(encoded_ps, is_b64=True)
+        else:
+            return self._run_ps_script(script, params)
+
+    def _run_ps(self, powershell, is_b64=False):
+        """Executes a powershell command, no checks for length are done in this version.
+        The lack of checks here is intentional so that we don't run into an infinte loop
+        when converting a long command to a script"""
         # connect
-        session = self._create_session()
+        session = self._get_session()
         # execute
-        response = self._winrm_run_ps(session, powershell, env=self._env, cwd=self._cwd)
+        response = self._winrm_run_ps(session, powershell, env=self._env, cwd=self._cwd,
+                                      is_b64=is_b64)
         # create triplet from WinRM response
         return self._translate_response(response)
 
+    def _run_ps_script(self, script, params=None):
+        tmp_dir = WINRM_DEFAULT_TMP_DIR_PS
+        # creates a temporary file,
+        # upload the contents of 'script' to the temporary file
+        # handle deletion of the temporary file on exit of the with block
+        with self._tmp_script(tmp_dir, script) as tmp_script:
+            # 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 = "& {%s}" % (tmp_script)
+            if params:
+                ps += " " + params
+            return self._run_ps(ps)
+
+    def _run_ps_or_raise(self, ps, error_msg):
+        response = self._run_ps(ps)
+        # response is a tuple: (status, result, None)
+        result = response[1]
+        if result['failed']:
+            raise RuntimeError(("{}:\n"
+                                "stdout = {}\n\n"
+                                "stderr = {}").format(error_msg,
+                                                      result['stdout'],
+                                                      result['stderr']))
+        return result
+
     def _multireplace(self, string, replacements):
         """
         Given a string and a replacement map, it returns the replaced string.
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
--- a/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py
+++ b/contrib/runners/winrm_runner/winrm_runner/winrm_ps_script_runner.py
@@ -48,13 +48,8 @@ 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
-        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, ps_params)
 
 
 def get_runner():


Test patch:
diff --git a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py
--- a/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py
+++ b/contrib/runners/winrm_runner/tests/unit/test_winrm_base.py
@@ -25,8 +25,9 @@
 from st2common.runners.base import ActionRunner
 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.winrm_base import WINRM_MAX_CMD_LENGTH
+from winrm_runner.winrm_base import WINRM_TIMEOUT_EXIT_CODE
 from winrm_runner import winrm_ps_command_runner
 
 
@@ -72,6 +73,7 @@ def test_pre_run(self, mock_pre_run):
         self._runner.runner_parameters = runner_parameters
         self._runner.pre_run()
         mock_pre_run.assert_called_with()
+        self.assertEquals(self._runner._session, None)
         self.assertEquals(self._runner._host, 'host@domain.tld')
         self.assertEquals(self._runner._username, 'user@domain.tld')
         self.assertEquals(self._runner._password, 'abc123')
@@ -174,7 +176,8 @@ def test_pre_run_ssl_verify_false(self, mock_pre_run):
         self.assertEquals(self._runner._server_cert_validation, 'ignore')
 
     @mock.patch('winrm_runner.winrm_base.Session')
-    def test_create_session(self, mock_session):
+    def test_get_session(self, mock_session):
+        self._runner._session = None
         self._runner._winrm_url = 'https://host@domain.tld:5986/wsman'
         self._runner._username = 'user@domain.tld'
         self._runner._password = 'abc123'
@@ -184,8 +187,9 @@ def test_create_session(self, mock_session):
         self._runner._read_timeout = 61
         mock_session.return_value = "session"
 
-        result = self._runner._create_session()
+        result = self._runner._get_session()
         self.assertEquals(result, "session")
+        self.assertEquals(result, self._runner._session)
         mock_session.assert_called_with('https://host@domain.tld:5986/wsman',
                                         auth=('user@domain.tld', 'abc123'),
                                         transport='ntlm',
@@ -193,7 +197,12 @@ def test_create_session(self, mock_session):
                                         operation_timeout_sec=60,
                                         read_timeout_sec=61)
 
-    def test_get_command_output(self):
+        # ensure calling _get_session again doesn't create a new one, it reuses the existing
+        old_session = self._runner._session
+        result = self._runner._get_session()
+        self.assertEquals(result, old_session)
+
+    def test_winrm_get_command_output(self):
         self._runner._timeout = 0
         mock_protocol = mock.MagicMock()
         mock_protocol._raw_get_command_output.side_effect = [
@@ -211,7 +220,7 @@ def test_get_command_output(self):
             mock.call(567, 890)
         ]
 
-    def test_get_command_output_timeout(self):
+    def test_winrm_get_command_output_timeout(self):
         self._runner._timeout = 0.1
 
         mock_protocol = mock.MagicMock()
@@ -231,7 +240,7 @@ def sleep_for_timeout(*args, **kwargs):
         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):
+    def test_winrm_get_command_output_operation_timeout(self):
         self._runner._timeout = 0.1
 
         mock_protocol = mock.MagicMock()
@@ -297,6 +306,15 @@ def test_winrm_run_cmd_timeout(self, mock_get_command_output):
         mock_protocol.cleanup_command.assert_called_with(123, 456)
         mock_protocol.close_shell.assert_called_with(123)
 
+    def test_winrm_encode(self):
+        result = self._runner._winrm_encode('hello world')
+        # result translated into UTF-16 little-endian
+        self.assertEquals(result, 'aABlAGwAbABvACAAdwBvAHIAbABkAA==')
+
+    def test_winrm_ps_cmd(self):
+        result = self._runner._winrm_ps_cmd('abc123==')
+        self.assertEquals(result, 'powershell -encodedcommand abc123==')
+
     @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))
@@ -336,6 +354,153 @@ def test_winrm_run_ps_clean_stderr(self, mock_run_cmd):
                                         cwd='C:\\st2')
         mock_session._clean_error_msg.assert_called_with('error')
 
+    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))
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._run_ps_or_raise')
+    def test_make_tmp_dir(self, mock_run_ps_or_raise):
+        mock_run_ps_or_raise.return_value = {'stdout': ' expected \n'}
+
+        result = self._runner._make_tmp_dir('C:\\Windows\\Temp')
+        self.assertEquals(result, 'expected')
+        mock_run_ps_or_raise.assert_called_with('''$parent = C:\\Windows\\Temp
+$name = [System.IO.Path]::GetRandomFileName()
+$path = Join-Path $parent $name
+New-Item -ItemType Directory -Path $path | Out-Null
+$path''',
+                                                ("Unable to make temporary directory for"
+                                                 " powershell script"))
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._run_ps_or_raise')
+    def test_rm_dir(self, mock_run_ps_or_raise):
+        self._runner._rm_dir('C:\\Windows\\Temp\\testtmpdir')
+        mock_run_ps_or_raise.assert_called_with(
+            'Remove-Item -Force -Recurse -Path "C:\\Windows\\Temp\\testtmpdir"',
+            "Unable to remove temporary directory for powershell script")
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._upload_chunk')
+    @mock.patch('winrm_runner.winrm_base.open')
+    @mock.patch('os.path.exists')
+    def test_upload_chunk_file(self, mock_os_path_exists, mock_open, mock_upload_chunk):
+        mock_os_path_exists.return_value = True
+        mock_src_file = mock.MagicMock()
+        mock_src_file.read.return_value = "test data"
+        mock_open.return_value.__enter__.return_value = mock_src_file
+
+        self._runner._upload('/opt/data/test.ps1', 'C:\\Windows\\Temp\\test.ps1')
+        mock_os_path_exists.assert_called_with('/opt/data/test.ps1')
+        mock_open.assert_called_with('/opt/data/test.ps1', 'r')
+        mock_src_file.read.assert_called_with()
+        mock_upload_chunk.assert_has_calls([
+            mock.call('C:\\Windows\\Temp\\test.ps1', 'test data')
+        ])
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._upload_chunk')
+    @mock.patch('os.path.exists')
+    def test_upload_chunk_data(self, mock_os_path_exists, mock_upload_chunk):
+        mock_os_path_exists.return_value = False
+
+        self._runner._upload('test data', 'C:\\Windows\\Temp\\test.ps1')
+        mock_os_path_exists.assert_called_with('test data')
+        mock_upload_chunk.assert_has_calls([
+            mock.call('C:\\Windows\\Temp\\test.ps1', 'test data')
+        ])
+
+    @mock.patch('winrm_runner.winrm_base.WINRM_UPLOAD_CHUNK_SIZE_BYTES', 2)
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._upload_chunk')
+    @mock.patch('os.path.exists')
+    def test_upload_chunk_multiple_chunks(self, mock_os_path_exists, mock_upload_chunk):
+        mock_os_path_exists.return_value = False
+
+        self._runner._upload('test data', 'C:\\Windows\\Temp\\test.ps1')
+        mock_os_path_exists.assert_called_with('test data')
+        mock_upload_chunk.assert_has_calls([
+            mock.call('C:\\Windows\\Temp\\test.ps1', 'te'),
+            mock.call('C:\\Windows\\Temp\\test.ps1', 'st'),
+            mock.call('C:\\Windows\\Temp\\test.ps1', ' d'),
+            mock.call('C:\\Windows\\Temp\\test.ps1', 'at'),
+            mock.call('C:\\Windows\\Temp\\test.ps1', 'a'),
+        ])
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._run_ps_or_raise')
+    def test_upload_chunk(self, mock_run_ps_or_raise):
+        self._runner._upload_chunk('C:\\Windows\\Temp\\testtmp.ps1', 'hello world')
+        mock_run_ps_or_raise.assert_called_with(
+            '''$filePath = "C:\\Windows\\Temp\\testtmp.ps1"
+$s = @"
+aGVsbG8gd29ybGQ=
+"@
+$data = [System.Convert]::FromBase64String($s)
+Add-Content -value $data -encoding byte -path $filePath
+''',
+            "Failed to upload chunk of powershell script")
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._rm_dir')
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._upload')
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._make_tmp_dir')
+    def test_tmp_script(self, mock_make_tmp_dir, mock_upload, mock_rm_dir):
+        mock_make_tmp_dir.return_value = 'C:\\Windows\\Temp\\abc123'
+
+        with self._runner._tmp_script('C:\\Windows\\Temp', 'Get-ChildItem') as tmp:
+            self.assertEquals(tmp, 'C:\\Windows\\Temp\\abc123\\script.ps1')
+        mock_make_tmp_dir.assert_called_with('C:\\Windows\\Temp')
+        mock_upload.assert_called_with('Get-ChildItem',
+                                       'C:\\Windows\\Temp\\abc123\\script.ps1')
+        mock_rm_dir.assert_called_with('C:\\Windows\\Temp\\abc123')
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._rm_dir')
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._upload')
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._make_tmp_dir')
+    def test_tmp_script_cleans_up_when_raises(self, mock_make_tmp_dir, mock_upload,
+                                              mock_rm_dir):
+        mock_make_tmp_dir.return_value = 'C:\\Windows\\Temp\\abc123'
+        mock_upload.side_effect = RuntimeError
+
+        with self.assertRaises(RuntimeError):
+            with self._runner._tmp_script('C:\\Windows\\Temp', 'Get-ChildItem') as tmp:
+                self.assertEquals(tmp, "can never get here")
+        mock_make_tmp_dir.assert_called_with('C:\\Windows\\Temp')
+        mock_upload.assert_called_with('Get-ChildItem',
+                                       'C:\\Windows\\Temp\\abc123\\script.ps1')
+        mock_rm_dir.assert_called_with('C:\\Windows\\Temp\\abc123')
+
     @mock.patch('winrm.Protocol')
     def test_run_cmd(self, mock_protocol_init):
         mock_protocol = mock.MagicMock()
@@ -352,8 +517,8 @@ def test_run_cmd(self, mock_protocol_init):
                                    {'failed': False,
                                     'succeeded': True,
                                     'return_code': 0,
-                                    'stdout': b'output1output2output3',
-                                    'stderr': b'error1error2error3'},
+                                    'stdout': 'output1output2output3',
+                                    'stderr': 'error1error2error3'},
                                    None))
 
     @mock.patch('winrm.Protocol')
@@ -372,8 +537,8 @@ def test_run_cmd_failed(self, mock_protocol_init):
                                    {'failed': True,
                                     'succeeded': False,
                                     'return_code': 1,
-                                    'stdout': b'output1output2output3',
-                                    'stderr': b'error1error2error3'},
+                                    'stdout': 'output1output2output3',
+                                    'stderr': 'error1error2error3'},
                                    None))
 
     @mock.patch('winrm.Protocol')
@@ -394,8 +559,8 @@ def sleep_for_timeout_then_raise(*args, **kwargs):
                                    {'failed': True,
                                     'succeeded': False,
                                     'return_code': -1,
-                                    'stdout': b'output1',
-                                    'stderr': b'error1'},
+                                    'stdout': 'output1',
+                                    'stderr': 'error1'},
                                    None))
 
     @mock.patch('winrm.Protocol')
@@ -414,7 +579,7 @@ def test_run_ps(self, mock_protocol_init):
                                    {'failed': False,
                                     'succeeded': True,
                                     'return_code': 0,
-                                    'stdout': b'output1output2output3',
+                                    'stdout': 'output1output2output3',
                                     'stderr': 'error1error2error3'},
                                    None))
 
@@ -434,7 +599,7 @@ def test_run_ps_failed(self, mock_protocol_init):
                                    {'failed': True,
                                     'succeeded': False,
                                     'return_code': 1,
-                                    'stdout': b'output1output2output3',
+                                    'stdout': 'output1output2output3',
                                     'stderr': 'error1error2error3'},
                                    None))
 
@@ -456,42 +621,94 @@ def sleep_for_timeout_then_raise(*args, **kwargs):
                                    {'failed': True,
                                     'succeeded': False,
                                     'return_code': -1,
-                                    'stdout': b'output1',
+                                    'stdout': 'output1',
                                     'stderr': 'error1'},
                                    None))
 
-    def test_translate_response_success(self):
-        response = Response(('output1', 'error1', 0))
-        response.timeout = False
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._run_ps')
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._winrm_encode')
+    def test_run_ps_params(self, mock_winrm_encode, mock_run_ps):
+        mock_winrm_encode.return_value = 'xyz123=='
+        mock_run_ps.return_value = "expected"
 
-        result = self._runner._translate_response(response)
+        self._init_runner()
+        result = self._runner.run_ps("Get-Location", '-param1 value1 arg1')
+
+        self.assertEquals(result, "expected")
+        mock_winrm_encode.assert_called_with('& {Get-Location} -param1 value1 arg1')
+        mock_run_ps.assert_called_with('xyz123==', is_b64=True)
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._winrm_ps_cmd')
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._run_ps_script')
+    def test_run_ps_large_command_convert_to_script(self, mock_run_ps_script,
+                                                    mock_winrm_ps_cmd):
+        mock_run_ps_script.return_value = "expected"
+
+        # max length of a command in powershelll
+        script = 'powershell -encodedcommand '
+        script += '#' * (WINRM_MAX_CMD_LENGTH + 1 - len(script))
+        mock_winrm_ps_cmd.return_value = script
+
+        self._init_runner()
+        result = self._runner.run_ps('$PSVersionTable')
+
+        self.assertEquals(result, "expected")
+        mock_run_ps_script.assert_called_with('$PSVersionTable', 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 = [
+            (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
+
+        self._init_runner()
+        result = self._runner._run_ps("Get-Location")
         self.assertEquals(result, ('succeeded',
                                    {'failed': False,
                                     'succeeded': True,
                                     'return_code': 0,
-                                    'stdout': 'output1',
-                                    'stderr': 'error1'},
+                                    'stdout': 'output1output2output3',
+                                    'stderr': 'error1error2error3'},
                                    None))
 
-    def test_translate_response_failure(self):
-        response = Response(('output1', 'error1', 123))
-        response.timeout = False
+    @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 = [
+            (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
 
-        result = self._runner._translate_response(response)
+        self._init_runner()
+        result = self._runner._run_ps("Get-Location")
         self.assertEquals(result, ('failed',
                                    {'failed': True,
                                     'succeeded': False,
-                                    'return_code': 123,
-                                    'stdout': 'output1',
-                                    'stderr': 'error1'},
-
+                                    'return_code': 1,
+                                    'stdout': 'output1output2output3',
+                                    'stderr': 'error1error2error3'},
                                    None))
 
-    def test_translate_response_timeout(self):
-        response = Response(('output1', 'error1', 123))
-        response.timeout = True
+    @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
 
-        result = self._runner._translate_response(response)
+        def sleep_for_timeout_then_raise(*args, **kwargs):
+            time.sleep(0.2)
+            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
+
+        result = self._runner._run_ps("Get-Location")
         self.assertEquals(result, ('timeout',
                                    {'failed': True,
                                     'succeeded': False,
@@ -500,6 +717,112 @@ def test_translate_response_timeout(self):
                                     'stderr': 'error1'},
                                    None))
 
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._winrm_run_ps')
+    def test__run_ps_b64_default(self, mock_winrm_run_ps):
+        mock_winrm_run_ps.return_value = mock.MagicMock(status_code=0,
+                                                        timeout=False,
+                                                        std_out='output1',
+                                                        std_err='error1')
+
+        self._init_runner()
+        result = self._runner._run_ps("$PSVersionTable")
+        self.assertEquals(result, ('succeeded',
+                                   {'failed': False,
+                                    'succeeded': True,
+                                    'return_code': 0,
+                                    'stdout': 'output1',
+                                    'stderr': 'error1'},
+                                   None))
+        mock_winrm_run_ps.assert_called_with(self._runner._session,
+                                             '$PSVersionTable',
+                                             env={},
+                                             cwd=None,
+                                             is_b64=False)
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._winrm_run_ps')
+    def test__run_ps_b64_true(self, mock_winrm_run_ps):
+        mock_winrm_run_ps.return_value = mock.MagicMock(status_code=0,
+                                                        timeout=False,
+                                                        std_out='output1',
+                                                        std_err='error1')
+
+        self._init_runner()
+        result = self._runner._run_ps("xyz123", is_b64=True)
+        self.assertEquals(result, ('succeeded',
+                                   {'failed': False,
+                                    'succeeded': True,
+                                    'return_code': 0,
+                                    'stdout': 'output1',
+                                    'stderr': 'error1'},
+                                   None))
+        mock_winrm_run_ps.assert_called_with(self._runner._session,
+                                             'xyz123',
+                                             env={},
+                                             cwd=None,
+                                             is_b64=True)
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._run_ps')
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._tmp_script')
+    def test__run_ps_script(self, mock_tmp_script, mock_run_ps):
+        mock_tmp_script.return_value.__enter__.return_value = 'C:\\tmpscript.ps1'
+        mock_run_ps.return_value = 'expected'
+
+        self._init_runner()
+        result = self._runner._run_ps_script("$PSVersionTable")
+        self.assertEquals(result, 'expected')
+        mock_tmp_script.assert_called_with('[System.IO.Path]::GetTempPath()',
+                                           '$PSVersionTable')
+        mock_run_ps.assert_called_with('& {C:\\tmpscript.ps1}')
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._run_ps')
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._tmp_script')
+    def test__run_ps_script_with_params(self, mock_tmp_script, mock_run_ps):
+        mock_tmp_script.return_value.__enter__.return_value = 'C:\\tmpscript.ps1'
+        mock_run_ps.return_value = 'expected'
+
+        self._init_runner()
+        result = self._runner._run_ps_script("Get-ChildItem", '-param1 value1 arg1')
+        self.assertEquals(result, 'expected')
+        mock_tmp_script.assert_called_with('[System.IO.Path]::GetTempPath()',
+                                           'Get-ChildItem')
+        mock_run_ps.assert_called_with('& {C:\\tmpscript.ps1} -param1 value1 arg1')
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._run_ps')
+    def test__run_ps_or_raise(self, mock_run_ps):
+        mock_run_ps.return_value = ('success',
+                                    {
+                                        'failed': False,
+                                        'succeeded': True,
+                                        'return_code': 0,
+                                        'stdout': 'output',
+                                        'stderr': 'error',
+                                    },
+                                    None)
+        self._init_runner()
+        result = self._runner._run_ps_or_raise('Get-ChildItem', 'my error message')
+        self.assertEquals(result, {
+            'failed': False,
+            'succeeded': True,
+            'return_code': 0,
+            'stdout': 'output',
+            'stderr': 'error',
+        })
+
+    @mock.patch('winrm_runner.winrm_base.WinRmBaseRunner._run_ps')
+    def test__run_ps_or_raise_raises_on_failure(self, mock_run_ps):
+        mock_run_ps.return_value = ('success',
+                                    {
+                                        'failed': True,
+                                        'succeeded': False,
+                                        'return_code': 1,
+                                        'stdout': 'output',
+                                        'stderr': 'error',
+                                    },
+                                    None)
+        self._init_runner()
+        with self.assertRaises(RuntimeError):
+            self._runner._run_ps_or_raise('Get-ChildItem', 'my error message')
+
     def test_multireplace(self):
         multireplace_map = {'a': 'x',
                             'c': 'y',
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
--- 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
@@ -54,7 +54,7 @@ def test_run(self, mock_run_ps, mock_get_script_args):
         result = self._runner.run({})
 
         self.assertEquals(result, 'expected')
-        mock_run_ps.assert_called_with('''& {[CmdletBinding()]
+        mock_run_ps.assert_called_with('''[CmdletBinding()]
 Param(
   [bool]$p_bool,
   [int]$p_integer,
@@ -77,4 +77,5 @@ def test_run(self, mock_run_ps, mock_get_script_args):
 Write-Output "p_obj = $($p_obj | ConvertTo-Json -Compress)"
 Write-Output "p_pos0 = $p_pos0"
 Write-Output "p_pos1 = $p_pos1"
-} -d @{"test" = @("`r", $true, 3)} 1 "abc"''')
+''',
+                                       '-d @{"test" = @("`r", $true, 3)} 1 "abc"')