Passed
Push — develop ( fdd284...466107 )
by Plexxi
07:24 queued 03:29
created

FabricRemoteAction   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 137
Duplicated Lines 0 %
Metric Value
dl 0
loc 137
rs 10
wmc 27

1 Method

Rating   Name   Duplication   Size   Complexity  
A ResolvedActionParameters.to_serializable_dict() 0 9 3
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
# pylint: disable=not-context-manager
16
17
import os
18
import pwd
19
import six
20
import sys
21
import copy
22
import traceback
23
24
from oslo_config import cfg
25
26
from st2common import log as logging
27
from st2common.models.base import DictSerializableClassMixin
28
from st2common.util.shell import quote_unix
29
from st2common.constants.action import LIBS_DIR as ACTION_LIBS_DIR
30
from st2common.util.secrets import get_secret_parameters
31
from st2common.util.secrets import mask_secret_parameters
32
33
__all__ = [
34
    'ShellCommandAction',
35
    'ShellScriptAction',
36
    'RemoteAction',
37
    'RemoteScriptAction',
38
    'ResolvedActionParameters'
39
]
40
41
LOG = logging.getLogger(__name__)
42
43
LOGGED_USER_USERNAME = pwd.getpwuid(os.getuid())[0]
44
45
# Flags which are passed to every sudo invocation
46
SUDO_COMMON_OPTIONS = [
47
    '-E'  # we want to preserve the environment of the user which ran sudo
48
]
49
50
# Flags which are only passed to sudo when not running as current user and when
51
# -u flag is used
52
SUDO_DIFFERENT_USER_OPTIONS = [
53
    '-H'  # we want $HOME to reflect the home directory of the requested / target user
54
]
55
56
57
class ShellCommandAction(object):
58
    EXPORT_CMD = 'export'
59
60
    def __init__(self, name, action_exec_id, command, user, env_vars=None, sudo=False,
61
                 timeout=None, cwd=None):
62
        self.name = name
63
        self.action_exec_id = action_exec_id
64
        self.command = command
65
        self.env_vars = env_vars or {}
66
        self.user = user
67
        self.sudo = sudo
68
        self.timeout = timeout
69
        self.cwd = cwd
70
71
    def get_full_command_string(self):
72
        # Note: We pass -E to sudo because we want to preserve user provided environment variables
73
        if self.sudo:
74
            command = quote_unix(self.command)
75
            sudo_arguments = ' '.join(self._get_common_sudo_arguments())
76
            command = 'sudo %s -- bash -c %s' % (sudo_arguments, command)
77
        else:
78
            if self.user and self.user != LOGGED_USER_USERNAME:
79
                # Need to use sudo to run as a different (requested) user
80
                user = quote_unix(self.user)
81
                sudo_arguments = ' '.join(self._get_user_sudo_arguments(user=user))
82
                command = quote_unix(self.command)
83
                command = 'sudo %s -- bash -c %s' % (sudo_arguments, command)
84
            else:
85
                command = self.command
86
87
        return command
88
89
    def get_timeout(self):
90
        return self.timeout
91
92
    def get_cwd(self):
93
        return self.cwd
94
95
    def _get_common_sudo_arguments(self):
96
        """
97
        Retrieve a list of flags which are passed to sudo on every invocation.
98
99
        :rtype: ``list``
100
        """
101
        flags = copy.copy(SUDO_COMMON_OPTIONS)
102
        return flags
103
104
    def _get_user_sudo_arguments(self, user):
105
        """
106
        Retrieve a list of flags which are passed to sudo when running as a different user and "-u"
107
        flag is used.
108
109
        :rtype: ``list``
110
        """
111
        flags = self._get_common_sudo_arguments()
112
        flags += copy.copy(SUDO_DIFFERENT_USER_OPTIONS)
113
        flags += ['-u', user]
114
        return flags
115
116
    def _get_env_vars_export_string(self):
117
        if self.env_vars:
118
            # Envrionment variables could contain spaces and open us to shell
119
            # injection attacks. Always quote the key and the value.
120
            exports = ' '.join(
121
                '%s=%s' % (quote_unix(k), quote_unix(v))
122
                for k, v in self.env_vars.iteritems()
123
            )
124
            shell_env_str = '%s %s' % (ShellCommandAction.EXPORT_CMD, exports)
125
        else:
126
            shell_env_str = ''
127
128
        return shell_env_str
129
130
    def _get_command_string(self, cmd, args):
131
        """
132
        Escape the command arguments and form a command string.
133
134
        :type cmd: ``str``
135
        :type args: ``list``
136
137
        :rtype: ``str``
138
        """
139
        assert isinstance(args, (list, tuple))
140
141
        args = [quote_unix(arg) for arg in args]
142
        args = ' '.join(args)
143
        result = '%s %s' % (cmd, args)
144
        return result
145
146
    def _get_error_result(self):
147
        """
148
        Prepares a structured error result based on the exception.
149
150
        :type e: ``Exception``
151
152
        :rtype: ``dict``
153
        """
154
        _, exc_value, exc_traceback = sys.exc_info()
155
156
        exc_value = str(exc_value)
157
        exc_traceback = ''.join(traceback.format_tb(exc_traceback))
158
159
        result = {}
160
        result['failed'] = True
161
        result['succeeded'] = False
162
        result['error'] = exc_value
163
        result['traceback'] = exc_traceback
164
        return result
165
166
167
class ShellScriptAction(ShellCommandAction):
168
    def __init__(self, name, action_exec_id, script_local_path_abs, named_args=None,
169
                 positional_args=None, env_vars=None, user=None, sudo=False, timeout=None,
170
                 cwd=None):
171
        super(ShellScriptAction, self).__init__(name=name, action_exec_id=action_exec_id,
172
                                                command=None, user=user, env_vars=env_vars,
173
                                                sudo=sudo, timeout=timeout, cwd=cwd)
174
        self.script_local_path_abs = script_local_path_abs
175
        self.named_args = named_args
176
        self.positional_args = positional_args
177
178
    def get_full_command_string(self):
179
        return self._format_command()
180
181
    def _format_command(self):
182
        script_arguments = self._get_script_arguments(named_args=self.named_args,
183
                                                      positional_args=self.positional_args)
184
        if self.sudo:
185
            if script_arguments:
186
                command = quote_unix('%s %s' % (self.script_local_path_abs, script_arguments))
187
            else:
188
                command = quote_unix(self.script_local_path_abs)
189
190
            sudo_arguments = ' '.join(self._get_common_sudo_arguments())
191
            command = 'sudo %s -- bash -c %s' % (sudo_arguments, command)
192
        else:
193
            if self.user and self.user != LOGGED_USER_USERNAME:
194
                # Need to use sudo to run as a different user
195
                user = quote_unix(self.user)
196
197
                if script_arguments:
198
                    command = quote_unix('%s %s' % (self.script_local_path_abs, script_arguments))
199
                else:
200
                    command = quote_unix(self.script_local_path_abs)
201
202
                sudo_arguments = ' '.join(self._get_user_sudo_arguments(user=user))
203
                command = 'sudo %s -- bash -c %s' % (sudo_arguments, command)
204
            else:
205
                script_path = quote_unix(self.script_local_path_abs)
206
207
                if script_arguments:
208
                    command = '%s %s' % (script_path, script_arguments)
209
                else:
210
                    command = script_path
211
        return command
212
213
    def _get_script_arguments(self, named_args=None, positional_args=None):
214
        """
215
        Build a string of named and positional arguments which are passed to the
216
        script.
217
218
        :param named_args: Dictionary with named arguments.
219
        :type named_args: ``dict``.
220
221
        :param positional_args: List with positional arguments.
222
        :type positional_args: ``dict``.
223
224
        :rtype: ``str``
225
        """
226
        command_parts = []
227
228
        # add all named_args in the format <kwarg_op>name=value (e.g. --name=value)
229
        if named_args is not None:
230
            for (arg, value) in six.iteritems(named_args):
231
                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...
232
                    LOG.debug('Ignoring arg %s as its value is %s.', arg, value)
233
                    continue
234
235
                if isinstance(value, bool):
236
                    if value is True:
237
                        command_parts.append(arg)
238
                else:
239
                    command_parts.append('%s=%s' % (quote_unix(arg), quote_unix(str(value))))
240
241
        # add the positional args
242
        if positional_args:
243
            quoted_pos_args = [quote_unix(pos_arg) for pos_arg in positional_args]
244
            pos_args_string = ' '.join(quoted_pos_args)
245
            command_parts.append(pos_args_string)
246
        return ' '.join(command_parts)
247
248
249
class SSHCommandAction(ShellCommandAction):
250
    def __init__(self, name, action_exec_id, command, env_vars, user, password=None, pkey=None,
251
                 hosts=None, parallel=True, sudo=False, timeout=None, cwd=None, passphrase=None):
252
        super(SSHCommandAction, self).__init__(name=name, action_exec_id=action_exec_id,
253
                                               command=command, env_vars=env_vars, user=user,
254
                                               sudo=sudo, timeout=timeout, cwd=cwd)
255
        self.hosts = hosts
256
        self.parallel = parallel
257
        self.pkey = pkey
258
        self.passphrase = passphrase
259
        self.password = password
260
261
    def is_parallel(self):
262
        return self.parallel
263
264
    def is_sudo(self):
265
        return self.sudo
266
267
    def get_user(self):
268
        return self.user
269
270
    def get_hosts(self):
271
        return self.hosts
272
273
    def is_pkey_authentication(self):
274
        return self.pkey is not None
275
276
    def get_pkey(self):
277
        return self.pkey
278
279
    def get_password(self):
280
        return self.password
281
282
    def get_command(self):
283
        return self.command
284
285
    def __str__(self):
286
        str_rep = []
287
        str_rep.append('%s@%s(name: %s' % (self.__class__.__name__, id(self), self.name))
288
        str_rep.append('id: %s' % self.action_exec_id)
289
        str_rep.append('command: %s' % self.command)
290
        str_rep.append('user: %s' % self.user)
291
        str_rep.append('sudo: %s' % str(self.sudo))
292
        str_rep.append('parallel: %s' % str(self.parallel))
293
        str_rep.append('hosts: %s)' % str(self.hosts))
294
        return ', '.join(str_rep)
295
296
297
class RemoteAction(SSHCommandAction):
298
    def __init__(self, name, action_exec_id, command, env_vars=None, on_behalf_user=None,
299
                 user=None, password=None, private_key=None, hosts=None, parallel=True, sudo=False,
300
                 timeout=None, cwd=None, passphrase=None):
301
        super(RemoteAction, self).__init__(name=name, action_exec_id=action_exec_id,
302
                                           command=command, env_vars=env_vars, user=user,
303
                                           hosts=hosts, parallel=parallel, sudo=sudo,
304
                                           timeout=timeout, cwd=cwd, passphrase=passphrase)
305
        self.password = password
306
        self.private_key = private_key
307
        self.passphrase = passphrase
308
        self.on_behalf_user = on_behalf_user  # Used for audit purposes.
309
        self.timeout = timeout
310
311
    def get_on_behalf_user(self):
312
        return self.on_behalf_user
313
314
    def __str__(self):
315
        str_rep = []
316
        str_rep.append('%s@%s(name: %s' % (self.__class__.__name__, id(self), self.name))
317
        str_rep.append('id: %s' % self.action_exec_id)
318
        str_rep.append('command: %s' % self.command)
319
        str_rep.append('user: %s' % self.user)
320
        str_rep.append('on_behalf_user: %s' % self.on_behalf_user)
321
        str_rep.append('sudo: %s' % str(self.sudo))
322
        str_rep.append('parallel: %s' % str(self.parallel))
323
        str_rep.append('hosts: %s)' % str(self.hosts))
324
        str_rep.append('timeout: %s)' % str(self.timeout))
325
326
        return ', '.join(str_rep)
327
328
329
class RemoteScriptAction(ShellScriptAction):
330
    def __init__(self, name, action_exec_id, script_local_path_abs, script_local_libs_path_abs,
331
                 named_args=None, positional_args=None, env_vars=None, on_behalf_user=None,
332
                 user=None, password=None, private_key=None, remote_dir=None, hosts=None,
333
                 parallel=True, sudo=False, timeout=None, cwd=None):
334
        super(RemoteScriptAction, self).__init__(name=name, action_exec_id=action_exec_id,
335
                                                 script_local_path_abs=script_local_path_abs,
336
                                                 user=user,
337
                                                 named_args=named_args,
338
                                                 positional_args=positional_args, env_vars=env_vars,
339
                                                 sudo=sudo, timeout=timeout, cwd=cwd)
340
        self.script_local_libs_path_abs = script_local_libs_path_abs
341
        self.script_local_dir, self.script_name = os.path.split(self.script_local_path_abs)
342
        self.remote_dir = remote_dir if remote_dir is not None else '/tmp'
343
        self.remote_libs_path_abs = os.path.join(self.remote_dir, ACTION_LIBS_DIR)
344
        self.on_behalf_user = on_behalf_user
345
        self.password = password
346
        self.private_key = private_key
347
        self.remote_script = os.path.join(self.remote_dir, quote_unix(self.script_name))
348
        self.hosts = hosts
349
        self.parallel = parallel
350
        self.command = self._format_command()
351
        LOG.debug('RemoteScriptAction: command to run on remote box: %s', self.command)
352
353
    def get_remote_script_abs_path(self):
354
        return self.remote_script
355
356
    def get_local_script_abs_path(self):
357
        return self.script_local_path_abs
358
359
    def get_remote_libs_path_abs(self):
360
        return self.remote_libs_path_abs
361
362
    def get_local_libs_path_abs(self):
363
        return self.script_local_libs_path_abs
364
365
    def get_remote_base_dir(self):
366
        return self.remote_dir
367
368
    def _format_command(self):
369
        script_arguments = self._get_script_arguments(named_args=self.named_args,
370
                                                      positional_args=self.positional_args)
371
372
        if script_arguments:
373
            command = '%s %s' % (self.remote_script, script_arguments)
374
        else:
375
            command = self.remote_script
376
377
        return command
378
379
    def __str__(self):
380
        str_rep = []
381
        str_rep.append('%s@%s(name: %s' % (self.__class__.__name__, id(self), self.name))
382
        str_rep.append('id: %s' % self.action_exec_id)
383
        str_rep.append('local_script: %s' % self.script_local_path_abs)
384
        str_rep.append('local_libs: %s' % self.script_local_libs_path_abs)
385
        str_rep.append('remote_dir: %s' % self.remote_dir)
386
        str_rep.append('remote_libs: %s' % self.remote_libs_path_abs)
387
        str_rep.append('named_args: %s' % self.named_args)
388
        str_rep.append('positional_args: %s' % self.positional_args)
389
        str_rep.append('user: %s' % self.user)
390
        str_rep.append('on_behalf_user: %s' % self.on_behalf_user)
391
        str_rep.append('sudo: %s' % self.sudo)
392
        str_rep.append('parallel: %s' % self.parallel)
393
        str_rep.append('hosts: %s)' % self.hosts)
394
395
        return ', '.join(str_rep)
396
397
398
class ResolvedActionParameters(DictSerializableClassMixin):
399
    """
400
    Class which contains resolved runner and action parameters for a particular action.
401
    """
402
403
    def __init__(self, action_db, runner_type_db, runner_parameters=None, action_parameters=None):
404
        self._action_db = action_db
405
        self._runner_type_db = runner_type_db
406
        self._runner_parameters = runner_parameters
407
        self._action_parameters = action_parameters
408
409
    def mask_secrets(self, value):
410
        result = copy.deepcopy(value)
411
412
        runner_parameters = result['runner_parameters']
413
        action_parameters = result['action_parameters']
414
415
        runner_parameters_specs = self._runner_type_db.runner_parameters
416
        action_parameters_sepcs = self._action_db.parameters
417
418
        secret_runner_parameters = get_secret_parameters(parameters=runner_parameters_specs)
419
        secret_action_parameters = get_secret_parameters(parameters=action_parameters_sepcs)
420
421
        runner_parameters = mask_secret_parameters(parameters=runner_parameters,
422
                                                   secret_parameters=secret_runner_parameters)
423
        action_parameters = mask_secret_parameters(parameters=action_parameters,
424
                                                   secret_parameters=secret_action_parameters)
425
        result['runner_parameters'] = runner_parameters
426
        result['action_parameters'] = action_parameters
427
428
        return result
429
430
    def to_serializable_dict(self, mask_secrets=False):
431
        result = {}
432
        result['runner_parameters'] = self._runner_parameters
433
        result['action_parameters'] = self._action_parameters
434
435
        if mask_secrets and cfg.CONF.log.mask_secrets:
436
            result = self.mask_secrets(value=result)
437
438
        return result
439