diff --git a/mistral/actions/std_actions.py b/mistral/actions/std_actions.py index 6de61e974..07df918a7 100644 --- a/mistral/actions/std_actions.py +++ b/mistral/actions/std_actions.py @@ -326,6 +326,9 @@ class SSHAction(base.Action): will be a single value, otherwise a list of results provided in the same order as provided hosts. """ + @property + def _execute_cmd_method(self): + return ssh_utils.execute_command def __init__(self, cmd, host, username, password=None, private_key=None): self.cmd = cmd @@ -334,6 +337,14 @@ class SSHAction(base.Action): self.password = password self.private_key = private_key + self.params = { + 'cmd': self.cmd, + 'host': self.host, + 'username': self.username, + 'password': self.password, + 'private_key': self.private_key + } + def run(self): def raise_exc(parent_exc=None): message = ("Failed to execute ssh cmd " @@ -349,13 +360,9 @@ class SSHAction(base.Action): self.host = [self.host] for host_name in self.host: - status_code, result = ssh_utils.execute_command( - self.cmd, - host_name, - self.username, - self.password, - pkey=self.private_key - ) + self.params['host'] = host_name + + status_code, result = self._execute_cmd_method(**self.params) if status_code > 0: return raise_exc() @@ -374,6 +381,33 @@ class SSHAction(base.Action): return None +class SSHProxiedAction(SSHAction): + @property + def _execute_cmd_method(self): + return ssh_utils.execute_command_via_gateway + + def __init__(self, cmd, host, username, private_key, gateway_host, + gateway_username=None, password=None, proxy_command=None): + super(SSHProxiedAction, self).__init__( + cmd, + host, + username, + password, + private_key + ) + + self.gateway_host = gateway_host + self.gateway_username = gateway_username + + self.params.update( + { + 'gateway_host': gateway_host, + 'gateway_username': gateway_username, + 'proxy_command': proxy_command + } + ) + + class JavaScriptAction(base.Action): """Evaluates given JavaScript. diff --git a/mistral/utils/ssh_utils.py b/mistral/utils/ssh_utils.py index ae0186c11..eb526016e 100644 --- a/mistral/utils/ssh_utils.py +++ b/mistral/utils/ssh_utils.py @@ -33,30 +33,41 @@ def _read_paramimko_stream(recv_func): return result -def _connect(host, username, password, pkey): - if pkey: - pkey = paramiko.RSAKey(file_obj=six.StringIO(pkey)) +def _to_paramiko_private_key(private_key_raw, password): + return paramiko.RSAKey( + file_obj=six.StringIO(private_key_raw), + password=password + ) + + +def _connect(host, username, password=None, pkey=None, proxy=None): + if isinstance(pkey, six.string_types): + pkey = _to_paramiko_private_key(pkey, password) LOG.debug('Creating SSH connection to %s' % host) - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(host, username=username, password=password, pkey=pkey) - return ssh + ssh_client = paramiko.SSHClient() + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + ssh_client.connect( + host, + username=username, + password=password, + pkey=pkey, + sock=proxy + ) + + return ssh_client -def _cleanup(ssh): - ssh.close() +def _cleanup(ssh_client): + ssh_client.close() -def execute_command(cmd, host, username, password=None, pkey=None, - get_stderr=False, raise_when_error=True): - ssh = _connect(host, username, password, pkey) - - LOG.debug("Executing command %s" % cmd) - +def _execute_command(ssh_client, cmd, get_stderr=False, + raise_when_error=True): try: - chan = ssh.get_transport().open_session() + chan = ssh_client.get_transport().open_session() chan.exec_command(cmd) # TODO(nmakhotkin): that could hang if stderr buffer overflows @@ -73,4 +84,64 @@ def execute_command(cmd, host, username, password=None, pkey=None, else: return ret_code, stdout finally: - _cleanup(ssh) + _cleanup(ssh_client) + + +def execute_command_via_gateway(cmd, host, username, private_key, + gateway_host, gateway_username=None, + proxy_command=None, password=None): + LOG.debug('Creating SSH connection') + + if isinstance(private_key, six.string_types): + private_key = _to_paramiko_private_key(private_key, password) + + proxy = None + + if proxy_command: + LOG.debug('Creating proxy using command: %s' % proxy_command) + + proxy = paramiko.ProxyCommand(proxy_command) + + _proxy_ssh_client = paramiko.SSHClient() + _proxy_ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + LOG.debug('Connecting to proxy gateway at: %s' % gateway_host) + + if not gateway_username: + gateway_username = username + + _proxy_ssh_client.connect( + gateway_host, + username=gateway_username, + pkey=private_key, + sock=proxy + ) + + proxy = _proxy_ssh_client.get_transport().open_session() + proxy.exec_command("nc {0} 22".format(host)) + + ssh_client = _connect( + host, + username=username, + pkey=private_key, + proxy=proxy + ) + + try: + return _execute_command( + ssh_client, + cmd, + get_stderr=False, + raise_when_error=True + ) + finally: + _cleanup(_proxy_ssh_client) + + +def execute_command(cmd, host, username, password=None, private_key=None, + get_stderr=False, raise_when_error=True): + ssh_client = _connect(host, username, password, private_key) + + LOG.debug("Executing command %s" % cmd) + + return _execute_command(ssh_client, cmd, get_stderr, raise_when_error) diff --git a/setup.cfg b/setup.cfg index 4c474830e..5bf0a61a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,5 +46,6 @@ mistral.actions = std.http = mistral.actions.std_actions:HTTPAction std.mistral_http = mistral.actions.std_actions:MistralHTTPAction std.ssh = mistral.actions.std_actions:SSHAction + std.ssh_proxied = mistral.actions.std_actions:SSHProxiedAction std.email = mistral.actions.std_actions:SendEmailAction std.javascript = mistral.actions.std_actions:JavaScriptAction