diff --git a/Atomic/backends/_docker.py b/Atomic/backends/_docker.py index cb06d0f5..f295910e 100644 --- a/Atomic/backends/_docker.py +++ b/Atomic/backends/_docker.py @@ -8,7 +8,7 @@ from requests import exceptions from Atomic.trust import Trust from Atomic.objects.layer import Layer - +from dateutil.parser import parse as dateparse class DockerBackend(Backend): def __init__(self): @@ -99,22 +99,39 @@ def _make_image(self, image, img_struct, deep=False, remote=False): return img_obj def _make_container(self, container, con_struct, deep=False): - con_obj = Container(container) + con_obj = Container(container, backend=self) con_obj.id = con_struct['Id'] - con_obj.created = con_struct['Created'] + try: + con_obj.created = float(con_struct['Created']) + except ValueError: + con_obj.created = dateparse(con_struct['Created']).strftime("%F %H:%M") # pylint: disable=no-member con_obj.original_structure = con_struct + try: + con_obj.name = con_struct['Names'][0] + except KeyError: + con_obj.name = con_struct['Name'] con_obj.input_name = container con_obj.backend = self + try: + con_obj.command = con_struct['Command'] + except KeyError: + con_obj.command = con_struct['Config']['Cmd'] + + con_obj.state = con_struct.get('State', None) or con_struct.get['State'].get('Status', None) + if isinstance(con_obj.state, dict): + con_obj.state = con_obj.state['Status'] + con_obj.running = True if con_obj.state.lower() in ['true', 'running'] else False if deep: # Add in the deep inspection stuff con_obj.status = con_struct['State']['Status'] - con_obj.running = con_struct['State']['Running'] con_obj.image = con_struct['Image'] + con_obj.image_name = con_struct['Config']['Image'] else: con_obj.status = con_struct['Status'] - con_obj.image = con_struct['ImageID'] + con_obj.image_id = con_struct['ImageID'] + con_obj.image_name = con_struct['Image'] return con_obj @@ -276,3 +293,4 @@ def get_layers(self, image): layers.append(layer) return layers + diff --git a/Atomic/backends/_ostree.py b/Atomic/backends/_ostree.py index 829a0da0..81a1d546 100644 --- a/Atomic/backends/_ostree.py +++ b/Atomic/backends/_ostree.py @@ -26,18 +26,20 @@ def backend(self): def _make_container(self, info): container_id = info['Id'] - runtime = self.syscontainers.get_container_runtime_info(container_id) - container = Container(container_id, backend=self) container.name = container_id + container.command = info['Command'] container.id = container_id + container.image_name = info['Image'] + container.image_id = info['ImageID'] container.created = info['Created'] - container.status = runtime['status'] + container.status = container.state = runtime['status'] container.input_name = container_id container.original_structure = info container.deep = True container.image = info['Image'] + container.running = False if container.status == 'inactive' else True return container @@ -142,7 +144,8 @@ def get_layers(self, image): layers.append(layer) return layers - def get_dangling_images(self): + @staticmethod + def get_dangling_images(): return [] def make_remote_image(self, image): @@ -152,3 +155,7 @@ def make_remote_image(self, image): def _make_remote_image(self, image): return self._make_image(image, None, remote=True) + + def delete_container(self, container, force=False): + return self.syscontainers.uninstall(container) + diff --git a/Atomic/backends/backend.py b/Atomic/backends/backend.py index 8ad24383..119062c3 100644 --- a/Atomic/backends/backend.py +++ b/Atomic/backends/backend.py @@ -96,6 +96,10 @@ def delete_image(self, image, force=False): """ pass + @abstractmethod + def delete_container(self, container, force=False): + pass + @abstractmethod def start_container(self, name): pass diff --git a/Atomic/backendutils.py b/Atomic/backendutils.py index 13ffb910..39796f2f 100644 --- a/Atomic/backendutils.py +++ b/Atomic/backendutils.py @@ -27,14 +27,15 @@ def backend_has_image(backend, img): def backend_has_container(backend, container): return True if backend.has_container(container) else False - def get_backend_and_image(self, img, str_preferred_backend=None): + def get_backend_and_image_obj(self, img, str_preferred_backend=None): """ Given an image name (str) and optionally a str reference to a backend, this method looks for the image firstly on the preferred backend and - then on the alternate backends. It returns a backend object. + then on the alternate backends. It returns a backend object and an + image object. :param img: name of image to look for :param str_preferred_backend: i.e. 'docker' - :return: backend object + :return: backend object and image object """ backends = list(self.BACKENDS) # Check preferred backend first @@ -62,35 +63,38 @@ def get_backend_and_image(self, img, str_preferred_backend=None): raise ValueError("Found {} in multiple storage backends: {}". format(img, ', '.join([x.backend for x in img_in_backends]))) - def get_backend_for_container(self, container, str_preferred_backend=None): + def get_backend_and_container_obj(self, container_name, str_preferred_backend=None): """ Given a container name (str) and optionally a str reference to a backend, this method looks for the container firstly on the preferred backend and - then on the alternate backends. It returns a backend object. - :param container: name of image to look for + then on the alternate backends. It returns a backend object and a container + object. + :param container_name: name of image to look for :param str_preferred_backend: i.e. 'docker' - :return: backend object + :return: backend object and container object """ backends = list(self.BACKENDS) # Check preferred backend first if str_preferred_backend: be = self.get_backend_from_string(str_preferred_backend) - if be.has_container(container): - return be + con_obj = be.has_container(container_name) + if con_obj: + return be, con_obj # Didnt find in preferred, need to remove it from the list now del backends[self._get_backend_index_from_string(str_preferred_backend)] container_in_backends = [] for backend in backends: be = backend() - if be.has_container(container): - container_in_backends.append(be) + con_obj = be.has_container(container_name) + if con_obj: + container_in_backends.append((be, con_obj)) if len(container_in_backends) == 1: return container_in_backends[0] if len(container_in_backends) == 0: - raise ValueError("Unable to find backend associated with container '{}'".format(container)) + raise ValueError("Unable to find backend associated with container '{}'".format(container_name)) raise ValueError("Found {} in multiple storage backends: {}". - format(container, ', '.join([x.backend for x in container_in_backends]))) + format(container_name, ', '.join([x.backend for x in container_in_backends]))) def get_images(self, get_all=False): backends = self.BACKENDS diff --git a/Atomic/containers.py b/Atomic/containers.py index 91cd5631..eda3918e 100644 --- a/Atomic/containers.py +++ b/Atomic/containers.py @@ -1,19 +1,21 @@ import argparse -import json import os +import copy from . import util from . import Atomic from .client import AtomicDocker -import datetime -from dateutil.parser import parse as dateparse -from docker.errors import NotFound, APIError +from docker.errors import APIError +from Atomic.backendutils import BackendUtils try: from subprocess import DEVNULL # pylint: disable=no-name-in-module except ImportError: DEVNULL = open(os.devnull, 'wb') +ATOMIC_CONFIG = util.get_atomic_config() +storage = ATOMIC_CONFIG.get('default_storage', "docker") + def cli(subparser): # atomic containers c = subparser.add_parser("containers", @@ -47,7 +49,7 @@ def cli(subparser): pss.set_defaults(_class=Containers, func='ps_tty') pss.add_argument("-a", "--all", action='store_true',dest="all", default=False, help=_("show all containers")) - pss.add_argument("-f", "--filter", metavar='FILTER', action='append', dest="filter", + pss.add_argument("-f", "--filter", metavar='FILTER', action='append', dest="filters", help=_("Filter output based on conditions given in the VARIABLE=VALUE form")) pss.add_argument("--json", action='store_true',dest="json", default=False, help=_("print in a machine parseable form")) @@ -66,6 +68,9 @@ def cli(subparser): class Containers(Atomic): + FILTER_KEYWORDS= {"container": "id", "image": "image_name", "command": "command", + "created": "created", "state": "state", "runtime": "runtime"} + def fstrim(self): with AtomicDocker() as client: for container in client.containers(): @@ -77,177 +82,137 @@ def fstrim(self): util.check_call(["/usr/sbin/fstrim", "-v", mp], stdout=DEVNULL) return - def ps_tty(self): - all_container_info = self.ps() - all_containers = [] - for each in all_container_info: - if each["Type"] == "system": - container = each["Id"] - status = "exited" - created = datetime.datetime.fromtimestamp(each["Created"]) - info = self.syscontainers.get_container_runtime_info(container) - if 'status' in info: - status = info["status"] - - if not self.args.all and status != "running": - continue - - image = each['Image'] - imageId = each['ImageID'] - command = each["Command"] - created = created.strftime("%F %H:%M") # pylint: disable=no-member - container_info = {"type" : "system", "container" : container, - "image" : image, "command" : command, "image_id" : imageId, - "created" : created, "status" : status, - "runtime" : "runc", "vulnerable" : each["vulnerable"]} - - if self.args.filter: - if not self._filter_include_container(container_info): - continue - all_containers.append(container_info) - - elif each["Type"] == "docker": - # Collect the docker containers - container = each["Id"] - ret = self._inspect_container(name=container) - status = ret["State"]["Status"] - image = ret['Config']['Image'] - imageId = ret['Image'] - command = u' '.join(ret['Config']['Cmd']) if ret['Config']['Cmd'] else "" - created = dateparse(ret['Created']).strftime("%F %H:%M") # pylint: disable=no-member - container_info = {"type" : "docker", "container" : container, - "image" : image, "image_id" : imageId, "command" : command, - "created" : created, "status" : status, - "runtime" : "Docker", "vulnerable" : each["vulnerable"]} - - if self.args.filter: - if not self._filter_include_container(container_info): - continue - all_containers.append(container_info) - - if not all_containers: - return + def filter_container_objects(self, con_objs): + def _walk(_filter_objs, _filter, _value): + _filtered = [] + for con_obj in _filter_objs: + if _value in getattr(con_obj, _filter, None): + _filtered.append(con_obj) + return _filtered + + if not self.args.filters: + return con_objs + filtered_objs = copy.deepcopy(con_objs) + for f in self.args.filters: + cfilter, value = f.split('=', 1) + cfilter = self.FILTER_KEYWORDS[cfilter] + filtered_objs = _walk(filtered_objs, cfilter, value) + return filtered_objs - if self.args.json: - util.write_out(json.dumps(all_containers)) - return + def ps_tty(self): + if self.args.debug: + util.write_out(str(self.args)) - if self.args.truncate: - max_len_container = 12 - max_len_image = 20 - max_len_command = 20 - else: - max_len_container = max(max([len(s["container"]) for s in all_containers]), 12) - max_len_image = max(max([len(s["image"]) for s in all_containers]), 20) - max_len_command = max(max([len(s["command"]) for s in all_containers]), 20) + container_objects = self._ps() + if not any([x.running for x in container_objects]) and not self.args.all: + return 0 if self.args.quiet: - for container in all_containers: - util.write_out(container["container"][0:max_len_container]) - return + for con_obj in container_objects: + util.write_out(con_obj.id[:12]) + return 0 + if self.args.json: + util.output_json(self._to_json(container_objects)) + return 0 - col_out = "{0:2} {1:%s} {2:%s} {3:%s} {4:16} {5:9} {6:10}" % (max_len_container, max_len_image, max_len_command) + if len(container_objects) == 0: + return 0 + max_container_id = 12 if self.args.truncate else max([len(x.id) for x in container_objects]) + max_image_name = 20 if self.args.truncate else max([len(x.image_name) for x in container_objects]) + max_command = 20 if self.args.truncate else max([len(x.command) for x in container_objects]) + col_out = "{0:2} {1:%s} {2:%s} {3:%s} {4:16} {5:9} {6:10}" % (max_container_id, max_image_name, max_command) if self.args.heading: util.write_out(col_out.format(" ", "CONTAINER ID", "IMAGE", "COMMAND", "CREATED", - "STATUS", + "STATE", "RUNTIME")) - - #if self.args.truncate: - for container in all_containers: + for con_obj in container_objects: indicator = "" - if container["vulnerable"]: + if con_obj.vulnerable: if util.is_python2: indicator = indicator + self.skull + " " else: indicator = indicator + str(self.skull, "utf-8") + " " util.write_out(col_out.format(indicator, - container["container"][0:max_len_container], - container["image"][0:max_len_image], - container["command"][0:max_len_command], - container["created"][0:16], - container["status"][0:9], - container["runtime"][0:10])) + con_obj.id[0:max_container_id], + con_obj.image_name[0:max_image_name], + con_obj.command[0:max_command], + con_obj.created[0:16], + con_obj.state[0:9], + con_obj.backend.backend[0:10])) + def ps(self): - all_containers = [] - vuln_ids = self.get_vulnerable_ids() - all_vuln_info = self.get_all_vulnerable_info() - - # Collect the system containers - for i in self.syscontainers.get_containers(): - i["vulnerable"] = i['Id'] in vuln_ids - if i["vulnerable"]: - i["vuln_info"] = all_vuln_info[i['Id']] - else: - i["vuln_info"] = dict() - all_containers.append(i) - - # Collect the docker containers - for container in [x["Id"] for x in self.d.containers(all=self.args.all)]: - ret = self._inspect_container(name=container) - ret["Type"] = "docker" - ret["vulnerable"] = ret["Image"] in vuln_ids - if ret["vulnerable"]: - ret["vuln_info"] = all_vuln_info[ret["Image"]] - else: - ret["vuln_info"] = dict() - all_containers.append(ret) - - return all_containers - - def _filter_include_container(self, container_info): - filterables = ["container", "image", "command", "created", "status", "runtime"] - for j in self.args.filter: - var, value = str(j).split("=") - var = var.lower() - - if var == "id" or var == "containerid": - var = "container" - - if var not in filterables: # If the filter does not exist, default to allowing all containers through - continue - - if value not in container_info[var]: - return False - - return True + container_objects = self._ps() + return self._to_json(container_objects) + + def _ps(self): + def _check_filters(): + if not self.args.filters: + return True + for f in self.args.filters: + _filter, _ = f.split('=', 1) + if _filter not in [x for x in self.FILTER_KEYWORDS]: + raise ValueError("The filter {} is not valid. " + "Please choose from {}".format(_filter, [x for x in self.FILTER_KEYWORDS])) + _check_filters() + beu = BackendUtils() + containers = self.filter_container_objects(beu.get_containers()) + self._mark_vulnerable(containers) + if self.args.all: + return containers + return [x for x in containers if x.running] + + @staticmethod + def _to_json(con_objects): + containers = [] + for con_obj in con_objects: + _con = {'id': con_obj.id, + 'image_name': con_obj.image_name, + 'command': con_obj.command, + 'created': con_obj.created, + 'state': con_obj.state, + 'runtime': con_obj.runtime, + 'vulnerable': con_obj.vulnerable, + 'running': con_obj.running + } + containers.append(_con) + return containers def delete(self): - results = 0 - sys_targets=[] - docker_targets=[] + + if self.args.debug: + util.write_out(str(self.args)) + + beu = BackendUtils() if self.args.all: - for c in self.get_containers(): - docker_targets.append(c["Id"]) - for c in self.syscontainers.get_containers(): - sys_targets.append(c["Id"]) + container_objects = beu.get_containers() else: - if not self.args.container and len(self.args.containers) == 0: - raise ValueError("No containers selected") - for c in [ self.args.container ] + self.args.containers: - if self.syscontainers.get_checkout(c): - sys_targets.append(c) - else: - docker_targets.append(c) + container_objects = [] + for con in self.args.container: + _, con_obj = beu.get_backend_and_container_obj(con, str_preferred_backend=storage) + container_objects.append(con_obj) + + four_col = " {0:12} {1:20} {2:25} {3:10}" + util.write_out(four_col.format("ID", "NAME", 'IMAGE_NAME', "STORAGE")) + for con in container_objects: + util.write_out(four_col.format(con.id[0:12], con.name[0:20], con.image_name[0:25], con.backend.backend)) + if not util.confirm_input("\nDo you wish to delete the following containers?\n"): + util.write_out("Aborting...") + return - for target in docker_targets: + for del_con in container_objects: try: - self.d.remove_container(target, force=self.args.force) - except NotFound as e: - util.write_err("Failed to delete container {}: {}".format(target, e)) - results += 1 + del_con.backend.delete_container(del_con.id, force=self.args.force) except APIError as e: - util.write_err("Failed operation for delete container {}: {}".format(target, e)) - results += 1 - - for target in sys_targets: - try: - self.syscontainers.uninstall(target) - except IOError as e: - util.write_err("Failed to delete container {}: {}".format(target, e)) - results += 1 - return results + util.write_err("Failed to delete container {}: {}".format(con.id, e)) + + def _mark_vulnerable(self, containers): + assert isinstance(containers, list) + vulnerable_uuids = self.get_vulnerable_ids() + for con in containers: + if con.id in vulnerable_uuids: + con.vulnerable = True diff --git a/Atomic/delete.py b/Atomic/delete.py index 72c817b0..9c4deb2c 100644 --- a/Atomic/delete.py +++ b/Atomic/delete.py @@ -26,7 +26,7 @@ def delete_image(self): # The failure here is basically that it couldnt verify/find the image. for image in self.args.delete_targets: - be, img_obj = beu.get_backend_and_image(image, str_preferred_backend=self.args.storage) + be, img_obj = beu.get_backend_and_image_obj(image, str_preferred_backend=self.args.storage) delete_objects.append((be, img_obj)) if self.args.remote: diff --git a/Atomic/info.py b/Atomic/info.py index 301c0c81..e0058a01 100644 --- a/Atomic/info.py +++ b/Atomic/info.py @@ -87,7 +87,7 @@ def _version(self, write_func): write_func("") def get_layer_objects(self): - _, img_obj = self.beu.get_backend_and_image(self.image, str_preferred_backend=self.args.storage) + _, img_obj = self.beu.get_backend_and_image_obj(self.image, str_preferred_backend=self.args.storage) return img_obj.layers def dbus_version(self): @@ -117,7 +117,7 @@ def info(self): img_obj = be.make_remote_image(self.image) else: # The image is local - be, img_obj = self.beu.get_backend_and_image(self.image, str_preferred_backend=self.args.storage) + be, img_obj = self.beu.get_backend_and_image_obj(self.image, str_preferred_backend=self.args.storage) with closing(StringIO()) as buf: try: diff --git a/Atomic/objects/container.py b/Atomic/objects/container.py index a37366b6..42c34066 100644 --- a/Atomic/objects/container.py +++ b/Atomic/objects/container.py @@ -1,5 +1,5 @@ from Atomic.util import output_json - +import datetime class Container(object): def __init__(self, input_name, backend=None): @@ -7,13 +7,18 @@ def __init__(self, input_name, backend=None): # Required self.name = None self.id = None - self.created = None + self._created = None self.status = None self.input_name = input_name self.original_structure = None self.deep = False self._backend = backend - self.image = None + self.runtime = backend.backend + self.image_id = None + self.image_name = None + self.command = None + self.state = None + self.vulnerable = False # Optional self.running = False @@ -41,4 +46,16 @@ def backend(self): @backend.setter def backend(self, value): - self._backend = value \ No newline at end of file + self._backend = value + + @property + def created(self): + return str(datetime.datetime.fromtimestamp(self._created)) + + @property + def created_raw(self): + return self._created + + @created.setter + def created(self, value): + self._created = value diff --git a/Atomic/util.py b/Atomic/util.py index bb3b7aea..d5574273 100644 --- a/Atomic/util.py +++ b/Atomic/util.py @@ -749,6 +749,12 @@ def getgnuhome(): return None +def confirm_input(msg): + write_out("{}\n".format(msg)) + confirm = input("\nConfirm (y/N)") + return confirm.strip().lower() in ['y', 'yes'] + + class Decompose(object): """ Class for decomposing an input string in its respective parts like registry, diff --git a/Atomic/verify.py b/Atomic/verify.py index 7eb04da0..6329cc87 100644 --- a/Atomic/verify.py +++ b/Atomic/verify.py @@ -86,7 +86,7 @@ def verify_dbus(self): return layers def _verify(self): - be, img_obj = self.backend_utils.get_backend_and_image(self.image, self.args.storage) + be, img_obj = self.backend_utils.get_backend_and_image_obj(self.image, self.args.storage) remote_img_name = "{}:latest".format(util.Decompose(img_obj.fq_name).no_tag) remote_img_obj = be.make_remote_image(remote_img_name) return img_obj.layers, remote_img_obj.layers diff --git a/tests/integration/test_containers_list.sh b/tests/integration/test_containers_list.sh index 37dce3a9..06f3d692 100755 --- a/tests/integration/test_containers_list.sh +++ b/tests/integration/test_containers_list.sh @@ -13,10 +13,11 @@ IFS=$'\n\t' # In addition, the test harness creates some images for use in testing. # See tests/test-images/ +ATOMIC=$(grep -v -- --debug <<< "$ATOMIC") OUTPUT=$(/bin/true) -${ATOMIC} containers list --all -q -f runtime=Docker | sort > atomic.ps.out +${ATOMIC} containers list --all -q -f runtime=docker | sort > atomic.ps.out docker ps --all -q | sort > docker.ps.out diff docker.ps.out atomic.ps.out diff --git a/tests/integration/test_system_containers.sh b/tests/integration/test_system_containers.sh index a503e14f..5841b7cb 100755 --- a/tests/integration/test_system_containers.sh +++ b/tests/integration/test_system_containers.sh @@ -111,18 +111,18 @@ ${ATOMIC} containers list --all > ps.out assert_matches "test-system" ps.out ${ATOMIC} containers list --all --no-trunc > ps.out assert_matches "test-system" ps.out -${ATOMIC} containers list --no-trunc --filter id=test-system > ps.out +${ATOMIC} containers list --no-trunc --filter container=test-system > ps.out assert_matches "test-system" ps.out ${ATOMIC} containers list --no-trunc > ps.out assert_matches "test-system" ps.out ${ATOMIC} containers list --no-trunc --quiet > ps.out assert_matches "test-system" ps.out -${ATOMIC} containers list -aq --no-trunc --filter id=test-system > ps.out +${ATOMIC} containers list -aq --no-trunc --filter container=test-system > ps.out assert_matches "test-system" ps.out -${ATOMIC} containers list -aq --no-trunc --filter id=non-existing-system > ps.out +${ATOMIC} containers list -aq --no-trunc --filter container=non-existing-system > ps.out assert_not_matches "test-system" ps.out -${ATOMIC} containers list --all --no-trunc --filter id=test-system | grep "test-system" > ps.out +${ATOMIC} containers list --all --no-trunc --filter container=test-system | grep "test-system" > ps.out # Check the command is included in the output assert_matches "run.sh" ps.out diff --git a/tests/unit/test_images.py b/tests/unit/test_images.py index 3cdd899e..53eec02a 100644 --- a/tests/unit/test_images.py +++ b/tests/unit/test_images.py @@ -51,7 +51,7 @@ def test_docker_info(self): args = self.Args() args.storage = 'docker' info.set_args(args) - info.beu.get_backend_and_image = MagicMock(return_value=(db, img_obj)) + info.beu.get_backend_and_image_obj = MagicMock(return_value=(db, img_obj)) result = info.info() self.assertEqual(result, _docker_centos_result) @@ -65,7 +65,7 @@ def test_ostree_info(self): args = self.Args() args.storage = 'ostree' info.set_args(args) - info.beu.get_backend_and_image = MagicMock(return_value=(ob, img_obj)) + info.beu.get_backend_and_image_obj = MagicMock(return_value=(ob, img_obj)) result = info.info() self.assertEqual(result, _ostree_centos_result) @@ -140,7 +140,7 @@ def __init__(self): self.image = None def test_verify_docker_same(self): - with patch('Atomic.backendutils.BackendUtils.get_backend_and_image') as mockobj: + with patch('Atomic.backendutils.BackendUtils.get_backend_and_image_obj') as mockobj: args = self.Args() args.storage = 'docker' args.image = 'docker.io/library/centos:latest' @@ -154,7 +154,7 @@ def test_verify_docker_same(self): self.assertEqual(v.verify_dbus(), docker_dbus_result) def test_verify_docker_diff(self): - with patch('Atomic.backendutils.BackendUtils.get_backend_and_image') as mockobj: + with patch('Atomic.backendutils.BackendUtils.get_backend_and_image_obj') as mockobj: args = self.Args() args.storage = 'docker' args.image = 'docker.io/library/centos:centos7.0.1406'