From fc9327371a361954a1a9cade5cb7f206c31c6ad4 Mon Sep 17 00:00:00 2001 From: Jake Hunsaker Date: Tue, 14 Apr 2020 22:11:18 -0400 Subject: [collector] Fix local collections requiring sudo Local report generation when running as a non-root user were broken following the transition away from the Configuration() approach. Now, adjust to the new way we execute so that we properly detect non-root runs, prompt for a sudo password if needed, and/or fail appropriately if we cannot use sudo locally for generating the report. Signed-off-by: Jake Hunsaker --- sos/collector/__init__.py | 42 ++++++++++++++++++++++++++++++++++++------ sos/collector/sosnode.py | 16 +++++++++++++--- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/sos/collector/__init__.py b/sos/collector/__init__.py index 74116106..0cac4320 100644 --- a/sos/collector/__init__.py +++ b/sos/collector/__init__.py @@ -90,7 +90,11 @@ class SoSCollector(SoSComponent): self.node_list = [] self.master = False self.retrieved = 0 - self.need_local_sudo = False + self.cluster = None + self.cluster_type = None + # add a place to set/get the sudo password, but do not expose it via + # the CLI, because security is a thing + setattr(self.opts, 'sudo_pw', '') # get the local hostname and addresses to filter from results later self.hostname = socket.gethostname() try: @@ -343,7 +347,8 @@ class SoSCollector(SoSComponent): 'need_sudo': True if self.opts.ssh_user != 'root' else False, 'opts': self.opts, 'tmpdir': self.tmpdir, - 'hostlen': len(self.opts.master) or len(self.hostname) + 'hostlen': len(self.opts.master) or len(self.hostname), + 'policy': self.policy } def parse_cluster_options(self): @@ -616,7 +621,32 @@ class SoSCollector(SoSComponent): self.opts.no_local = True else: try: - self.master = SosNode('localhost', self.commons) + can_run_local = True + local_sudo = None + skip_local_msg = ( + "Local sos report generation forcibly skipped due " + "to lack of root privileges.\nEither use --insecure-sudo, " + "run as root, or do not use --batch so that you will be " + "prompted for a password\n" + ) + if (not self.opts.no_local and (os.getuid() != 0 and not + self.opts.insecure_sudo)): + if not self.opts.batch: + msg = ("Enter local sudo password to generate local " + "sos report: ") + local_sudo = getpass(msg) + if local_sudo == '': + self.ui_log.info(skip_local_msg) + can_run_local = False + self.opts.no_local = True + local_sudo = None + else: + self.ui_log.info(skip_local_msg) + can_run_local = False + self.opts.no_local = True + self.master = SosNode('localhost', self.commons, + local_sudo=local_sudo, + load_facts=can_run_local) except Exception as err: self.log_debug("Unable to determine local installation: %s" % err) @@ -666,9 +696,9 @@ class SoSCollector(SoSComponent): 'installed.\nAborting...') self.ui_log.info('The following is a list of nodes to collect from:') - if self.master.connected: + if self.master.connected and self.master.hostname is not None: self.ui_log.info('\t%-*s' % (self.commons['hostlen'], - self.opts.master)) + self.master.hostname)) for node in sorted(self.node_list): self.ui_log.info("\t%-*s" % (self.commons['hostlen'], node)) @@ -731,7 +761,6 @@ class SoSCollector(SoSComponent): If a list of nodes is given, this is not run, however the cluster can still be run if the user sets a --cluster-type manually """ - self.cluster = None checks = list(self.clusters.values()) for cluster in self.clusters.values(): checks.remove(cluster) @@ -767,6 +796,7 @@ class SoSCollector(SoSComponent): nodes = self.cluster._get_nodes() self.log_debug('Node list: %s' % nodes) return nodes + return [] def reduce_node_list(self): """Reduce duplicate entries of the localhost and/or master node diff --git a/sos/collector/sosnode.py b/sos/collector/sosnode.py index b9e9c028..9a845e2a 100644 --- a/sos/collector/sosnode.py +++ b/sos/collector/sosnode.py @@ -24,17 +24,22 @@ from sos.collector.exceptions import * class SosNode(): - def __init__(self, address, commons, password=None, - force=False, load_facts=True): + def __init__(self, address, commons, password=None, local_sudo=None, + load_facts=True): self.address = address.strip() + self.commons = commons self.opts = commons['opts'] self.tmpdir = commons['tmpdir'] self.hostlen = commons['hostlen'] self.need_sudo = commons['need_sudo'] self.local = False + self.host = None self.cluster = None self.hostname = None self._password = password or self.opts.password + # override local sudo from any other source + if local_sudo: + self.opts.sudo_pw = local_sudo self.sos_path = None self.retrieved = False self.hash_retrieved = False @@ -53,7 +58,7 @@ class SosNode(): self.control_path = ("%s/.sos-collector-%s" % (self.tmpdir, self.address)) self.ssh_cmd = self._create_ssh_command() - if self.address not in filt or force: + if self.address not in filt: try: self.connected = self._create_ssh_session() except Exception as err: @@ -62,6 +67,7 @@ class SosNode(): else: self.connected = True self.local = True + self.need_sudo = os.getuid() != 0 if self.connected and load_facts: self.host = self.determine_host_policy() if not self.host: @@ -293,6 +299,8 @@ class SosNode(): """Attempts to identify the host installation against supported distributions """ + if self.local: + return self.commons['policy'] host = load(cache={}, sysroot=self.opts.sysroot, init=InitSystem(), probe_runtime=False, remote_exec=self.ssh_cmd, remote_check=self.read_file('/etc/os-release')) @@ -311,6 +319,8 @@ class SosNode(): def is_installed(self, pkg): """Checks if a given package is installed on the node""" + if not self.host: + return False return self.host.package_manager.pkg_by_name(pkg) is not None def run_command(self, cmd, timeout=180, get_pty=False, need_root=False, -- cgit