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