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 | import os |
||
18 | import sys |
||
19 | import traceback |
||
20 | import uuid |
||
21 | |||
22 | from oslo_config import cfg |
||
23 | |||
24 | from st2common import log as logging |
||
25 | from st2common.runners.paramiko_ssh_runner import RUNNER_REMOTE_DIR |
||
26 | from st2common.runners.paramiko_ssh_runner import BaseParallelSSHRunner |
||
27 | from st2common.runners.base import get_metadata as get_runner_metadata |
||
28 | from st2common.models.system.paramiko_script_action import ParamikoRemoteScriptAction |
||
29 | |||
30 | __all__ = [ |
||
31 | 'ParamikoRemoteScriptRunner', |
||
32 | |||
33 | 'get_runner', |
||
34 | 'get_metadata' |
||
35 | ] |
||
36 | |||
37 | LOG = logging.getLogger(__name__) |
||
38 | |||
39 | |||
40 | class ParamikoRemoteScriptRunner(BaseParallelSSHRunner): |
||
41 | def run(self, action_parameters): |
||
42 | remote_action = self._get_remote_action(action_parameters) |
||
43 | |||
44 | LOG.debug('Executing remote action.', extra={'_action_params': remote_action}) |
||
45 | result = self._run(remote_action) |
||
46 | LOG.debug('Executed remote action.', extra={'_result': result}) |
||
47 | status = self._get_result_status(result, cfg.CONF.ssh_runner.allow_partial_failure) |
||
48 | |||
49 | return (status, result, None) |
||
50 | |||
51 | def _run(self, remote_action): |
||
52 | try: |
||
53 | copy_results = self._copy_artifacts(remote_action) |
||
54 | except: |
||
55 | # If for whatever reason there is a top level exception, |
||
56 | # we just bail here. |
||
57 | error = 'Failed copying content to remote boxes.' |
||
58 | LOG.exception(error) |
||
59 | _, ex, tb = sys.exc_info() |
||
60 | copy_results = self._generate_error_results(' '.join([error, str(ex)]), tb) |
||
61 | return copy_results |
||
62 | |||
63 | try: |
||
64 | exec_results = self._run_script_on_remote_host(remote_action) |
||
65 | try: |
||
66 | remote_dir = remote_action.get_remote_base_dir() |
||
67 | LOG.debug('Deleting remote execution dir.', extra={'_remote_dir': remote_dir}) |
||
68 | delete_results = self._parallel_ssh_client.delete_dir(path=remote_dir, |
||
69 | force=True) |
||
70 | LOG.debug('Deleted remote execution dir.', extra={'_result': delete_results}) |
||
71 | except: |
||
72 | LOG.exception('Failed deleting remote dir.', extra={'_remote_dir': remote_dir}) |
||
73 | finally: |
||
74 | return exec_results |
||
0 ignored issues
–
show
|
|||
75 | except: |
||
76 | error = 'Failed executing script on remote boxes.' |
||
77 | LOG.exception(error, extra={'_action_params': remote_action}) |
||
78 | _, ex, tb = sys.exc_info() |
||
79 | exec_results = self._generate_error_results(' '.join([error, str(ex)]), tb) |
||
80 | return exec_results |
||
81 | |||
82 | def _copy_artifacts(self, remote_action): |
||
83 | # First create remote execution directory. |
||
84 | remote_dir = remote_action.get_remote_base_dir() |
||
85 | LOG.debug('Creating remote execution dir.', extra={'_path': remote_dir}) |
||
86 | mkdir_result = self._parallel_ssh_client.mkdir(path=remote_action.get_remote_base_dir()) |
||
87 | |||
88 | # Copy the script to remote dir in remote host. |
||
89 | local_script_abs_path = remote_action.get_local_script_abs_path() |
||
90 | remote_script_abs_path = remote_action.get_remote_script_abs_path() |
||
91 | file_mode = 0o744 |
||
92 | extra = {'_local_script': local_script_abs_path, '_remote_script': remote_script_abs_path, |
||
93 | 'mode': file_mode} |
||
94 | LOG.debug('Copying local script to remote box.', extra=extra) |
||
95 | put_result_1 = self._parallel_ssh_client.put(local_path=local_script_abs_path, |
||
96 | remote_path=remote_script_abs_path, |
||
97 | mirror_local_mode=False, mode=file_mode) |
||
98 | |||
99 | # If `lib` exist for the script, copy that to remote host. |
||
100 | local_libs_path = remote_action.get_local_libs_path_abs() |
||
101 | if os.path.exists(local_libs_path): |
||
102 | extra = {'_local_libs': local_libs_path, '_remote_path': remote_dir} |
||
103 | LOG.debug('Copying libs to remote host.', extra=extra) |
||
104 | put_result_2 = self._parallel_ssh_client.put(local_path=local_libs_path, |
||
105 | remote_path=remote_dir, |
||
106 | mirror_local_mode=True) |
||
107 | |||
108 | result = mkdir_result or put_result_1 or put_result_2 |
||
109 | return result |
||
110 | |||
111 | def _run_script_on_remote_host(self, remote_action): |
||
112 | command = remote_action.get_full_command_string() |
||
113 | LOG.info('Command to run: %s', command) |
||
114 | results = self._parallel_ssh_client.run(command, timeout=remote_action.get_timeout()) |
||
115 | LOG.debug('Results from script: %s', results) |
||
116 | return results |
||
117 | |||
118 | def _get_remote_action(self, action_parameters): |
||
119 | # remote script actions without entry_point don't make sense, user probably wanted to use |
||
120 | # "remote-shell-cmd" action |
||
121 | if not self.entry_point: |
||
122 | msg = ('Action "%s" is missing "entry_point" attribute. Perhaps wanted to use ' |
||
123 | '"remote-shell-script" runner?' % (self.action_name)) |
||
124 | raise Exception(msg) |
||
125 | |||
126 | script_local_path_abs = self.entry_point |
||
127 | pos_args, named_args = self._get_script_args(action_parameters) |
||
128 | named_args = self._transform_named_args(named_args) |
||
129 | env_vars = self._get_env_vars() |
||
130 | remote_dir = self.runner_parameters.get(RUNNER_REMOTE_DIR, |
||
131 | cfg.CONF.ssh_runner.remote_dir) |
||
132 | remote_dir = os.path.join(remote_dir, self.liveaction_id) |
||
133 | return ParamikoRemoteScriptAction(self.action_name, |
||
134 | str(self.liveaction_id), |
||
135 | script_local_path_abs, |
||
136 | self.libs_dir_path, |
||
137 | named_args=named_args, |
||
138 | positional_args=pos_args, |
||
139 | env_vars=env_vars, |
||
140 | on_behalf_user=self._on_behalf_user, |
||
141 | user=self._username, |
||
142 | password=self._password, |
||
143 | private_key=self._private_key, |
||
144 | remote_dir=remote_dir, |
||
145 | hosts=self._hosts, |
||
146 | parallel=self._parallel, |
||
147 | sudo=self._sudo, |
||
148 | sudo_password=self._sudo_password, |
||
149 | timeout=self._timeout, |
||
150 | cwd=self._cwd) |
||
151 | |||
152 | @staticmethod |
||
153 | def _generate_error_results(error, tb): |
||
154 | error_dict = { |
||
155 | 'error': error, |
||
156 | 'traceback': ''.join(traceback.format_tb(tb, 20)) if tb else '', |
||
157 | 'failed': True, |
||
158 | 'succeeded': False, |
||
159 | 'return_code': 255 |
||
160 | } |
||
161 | return error_dict |
||
162 | |||
163 | |||
164 | def get_runner(): |
||
165 | return ParamikoRemoteScriptRunner(str(uuid.uuid4())) |
||
166 | |||
167 | |||
168 | def get_metadata(): |
||
169 | return get_runner_metadata('remote_script_runner') |
||
170 |
Placing a return statement inside finally will swallow all exceptions that may have been thrown in the try block.