diff options
author | Trevor Benson <trevor.benson@gmail.com> | 2023-01-19 14:59:32 -0800 |
---|---|---|
committer | Jake Hunsaker <jhunsake@redhat.com> | 2023-01-20 08:56:02 -0500 |
commit | 76c27a82b2688d0fb9a8b6bd5ea9ffd1c896fde9 (patch) | |
tree | cc508f608805bcc0b1d79bdb1f1b264872b01c9e | |
parent | 3aea91f2d984051831d95e85bd8a85c2dc0fae29 (diff) | |
download | sos-76c27a82b2688d0fb9a8b6bd5ea9ffd1c896fde9.tar.gz |
[collector] add saltstack transport
Signed-off-by: Trevor Benson <trevor.benson@gmail.com>
-rw-r--r-- | sos/collector/exceptions.py | 9 | ||||
-rw-r--r-- | sos/collector/sosnode.py | 4 | ||||
-rw-r--r-- | sos/collector/transports/saltstack.py | 136 |
3 files changed, 148 insertions, 1 deletions
diff --git a/sos/collector/exceptions.py b/sos/collector/exceptions.py index 2bb07e7b..5cfc8a02 100644 --- a/sos/collector/exceptions.py +++ b/sos/collector/exceptions.py @@ -104,6 +104,14 @@ class InvalidTransportException(Exception): super(InvalidTransportException, self).__init__(message) +class SaltStackMasterUnsupportedException(Exception): + """Raised when SaltStack Master is unsupported locally""" + + def __init__(self): + message = 'Master unsupported by local SaltStack installation' + super(SaltStackMasterUnsupportedException, self).__init__(message) + + __all__ = [ 'AuthPermissionDeniedException', 'CommandTimeoutException', @@ -113,6 +121,7 @@ __all__ = [ 'ControlSocketMissingException', 'InvalidPasswordException', 'PasswordRequestException', + 'SaltStackMasterUnsupportedException', 'TimeoutPasswordAuthException', 'UnsupportedHostException', 'InvalidTransportException' diff --git a/sos/collector/sosnode.py b/sos/collector/sosnode.py index 56408753..38f739d7 100644 --- a/sos/collector/sosnode.py +++ b/sos/collector/sosnode.py @@ -21,6 +21,7 @@ from sos.policies.init_systems import InitSystem from sos.collector.transports.control_persist import SSHControlPersist from sos.collector.transports.local import LocalTransport from sos.collector.transports.oc import OCTransport +from sos.collector.transports.saltstack import SaltStackMaster from sos.collector.exceptions import (CommandTimeoutException, ConnectionException, UnsupportedHostException, @@ -29,7 +30,8 @@ from sos.collector.exceptions import (CommandTimeoutException, TRANSPORTS = { 'local': LocalTransport, 'control_persist': SSHControlPersist, - 'oc': OCTransport + 'oc': OCTransport, + 'saltstack': SaltStackMaster } diff --git a/sos/collector/transports/saltstack.py b/sos/collector/transports/saltstack.py new file mode 100644 index 00000000..8c127087 --- /dev/null +++ b/sos/collector/transports/saltstack.py @@ -0,0 +1,136 @@ +# Copyright Red Hat 2022, Trevor Benson <trevor.benson@gmail.com> + +# This file is part of the sos project: https://github.com/sosreport/sos +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# version 2 of the GNU General Public License. +# +# See the LICENSE file in the source distribution for further information. + +import contextlib +import json +import os +import shutil +from sos.collector.transports import RemoteTransport +from sos.collector.exceptions import (ConnectionException, + SaltStackMasterUnsupportedException) +from sos.utilities import (is_executable, + sos_get_command_output) + + +class SaltStackMaster(RemoteTransport): + """ + A transport for collect that leverages SaltStack's Master Pub/Sub + functionality to send commands to minions. + + This transport will by default assume the use cmd.shell module to + execute commands on the minions. + """ + + name = 'saltstack' + + def _convert_output_json(self, json_output): + return list(json.loads(json_output).values())[0] + + def run_command( + self, cmd, timeout=180, need_root=False, env=None, get_pty=False): + """ + Run a command on the remote host using SaltStack Master. + If the output is json, convert it to a string. + """ + ret = super(SaltStackMaster, self).run_command( + cmd, timeout, need_root, env, get_pty) + with contextlib.suppress(Exception): + ret['output'] = self._convert_output_json(ret['output']) + return ret + + def _salt_retrieve_file(self, node, fname, dest): + """ + Execute cp.push on the remote host using SaltStack Master + """ + cmd = f"salt {node} cp.push {fname}" + res = sos_get_command_output(cmd) + if res['status'] == 0: + cachedir = f"/var/cache/salt/master/minions/{self.address}/files" + cachedir_file = os.path.join(cachedir, fname.lstrip('/')) + shutil.move(cachedir_file, dest) + return True + return False + + @property + def connected(self): + """Check if the remote host is responding using SaltStack Master.""" + up = self.run_command("echo Connected", timeout=10) + return up['status'] == 0 + + def _check_for_saltstack(self, password=None): + """Checks to see if the local system supported SaltStack Master. + + This check relies on feedback from the salt binary. The command being + run should always generate stderr output, but depending on what that + output reads we can determine if SaltStack Master is supported or not. + + For our purposes, a host that does not support SaltStack Master is not + able to run sos-collector. + + Returns + True if SaltStack Master is supported, else raise Exception + """ + + cmd = 'salt-run manage.status' + res = sos_get_command_output(cmd) + if res['status'] == 0: + return res['status'] == 0 + else: + raise SaltStackMasterUnsupportedException + + def _connect(self, password=None): + """Connect to the remote host using SaltStack Master. + + This method will attempt to connect to the remote host using SaltStack + Master. If the connection fails, an exception will be raised. + + If the connection is successful, the connection will be stored in the + self._connection attribute. + """ + if not is_executable('salt'): + self.log_error("salt command is not executable. ") + return False + + try: + self._check_for_saltstack() + except ConnectionException: + self.log_error("Transport is not locally supported. ") + raise + self.log_info("Transport is locally supported and service running. ") + cmd = "echo Connected" + result = self.run_command(cmd, timeout=180) + return result['status'] == 0 + + def _disconnect(self): + return True + + @property + def remote_exec(self): + """The remote execution command to use for this transport.""" + salt_args = "--out json --static --no-color" + return f"salt {salt_args} {self.address} cmd.shell " + + def _retrieve_file(self, fname, dest): + """Retrieve a file from the remote host using saltstack + + Parameters + fname The path to the file on the remote host + dest The path to the destination directory on the master + + Returns + True if the file was retrieved, else False + """ + return ( + self._salt_retrieve_file(self.address, fname, dest) + if self.connected + else False + ) + +# vim: set et ts=4 sw=4 : |