LocalShellRunner.run()   F
last analyzed

Complexity

Conditions 9

Size

Total Lines 140

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 9
c 4
b 0
f 0
dl 0
loc 140
rs 3.1304

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
import os
17
import re
18
import pwd
19
import uuid
20
import functools
21
from StringIO import StringIO
22
23
from oslo_config import cfg
24
from eventlet.green import subprocess
25
26
from st2common.constants import action as action_constants
27
from st2common.constants import exit_codes as exit_code_constants
28
from st2common.constants import runners as runner_constants
29
from st2common import log as logging
30
from st2common.runners.base import ActionRunner
31
from st2common.runners.base import ShellRunnerMixin
32
from st2common.runners.base import get_metadata as get_runner_metadata
33
from st2common.models.system.action import ShellCommandAction
34
from st2common.models.system.action import ShellScriptAction
35
from st2common.util.misc import strip_shell_chars
36
from st2common.util.green import shell
37
from st2common.util.shell import kill_process
38
from st2common.util import jsonify
39
from st2common.services.action import store_execution_output_data
40
from st2common.runners.utils import make_read_and_store_stream_func
41
42
__all__ = [
43
    'LocalShellRunner',
44
45
    'get_runner',
46
    'get_metadata'
47
]
48
49
LOG = logging.getLogger(__name__)
50
51
DEFAULT_KWARG_OP = '--'
52
LOGGED_USER_USERNAME = pwd.getpwuid(os.getuid())[0]
53
54
# constants to lookup in runner_parameters.
55
RUNNER_SUDO = 'sudo'
56
RUNNER_SUDO_PASSWORD = 'sudo_password'
57
RUNNER_ON_BEHALF_USER = 'user'
58
RUNNER_COMMAND = 'cmd'
59
RUNNER_CWD = 'cwd'
60
RUNNER_ENV = 'env'
61
RUNNER_KWARG_OP = 'kwarg_op'
62
RUNNER_TIMEOUT = 'timeout'
63
64
PROC_EXIT_CODE_TO_LIVEACTION_STATUS_MAP = {
65
    str(exit_code_constants.SUCCESS_EXIT_CODE): action_constants.LIVEACTION_STATUS_SUCCEEDED,
66
    str(exit_code_constants.FAILURE_EXIT_CODE): action_constants.LIVEACTION_STATUS_FAILED,
67
    str(-1 * exit_code_constants.SIGKILL_EXIT_CODE): action_constants.LIVEACTION_STATUS_TIMED_OUT,
68
    str(-1 * exit_code_constants.SIGTERM_EXIT_CODE): action_constants.LIVEACTION_STATUS_ABANDONED
69
}
70
71
72
class LocalShellRunner(ActionRunner, ShellRunnerMixin):
73
    """
74
    Runner which executes actions locally using the user under which the action runner service is
75
    running or under the provided user.
76
77
    Note: The user under which the action runner service is running (stanley user by default) needs
78
    to have pasworless sudo access set up.
79
    """
80
    KEYS_TO_TRANSFORM = ['stdout', 'stderr']
81
82
    def __init__(self, runner_id):
83
        super(LocalShellRunner, self).__init__(runner_id=runner_id)
84
85
    def pre_run(self):
86
        super(LocalShellRunner, self).pre_run()
87
88
        self._sudo = self.runner_parameters.get(RUNNER_SUDO, False)
89
        self._sudo_password = self.runner_parameters.get(RUNNER_SUDO_PASSWORD, None)
90
        self._on_behalf_user = self.context.get(RUNNER_ON_BEHALF_USER, LOGGED_USER_USERNAME)
91
        self._user = cfg.CONF.system_user.user
92
        self._cwd = self.runner_parameters.get(RUNNER_CWD, None)
93
        self._env = self.runner_parameters.get(RUNNER_ENV, {})
94
        self._env = self._env or {}
95
        self._kwarg_op = self.runner_parameters.get(RUNNER_KWARG_OP, DEFAULT_KWARG_OP)
96
        self._timeout = self.runner_parameters.get(
97
            RUNNER_TIMEOUT, runner_constants.LOCAL_RUNNER_DEFAULT_ACTION_TIMEOUT)
98
99
    def run(self, action_parameters):
100
        env_vars = self._env
101
102
        if not self.entry_point:
103
            script_action = False
104
            command = self.runner_parameters.get(RUNNER_COMMAND, None)
105
            action = ShellCommandAction(name=self.action_name,
106
                                        action_exec_id=str(self.liveaction_id),
107
                                        command=command,
108
                                        user=self._user,
109
                                        env_vars=env_vars,
110
                                        sudo=self._sudo,
111
                                        timeout=self._timeout,
112
                                        sudo_password=self._sudo_password)
113
        else:
114
            script_action = True
115
            script_local_path_abs = self.entry_point
116
            positional_args, named_args = self._get_script_args(action_parameters)
117
            named_args = self._transform_named_args(named_args)
118
119
            action = ShellScriptAction(name=self.action_name,
120
                                       action_exec_id=str(self.liveaction_id),
121
                                       script_local_path_abs=script_local_path_abs,
122
                                       named_args=named_args,
123
                                       positional_args=positional_args,
124
                                       user=self._user,
125
                                       env_vars=env_vars,
126
                                       sudo=self._sudo,
127
                                       timeout=self._timeout,
128
                                       cwd=self._cwd,
129
                                       sudo_password=self._sudo_password)
130
131
        args = action.get_full_command_string()
132
        sanitized_args = action.get_sanitized_full_command_string()
133
134
        # For consistency with the old Fabric based runner, make sure the file is executable
135
        if script_action:
136
            args = 'chmod +x %s ; %s' % (script_local_path_abs, args)
137
            sanitized_args = 'chmod +x %s ; %s' % (script_local_path_abs, sanitized_args)
138
139
        env = os.environ.copy()
140
141
        # Include user provided env vars (if any)
142
        env.update(env_vars)
143
144
        # Include common st2 env vars
145
        st2_env_vars = self._get_common_action_env_variables()
146
        env.update(st2_env_vars)
147
148
        LOG.info('Executing action via LocalRunner: %s', self.runner_id)
149
        LOG.info('[Action info] name: %s, Id: %s, command: %s, user: %s, sudo: %s' %
150
                 (action.name, action.action_exec_id, sanitized_args, action.user, action.sudo))
151
152
        stdout = StringIO()
153
        stderr = StringIO()
154
155
        store_execution_stdout_line = functools.partial(store_execution_output_data,
156
                                                        output_type='stdout')
157
        store_execution_stderr_line = functools.partial(store_execution_output_data,
158
                                                        output_type='stderr')
159
160
        read_and_store_stdout = make_read_and_store_stream_func(execution_db=self.execution,
161
            action_db=self.action, store_data_func=store_execution_stdout_line)
162
        read_and_store_stderr = make_read_and_store_stream_func(execution_db=self.execution,
163
            action_db=self.action, store_data_func=store_execution_stderr_line)
164
165
        # If sudo password is provided, pass it to the subprocess via stdin>
166
        # Note: We don't need to explicitly escape the argument because we pass command as a list
167
        # to subprocess.Popen and all the arguments are escaped by the function.
168
        if self._sudo_password:
169
            LOG.debug('Supplying sudo password via stdin')
170
            echo_process = subprocess.Popen(['echo', self._sudo_password + '\n'],
171
                                            stdout=subprocess.PIPE)
172
            stdin = echo_process.stdout
173
        else:
174
            stdin = None
175
176
        # Make sure os.setsid is called on each spawned process so that all processes
177
        # are in the same group.
178
179
        # Process is started as sudo -u {{system_user}} -- bash -c {{command}}. Introduction of the
180
        # bash means that multiple independent processes are spawned without them being
181
        # children of the process we have access to and this requires use of pkill.
182
        # Ideally os.killpg should have done the trick but for some reason that failed.
183
        # Note: pkill will set the returncode to 143 so we don't need to explicitly set
184
        # it to some non-zero value.
185
        exit_code, stdout, stderr, timed_out = shell.run_command(cmd=args,
186
                                                                 stdin=stdin,
187
                                                                 stdout=subprocess.PIPE,
188
                                                                 stderr=subprocess.PIPE,
189
                                                                 shell=True,
190
                                                                 cwd=self._cwd,
191
                                                                 env=env,
192
                                                                 timeout=self._timeout,
193
                                                                 preexec_func=os.setsid,
194
                                                                 kill_func=kill_process,
195
                                                           read_stdout_func=read_and_store_stdout,
196
                                                           read_stderr_func=read_and_store_stderr,
197
                                                           read_stdout_buffer=stdout,
198
                                                           read_stderr_buffer=stderr)
199
200
        error = None
201
202
        if timed_out:
203
            error = 'Action failed to complete in %s seconds' % (self._timeout)
204
            exit_code = -1 * exit_code_constants.SIGKILL_EXIT_CODE
205
206
        # Detect if user provided an invalid sudo password or sudo is not configured for that user
207
        if self._sudo_password:
208
            if re.search('sudo: \d+ incorrect password attempts', stderr):
0 ignored issues
show
Bug introduced by
A suspicious escape sequence \d was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
209
                match = re.search('\[sudo\] password for (.+?)\:', stderr)
0 ignored issues
show
Bug introduced by
A suspicious escape sequence \[ was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
Bug introduced by
A suspicious escape sequence \] was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
Bug introduced by
A suspicious escape sequence \: was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
210
211
                if match:
212
                    username = match.groups()[0]
213
                else:
214
                    username = 'unknown'
215
216
                error = ('Invalid sudo password provided or sudo is not configured for this user '
217
                        '(%s)' % (username))
218
                exit_code = -1
219
220
        succeeded = (exit_code == exit_code_constants.SUCCESS_EXIT_CODE)
221
222
        result = {
223
            'failed': not succeeded,
224
            'succeeded': succeeded,
225
            'return_code': exit_code,
226
            'stdout': strip_shell_chars(stdout),
227
            'stderr': strip_shell_chars(stderr)
228
        }
229
230
        if error:
231
            result['error'] = error
232
233
        status = PROC_EXIT_CODE_TO_LIVEACTION_STATUS_MAP.get(
234
            str(exit_code),
235
            action_constants.LIVEACTION_STATUS_FAILED
236
        )
237
238
        return (status, jsonify.json_loads(result, LocalShellRunner.KEYS_TO_TRANSFORM), None)
239
240
241
def get_runner():
242
    return LocalShellRunner(str(uuid.uuid4()))
243
244
245
def get_metadata():
246
    return get_runner_metadata('local_runner')
247