"""
Tools for asserting on processes running in containers using ``ps``.
"""
import attr
from .utils import output_lines
[docs]class PsException(Exception):
"""
Exception indicating problems operating on process lists and trees.
"""
[docs]@attr.s
class PsRow:
"""
Representation of a process list entry, containing the details of a single
process.
"""
pid = attr.ib(converter=int)
ppid = attr.ib(converter=int)
ruser = attr.ib()
args = attr.ib()
[docs] @classmethod
def columns(cls):
"""
List the columns required to construct a suitable ``ps`` command.
"""
return [a.name for a in attr.fields(cls)]
[docs]def list_container_processes(container):
"""
List the processes running inside a container.
We use an exec rather than `container.top()` because we want to run 'ps'
inside the container. This is because we want to get PIDs and usernames in
the container's namespaces. `container.top()` uses 'ps' from outside the
container in the host's namespaces. Note that this requires the container
to have a 'ps' that responds to the arguments we give it-- we use BusyBox's
(Alpine's) 'ps' as a baseline for available functionality.
:param container: the container to query
:return: a list of PsRow objects
"""
cmd = ['ps', 'ax', '-o', ','.join(PsRow.columns())]
ps_lines = output_lines(container.exec_run(cmd))
header = ps_lines.pop(0)
# We can't trust the header alignment because different ps implementations
# use different alignments, some of which depend on the alignment of the
# columns. Instead, we assume that all columns are whitespace-separated and
# that only the last column may contain spaces.
maxsplit = len(header.strip().split()) - 1
ps_entries = [line.strip().split(None, maxsplit) for line in ps_lines]
# Convert to PsRows
ps_rows = [PsRow(*entry) for entry in ps_entries]
# Filter out the row for ps itself
cmd_string = ' '.join(cmd)
ps_rows = [row for row in ps_rows if row.args != cmd_string]
return ps_rows
[docs]@attr.s
class PsTree:
"""
Node in a process tree, linking a :class:`PsRow` to its child processes.
"""
row = attr.ib()
children = attr.ib(default=attr.Factory(list))
[docs] def count(self):
"""
Return the number of processes in this subtree.
"""
return 1 + sum(row.count() for row in self.children)
def _build_process_subtree(ps_rows, ps_tree, pids_seen):
for row in ps_rows:
if row.ppid == ps_tree.row.pid:
if row.pid in pids_seen:
raise PsException("Duplicate pid found: {}".format(row.pid))
pids_seen.add(row.pid)
tree = PsTree(row=row, children=[])
ps_tree.children.append(tree)
_build_process_subtree(ps_rows, tree, pids_seen)
[docs]def build_process_tree(ps_rows):
"""
Build a tree structure from a list of PsRow objects.
:param ps_rows: a list of PsRow objects
:return: a PsTree object
"""
ps_tree = None
for row in ps_rows:
if row.ppid == 0:
if ps_tree is not None:
raise PsException("Too many process tree roots (ppid=0) found")
ps_tree = PsTree(row)
if ps_tree is None:
raise PsException("No process tree root (ppid=0) found")
_build_process_subtree(ps_rows, ps_tree, set([ps_tree.row.pid]))
if ps_tree.count() < len(ps_rows):
raise PsException("Unreachable processes detected")
assert ps_tree.count() == len(ps_rows)
return ps_tree
__all__ = [
'build_process_tree', 'list_container_processes',
'PsException', 'PsRow', 'PsTree',
]