diff options
author | Jake Hunsaker <jhunsake@redhat.com> | 2019-11-21 14:17:21 -0500 |
---|---|---|
committer | Jake Hunsaker <jhunsake@redhat.com> | 2020-04-07 16:43:35 -0400 |
commit | 49b1e05911c0095cb8a89e1ad2072d2a06054865 (patch) | |
tree | 8e10676e218e878437fb862f13123655f3f85942 | |
parent | 6ada914302004f82cd8ca1876bb97c02bc97a0ef (diff) | |
download | sos-49b1e05911c0095cb8a89e1ad2072d2a06054865.tar.gz |
[Policy] Add Container Runtime abstraction class
Adds a `ContainerRuntime()` class to allow policies to specify a
container runtime to allow plugins to utilize.
The `ContainerRuntime` is intended to allow for the discovery of
containers, including specific ones by name, and for the execution of
commands inside those containers.
This is meant to remove the overhead of manually defining ways to
determine an active runtime and if a component is containerized within
plugins.
Related: #1866
Signed-off-by: Jake Hunsaker <jhunsake@redhat.com>
-rw-r--r-- | sos/policies/__init__.py | 150 | ||||
-rw-r--r-- | sos/policies/redhat.py | 1 | ||||
-rw-r--r-- | tests/plugin_tests.py | 102 |
3 files changed, 218 insertions, 35 deletions
diff --git a/sos/policies/__init__.py b/sos/policies/__init__.py index ed3f0cc1..13211bfa 100644 --- a/sos/policies/__init__.py +++ b/sos/policies/__init__.py @@ -14,6 +14,7 @@ from getpass import getpass from pwd import getpwuid from sos.utilities import (ImporterHelper, import_module, + is_executable, shell_out, sos_get_command_output) from sos.plugins import IndependentPlugin, ExperimentalPlugin @@ -22,6 +23,7 @@ from sos import SoSOptions, _arg_names from textwrap import fill from six import print_ from six.moves import input +from pipes import quote PRESETS_PATH = "/var/lib/sos/presets" @@ -67,6 +69,126 @@ def load(cache={}, sysroot=None): return cache['policy'] +class ContainerRuntime(object): + """Encapsulates a container runtime that provides the ability to plugins to + check runtime status, check for the presence of specific containers, and + to format commands to run in those containers + """ + + name = 'Undefined' + containers = [] + images = [] + volumes = [] + binary = '' + active = False + + def __init__(self, policy=None): + self.policy = policy + self.run_cmd = "%s exec " % self.binary + + def load_container_info(self): + """If this runtime is found to be active, attempt to load information + on the objects existing in the runtime. + """ + self.containers = self.get_containers() + self.images = self.get_images() + self.volumes = self.get_volumes() + + def check_is_active(self): + """Check to see if the container runtime is both present AND active. + + Active in this sense means that the runtime can be used to glean + information about the runtime itself and containers that are running. + """ + if is_executable(self.binary): + self.active = True + return True + return False + + def get_containers(self, get_all=False): + """Get a list of containers present on the system. + + If `get_all` is `True`, also include non-running containers + """ + containers = [] + _cmd = "%s ps %s" % (self.binary, '-a' if get_all else '') + if self.active: + out = sos_get_command_output(_cmd) + if out['status'] == 0: + for ent in out['output'].splitlines()[1:]: + ent = ent.split() + # takes the form (container_id, container_name) + containers.append((ent[0], ent[-1])) + return containers + + def get_container_by_name(self, name): + """Get the container ID for the container matching the provided + name + """ + if not self.active or name is None: + return None + for c in self.containers: + if re.match(name, c[1]): + return c[1] + return None + + def get_images(self): + """Get a list of images present on the system + """ + images = [] + fmt = '{{lower .Repository}}:{{lower .Tag}} {{lower .ID}}' + if self.active: + out = sos_get_command_output("%s images --format '%s'" + % (self.binary, fmt)) + if out['status'] == 0: + for ent in out['output'].splitlines(): + ent = ent.split() + # takes the form (image_name, image_id) + images.append((ent[0], ent[1])) + return images + + def get_volumes(self): + """Get a list of container volumes present on the system + """ + vols = [] + if self.active: + out = sos_get_command_output("%s volume ls" % self.binary) + if out['status'] == 0: + for ent in out['output'].splitlines()[1:]: + ent = ent.split() + vols.append(ent[-1]) + return vols + + def fmt_container_cmd(self, container, cmd): + return "%s %s %s" % (self.run_cmd, container, quote(cmd)) + + def get_logs_command(self, container): + """Return the command string used to dump container logs from the + runtime + """ + return "%s logs -t %s" % (self.binary, container) + + +class DockerContainerRuntime(ContainerRuntime): + + name = 'docker' + binary = 'docker' + + def check_is_active(self): + # the daemon must be running + if (is_executable('docker') and + self.policy.init_system.is_running('docker')): + self.active = True + return True + return False + + +class PodmanContainerRuntime(ContainerRuntime): + + name = 'podman' + binary = 'podman' + + class InitSystem(object): """Encapsulates an init system to provide service-oriented functions to sos. @@ -464,7 +586,7 @@ any third party. _in_container = False _host_sysroot = '/' - def __init__(self, sysroot=None): + def __init__(self, sysroot=None, probe_runtime=True): """Subclasses that choose to override this initializer should call super() to ensure that they get the required platform bits attached. super(SubClass, self).__init__(). Policies that require runtime @@ -472,6 +594,7 @@ any third party. modifying PATH in their own initializer.""" self._parse_uname() self.case_id = None + self.probe_runtime = probe_runtime self.package_manager = PackageManager() self._valid_subclasses = [] self.set_exec_path() @@ -861,13 +984,15 @@ class LinuxPolicy(Policy): _upload_user = None _upload_password = None _use_https_streaming = False + default_container_runtime = 'docker' _preferred_hash_name = None upload_url = None upload_user = None upload_password = None - def __init__(self, sysroot=None, init=None): - super(LinuxPolicy, self).__init__(sysroot=sysroot) + def __init__(self, sysroot=None, init=None, probe_runtime=True): + super(LinuxPolicy, self).__init__(sysroot=sysroot, + probe_runtime=probe_runtime) self.init_kernel_modules() if init is not None: @@ -877,6 +1002,25 @@ class LinuxPolicy(Policy): else: self.init_system = InitSystem() + self.runtimes = {} + if self.probe_runtime: + _crun = [ + PodmanContainerRuntime(policy=self), + DockerContainerRuntime(policy=self) + ] + for runtime in _crun: + if runtime.check_is_active(): + self.runtimes[runtime.name] = runtime + if runtime.name == self.default_container_runtime: + self.runtimes['default'] = self.runtimes[runtime.name] + self.runtimes[runtime.name].load_container_info() + + if self.runtimes and 'default' not in self.runtimes.keys(): + # still allow plugins to query a runtime present on the system + # even if that is not the policy default one + idx = list(self.runtimes.keys()) + self.runtimes['default'] = self.runtimes[idx[0]] + def get_preferred_hash_name(self): if self._preferred_hash_name: diff --git a/sos/policies/redhat.py b/sos/policies/redhat.py index 9fbe7431..eb5b09d8 100644 --- a/sos/policies/redhat.py +++ b/sos/policies/redhat.py @@ -47,6 +47,7 @@ class RedHatPolicy(LinuxPolicy): upload_url = 'dropbox.redhat.com' upload_user = 'anonymous' upload_directory = '/incoming' + default_container_runtime = 'podman' def __init__(self, sysroot=None): super(RedHatPolicy, self).__init__(sysroot=sysroot) diff --git a/tests/plugin_tests.py b/tests/plugin_tests.py index fd0d0f14..72ec895b 100644 --- a/tests/plugin_tests.py +++ b/tests/plugin_tests.py @@ -158,60 +158,86 @@ class PluginTests(unittest.TestCase): def setUp(self): self.mp = MockPlugin({ - 'cmdlineopts': MockOptions(), - 'policy': LinuxPolicy(init=InitSystem()), 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), 'cmdlineopts': MockOptions() }) self.mp.archive = MockArchive() def test_plugin_default_name(self): - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) self.assertEquals(p.name(), "mockplugin") def test_plugin_set_name(self): - p = NamedMockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = NamedMockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) self.assertEquals(p.name(), "testing") def test_plugin_no_descrip(self): - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) self.assertEquals(p.get_description(), "<no description available>") def test_plugin_no_descrip(self): - p = NamedMockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = NamedMockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) self.assertEquals(p.get_description(), "This plugin has a description.") def test_set_plugin_option(self): - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) p.set_option("opt", "testing") self.assertEquals(p.get_option("opt"), "testing") def test_set_nonexistant_plugin_option(self): - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) self.assertFalse(p.set_option("badopt", "testing")) def test_get_nonexistant_plugin_option(self): - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) self.assertEquals(p.get_option("badopt"), 0) def test_get_unset_plugin_option(self): - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) self.assertEquals(p.get_option("opt"), 0) def test_get_unset_plugin_option_with_default(self): # this shows that even when we pass in a default to get, # we'll get the option's default as set in the plugin # this might not be what we really want - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) self.assertEquals(p.get_option("opt", True), True) def test_get_unset_plugin_option_with_default_not_none(self): @@ -219,24 +245,36 @@ class PluginTests(unittest.TestCase): # if the plugin default is not None # we'll get the option's default as set in the plugin # this might not be what we really want - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) self.assertEquals(p.get_option("opt2", True), False) def test_get_option_as_list_plugin_option(self): - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) p.set_option("opt", "one,two,three") self.assertEquals(p.get_option_as_list("opt"), ['one', 'two', 'three']) def test_get_option_as_list_plugin_option_default(self): - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) self.assertEquals(p.get_option_as_list("opt", default=[]), []) def test_get_option_as_list_plugin_option_not_list(self): - p = MockPlugin({'sysroot': self.sysroot, 'policy': LinuxPolicy(init=InitSystem()), - 'cmdlineopts': MockOptions()}) + p = MockPlugin({ + 'sysroot': self.sysroot, + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), + 'cmdlineopts': MockOptions() + }) p.set_option("opt", "testing") self.assertEquals(p.get_option_as_list("opt"), ['testing']) @@ -252,7 +290,7 @@ class PluginTests(unittest.TestCase): p = ForbiddenMockPlugin({ 'cmdlineopts': MockOptions(), 'sysroot': self.sysroot, - 'policy': LinuxPolicy(init=InitSystem()) + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False) }) p.archive = MockArchive() p.setup() @@ -276,7 +314,7 @@ class AddCopySpecTests(unittest.TestCase): def setUp(self): self.mp = MockPlugin({ 'cmdlineopts': MockOptions(), - 'policy': LinuxPolicy(init=InitSystem()), + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), 'sysroot': os.getcwd(), 'cmdlineopts': MockOptions() }) @@ -353,7 +391,7 @@ class CheckEnabledTests(unittest.TestCase): def setUp(self): self.mp = EnablerPlugin({ - 'policy': sos.policies.load(), + 'policy': LinuxPolicy(probe_runtime=False), 'sysroot': os.getcwd(), 'cmdlineopts': MockOptions() }) @@ -382,7 +420,7 @@ class RegexSubTests(unittest.TestCase): def setUp(self): self.mp = MockPlugin({ 'cmdlineopts': MockOptions(), - 'policy': LinuxPolicy(init=InitSystem()), + 'policy': LinuxPolicy(init=InitSystem(), probe_runtime=False), 'sysroot': os.getcwd() }) self.mp.archive = MockArchive() |