Passed
Push — master ( b5a2f1...2b47b0 )
by W
09:03 queued 03:49
created

LocalShellRunner.run()   B

Complexity

Conditions 5

Size

Total Lines 93

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
dl 0
loc 93
rs 7.8273
c 0
b 0
f 0

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