diff --git a/sos/policies/runtimes/__init__.py b/sos/policies/runtimes/__init__.py index bb5b8f0d49..f13549513f 100644 --- a/sos/policies/runtimes/__init__.py +++ b/sos/policies/runtimes/__init__.py @@ -42,6 +42,12 @@ class ContainerRuntime(): volumes = [] binary = '' active = False + # Set to False for runtimes whose `logs` command does not support + # limiting the output to the last N lines (e.g. LXD) + log_line_limit = True + # Set to False for runtimes that cannot be run as a non-root user to + # query rootless containers (e.g. CRI-O and LXD daemon-based runtimes) + rootless = True def __init__(self, policy=None): self.policy = policy @@ -75,16 +81,22 @@ def check_can_copy(self): """ return True - def get_containers(self, get_all=False): + def get_containers(self, get_all=False, runas=None): """Get a list of containers present on the system. :param get_all: If set, include stopped containers as well :type get_all: ``bool`` + + :param runas: If set, query the container runtime as this user, which + allows discovering rootless containers owned by a + non-root user + :type runas: ``str`` or ``None`` """ containers = [] _cmd = f"{self.binary} ps {'-a' if get_all else ''}" if self.active: - out = sos_get_command_output(_cmd, chroot=self.policy.sysroot) + out = sos_get_command_output(_cmd, chroot=self.policy.sysroot, + runas=runas) if out['status'] == 0: for ent in out['output'].splitlines()[1:]: ent = ent.split() @@ -212,16 +224,23 @@ def fmt_registry_authfile(self, authfile): return f"--authfile {authfile}" return '' - def get_logs_command(self, container): + def get_logs_command(self, container, log_lines=None): """Get the command string used to dump container logs from the runtime :param container: The name or ID of the container to get logs for :type container: ``str`` + :param log_lines: Limit the log output to the last `log_lines` lines + of the container's logs. If not set, all available + logs are collected + :type log_lines: ``int`` or ``None`` + :returns: Formatted runtime command to get logs from `container` :type: ``str`` """ + if log_lines is not None and self.log_line_limit: + return f"{self.binary} logs -t --tail {log_lines} {container}" return f"{self.binary} logs -t {container}" def get_copy_command(self, container, path, dest, sizelimit=None): diff --git a/sos/policies/runtimes/crio.py b/sos/policies/runtimes/crio.py index 8221cf1d12..051d537fdb 100644 --- a/sos/policies/runtimes/crio.py +++ b/sos/policies/runtimes/crio.py @@ -19,11 +19,15 @@ class CrioContainerRuntime(ContainerRuntime): name = 'crio' binary = 'crictl' + rootless = False def check_can_copy(self): return False - def get_containers(self, get_all=False): + def get_containers(self, get_all=False, runas=None): + # CRI-O is a daemon-based and does not support rootless + # mode (runas), but accepts the parameter for signature parity + # with ContainerRuntime. """Get a list of containers present on the system. :param get_all: If set, include stopped containers as well @@ -41,6 +45,28 @@ def get_containers(self, get_all=False): (container["id"], container["metadata"]["name"])) return containers + def get_logs_command(self, container, log_lines=None): + """Get the command string used to dump container logs from the + runtime + Note that the `crictl logs` uses `-t` as a shorthand for `--tail`, not + for timestamps, so it is not included here the way it is for the + docker/podman based implementations. + + :param container: The name or ID of the container to get logs for + :type container: ``str`` + + :param log_lines: Limit the log output to the last `log_lines` lines + of the container's logs. If not set, all available + logs are collected. + :type log_lines: ``int`` or ``None`` + + :returns: Formatted runtime command to get logs from `container` + :rtype: ``str`` + """ + if log_lines is not None and self.log_line_limit: + return f"{self.binary} logs --tail {log_lines} {container}" + return f"{self.binary} logs {container}" + def get_images(self): """Get a list of images present on the system diff --git a/sos/policies/runtimes/lxd.py b/sos/policies/runtimes/lxd.py index a6962782c7..a1a1b1271a 100644 --- a/sos/policies/runtimes/lxd.py +++ b/sos/policies/runtimes/lxd.py @@ -20,6 +20,8 @@ class LxdContainerRuntime(ContainerRuntime): name = 'lxd' binary = 'lxc' + log_line_limit = False + rootless = False def check_is_active(self): # the daemon must be running @@ -31,7 +33,9 @@ def check_is_active(self): return True return False - def get_containers(self, get_all=False): + def get_containers(self, get_all=False, runas=None): + # LXD is a daemon-based and does not support rootless mode (runas) + # but accepts the parameter for signature parity with ContainerRuntime. """Get a list of containers present on the system. :param get_all: If set, include stopped containers as well @@ -110,7 +114,9 @@ def get_volumes(self): vols.append(ent['name']) return vols - def get_logs_command(self, container): + def get_logs_command(self, container, log_lines=None): + # LXD does not support log lines limit (log_lines), but accepts + # the parameter for signature parity with ContainerRuntime """Get the command string used to dump container logs from the runtime diff --git a/sos/policies/runtimes/podman.py b/sos/policies/runtimes/podman.py index ef53ce2ee0..680c078ab4 100644 --- a/sos/policies/runtimes/podman.py +++ b/sos/policies/runtimes/podman.py @@ -17,5 +17,148 @@ class PodmanContainerRuntime(ContainerRuntime): name = 'podman' binary = 'podman' + def get_info_command(self, run_debug=False): + """Return the command to gather runtime-wide information from podman + + :param run_debug: If True, add the --debug flag to the command + :type run_debug: ``bool`` + + :returns: Formatted runtime info command + :rtype: ``str`` + """ + if run_debug: + return f"{self.binary} info --debug" + return f"{self.binary} info" + + def get_list_command(self, get_all=False): + """Return the command to run to get a list of containers known to the + runtime, formatted as json + + Note that the system-level runtime already collects this information + for the root user when it is loaded. This method exists so that + plugins collecting from a non-root (rootless) user's runtime can + build the equivalent command and run it with ``runas=``, since + the runtime's cached list only reflects the root instance. + + :param get_all: If True, return all containers, otherwise only return + running containers + :type get_all: ``bool`` + + :returns: Formatted runtime command to list containers as json + :rtype: ``str`` + """ + all_flag = '-a' if get_all else '' + return f"{self.binary} ps {all_flag} --format json" + + def get_inspect_command(self, *containers): + """Return the command used to inspect one or more containers + + Multiple containers are inspected in a single runtime invocation, + avoiding one call per container + + :param containers: One or more names of containers to inspect + :type containers: ``tuple`` + + :returns: Formatted inspect command + :rtype: ``str`` + """ + return f"{self.binary} inspect {' '.join(containers)}" + + def get_exec_command(self, container, cmd): + """Return the command used to run `cmd` inside a single container + + :param container: The name of the container to execute the command in + :type container: ``str`` + + :param cmd: The command to run inside the container + :type cmd: ``str`` + + :returns: Formatted exec command + :rtype: ``str`` + """ + return f"{self.run_cmd} {container} {cmd}" + + def get_secrets_command(self): + """Returns the podman secrets known to the runtime + + :returns: Formatted podman secrets list + :rtype: ``str`` + """ + return f"{self.binary} secret ls" + + def get_network_command(self): + """Returns a list of podman networks known to the runtime + + :returns: Formatted podman network list + :rtye: ``str`` + """ + return f"{self.binary} network ls" + + def get_volume_command(self): + """Returns a list of podman volumes known to the runtime + + :returns: Formatted podman volume list + """ + return f"{self.binary} volume list" + + def get_stats_command(self): + """Returns the resource usage statistics for all containers + known to the runtime, without streaming. + + :returns: Formatted podman containers stats + :rtype: ``str`` + """ + return f"{self.binary} stats --no-stream --all" + + def get_images_command(self, include_digests=False): + """Returns the list of podman images known to the runtime + + :param include_digests: If True, add the container digest + information in the ouput + :type include_digests: ``bool`` + + :returns: Formatted podman images list + :rtype: ``str`` + """ + if include_digests: + return f"{self.binary} images --digests" + return f"{self.binary} images" + + def get_system_df_command(self): + """Return the disk uages of the runtime's storage, broken + down per image, container and volume + + :returns: Formatted podman storage disk usage + :rtype: ``str`` + """ + return f"{self.binary} system df -v" + + def get_network_inspect_command(self, *networks): + """Returns the inspect of one or more podman networks + + Multiple networks are inspected in a singe runtime + invocation, avoding one call per network + + :param networks: One or more names of the networks to inspect + :type networks: ``tuple`` + + :returns: Formatted podman inspect networks + :rtype: ``str`` + """ + return f"{self.binary} network inspect {' '.join(networks)}" + + def get_volume_inspect_command(self, *volumes): + """Return the insepct of one or more podman volumes + + Multiple volumes are inspected in a single runtime + invocation, avoiding one call per volume + + :param volumes: One or more names of the networks to inspect + :type volumes: ``tuples`` + + :returns: Formatted podman inspect volumes + :rtype: ``str`` + """ + return f"{self.binary} volume inspect {' '.join(volumes)}" # vim: set et ts=4 sw=4 : diff --git a/sos/report/plugins/__init__.py b/sos/report/plugins/__init__.py index 40e2b2469f..d5691b3c89 100644 --- a/sos/report/plugins/__init__.py +++ b/sos/report/plugins/__init__.py @@ -2896,6 +2896,39 @@ def get_containers(self, runtime=None, get_all=False): return _runtime.containers return [] + def get_containers_by_user(self, user, get_all=False): + """Return a list of containers owned by a specific user's rootless + container runtime + The system-level ``ContainerRuntime`` loaded through the ``Policy`` + only has visibility into the root(system) instance of the runtime. + Rootless containers managed by a non-root user are therefore not + visible via :meth:`get_containers`. This method queries the user's + own container runtime directly, running the runtime as `user` + (``runas``), so that plugins can discover and operate on rootless + containers. + + :param user: The name of the user whose rootless containers should + be listed + :type user: ``str`` + + :param get_all: Return all containers known to the users runtime, + including those that have terminated + :type get_all: ``bool`` + + :returns: All container IDs and names found in the user's rootless + container runtime + :rtype: ``list`` of ``tuples`` as (id, name) + """ + _runtime = self._get_container_runtime() + if _runtime is None: + return [] + if not _runtime.rootless: + self._log_debug(f"Runtime '{_runtime.name}' does not support " + f"rootless containers; cannot query user " + f"'{user}' container runtime") + return [] + return _runtime.get_containers(get_all=get_all, runas=user) + def get_container_images(self, runtime=None): """Return a list of all image names from the Policy's ContainerRuntime @@ -2934,32 +2967,78 @@ def get_container_volumes(self, runtime=None): return _runtime.volumes return [] - def add_container_logs(self, containers, get_all=False, **kwargs): + def add_container_logs(self, containers, get_all=False, runas=None, + log_lines=None, **kwargs): """Helper to get the ``logs`` output for a given container or list of container names and/or regexes. Supports passthru of add_cmd_output() options :param containers: The name of the container to retrieve logs from, - may be a single name or a regex + may be a single name or a regex. Regexes are + matched for both system-level and rootless + (``runas``) runtimes. :type containers: ``str`` or ``list`` of strs :param get_all: Should non-running containers also be queried? Default: False :type get_all: ``bool`` + :param runas: When set, collect logs from the rootless container + runtime owned by this user instead of the + system-level runtime. Container names are matched + as regexes against the user's runtime registry, + consistent with the system-level runtime behaviour. + :type runas: ``str`` or ``None`` + + :param log_lines: Limit the log output collected for each container + to the last `log_lines` lines. Only honored by + runtimes that support it (see + ``ContainerRuntime.log_line_limit``). + When not set, all available logs are collected. + Plugins that need to limit the collected output + should define their own default and pass it in + here. + :type log_lines: ``int`` or ``None`` + :param kwargs: Any kwargs supported by ``add_cmd_output()`` are supported here """ _runtime = self._get_container_runtime() - if _runtime is not None: - if isinstance(containers, str): - containers = [containers] - for container in containers: + if _runtime is None: + self._log_debug(f"No container runtime available to collect " + f"logs for '{containers}'") + return + + if isinstance(containers, str): + containers = [containers] + + # For rootless collection, query the user's runtime once, outside the + # loop, then filter by regex just like the root path + _user_containers = None + if runas: + _user_containers = self.get_containers_by_user(runas, + get_all=get_all) + + for container in containers: + if runas: + _cons = [c for c in _user_containers + if re.match(container, c[1])] + if not _cons: + self._log_debug(f"Container '{container}' not found in " + f"user '{runas}' container runtime") + continue + else: _cons = self.get_all_containers_by_regex(container, get_all) - for _con in _cons: - cmd = _runtime.get_logs_command(_con[1]) - self.add_cmd_output(cmd, **kwargs) + for _con in _cons: + if (log_lines is not None and + not _runtime.log_line_limit): + self._log_debug(f"Runtime '{_runtime.name}' does not " + f"support limiting log output; collecting " + f"full logs for '{_con[1]}'") + cmd = _runtime.get_logs_command(_con[1], + log_lines=log_lines) + self.add_cmd_output(cmd, runas=runas, **kwargs) def fmt_container_cmd(self, container, cmd, quotecmd=False, runtime=None, runas=None, env=None):