Passed
Push — master ( 7dda3e...d8a330 )
by
unknown
04:06
created

ShellCommandAction._get_error_result()   A

Complexity

Conditions 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 19
rs 9.4285
1
# -*- coding: utf-8 -*-
2
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
3
# contributor license agreements.  See the NOTICE file distributed with
4
# this work for additional information regarding copyright ownership.
5
# The ASF licenses this file to You under the Apache License, Version 2.0
6
# (the "License"); you may not use this file except in compliance with
7
# the License.  You may obtain a copy of the License at
8
#
9
#     http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
# See the License for the specific language governing permissions and
15
# limitations under the License.
16
# pylint: disable=not-context-manager
17
18
import os
19
import pwd
20
import six
21
import sys
22
import copy
23
import traceback
24
import collections
25
26
from oslo_config import cfg
27
28
from st2common import log as logging
29
from st2common.models.base import DictSerializableClassMixin
30
from st2common.util.shell import quote_unix
31
from st2common.constants.action import LIBS_DIR as ACTION_LIBS_DIR
32
from st2common.util.secrets import get_secret_parameters
33
from st2common.util.secrets import mask_secret_parameters
34
from st2common.constants.secrets import MASKED_ATTRIBUTE_VALUE
35
36
__all__ = [
37
    'ShellCommandAction',
38
    'ShellScriptAction',
39
    'RemoteAction',
40
    'RemoteScriptAction',
41
    'ResolvedActionParameters'
42
]
43
44
LOG = logging.getLogger(__name__)
45
46
LOGGED_USER_USERNAME = pwd.getpwuid(os.getuid())[0]
47
48
# Flags which are passed to every sudo invocation
49
SUDO_COMMON_OPTIONS = [
50
    '-E'  # we want to preserve the environment of the user which ran sudo
51
]
52
53
# Flags which are only passed to sudo when not running as current user and when
54
# -u flag is used
55
SUDO_DIFFERENT_USER_OPTIONS = [
56
    '-H'  # we want $HOME to reflect the home directory of the requested / target user
57
]
58
59
60
class ShellCommandAction(object):
61
    EXPORT_CMD = 'export'
62
63
    def __init__(self, name, action_exec_id, command, user, env_vars=None, sudo=False,
64
                 timeout=None, cwd=None, sudo_password=None):
65
        self.name = name
66
        self.action_exec_id = action_exec_id
67
        self.command = command
68
        self.env_vars = env_vars or {}
69
        self.user = user
70
        self.sudo = sudo
71
        self.timeout = timeout
72
        self.cwd = cwd
73
        self.sudo_password = sudo_password
74
75
    def get_full_command_string(self):
76
        # Note: We pass -E to sudo because we want to preserve user provided environment variables
77
        if self.sudo:
78
            command = quote_unix(self.command)
79
            sudo_arguments = ' '.join(self._get_common_sudo_arguments())
80
            command = 'sudo %s -- bash -c %s' % (sudo_arguments, command)
81
        else:
82
            if self.user and self.user != LOGGED_USER_USERNAME:
83
                # Need to use sudo to run as a different (requested) user
84
                user = quote_unix(self.user)
85
                sudo_arguments = ' '.join(self._get_user_sudo_arguments(user=user))
86
                command = quote_unix(self.command)
87
                command = 'sudo %s -- bash -c %s' % (sudo_arguments, command)
88
89
            else:
90
                command = self.command
91
92
        return command
93
94
    def get_sanitized_full_command_string(self):
95
        """
96
        Get a command string which can be used inside the log messages (if provided, sudo password
97
        is masked).
98
99
        :rtype: ``password``
100
        """
101
        command_string = self.get_full_command_string()
102
103
        if self.sudo_password:
104
            # Mask sudo password
105
            command_string = 'echo -e \'%s\n\' | %s' % (MASKED_ATTRIBUTE_VALUE, command_string)
106
107
        return command_string
108
109
    def get_timeout(self):
110
        return self.timeout
111
112
    def get_cwd(self):
113
        return self.cwd
114
115
    def _get_common_sudo_arguments(self):
116
        """
117
        Retrieve a list of flags which are passed to sudo on every invocation.
118
119
        :rtype: ``list``
120
        """
121
        flags = []
122
123
        if self.sudo_password:
124
            # Note: We use subprocess.Popen in local runner so we provide password via subprocess
125
            # stdin (using echo -e won't work when using subprocess.Popen)
126
            flags.append('-S')
127
128
        flags = flags + SUDO_COMMON_OPTIONS
129
130
        return flags
131
132
    def _get_user_sudo_arguments(self, user):
133
        """
134
        Retrieve a list of flags which are passed to sudo when running as a different user and "-u"
135
        flag is used.
136
137
        :rtype: ``list``
138
        """
139
        flags = self._get_common_sudo_arguments()
140
        flags += SUDO_DIFFERENT_USER_OPTIONS
141
        flags += ['-u', user]
142
143
        return flags
144
145
    def _get_env_vars_export_string(self):
146
        if self.env_vars:
147
            env_vars = copy.copy(self.env_vars)
148
149
            # If sudo_password is provided, explicitly disable bash history to make sure password
150
            # is not logged, because password is provided via command line
151
            if self.sudo and self.sudo_password:
152
                env_vars['HISTFILE'] = '/dev/null'
153
                env_vars['HISTSIZE'] = '0'
154
155
            # Sort the dict to guarantee consistent order
156
            env_vars = collections.OrderedDict(sorted(env_vars.items()))
157
158
            # Environment variables could contain spaces and open us to shell
159
            # injection attacks. Always quote the key and the value.
160
            exports = ' '.join(
161
                '%s=%s' % (quote_unix(k), quote_unix(v))
162
                for k, v in env_vars.iteritems()
163
            )
164
            shell_env_str = '%s %s' % (ShellCommandAction.EXPORT_CMD, exports)
165
        else:
166
            shell_env_str = ''
167
168
        return shell_env_str
169
170
    def _get_command_string(self, cmd, args):
171
        """
172
        Escape the command arguments and form a command string.
173
174
        :type cmd: ``str``
175
        :type args: ``list``
176
177
        :rtype: ``str``
178
        """
179
        assert isinstance(args, (list, tuple))
180
181
        args = [quote_unix(arg) for arg in args]
182
        args = ' '.join(args)
183
        result = '%s %s' % (cmd, args)
184
        return result
185
186
    def _get_error_result(self):
187
        """
188
        Prepares a structured error result based on the exception.
189
190
        :type e: ``Exception``
191
192
        :rtype: ``dict``
193
        """
194
        _, exc_value, exc_traceback = sys.exc_info()
195
196
        exc_value = str(exc_value)
197
        exc_traceback = ''.join(traceback.format_tb(exc_traceback))
198
199
        result = {}
200
        result['failed'] = True
201
        result['succeeded'] = False
202
        result['error'] = exc_value
203
        result['traceback'] = exc_traceback
204
        return result
205
206
207
class ShellScriptAction(ShellCommandAction):
208
    def __init__(self, name, action_exec_id, script_local_path_abs, named_args=None,
209
                 positional_args=None, env_vars=None, user=None, sudo=False, timeout=None,
210
                 cwd=None, sudo_password=None):
211
        super(ShellScriptAction, self).__init__(name=name, action_exec_id=action_exec_id,
212
                                                command=None, user=user, env_vars=env_vars,
213
                                                sudo=sudo, timeout=timeout,
214
                                                cwd=cwd, sudo_password=sudo_password)
215
        self.script_local_path_abs = script_local_path_abs
216
        self.named_args = named_args
217
        self.positional_args = positional_args
218
219
    def get_full_command_string(self):
220
        return self._format_command()
221
222
    def _format_command(self):
223
        script_arguments = self._get_script_arguments(named_args=self.named_args,
224
                                                      positional_args=self.positional_args)
225
        if self.sudo:
226
            if script_arguments:
227
                command = quote_unix('%s %s' % (self.script_local_path_abs, script_arguments))
228
            else:
229
                command = quote_unix(self.script_local_path_abs)
230
231
            sudo_arguments = ' '.join(self._get_common_sudo_arguments())
232
            command = 'sudo %s -- bash -c %s' % (sudo_arguments, command)
233
        else:
234
            if self.user and self.user != LOGGED_USER_USERNAME:
235
                # Need to use sudo to run as a different user
236
                user = quote_unix(self.user)
237
238
                if script_arguments:
239
                    command = quote_unix('%s %s' % (self.script_local_path_abs, script_arguments))
240
                else:
241
                    command = quote_unix(self.script_local_path_abs)
242
243
                sudo_arguments = ' '.join(self._get_user_sudo_arguments(user=user))
244
                command = 'sudo %s -- bash -c %s' % (sudo_arguments, command)
245
            else:
246
                script_path = quote_unix(self.script_local_path_abs)
247
248
                if script_arguments:
249
                    command = '%s %s' % (script_path, script_arguments)
250
                else:
251
                    command = script_path
252
        return command
253
254
    def _get_script_arguments(self, named_args=None, positional_args=None):
255
        """
256
        Build a string of named and positional arguments which are passed to the
257
        script.
258
259
        :param named_args: Dictionary with named arguments.
260
        :type named_args: ``dict``.
261
262
        :param positional_args: List with positional arguments.
263
        :type positional_args: ``dict``.
264
265
        :rtype: ``str``
266
        """
267
        command_parts = []
268
269
        # add all named_args in the format <kwarg_op>name=value (e.g. --name=value)
270
        if named_args is not None:
271
            for (arg, value) in six.iteritems(named_args):
272
                if value is None or (isinstance(value, (str, unicode)) and len(value) < 1):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'unicode'
Loading history...
273
                    LOG.debug('Ignoring arg %s as its value is %s.', arg, value)
274
                    continue
275
276
                if isinstance(value, bool):
277
                    if value is True:
278
                        command_parts.append(arg)
279
                else:
280
                    values = (quote_unix(arg), quote_unix(six.text_type(value)))
281
                    command_parts.append(six.text_type('%s=%s' % values))
282
283
        # add the positional args
284
        if positional_args:
285
            quoted_pos_args = [quote_unix(pos_arg) for pos_arg in positional_args]
286
            pos_args_string = ' '.join(quoted_pos_args)
287
            command_parts.append(pos_args_string)
288
        return ' '.join(command_parts)
289
290
291
class SSHCommandAction(ShellCommandAction):
292
    def __init__(self, name, action_exec_id, command, env_vars, user, password=None, pkey=None,
293
                 hosts=None, parallel=True, sudo=False, timeout=None, cwd=None, passphrase=None,
294
                 sudo_password=None):
295
        super(SSHCommandAction, self).__init__(name=name, action_exec_id=action_exec_id,
296
                                               command=command, env_vars=env_vars, user=user,
297
                                               sudo=sudo, timeout=timeout, cwd=cwd,
298
                                               sudo_password=sudo_password)
299
        self.hosts = hosts
300
        self.parallel = parallel
301
        self.pkey = pkey
302
        self.passphrase = passphrase
303
        self.password = password
304
305
    def is_parallel(self):
306
        return self.parallel
307
308
    def is_sudo(self):
309
        return self.sudo
310
311
    def get_user(self):
312
        return self.user
313
314
    def get_hosts(self):
315
        return self.hosts
316
317
    def is_pkey_authentication(self):
318
        return self.pkey is not None
319
320
    def get_pkey(self):
321
        return self.pkey
322
323
    def get_password(self):
324
        return self.password
325
326
    def get_command(self):
327
        return self.command
328
329
    def __str__(self):
330
        str_rep = []
331
        str_rep.append('%s@%s(name: %s' % (self.__class__.__name__, id(self), self.name))
332
        str_rep.append('id: %s' % self.action_exec_id)
333
        str_rep.append('command: %s' % self.command)
334
        str_rep.append('user: %s' % self.user)
335
        str_rep.append('sudo: %s' % str(self.sudo))
336
        str_rep.append('parallel: %s' % str(self.parallel))
337
        str_rep.append('hosts: %s)' % str(self.hosts))
338
        return ', '.join(str_rep)
339
340
341
class RemoteAction(SSHCommandAction):
342
    def __init__(self, name, action_exec_id, command, env_vars=None, on_behalf_user=None,
343
                 user=None, password=None, private_key=None, hosts=None, parallel=True, sudo=False,
344
                 timeout=None, cwd=None, passphrase=None, sudo_password=None):
345
        super(RemoteAction, self).__init__(name=name, action_exec_id=action_exec_id,
346
                                           command=command, env_vars=env_vars, user=user,
347
                                           hosts=hosts, parallel=parallel, sudo=sudo,
348
                                           timeout=timeout, cwd=cwd, passphrase=passphrase,
349
                                           sudo_password=sudo_password)
350
        self.password = password
351
        self.private_key = private_key
352
        self.passphrase = passphrase
353
        self.on_behalf_user = on_behalf_user  # Used for audit purposes.
354
        self.timeout = timeout
355
356
    def get_on_behalf_user(self):
357
        return self.on_behalf_user
358
359
    def __str__(self):
360
        str_rep = []
361
        str_rep.append('%s@%s(name: %s' % (self.__class__.__name__, id(self), self.name))
362
        str_rep.append('id: %s' % self.action_exec_id)
363
        str_rep.append('command: %s' % self.command)
364
        str_rep.append('user: %s' % self.user)
365
        str_rep.append('on_behalf_user: %s' % self.on_behalf_user)
366
        str_rep.append('sudo: %s' % str(self.sudo))
367
        str_rep.append('parallel: %s' % str(self.parallel))
368
        str_rep.append('hosts: %s)' % str(self.hosts))
369
        str_rep.append('timeout: %s)' % str(self.timeout))
370
371
        return ', '.join(str_rep)
372
373
374
class RemoteScriptAction(ShellScriptAction):
375
    def __init__(self, name, action_exec_id, script_local_path_abs, script_local_libs_path_abs,
376
                 named_args=None, positional_args=None, env_vars=None, on_behalf_user=None,
377
                 user=None, password=None, private_key=None, remote_dir=None, hosts=None,
378
                 parallel=True, sudo=False, timeout=None, cwd=None, sudo_password=None):
379
        super(RemoteScriptAction, self).__init__(name=name, action_exec_id=action_exec_id,
380
                                                 script_local_path_abs=script_local_path_abs,
381
                                                 user=user,
382
                                                 named_args=named_args,
383
                                                 positional_args=positional_args, env_vars=env_vars,
384
                                                 sudo=sudo, timeout=timeout, cwd=cwd,
385
                                                 sudo_password=sudo_password)
386
        self.script_local_libs_path_abs = script_local_libs_path_abs
387
        self.script_local_dir, self.script_name = os.path.split(self.script_local_path_abs)
388
        self.remote_dir = remote_dir if remote_dir is not None else '/tmp'
389
        self.remote_libs_path_abs = os.path.join(self.remote_dir, ACTION_LIBS_DIR)
390
        self.on_behalf_user = on_behalf_user
391
        self.password = password
392
        self.private_key = private_key
393
        self.remote_script = os.path.join(self.remote_dir, quote_unix(self.script_name))
394
        self.hosts = hosts
395
        self.parallel = parallel
396
        self.command = self._format_command()
397
        LOG.debug('RemoteScriptAction: command to run on remote box: %s', self.command)
398
399
    def get_remote_script_abs_path(self):
400
        return self.remote_script
401
402
    def get_local_script_abs_path(self):
403
        return self.script_local_path_abs
404
405
    def get_remote_libs_path_abs(self):
406
        return self.remote_libs_path_abs
407
408
    def get_local_libs_path_abs(self):
409
        return self.script_local_libs_path_abs
410
411
    def get_remote_base_dir(self):
412
        return self.remote_dir
413
414
    def _format_command(self):
415
        script_arguments = self._get_script_arguments(named_args=self.named_args,
416
                                                      positional_args=self.positional_args)
417
418
        if script_arguments:
419
            command = '%s %s' % (self.remote_script, script_arguments)
420
        else:
421
            command = self.remote_script
422
423
        return command
424
425
    def __str__(self):
426
        str_rep = []
427
        str_rep.append('%s@%s(name: %s' % (self.__class__.__name__, id(self), self.name))
428
        str_rep.append('id: %s' % self.action_exec_id)
429
        str_rep.append('local_script: %s' % self.script_local_path_abs)
430
        str_rep.append('local_libs: %s' % self.script_local_libs_path_abs)
431
        str_rep.append('remote_dir: %s' % self.remote_dir)
432
        str_rep.append('remote_libs: %s' % self.remote_libs_path_abs)
433
        str_rep.append('named_args: %s' % self.named_args)
434
        str_rep.append('positional_args: %s' % self.positional_args)
435
        str_rep.append('user: %s' % self.user)
436
        str_rep.append('on_behalf_user: %s' % self.on_behalf_user)
437
        str_rep.append('sudo: %s' % self.sudo)
438
        str_rep.append('parallel: %s' % self.parallel)
439
        str_rep.append('hosts: %s)' % self.hosts)
440
441
        return ', '.join(str_rep)
442
443
444
class ResolvedActionParameters(DictSerializableClassMixin):
445
    """
446
    Class which contains resolved runner and action parameters for a particular action.
447
    """
448
449
    def __init__(self, action_db, runner_type_db, runner_parameters=None, action_parameters=None):
450
        self._action_db = action_db
451
        self._runner_type_db = runner_type_db
452
        self._runner_parameters = runner_parameters
453
        self._action_parameters = action_parameters
454
455
    def mask_secrets(self, value):
456
        result = copy.deepcopy(value)
457
458
        runner_parameters = result['runner_parameters']
459
        action_parameters = result['action_parameters']
460
461
        runner_parameters_specs = self._runner_type_db.runner_parameters
462
        action_parameters_sepcs = self._action_db.parameters
463
464
        secret_runner_parameters = get_secret_parameters(parameters=runner_parameters_specs)
465
        secret_action_parameters = get_secret_parameters(parameters=action_parameters_sepcs)
466
467
        runner_parameters = mask_secret_parameters(parameters=runner_parameters,
468
                                                   secret_parameters=secret_runner_parameters)
469
        action_parameters = mask_secret_parameters(parameters=action_parameters,
470
                                                   secret_parameters=secret_action_parameters)
471
        result['runner_parameters'] = runner_parameters
472
        result['action_parameters'] = action_parameters
473
474
        return result
475
476
    def to_serializable_dict(self, mask_secrets=False):
477
        result = {}
478
        result['runner_parameters'] = self._runner_parameters
479
        result['action_parameters'] = self._action_parameters
480
481
        if mask_secrets and cfg.CONF.log.mask_secrets:
482
            result = self.mask_secrets(value=result)
483
484
        return result
485