aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTrevor Benson <trevor.benson@gmail.com>2023-01-19 14:59:32 -0800
committerJake Hunsaker <jhunsake@redhat.com>2023-01-20 08:56:02 -0500
commit76c27a82b2688d0fb9a8b6bd5ea9ffd1c896fde9 (patch)
treecc508f608805bcc0b1d79bdb1f1b264872b01c9e
parent3aea91f2d984051831d95e85bd8a85c2dc0fae29 (diff)
downloadsos-76c27a82b2688d0fb9a8b6bd5ea9ffd1c896fde9.tar.gz
[collector] add saltstack transport
Signed-off-by: Trevor Benson <trevor.benson@gmail.com>
-rw-r--r--sos/collector/exceptions.py9
-rw-r--r--sos/collector/sosnode.py4
-rw-r--r--sos/collector/transports/saltstack.py136
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 :