Test Setup Failed
Push — master ( 9577ff...5130c5 )
by Tomaz
02:17 queued 10s
created

windows_runner/windows_script_runner.py (2 issues)

Labels
Severity
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
from __future__ import absolute_import
17
import os
18
import re
19
import uuid
20
21
import six
22
23
from eventlet.green import subprocess
24
25
from st2common import log as logging
26
from st2common.util.green.shell import run_command
27
from st2common.util.shell import quote_windows
28
from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED
29
from st2common.constants.action import LIVEACTION_STATUS_FAILED
30
from st2common.constants.action import LIVEACTION_STATUS_TIMED_OUT
31
from st2common.constants.runners import WINDOWS_RUNNER_DEFAULT_ACTION_TIMEOUT
32
from st2common.runners.base import ShellRunnerMixin
33
from st2common.runners.base import get_metadata as get_runner_metadata
34
35
from windows_runner.base import BaseWindowsRunner
36
37
__all__ = [
38
    'WindowsScriptRunner',
39
40
    'get_runner',
41
    'get_metadata',
42
]
43
44
LOG = logging.getLogger(__name__)
45
46
PATH_SEPARATOR = '\\'
47
48
# constants to lookup in runner_parameters
49
RUNNER_HOST = 'host'
50
RUNNER_USERNAME = 'username'
51
RUNNER_PASSWORD = 'password'
52
RUNNER_COMMAND = 'cmd'
53
RUNNER_TIMEOUT = 'timeout'
54
RUNNER_SHARE_NAME = 'share'
55
56
# Timeouts for different steps
57
UPLOAD_FILE_TIMEOUT = 30
58
CREATE_DIRECTORY_TIMEOUT = 10
59
DELETE_FILE_TIMEOUT = 10
60
DELETE_DIRECTORY_TIMEOUT = 10
61
62
POWERSHELL_COMMAND = 'powershell.exe -InputFormat None'
63
64
65
class WindowsScriptRunner(BaseWindowsRunner, ShellRunnerMixin):
66
    """
67
    Runner which executes PowerShell scripts on a remote Windows machine.
68
    """
69
70
    def __init__(self, runner_id, timeout=WINDOWS_RUNNER_DEFAULT_ACTION_TIMEOUT):
71
        """
72
        :param timeout: Action execution timeout in seconds.
73
        :type timeout: ``int``
74
        """
75
        super(WindowsScriptRunner, self).__init__(runner_id=runner_id)
76
        self._timeout = timeout
77
78
    def pre_run(self):
79
        super(WindowsScriptRunner, self).pre_run()
80
81
        # TODO :This is awful, but the way "runner_parameters" and other variables get
82
        # assigned on the runner instance is even worse. Those arguments should
83
        # be passed to the constructor.
84
        self._host = self.runner_parameters.get(RUNNER_HOST, None)
85
        self._username = self.runner_parameters.get(RUNNER_USERNAME, None)
86
        self._password = self.runner_parameters.get(RUNNER_PASSWORD, None)
87
        self._command = self.runner_parameters.get(RUNNER_COMMAND, None)
88
        self._timeout = self.runner_parameters.get(RUNNER_TIMEOUT, self._timeout)
89
90
        self._share = self.runner_parameters.get(RUNNER_SHARE_NAME, 'C$')
91
92
    def run(self, action_parameters):
93
        # Make sure the dependencies are available
94
        self._verify_winexe_exists()
95
        self._verify_smbclient_exists()
96
97
        # Parse arguments, if any
98
        pos_args, named_args = self._get_script_args(action_parameters)
99
        args = self._get_script_arguments(named_args=named_args, positional_args=pos_args)
100
101
        # 1. Retrieve full absolute path for the share name
102
        # TODO: Cache resolved paths
103
        base_path = self._get_share_absolute_path(share=self._share)
104
105
        # 2. Upload script file to a temporary location
106
        local_path = self.entry_point
107
        script_path, temporary_directory_path = self._upload_file(local_path=local_path,
108
                                                                  base_path=base_path)
109
110
        # 3. Execute the script
111
        exit_code, stdout, stderr, timed_out = self._run_script(script_path=script_path,
112
                                                                arguments=args)
113
114
        # 4. Delete temporary directory
115
        self._delete_directory(directory_path=temporary_directory_path)
116
117
        succeeded = (exit_code == 0)
118
119
        if timed_out:
120
            succeeded = False
121
            error = 'Action failed to complete in %s seconds' % (self._timeout)
122
123
            winexe_error = self._parse_winexe_error(stdout=stdout, stderr=stderr)
124
125
            if winexe_error:
126
                error += ': %s' % (winexe_error)
127
        else:
128
            error = None
129
130
        if not succeeded and not timed_out:
131
            error = self._parse_winexe_error()
0 ignored issues
show
It seems like a value for argument stdout is missing in the method call.
Loading history...
It seems like a value for argument stderr is missing in the method call.
Loading history...
132
133
        output = {
134
            'stdout': stdout,
135
            'stderr': stderr,
136
            'return_code': exit_code,
137
            'succeeded': succeeded,
138
            'failed': not succeeded
139
        }
140
141
        if error:
142
            output['error'] = error
143
144
        if timed_out:
145
            status = LIVEACTION_STATUS_TIMED_OUT
146
        else:
147
            status = LIVEACTION_STATUS_SUCCEEDED if exit_code == 0 else LIVEACTION_STATUS_FAILED
148
149
        return (status, output, None)
150
151
    def _run_script(self, script_path, arguments=None):
152
        """
153
        :param script_path: Full path to the script on the remote server.
154
        :type script_path: ``str``
155
156
        :param arguments: The arguments to pass to the script.
157
        :type arguments: ``str``
158
        """
159
        if arguments is not None:
160
            command = '%s %s %s' % (POWERSHELL_COMMAND, quote_windows(script_path), arguments)
161
        else:
162
            command = '%s %s' % (POWERSHELL_COMMAND, quote_windows(script_path))
163
        args = self._get_winexe_command_args(host=self._host, username=self._username,
164
                                             password=self._password,
165
                                             command=command)
166
167
        LOG.debug('Running script "%s"' % (script_path))
168
169
        # Note: We don't send anything over stdin, we just create an unused pipe
170
        # to avoid some obscure failures
171
        exit_code, stdout, stderr, timed_out = run_command(cmd=args,
172
                                                           stdin=subprocess.PIPE,
173
                                                           stdout=subprocess.PIPE,
174
                                                           stderr=subprocess.PIPE,
175
                                                           shell=False,
176
                                                           timeout=self._timeout)
177
178
        extra = {'exit_code': exit_code, 'stdout': stdout, 'stderr': stderr}
179
        LOG.debug('Command returned', extra=extra)
180
181
        return exit_code, stdout, stderr, timed_out
182
183
    def _get_script_arguments(self, named_args=None, positional_args=None):
184
        """
185
        Builds a string of named and positional arguments in PowerShell format,
186
        which are passed to the script.
187
188
        :param named_args: Dictionary with named arguments
189
        :type named_args: ``dict``.
190
191
        :param positional_args: List of positional arguments
192
        :type positional_args: ``str``
193
194
        :rtype: ``str``
195
        """
196
        cmd_parts = []
197
        if positional_args:
198
            cmd_parts.append(positional_args)
199
        if named_args:
200
            for (arg, value) in six.iteritems(named_args):
201
                arg = quote_windows(arg)
202
                if value is None or (isinstance(value, six.string_types) and len(value) < 1):
203
                    LOG.debug('Ignoring arg %s as its value is %s.', arg, value)
204
                    continue
205
                if isinstance(value, bool):
206
                    if value:
207
                        cmd_parts.append('-%s' % (arg))
208
                    else:
209
                        cmd_parts.append('-%s:$false' % (arg))
210
                elif isinstance(value, (list, tuple)):
211
                    # Array support, pass parameters to shell script
212
                    cmd_parts.append('-%s %s' % (arg, ','.join(value)))
213
                else:
214
                    cmd_parts.append('-%s %s' % (arg, quote_windows(str(value))))
215
        return ' '.join(cmd_parts)
216
217
    def _upload_file(self, local_path, base_path):
218
        """
219
        Upload provided file to the remote server in a temporary directory.
220
221
        :param local_path: Local path to the file to upload.
222
        :type local_path: ``str``
223
224
        :param base_path: Absolute base path for the share.
225
        :type base_path: ``str``
226
        """
227
        file_name = os.path.basename(local_path)
228
229
        temporary_directory_name = str(uuid.uuid4())
230
        command = 'mkdir %s' % (quote_windows(temporary_directory_name))
231
232
        # 1. Create a temporary dir for out scripts (ignore errors if it already exists)
233
        # Note: We don't necessary have access to $TEMP so we create a temporary directory for our
234
        # us in the root of the share we are using and have access to
235
        args = self._get_smbclient_command_args(host=self._host, username=self._username,
236
                                                password=self._password, command=command,
237
                                                share=self._share)
238
239
        LOG.debug('Creating temp directory "%s"' % (temporary_directory_name))
240
241
        exit_code, stdout, stderr, timed_out = run_command(cmd=args, stdout=subprocess.PIPE,
242
                                                           stderr=subprocess.PIPE, shell=False,
243
                                                           timeout=CREATE_DIRECTORY_TIMEOUT)
244
245
        extra = {'exit_code': exit_code, 'stdout': stdout, 'stderr': stderr}
246
        LOG.debug('Directory created', extra=extra)
247
248
        # 2. Upload file to temporary directory
249
        remote_path = PATH_SEPARATOR.join([temporary_directory_name, file_name])
250
251
        values = {
252
            'local_path': quote_windows(local_path),
253
            'remote_path': quote_windows(remote_path)
254
        }
255
        command = 'put %(local_path)s %(remote_path)s' % values
256
        args = self._get_smbclient_command_args(host=self._host, username=self._username,
257
                                                password=self._password, command=command,
258
                                                share=self._share)
259
260
        extra = {'local_path': local_path, 'remote_path': remote_path}
261
        LOG.debug('Uploading file to "%s"' % (remote_path))
262
263
        exit_code, stdout, stderr, timed_out = run_command(cmd=args, stdout=subprocess.PIPE,
264
                                                           stderr=subprocess.PIPE, shell=False,
265
                                                           timeout=UPLOAD_FILE_TIMEOUT)
266
267
        extra = {'exit_code': exit_code, 'stdout': stdout, 'stderr': stderr}
268
        LOG.debug('File uploaded to "%s"' % (remote_path), extra=extra)
269
270
        full_remote_file_path = base_path + '\\' + remote_path
271
        full_temporary_directory_path = base_path + '\\' + temporary_directory_name
272
273
        return full_remote_file_path, full_temporary_directory_path
274
275
    def _get_share_absolute_path(self, share):
276
        """
277
        Retrieve full absolute path for a share with the provided name.
278
279
        :param share: Share name.
280
        :type share: ``str``
281
        """
282
        command = 'net share %s' % (quote_windows(share))
283
        args = self._get_winexe_command_args(host=self._host, username=self._username,
284
                                             password=self._password,
285
                                             command=command)
286
287
        LOG.debug('Retrieving full absolute path for share "%s"' % (share))
288
        exit_code, stdout, stderr, timed_out = run_command(cmd=args, stdout=subprocess.PIPE,
289
                                                           stderr=subprocess.PIPE, shell=False,
290
                                                           timeout=self._timeout)
291
292
        if exit_code != 0:
293
            msg = 'Failed to retrieve absolute path for share "%s"' % (share)
294
            raise Exception(msg)
295
296
        share_info = self._parse_share_information(stdout=stdout)
297
        share_path = share_info.get('path', None)
298
299
        if not share_path:
300
            msg = 'Failed to retrieve absolute path for share "%s"' % (share)
301
            raise Exception(msg)
302
303
        return share_path
304
305
    def _parse_share_information(self, stdout):
306
        """
307
        Parse share information retrieved using "net share <share name>".
308
309
        :rtype: ``dict``
310
        """
311
        lines = stdout.split('\n')
312
313
        result = {}
314
315
        for line in lines:
316
            line = line.strip()
317
            split = re.split(r'\s{3,}', line)
318
319
            if len(split) not in [1, 2]:
320
                # Invalid line, skip it
321
                continue
322
323
            key = split[0]
324
            key = key.lower().replace(' ', '_')
325
326
            if len(split) == 2:
327
                value = split[1].strip()
328
            else:
329
                value = None
330
331
            result[key] = value
332
333
        return result
334
335
    def _delete_file(self, file_path):
336
        command = 'rm %(file_path)s' % {'file_path': quote_windows(file_path)}
337
        args = self._get_smbclient_command_args(host=self._host, username=self._username,
338
                                                password=self._password, command=command,
339
                                                share=self._share)
340
341
        exit_code, _, _, _ = run_command(cmd=args, stdout=subprocess.PIPE,
342
                                         stderr=subprocess.PIPE, shell=False,
343
                                         timeout=DELETE_FILE_TIMEOUT)
344
345
        return exit_code == 0
346
347
    def _delete_directory(self, directory_path):
348
        command = 'rmdir %(directory_path)s' % {'directory_path': quote_windows(directory_path)}
349
        args = self._get_smbclient_command_args(host=self._host, username=self._username,
350
                                                password=self._password, command=command,
351
                                                share=self._share)
352
353
        LOG.debug('Removing directory "%s"' % (directory_path))
354
        exit_code, _, _, _ = run_command(cmd=args, stdout=subprocess.PIPE,
355
                                         stderr=subprocess.PIPE, shell=False,
356
                                         timeout=DELETE_DIRECTORY_TIMEOUT)
357
358
        return exit_code == 0
359
360
361
def get_runner():
362
    return WindowsScriptRunner(str(uuid.uuid4()))
363
364
365
def get_metadata():
366
    metadata = get_runner_metadata('windows_runner')
367
    metadata = [runner for runner in metadata if
368
                runner['runner_module'] == __name__.split('.')[-1]][0]
369
    return metadata
370