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
|
|||
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 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 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 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 |
Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with
r
orR
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.