Test Failed
Push — master ( e380d0...f5671d )
by W
02:58
created

contrib/runners/local_runner/local_runner/base.py (4 issues)

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
18
import os
19
import re
20
import abc
21
import pwd
22
import functools
23
24
import six
25
from oslo_config import cfg
26
from eventlet.green import subprocess
27
from six.moves import StringIO
28
29
from st2common.constants import action as action_constants
30
from st2common.constants import exit_codes as exit_code_constants
31
from st2common.constants import runners as runner_constants
32
from st2common import log as logging
33
from st2common.runners.base import ActionRunner
34
from st2common.runners.base import ShellRunnerMixin
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
    'BaseLocalShellRunner',
44
45
    'RUNNER_COMMAND'
46
]
47
48
LOG = logging.getLogger(__name__)
49
50
DEFAULT_KWARG_OP = '--'
51
LOGGED_USER_USERNAME = pwd.getpwuid(os.getuid())[0]
52
53
# constants to lookup in runner_parameters.
54
RUNNER_SUDO = 'sudo'
55
RUNNER_SUDO_PASSWORD = 'sudo_password'
56
RUNNER_ON_BEHALF_USER = 'user'
57
RUNNER_COMMAND = 'cmd'
58
RUNNER_CWD = 'cwd'
59
RUNNER_ENV = 'env'
60
RUNNER_KWARG_OP = 'kwarg_op'
61
RUNNER_TIMEOUT = 'timeout'
62
63
PROC_EXIT_CODE_TO_LIVEACTION_STATUS_MAP = {
64
    str(exit_code_constants.SUCCESS_EXIT_CODE): action_constants.LIVEACTION_STATUS_SUCCEEDED,
65
    str(exit_code_constants.FAILURE_EXIT_CODE): action_constants.LIVEACTION_STATUS_FAILED,
66
    str(-1 * exit_code_constants.SIGKILL_EXIT_CODE): action_constants.LIVEACTION_STATUS_TIMED_OUT,
67
    str(-1 * exit_code_constants.SIGTERM_EXIT_CODE): action_constants.LIVEACTION_STATUS_ABANDONED
68
}
69
70
71
@six.add_metaclass(abc.ABCMeta)
72
class BaseLocalShellRunner(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(BaseLocalShellRunner, self).__init__(runner_id=runner_id)
84
85
    def pre_run(self):
86
        super(BaseLocalShellRunner, 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):
100
        env_vars = self._env
101
102
        if not self.entry_point:
103
            script_action = False
104
        else:
105
            script_action = True
106
107
        args = action.get_full_command_string()
108
        sanitized_args = action.get_sanitized_full_command_string()
109
110
        # For consistency with the old Fabric based runner, make sure the file is executable
111
        if script_action:
112
            script_local_path_abs = self.entry_point
113
            args = 'chmod +x %s ; %s' % (script_local_path_abs, args)
114
            sanitized_args = 'chmod +x %s ; %s' % (script_local_path_abs, sanitized_args)
115
116
        env = os.environ.copy()
117
118
        # Include user provided env vars (if any)
119
        env.update(env_vars)
120
121
        # Include common st2 env vars
122
        st2_env_vars = self._get_common_action_env_variables()
123
        env.update(st2_env_vars)
124
125
        LOG.info('Executing action via LocalRunner: %s', self.runner_id)
126
        LOG.info('[Action info] name: %s, Id: %s, command: %s, user: %s, sudo: %s' %
127
                 (action.name, action.action_exec_id, sanitized_args, action.user, action.sudo))
128
129
        stdout = StringIO()
130
        stderr = StringIO()
131
132
        store_execution_stdout_line = functools.partial(store_execution_output_data,
133
                                                        output_type='stdout')
134
        store_execution_stderr_line = functools.partial(store_execution_output_data,
135
                                                        output_type='stderr')
136
137
        read_and_store_stdout = make_read_and_store_stream_func(execution_db=self.execution,
138
            action_db=self.action, store_data_func=store_execution_stdout_line)
139
        read_and_store_stderr = make_read_and_store_stream_func(execution_db=self.execution,
140
            action_db=self.action, store_data_func=store_execution_stderr_line)
141
142
        # If sudo password is provided, pass it to the subprocess via stdin>
143
        # Note: We don't need to explicitly escape the argument because we pass command as a list
144
        # to subprocess.Popen and all the arguments are escaped by the function.
145
        if self._sudo_password:
146
            LOG.debug('Supplying sudo password via stdin')
147
            echo_process = subprocess.Popen(['echo', self._sudo_password + '\n'],
148
                                            stdout=subprocess.PIPE)
149
            stdin = echo_process.stdout
150
        else:
151
            stdin = None
152
153
        # Make sure os.setsid is called on each spawned process so that all processes
154
        # are in the same group.
155
156
        # Process is started as sudo -u {{system_user}} -- bash -c {{command}}. Introduction of the
157
        # bash means that multiple independent processes are spawned without them being
158
        # children of the process we have access to and this requires use of pkill.
159
        # Ideally os.killpg should have done the trick but for some reason that failed.
160
        # Note: pkill will set the returncode to 143 so we don't need to explicitly set
161
        # it to some non-zero value.
162
        exit_code, stdout, stderr, timed_out = shell.run_command(cmd=args,
163
                                                                 stdin=stdin,
164
                                                                 stdout=subprocess.PIPE,
165
                                                                 stderr=subprocess.PIPE,
166
                                                                 shell=True,
167
                                                                 cwd=self._cwd,
168
                                                                 env=env,
169
                                                                 timeout=self._timeout,
170
                                                                 preexec_func=os.setsid,
171
                                                                 kill_func=kill_process,
172
                                                           read_stdout_func=read_and_store_stdout,
173
                                                           read_stderr_func=read_and_store_stderr,
174
                                                           read_stdout_buffer=stdout,
175
                                                           read_stderr_buffer=stderr)
176
177
        error = None
178
179
        if timed_out:
180
            error = 'Action failed to complete in %s seconds' % (self._timeout)
181
            exit_code = -1 * exit_code_constants.SIGKILL_EXIT_CODE
182
183
        # Detect if user provided an invalid sudo password or sudo is not configured for that user
184
        if self._sudo_password:
185
            if re.search('sudo: \d+ incorrect password attempts', stderr):
0 ignored issues
show
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...
186
                match = re.search('\[sudo\] password for (.+?)\:', stderr)
0 ignored issues
show
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...
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...
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...
187
188
                if match:
189
                    username = match.groups()[0]
190
                else:
191
                    username = 'unknown'
192
193
                error = ('Invalid sudo password provided or sudo is not configured for this user '
194
                        '(%s)' % (username))
195
                exit_code = -1
196
197
        succeeded = (exit_code == exit_code_constants.SUCCESS_EXIT_CODE)
198
199
        result = {
200
            'failed': not succeeded,
201
            'succeeded': succeeded,
202
            'return_code': exit_code,
203
            'stdout': strip_shell_chars(stdout),
204
            'stderr': strip_shell_chars(stderr)
205
        }
206
207
        if error:
208
            result['error'] = error
209
210
        status = PROC_EXIT_CODE_TO_LIVEACTION_STATUS_MAP.get(
211
            str(exit_code),
212
            action_constants.LIVEACTION_STATUS_FAILED
213
        )
214
215
        return (status, jsonify.json_loads(result, BaseLocalShellRunner.KEYS_TO_TRANSFORM), None)
216