Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions sos/policies/runtimes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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}"
Comment thread
pmoravec marked this conversation as resolved.
return f"{self.binary} logs -t {container}"

def get_copy_command(self, container, path, dest, sizelimit=None):
Expand Down
28 changes: 27 additions & 1 deletion sos/policies/runtimes/crio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Comment thread
pmoravec marked this conversation as resolved.
"""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

Expand Down
10 changes: 8 additions & 2 deletions sos/policies/runtimes/lxd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
57 changes: 57 additions & 0 deletions sos/policies/runtimes/podman.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where we're using any of these new methods?

Also, I don't see why these would be methods at all. If they aren't class attrs being set like we do with PackageManager() classes, then they'd probably need to be properties.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These four helpers form the runtime API surface is consumed by dependent PR #4450 (which refactors aap_containerized plugin to remove hardcoded su - <user> -c 'podman ...' strings). Because #4450 stacks on this PR, they appear unused in isolation.

On methods vs. properties: They are methods because they accept parameters (like container, cmd, get_all, or list_fmt), which properties can't do. This matches the established parameterized pattern on ContainerRuntime (e.g., get_logs_command() and get_copy_command()).

Fixed a docstring copy-paste error in info_command() and refactored exec_command() to reuse the self.run_cmd prefix as well.

Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,62 @@ 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=<user>``, 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, container):
"""Return the command used to inspect a single container

:param container: The name of the container to inspect
:type container: ``str``

:returns: Formatted inspect command
:rtype: ``str``
"""
return f"{self.binary} inspect {container}"

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}"

# vim: set et ts=4 sw=4 :
97 changes: 88 additions & 9 deletions sos/report/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Loading