From e74bd877aef4795d53ceed70045fab08d2169c98 Mon Sep 17 00:00:00 2001 From: Jhon Honce Date: Wed, 26 Jan 2022 10:25:29 -0700 Subject: [PATCH] Update CI to fail on Podman 4.0 errors CI has been testing against containers/podman/main to report issues. With the release of Podman 4.0, any errors are now failures. * Makefile target "test" and "lint" updated to use tox covering python 3.6, 3.8, 3.9 and 3.10. * Added black formatting check to Makefile target "lint" * Source code changes made to satisfy "tox -e black-format" and "tox -e pylint" Signed-off-by: Jhon Honce --- .cirrus.yml | 1 + .pylintrc | 4 +- Makefile | 22 +- contrib/cirrus/build_podman.sh | 10 +- contrib/cirrus/test.sh | 2 - podman/api/tar_utils.py | 7 +- podman/api/typing_extensions.py | 18 +- podman/domain/containers_create.py | 7 +- podman/domain/containers_manager.py | 7 +- podman/domain/images_manager.py | 1 + podman/domain/manifests.py | 70 ++++--- podman/domain/networks.py | 26 +-- podman/domain/networks_manager.py | 147 +++++-------- podman/domain/pods.py | 4 +- podman/domain/pods_manager.py | 4 +- podman/tests/__init__.py | 2 +- podman/tests/integration/base.py | 16 +- .../integration/test_container_create.py | 4 - podman/tests/integration/test_containers.py | 4 +- podman/tests/integration/test_manifests.py | 20 +- podman/tests/integration/test_networks.py | 11 +- podman/tests/integration/test_pods.py | 85 +++++--- podman/tests/integration/utils.py | 11 +- podman/tests/unit/test_config.py | 4 +- podman/tests/unit/test_containersmanager.py | 8 +- podman/tests/unit/test_manifests.py | 17 +- podman/tests/unit/test_network.py | 56 +++-- podman/tests/unit/test_networksmanager.py | 194 ++++++------------ podman/tests/unit/test_volumesmanager.py | 6 +- podman/version.py | 2 +- pyproject.toml | 12 +- requirements.txt | 7 +- setup.cfg | 3 +- setup.py | 12 +- test-requirements.txt | 4 +- tox.ini | 8 +- 36 files changed, 353 insertions(+), 463 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 1ec4c34e..0285a8c8 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -61,6 +61,7 @@ test_task: script: - ${SCRIPT_BASE}/enable_ssh.sh + - ${SCRIPT_BASE}/build_podman.sh - ${SCRIPT_BASE}/enable_podman.sh - ${SCRIPT_BASE}/test.sh diff --git a/.pylintrc b/.pylintrc index a901c2fa..e10c94f3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -11,7 +11,9 @@ ignore=CVS,docs # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. -ignore-patterns=test_.* +# ignore-patterns=test_.* + +ignore-paths=^podman/tests/.*$ # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). diff --git a/Makefile b/Makefile index 8ce24578..cebbd395 100644 --- a/Makefile +++ b/Makefile @@ -8,25 +8,23 @@ DESTDIR ?= EPOCH_TEST_COMMIT ?= $(shell git merge-base $${DEST_BRANCH:-main} HEAD) HEAD ?= HEAD -export PODMAN_VERSION ?= "3.2.0" +export PODMAN_VERSION ?= "4.0.0" .PHONY: podman podman: rm dist/* || : - python -m pip install --user -r requirements.txt + $(PYTHON) -m pip install --user -r requirements.txt PODMAN_VERSION=$(PODMAN_VERSION) \ $(PYTHON) setup.py sdist bdist bdist_wheel .PHONY: lint -lint: - $(PYTHON) -m pylint podman || exit $$(($$? % 4)); +lint: tox + $(PYTHON) -m tox -e black,pylint .PHONY: tests -tests: - python -m pip install --user -r test-requirements.txt - DEBUG=1 coverage run -m unittest discover -s podman/tests - coverage report -m --skip-covered --fail-under=80 --omit=./podman/tests/* --omit=.tox/* \ - --omit=/usr/lib/* --omit=*/lib/python* +tests: tox + # see tox.ini for environment variable settings + $(PYTHON) -m tox -e pylint,coverage,py36,py38,py39,py310 .PHONY: unittest unittest: @@ -38,6 +36,12 @@ integration: coverage run -m unittest discover -s podman/tests/integration coverage report -m --skip-covered --fail-under=80 --omit=./podman/tests/* --omit=.tox/* --omit=/usr/lib/* +.PHONY: tox +tox: + -dnf install -y python3 python3.6 python3.8 python3.9 + # ensure tox is available. It will take care of other testing requirements + $(PYTHON) -m pip install --user tox + .PHONY: test-release test-release: SOURCE = $(shell find dist -regex '.*/podman-[0-9][0-9\.]*.tar.gz' -print) test-release: diff --git a/contrib/cirrus/build_podman.sh b/contrib/cirrus/build_podman.sh index e2120c90..9a217b0b 100755 --- a/contrib/cirrus/build_podman.sh +++ b/contrib/cirrus/build_podman.sh @@ -2,13 +2,9 @@ set -xeo pipefail -mkdir -p "$GOPATH/src/github.com/containers/" -cd "$GOPATH/src/github.com/containers/" +systemctl stop podman.socket || : -systemctl stop podman.socket ||: dnf erase podman -y -git clone https://github.com/containers/podman.git +dnf copr enable rhcontainerbot/podman-next -y +dnf install podman -y -cd podman -make binaries -make install PREFIX=/usr diff --git a/contrib/cirrus/test.sh b/contrib/cirrus/test.sh index 63d63fc2..fe4de52b 100755 --- a/contrib/cirrus/test.sh +++ b/contrib/cirrus/test.sh @@ -2,6 +2,4 @@ set -eo pipefail - - make tests diff --git a/podman/api/tar_utils.py b/podman/api/tar_utils.py index 0ad9cbbe..df16b26f 100644 --- a/podman/api/tar_utils.py +++ b/podman/api/tar_utils.py @@ -84,7 +84,7 @@ def add_filter(info: tarfile.TarInfo) -> Optional[tarfile.TarInfo]: return None # Workaround https://bugs.python.org/issue32713. Fixed in Python 3.7 - if info.mtime < 0 or info.mtime > 8 ** 11 - 1: + if info.mtime < 0 or info.mtime > 8**11 - 1: info.mtime = int(info.mtime) # do not leak client information to service @@ -97,9 +97,8 @@ def add_filter(info: tarfile.TarInfo) -> Optional[tarfile.TarInfo]: return info if name is None: - name = tempfile.NamedTemporaryFile( - prefix="podman_context", suffix=".tar" - ) # pylint: disable=consider-using-with + # pylint: disable=consider-using-with + name = tempfile.NamedTemporaryFile(prefix="podman_context", suffix=".tar") else: name = pathlib.Path(name) diff --git a/podman/api/typing_extensions.py b/podman/api/typing_extensions.py index ac00e1a7..291497de 100644 --- a/podman/api/typing_extensions.py +++ b/podman/api/typing_extensions.py @@ -884,7 +884,6 @@ def __new__(cls, *args, **kwds): return collections.deque(*args, **kwds) return _generic_new(collections.deque, cls, *args, **kwds) - else: class Deque( @@ -912,7 +911,6 @@ class ContextManager( ): __slots__ = () - else: class ContextManager(typing.Generic[T_co]): @@ -994,7 +992,6 @@ def __new__(cls, *args, **kwds): return collections.defaultdict(*args, **kwds) return _generic_new(collections.defaultdict, cls, *args, **kwds) - else: class DefaultDict( @@ -1032,7 +1029,6 @@ def __new__(cls, *args, **kwds): return collections.OrderedDict(*args, **kwds) return _generic_new(collections.OrderedDict, cls, *args, **kwds) - else: class OrderedDict( @@ -1073,7 +1069,6 @@ def __new__(cls, *args, **kwds): return collections.Counter(*args, **kwds) return _generic_new(collections.Counter, cls, *args, **kwds) - elif _geqv_defined: class Counter( @@ -1090,7 +1085,6 @@ def __new__(cls, *args, **kwds): return collections.Counter(*args, **kwds) return _generic_new(collections.Counter, cls, *args, **kwds) - else: class Counter( @@ -1353,9 +1347,7 @@ def __new__( bases = tuple(b for b in bases if b is not Generic) namespace.update({'__origin__': origin, '__extra__': extra}) self = super(GenericMeta, cls).__new__(cls, name, bases, namespace, _root=True) - super(GenericMeta, self).__setattr__( - '_gorg', self if not origin else _gorg(origin) - ) + super(GenericMeta, self).__setattr__('_gorg', self if not origin else _gorg(origin)) self.__parameters__ = tvars self.__args__ = ( tuple( @@ -1479,9 +1471,7 @@ def __getitem__(self, params): if not isinstance(params, tuple): params = (params,) if not params and _gorg(self) is not Tuple: - raise TypeError( - "Parameter list to %s[...] cannot be empty" % self.__qualname__ - ) + raise TypeError("Parameter list to %s[...] cannot be empty" % self.__qualname__) msg = "Parameters to generic types must be types." params = tuple(_type_check(p, msg) for p in params) if self in (Generic, Protocol): @@ -2108,7 +2098,6 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): return hint return {k: _strip_annotations(t) for k, t in hint.items()} - elif HAVE_ANNOTATED: def _is_dunder(name): @@ -2344,7 +2333,6 @@ def TypeAlias(self, parameters): """ raise TypeError("{} is not subscriptable".format(self)) - elif sys.version_info[:2] >= (3, 7): class _TypeAliasForm(typing._SpecialForm, _root=True): @@ -2672,7 +2660,6 @@ def Concatenate(self, parameters): """ return _concatenate_getitem(self, parameters) - elif sys.version_info[:2] >= (3, 7): class _ConcatenateForm(typing._SpecialForm, _root=True): @@ -2821,7 +2808,6 @@ def is_str(val: Union[str, float]): item = typing._type_check(parameters, '{} accepts only single type.'.format(self)) return _GenericAlias(self, (item,)) - elif sys.version_info[:2] >= (3, 7): class _TypeGuardForm(typing._SpecialForm, _root=True): diff --git a/podman/domain/containers_create.py b/podman/domain/containers_create.py index 5dd09b6e..c24b3cdf 100644 --- a/podman/domain/containers_create.py +++ b/podman/domain/containers_create.py @@ -310,8 +310,7 @@ def to_bytes(size: Union[int, str, None]) -> Union[int, None]: if search: return int(search.group(1)) * (1024 ** mapping[search.group(2)]) raise TypeError( - f"Passed string size {size} should be in format\\d+[bBkKmMgG] (e.g." - " '100m')" + f"Passed string size {size} should be in format\\d+[bBkKmMgG] (e.g. '100m')" ) from bad_size else: raise TypeError( @@ -415,9 +414,7 @@ def to_bytes(size: Union[int, str, None]) -> Union[int, None]: if "Config" in args["log_config"]: params["log_configuration"]["path"] = args["log_config"]["Config"].get("path") params["log_configuration"]["size"] = args["log_config"]["Config"].get("size") - params["log_configuration"]["options"] = args["log_config"]["Config"].get( - "options" - ) + params["log_configuration"]["options"] = args["log_config"]["Config"].get("options") args.pop("log_config") for item in args.pop("mounts", []): diff --git a/podman/domain/containers_manager.py b/podman/domain/containers_manager.py index 62b890f1..4d256c1a 100644 --- a/podman/domain/containers_manager.py +++ b/podman/domain/containers_manager.py @@ -25,10 +25,7 @@ def exists(self, key: str) -> bool: response = self.client.get(f"/containers/{key}/exists") return response.ok - # pylint is flagging 'container_id' here vs. 'key' parameter in super.get() - def get( - self, container_id: str - ) -> Container: # pylint: disable=arguments-differ,arguments-renamed + def get(self, key: str) -> Container: """Get container by name or id. Args: @@ -38,7 +35,7 @@ def get( NotFound: when Container does not exist APIError: when an error return by service """ - container_id = urllib.parse.quote_plus(container_id) + container_id = urllib.parse.quote_plus(key) response = self.client.get(f"/containers/{container_id}/json") response.raise_for_status() return self.prepare_model(attrs=response.json()) diff --git a/podman/domain/images_manager.py b/podman/domain/images_manager.py index 93a1bc69..944039f5 100644 --- a/podman/domain/images_manager.py +++ b/podman/domain/images_manager.py @@ -27,6 +27,7 @@ def resource(self): return Image def exists(self, key: str) -> bool: + """Return true when image exists.""" key = urllib.parse.quote_plus(key) response = self.client.get(f"/images/{key}/exists") return response.ok diff --git a/podman/domain/manifests.py b/podman/domain/manifests.py index 0dfbff48..bff805ff 100644 --- a/podman/domain/manifests.py +++ b/podman/domain/manifests.py @@ -2,7 +2,7 @@ import logging import urllib.parse from contextlib import suppress -from typing import List, Optional, Union +from typing import Any, Dict, List, Optional, Union from podman import api from podman.domain.images import Image @@ -17,20 +17,18 @@ class Manifest(PodmanResource): @property def id(self): - """str: Returns the identifier of the manifest.""" + """str: Returns the identifier of the manifest list.""" with suppress(KeyError, TypeError, IndexError): - return self.attrs["manifests"][0]["digest"] + digest = self.attrs["manifests"][0]["digest"] + if digest.startswith("sha256:"): + return digest[7:] + return digest return self.name @property def name(self): - """str: Returns the identifier of the manifest.""" - try: - if len(self.names[0]) == 0: - raise ValueError("Manifest attribute 'names' is empty.") - return self.names[0] - except (TypeError, IndexError) as e: - raise ValueError("Manifest attribute 'names' is missing.") from e + """str: Returns the human-formatted identifier of the manifest list.""" + return self.attrs.get("names") @property def quoted_name(self): @@ -40,7 +38,7 @@ def quoted_name(self): @property def names(self): """List[str]: Returns the identifier of the manifest.""" - return self.attrs.get("names") + return self.name @property def media_type(self): @@ -71,7 +69,7 @@ def add(self, images: List[Union[Image, str]], **kwargs) -> None: ImageNotFound: when Image(s) could not be found APIError: when service reports an error """ - params = { + data = { "all": kwargs.get("all"), "annotation": kwargs.get("annotation"), "arch": kwargs.get("arch"), @@ -80,14 +78,15 @@ def add(self, images: List[Union[Image, str]], **kwargs) -> None: "os": kwargs.get("os"), "os_version": kwargs.get("os_version"), "variant": kwargs.get("variant"), + "operation": "update", } for item in images: if isinstance(item, Image): item = item.attrs["RepoTags"][0] - params["images"].append(item) + data["images"].append(item) - data = api.prepare_body(params) - response = self.client.post(f"/manifests/{self.quoted_name}/add", data=data) + data = api.prepare_body(data) + response = self.client.put(f"/manifests/{self.quoted_name}", data=data) response.raise_for_status(not_found=ImageNotFound) return self.reload() @@ -127,7 +126,10 @@ def remove(self, digest: str) -> None: if "@" in digest: digest = digest.split("@", maxsplit=2)[1] - response = self.client.delete(f"/manifests/{self.quoted_name}", params={"digest": digest}) + data = {"operation": "remove", "images": [digest]} + data = api.prepare_body(data) + + response = self.client.put(f"/manifests/{self.quoted_name}", data=data) response.raise_for_status(not_found=ImageNotFound) return self.reload() @@ -147,14 +149,14 @@ def resource(self): def create( self, - names: List[str], + name: str, images: Optional[List[Union[Image, str]]] = None, all: Optional[bool] = None, # pylint: disable=redefined-builtin ) -> Manifest: """Create a Manifest. Args: - names: Identifiers to be added to the manifest. There must be at least one. + name: Name of manifest list. images: Images or Image identifiers to be included in the manifest. all: When True, add all contents from images given. @@ -162,26 +164,24 @@ def create( ValueError: when no names are provided NotFoundImage: when a given image does not exist """ - if names is None or len(names) == 0: - raise ValueError("At least one manifest name is required.") - - params = {"name": names} + params: Dict[str, Any] = {} if images is not None: - params["image"] = [] + params["images"] = [] for item in images: if isinstance(item, Image): item = item.attrs["RepoTags"][0] - params["image"].append(item) + params["images"].append(item) if all is not None: params["all"] = all - response = self.client.post("/manifests/create", params=params) + name_quoted = urllib.parse.quote_plus(name) + response = self.client.post(f"/manifests/{name_quoted}", params=params) response.raise_for_status(not_found=ImageNotFound) body = response.json() manifest = self.get(body["Id"]) - manifest.attrs["names"] = names + manifest.attrs["names"] = name if manifest.attrs["manifests"] is None: manifest.attrs["manifests"] = [] @@ -198,9 +198,6 @@ def get(self, key: str) -> Manifest: To have Manifest conform with other PodmanResource's, we use the key that retrieved the Manifest be its name. - See https://issues.redhat.com/browse/RUN-1217 for details on refactoring Podman service - manifests API. - Args: key: Manifest name for which to search @@ -213,10 +210,23 @@ def get(self, key: str) -> Manifest: response.raise_for_status() body = response.json() - body["names"] = [key] + if "names" not in body: + body["names"] = key return self.prepare_model(attrs=body) def list(self, **kwargs) -> List[Manifest]: """Not Implemented.""" raise NotImplementedError("Podman service currently does not support listing manifests.") + + def remove(self, name: Union[Manifest, str]) -> Dict[str, Any]: + """Delete the manifest list from the Podman service.""" + if isinstance(name, Manifest): + name = name.name + + response = self.client.delete(f"/manifests/{name}") + response.raise_for_status(not_found=ImageNotFound) + + body = response.json() + body["ExitCode"] = response.status_code + return body diff --git a/podman/domain/networks.py b/podman/domain/networks.py index c772ff26..d97b23f1 100644 --- a/podman/domain/networks.py +++ b/podman/domain/networks.py @@ -1,8 +1,10 @@ -"""Model and Manager for Network resources. +"""Model for Network resources. -By default, most methods in this module uses the Podman compatible API rather than the -libpod API as the results are so different. To use the libpod API add the keyword argument -compatible=False to any method call. +Example: + + with PodmanClient(base_url="unix:///run/user/1000/podman/podman.sock") as client: + net = client.networks.get("db_network") + print(net.name, "\n") """ import hashlib import json @@ -42,11 +44,12 @@ def containers(self): with suppress(KeyError): container_manager = ContainersManager(client=self.client) return [container_manager.get(ident) for ident in self.attrs["Containers"].keys()] - return {} + return [] @property def name(self): """str: Returns the name of the network.""" + if "Name" in self.attrs: return self.attrs["Name"] @@ -68,7 +71,6 @@ def connect(self, container: Union[str, Container], *_, **kwargs) -> None: Keyword Args: aliases (List[str]): Aliases to add for this endpoint - compatible (bool): Should compatible API be used. Default: True driver_opt (Dict[str, Any]): Options to provide to network driver ipv4_address (str): IPv4 address for given Container on this network ipv6_address (str): IPv6 address for given Container on this network @@ -78,8 +80,6 @@ def connect(self, container: Union[str, Container], *_, **kwargs) -> None: Raises: APIError: when Podman service reports an error """ - compatible = kwargs.get("compatible", True) - if isinstance(container, Container): container = container.id @@ -110,7 +110,6 @@ def connect(self, container: Union[str, Container], *_, **kwargs) -> None: f"/networks/{self.name}/connect", data=json.dumps(data), headers={"Content-type": "application/json"}, - compatible=compatible, ) response.raise_for_status() @@ -126,15 +125,11 @@ def disconnect(self, container: Union[str, Container], **kwargs) -> None: Raises: APIError: when Podman service reports an error """ - compatible = kwargs.get("compatible", True) - if isinstance(container, Container): container = container.id data = {"Container": container, "Force": kwargs.get("force")} - response = self.client.post( - f"/networks/{self.name}/disconnect", data=json.dumps(data), compatible=compatible - ) + response = self.client.post(f"/networks/{self.name}/disconnect", data=json.dumps(data)) response.raise_for_status() def remove(self, force: Optional[bool] = None, **kwargs) -> None: @@ -143,9 +138,6 @@ def remove(self, force: Optional[bool] = None, **kwargs) -> None: Args: force: Remove network and any associated containers - Keyword Args: - compatible (bool): Should compatible API be used. Default: True - Raises: APIError: when Podman service reports an error """ diff --git a/podman/domain/networks_manager.py b/podman/domain/networks_manager.py index 073d0cc9..02e3d71e 100644 --- a/podman/domain/networks_manager.py +++ b/podman/domain/networks_manager.py @@ -1,16 +1,21 @@ -"""PodmanResource manager subclassed for Networks. +"""PodmanResource manager subclassed for Network resources. -By default, most methods in this module uses the Podman compatible API rather than the -libpod API as the results are so different. To use the libpod API add the keyword argument -compatible=False to any method call. +Classes and methods for manipulating network resources via Podman API service. + +Example: + + with PodmanClient(base_url="unix:///run/user/1000/podman/podman.sock") as client: + for net in client.networks.list(): + print(net.id, "\n") """ import ipaddress import logging +import sys from contextlib import suppress from typing import Any, Dict, List, Optional -import podman.api.http_utils from podman import api +from podman.api import http_utils from podman.domain.manager import Manager from podman.domain.networks import Network from podman.errors import APIError @@ -27,7 +32,7 @@ def resource(self): return Network def create(self, name: str, **kwargs) -> Network: - """Create a Network. + """Create a Network resource. Args: name: Name of network to be created @@ -35,14 +40,13 @@ def create(self, name: str, **kwargs) -> Network: Keyword Args: attachable (bool): Ignored, always False. check_duplicate (bool): Ignored, always False. - disabled_dns (bool): When True, do not provision DNS for this network. + dns_enabled (bool): When True, do not provision DNS for this network. driver (str): Which network driver to use when creating network. enable_ipv6 (bool): Enable IPv6 on the network. ingress (bool): Ignored, always False. internal (bool): Restrict external access to the network. ipam (IPAMConfig): Optional custom IP scheme for the network. labels (Dict[str, str]): Map of labels to set on the network. - macvlan (str): options (Dict[str, Any]): Driver options. scope (str): Ignored, always "local". @@ -50,85 +54,66 @@ def create(self, name: str, **kwargs) -> Network: APIError: when Podman service reports an error """ data = { - "DisabledDNS": kwargs.get("disabled_dns"), - "Driver": kwargs.get("driver"), - "Internal": kwargs.get("internal"), - "IPv6": kwargs.get("enable_ipv6"), - "Labels": kwargs.get("labels"), - "MacVLAN": kwargs.get("macvlan"), - "Options": kwargs.get("options"), + "name": name, + "driver": kwargs.get("driver"), + "dns_enabled": kwargs.get("dns_enabled"), + "subnets": kwargs.get("subnets"), + "ipv6_enabled": kwargs.get("enable_ipv6"), + "internal": kwargs.get("internal"), + "labels": kwargs.get("labels"), + "options": kwargs.get("options"), } with suppress(KeyError): - ipam = kwargs["ipam"] - if len(ipam["Config"]) > 0: - - if len(ipam["Config"]) > 1: - raise ValueError("Podman service only supports one IPAM config.") - - ip_config = ipam["Config"][0] - data["Gateway"] = ip_config.get("Gateway") - - if "IPRange" in ip_config: - iprange = ipaddress.ip_network(ip_config["IPRange"]) - iprange, mask = api.prepare_cidr(iprange) - data["Range"] = { - "IP": iprange, - "Mask": mask, - } - - if "Subnet" in ip_config: - subnet = ipaddress.ip_network(ip_config["Subnet"]) - subnet, mask = api.prepare_cidr(subnet) - data["Subnet"] = { - "IP": subnet, - "Mask": mask, - } + self._prepare_ipam(data, kwargs["ipam"]) response = self.client.post( "/networks/create", - params={"name": name}, - data=podman.api.http_utils.prepare_body(data), + data=http_utils.prepare_body(data), headers={"Content-Type": "application/json"}, ) response.raise_for_status() + sys.stderr.write(str(response.json())) + return self.prepare_model(attrs=response.json()) + + def _prepare_ipam(self, data: Dict[str, Any], ipam: Dict[str, Any]): + if "Config" not in ipam: + return - return self.get(name, **kwargs) + data["subnets"] = [] + for cfg in ipam["Config"]: + subnet = { + "gateway": cfg.get("Gateway"), + "subnet": cfg.get("Subnet"), + } + + with suppress(KeyError): + net = ipaddress.ip_network(cfg["IPRange"]) + subnet["lease_range"] = { + "start_ip": str(net[1]), + "end_ip": str(net[-2]), + } + + data["subnets"].append(subnet) def exists(self, key: str) -> bool: response = self.client.get(f"/networks/{key}/exists") return response.ok - # pylint is flagging 'network_id' here vs. 'key' parameter in super.get() - def get(self, network_id: str, *_, **kwargs) -> Network: # pylint: disable=arguments-differ + def get(self, key: str) -> Network: """Return information for the network_id. Args: - network_id: Network name or id. - - Keyword Args: - compatible (bool): Should compatible API be used. Default: True + key: Network name or id. Raises: NotFound: when Network does not exist APIError: when error returned by service - - Note: - The compatible API is used, this allows the server to provide dynamic fields. - id is the most important example. """ - compatible = kwargs.get("compatible", True) - - path = f"/networks/{network_id}" + ("" if compatible else "/json") - - response = self.client.get(path, compatible=compatible) + response = self.client.get(f"/networks/{key}") response.raise_for_status() - body = response.json() - if not compatible: - body = body[0] - - return self.prepare_model(attrs=body) + return self.prepare_model(attrs=response.json()) def list(self, **kwargs) -> List[Network]: """Report on networks. @@ -162,23 +147,19 @@ def list(self, **kwargs) -> List[Network]: Raises: APIError: when error returned by service """ - compatible = kwargs.get("compatible", True) - filters = kwargs.get("filters", {}) filters["name"] = kwargs.get("names") filters["id"] = kwargs.get("ids") filters = api.prepare_filters(filters) params = {"filters": filters} - path = f"/networks{'' if compatible else '/json'}" - - response = self.client.get(path, params=params, compatible=compatible) + response = self.client.get("/networks/json", params=params) response.raise_for_status() return [self.prepare_model(i) for i in response.json()] def prune( - self, filters: Optional[Dict[str, Any]] = None, **kwargs + self, filters: Optional[Dict[str, Any]] = None ) -> Dict[api.Literal["NetworksDeleted", "SpaceReclaimed"], Any]: """Delete unused Networks. @@ -187,25 +168,14 @@ def prune( Args: filters: Criteria for selecting volumes to delete. Ignored. - Keyword Args: - compatible (bool): Should compatible API be used. Default: True - Raises: APIError: when service reports error """ - compatible = kwargs.get("compatible", True) - - response = self.client.post( - "/networks/prune", filters=api.prepare_filters(filters), compatible=compatible - ) + response = self.client.post("/networks/prune", filters=api.prepare_filters(filters)) response.raise_for_status() - body = response.json() - if compatible: - return body - deleted: List[str] = [] - for item in body: + for item in response.json(): if item["Error"] is not None: raise APIError( item["Error"], @@ -216,27 +186,18 @@ def prune( return {"NetworksDeleted": deleted, "SpaceReclaimed": 0} - def remove(self, name: [Network, str], force: Optional[bool] = None, **kwargs) -> None: - """Remove this network. + def remove(self, name: [Network, str], force: Optional[bool] = None) -> None: + """Remove Network resource. Args: name: Identifier of Network to delete. force: Remove network and any associated containers - Keyword Args: - compatible (bool): Should compatible API be used. Default: True - Raises: APIError: when Podman service reports an error - - Notes: - Podman only. """ if isinstance(name, Network): name = name.name - compatible = kwargs.get("compatible", True) - response = self.client.delete( - f"/networks/{name}", params={"force": force}, compatible=compatible - ) + response = self.client.delete(f"/networks/{name}", params={"force": force}) response.raise_for_status() diff --git a/podman/domain/pods.py b/podman/domain/pods.py index 0cd3ded5..f2a72ec8 100644 --- a/podman/domain/pods.py +++ b/podman/domain/pods.py @@ -1,6 +1,6 @@ """Model and Manager for Pod resources.""" import logging -from typing import Any, Dict, Tuple, Union, Optional +from typing import Any, Dict, Optional, Tuple, Union from podman.domain.manager import PodmanResource @@ -104,6 +104,8 @@ def top(self, **kwargs) -> Dict[str, Any]: response = self.client.get(f"/pods/{self.id}/top", params=params) response.raise_for_status() + if len(response.text) == 0: + return {"Processes": [], "Titles": []} return response.json() def unpause(self) -> None: diff --git a/podman/domain/pods_manager.py b/podman/domain/pods_manager.py index 0051f496..d591984f 100644 --- a/podman/domain/pods_manager.py +++ b/podman/domain/pods_manager.py @@ -94,9 +94,7 @@ def prune(self, filters: Optional[Dict[str, str]] = None) -> Dict[str, Any]: Raises: APIError: when service reports error """ - response = self.client.post( - "/pods/prune", params={"filters": api.prepare_filters(filters)} - ) + response = self.client.post("/pods/prune", params={"filters": api.prepare_filters(filters)}) response.raise_for_status() deleted: List[str] = [] diff --git a/podman/tests/__init__.py b/podman/tests/__init__.py index d48e5284..fbbbb2a1 100644 --- a/podman/tests/__init__.py +++ b/podman/tests/__init__.py @@ -3,5 +3,5 @@ # Do not auto-update these from version.py, # as test code should be changed to reflect changes in Podman API versions BASE_SOCK = "unix:///run/api.sock" -LIBPOD_URL = "http://%2Frun%2Fapi.sock/v3.2.1/libpod" +LIBPOD_URL = "http://%2Frun%2Fapi.sock/v4.0.0/libpod" COMPATIBLE_URL = "http://%2Frun%2Fapi.sock/v1.40" diff --git a/podman/tests/integration/base.py b/podman/tests/integration/base.py index 4f34d722..d79711d9 100644 --- a/podman/tests/integration/base.py +++ b/podman/tests/integration/base.py @@ -37,24 +37,22 @@ class IntegrationTest(fixtures.TestWithFixtures): @classmethod def setUpClass(cls) -> None: + super(fixtures.TestWithFixtures, cls).setUpClass() + command = os.environ.get("PODMAN_BINARY", "podman") if shutil.which(command) is None: raise AssertionError(f"'{command}' not found.") IntegrationTest.podman = command - # For testing, lock in logging configuration - if "DEBUG" in os.environ: - logging.basicConfig(level=logging.DEBUG) - else: - logging.basicConfig(level=logging.INFO) + # This log_level is for our python code + log_level = os.environ.get("PODMAN_LOG_LEVEL", "INFO") + log_level = logging.getLevelName(log_level) + logging.basicConfig(level=log_level) def setUp(self): super().setUp() - # This is the log_level to pass to podman service - self.log_level = logging.WARNING - if "DEBUG" in os.environ: - self.log_level = logging.DEBUG + self.log_level = os.environ.get("PODMAN_LOG_LEVEL", "INFO") self.test_dir = self.useFixture(fixtures.TempDir()).path self.socket_file = os.path.join(self.test_dir, uuid.uuid4().hex) diff --git a/podman/tests/integration/test_container_create.py b/podman/tests/integration/test_container_create.py index cc938a47..a3f4fc79 100644 --- a/podman/tests/integration/test_container_create.py +++ b/podman/tests/integration/test_container_create.py @@ -67,10 +67,6 @@ def _test_memory_limit(self, parameter_name, host_config_name): test['expected_value'], ) - def test_container_kernel_memory(self): - """Test passing kernel memory""" - self._test_memory_limit('kernel_memory', 'KernelMemory') - def test_container_mem_limit(self): """Test passing memory limit""" self._test_memory_limit('mem_limit', 'Memory') diff --git a/podman/tests/integration/test_containers.py b/podman/tests/integration/test_containers.py index 3a5ba822..d46d050a 100644 --- a/podman/tests/integration/test_containers.py +++ b/podman/tests/integration/test_containers.py @@ -105,9 +105,7 @@ def test_container_crud(self): self.assertIsInstance(logs_iter, Iterator) logs = list(logs_iter) - self.assertIn(random_string.encode("utf-8"), logs) - # podman 4.0 API support... - # self.assertIn((random_string + "\n").encode("utf-8"), logs) + self.assertIn((random_string + "\n").encode("utf-8"), logs) with self.subTest("Delete Container"): container.remove() diff --git a/podman/tests/integration/test_manifests.py b/podman/tests/integration/test_manifests.py index 8dd6d5cc..9bead625 100644 --- a/podman/tests/integration/test_manifests.py +++ b/podman/tests/integration/test_manifests.py @@ -25,23 +25,25 @@ def tearDown(self) -> None: self.client.images.remove(self.alpine_image, force=True) with suppress(ImageNotFound): - self.client.images.remove("quay.io/unittest/alpine:latest", force=True) + self.client.images.remove("localhost/unittest/alpine", force=True) def test_manifest_crud(self): """Test Manifest CRUD.""" self.assertFalse( - self.client.manifests.exists("quay.io/unittest/alpine:latest"), + self.client.manifests.exists("localhost/unittest/alpine"), "Image store is corrupt from previous run", ) with self.subTest("Create"): - manifest = self.client.manifests.create(["quay.io/unittest/alpine:latest"]) - self.assertEqual(len(manifest.attrs["manifests"]), 0) - self.assertTrue(self.client.manifests.exists(manifest.id)) + manifest = self.client.manifests.create( + "localhost/unittest/alpine", ["quay.io/libpod/alpine:latest"] + ) + self.assertEqual(len(manifest.attrs["manifests"]), 1, manifest.attrs) + self.assertTrue(self.client.manifests.exists(manifest.names), manifest.id) with self.assertRaises(APIError): - self.client.manifests.create(["123456!@#$%^"]) + self.client.manifests.create("123456!@#$%^") with self.subTest("Add"): manifest.add([self.alpine_image]) @@ -54,12 +56,14 @@ def test_manifest_crud(self): ) with self.subTest("Inspect"): - actual = self.client.manifests.get("quay.io/unittest/alpine:latest") + actual = self.client.manifests.get("quay.io/libpod/alpine:latest") self.assertEqual(actual.id, manifest.id) actual = self.client.manifests.get(manifest.name) self.assertEqual(actual.id, manifest.id) + self.assertEqual(actual.version, 2) + with self.subTest("Remove digest"): manifest.remove(self.alpine_image.attrs["RepoDigests"][0]) self.assertEqual(len(manifest.attrs["manifests"]), 0) @@ -67,7 +71,7 @@ def test_manifest_crud(self): def test_create_409(self): """Test that invalid Image names are caught and not corrupt storage.""" with self.assertRaises(APIError): - self.client.manifests.create([self.invalid_manifest_name]) + self.client.manifests.create(self.invalid_manifest_name) if __name__ == '__main__': diff --git a/podman/tests/integration/test_networks.py b/podman/tests/integration/test_networks.py index 981626d6..0b5d44d1 100644 --- a/podman/tests/integration/test_networks.py +++ b/podman/tests/integration/test_networks.py @@ -27,26 +27,29 @@ class NetworksIntegrationTest(base.IntegrationTest): """networks call integration test""" - pool = IPAMPool(subnet="172.16.0.0/16", iprange="172.16.0.0/24", gateway="172.16.0.1") + pool = IPAMPool(subnet="10.11.13.0/24", iprange="10.11.13.0/26", gateway="10.11.13.1") ipam = IPAMConfig(pool_configs=[pool]) def setUp(self): super().setUp() + self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) + def tearDown(self): with suppress(NotFound): self.client.networks.get("integration_test").remove(force=True) + super().tearDown() + def test_network_crud(self): """integration: networks create and remove calls""" with self.subTest("Create Network"): network = self.client.networks.create( "integration_test", - disabled_dns=True, - enable_ipv6=False, + dns_enabled=False, ipam=NetworksIntegrationTest.ipam, ) self.assertEqual(network.name, "integration_test") @@ -68,7 +71,7 @@ def test_network_crud(self): with self.assertRaises(NotFound): self.client.networks.get("integration_test") - @unittest.skipIf(os.geteuid() != 0, 'Skipping, not running as root') + @unittest.skip("Skipping, libpod endpoint does not report container count") def test_network_connect(self): self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") diff --git a/podman/tests/integration/test_pods.py b/podman/tests/integration/test_pods.py index f9a6a274..396a42e3 100644 --- a/podman/tests/integration/test_pods.py +++ b/podman/tests/integration/test_pods.py @@ -18,13 +18,13 @@ def setUp(self): self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") self.pod_name = f"pod_{random.getrandbits(160):x}" - # TODO should this use podman binary instead? for container in self.client.containers.list(): container.remove(force=True) def tearDown(self): if self.client.pods.exists(self.pod_name): self.client.pods.remove(self.pod_name) + super().tearDown() def test_pod_crud(self): """Test Pod CRUD.""" @@ -62,6 +62,9 @@ def test_pod_crud(self): with self.assertRaises(NotFound): pod.reload() + def test_pod_crud_infra(self): + """Test Pod CRUD with infra container.""" + with self.subTest("Create with infra"): pod = self.client.pods.create( self.pod_name, @@ -75,22 +78,7 @@ def test_pod_crud(self): actual = self.client.pods.get(pod.id) self.assertEqual(actual.name, pod.name) self.assertIn("Containers", actual.attrs) - - with self.subTest("Stop/Start"): - actual.stop() - actual.start() - - with self.subTest("Restart"): - actual.restart() - - with self.subTest("Pause/Unpause"): - actual.pause() - actual.reload() - self.assertEqual(actual.attrs["State"], "Paused") - - actual.unpause() - actual.reload() - self.assertEqual(actual.attrs["State"], "Running") + self.assertEqual(actual.attrs["State"], "Created") with self.subTest("Add container"): container = self.client.containers.create(self.alpine_image, command=["ls"], pod=actual) @@ -99,12 +87,6 @@ def test_pod_crud(self): ids = {c["Id"] for c in actual.attrs["Containers"]} self.assertIn(container.id, ids) - with self.subTest("Ps"): - procs = actual.top() - - self.assertGreater(len(procs["Processes"]), 0) - self.assertGreater(len(procs["Titles"]), 0) - with self.subTest("List"): pods = self.client.pods.list() self.assertGreaterEqual(len(pods), 1) @@ -112,15 +94,64 @@ def test_pod_crud(self): ids = {p.id for p in pods} self.assertIn(actual.id, ids) - with self.subTest("Stats"): - report = self.client.pods.stats(all=True) - self.assertGreaterEqual(len(report), 1) - with self.subTest("Delete"): pod.remove(force=True) with self.assertRaises(NotFound): pod.reload() + def test_ps(self): + pod = self.client.pods.create( + self.pod_name, + labels={ + "unittest": "true", + }, + no_infra=True, + ) + self.assertTrue(self.client.pods.exists(pod.id)) + self.client.containers.create( + self.alpine_image, command=["top"], detach=True, tty=True, pod=pod + ) + pod.start() + pod.reload() + + with self.subTest("top"): + # this is the API top call not the + # top command running in the container + procs = pod.top() + + self.assertGreater(len(procs["Processes"]), 0) + self.assertGreater(len(procs["Titles"]), 0) + + with self.subTest("stats"): + report = self.client.pods.stats(all=True) + self.assertGreaterEqual(len(report), 1) + + with self.subTest("Stop/Start"): + pod.stop() + pod.reload() + self.assertIn(pod.attrs["State"], ("Stopped", "Exited")) + + pod.start() + pod.reload() + self.assertEqual(pod.attrs["State"], "Running") + + with self.subTest("Restart"): + pod.stop() + pod.restart() + pod.reload() + self.assertEqual(pod.attrs["State"], "Running") + + with self.subTest("Pause/Unpause"): + pod.pause() + pod.reload() + self.assertEqual(pod.attrs["State"], "Paused") + + pod.unpause() + pod.reload() + self.assertEqual(pod.attrs["State"], "Running") + + pod.stop() + if __name__ == '__main__': unittest.main() diff --git a/podman/tests/integration/utils.py b/podman/tests/integration/utils.py index 792b7df9..78aea63a 100644 --- a/podman/tests/integration/utils.py +++ b/podman/tests/integration/utils.py @@ -18,6 +18,7 @@ import shutil import subprocess import threading +from contextlib import suppress from typing import List, Optional import time @@ -36,7 +37,7 @@ def __init__( podman_path: Optional[str] = None, timeout: int = 0, privileged: bool = False, - log_level: int = logging.WARNING, + log_level: str = "WARNING", ) -> None: """create a launcher and build podman command""" podman_exe: str = podman_path @@ -57,7 +58,10 @@ def __init__( self.cmd.append(podman_exe) - self.cmd.append(f"--log-level={logging.getLevelName(log_level).lower()}") + logger.setLevel(logging.getLevelName(log_level)) + + # Map from python to go logging levels, FYI trace level breaks cirrus logging + self.cmd.append(f"--log-level={log_level.lower()}") if os.environ.get("container") == "oci": self.cmd.append("--storage-driver=vfs") @@ -121,4 +125,7 @@ def stop(self) -> None: return_code = self.proc.wait() self.proc = None + with suppress(FileNotFoundError): + os.remove(self.socket_file) + logger.info("Command return Code: %d refid=%s", return_code, self.reference_id) diff --git a/podman/tests/unit/test_config.py b/podman/tests/unit/test_config.py index c7e011ad..7ecb475a 100644 --- a/podman/tests/unit/test_config.py +++ b/podman/tests/unit/test_config.py @@ -47,9 +47,7 @@ def test_connections(self): expected = urllib.parse.urlparse("ssh://qe@localhost:2222/run/podman/podman.sock") self.assertEqual(config.active_service.url, expected) - self.assertEqual( - config.services["production"].identity, Path("/home/root/.ssh/id_rsa") - ) + self.assertEqual(config.services["production"].identity, Path("/home/root/.ssh/id_rsa")) PodmanConfigTestCase.opener.assert_called_with( Path("/home/developer/containers.conf"), encoding='utf-8' diff --git a/podman/tests/unit/test_containersmanager.py b/podman/tests/unit/test_containersmanager.py index e7157184..7187919f 100644 --- a/podman/tests/unit/test_containersmanager.py +++ b/podman/tests/unit/test_containersmanager.py @@ -242,9 +242,7 @@ def test_run_detached(self, mock): json=FIRST_CONTAINER, ) - with patch.multiple( - Container, logs=DEFAULT, wait=DEFAULT, autospec=True - ) as mock_container: + with patch.multiple(Container, logs=DEFAULT, wait=DEFAULT, autospec=True) as mock_container: mock_container["logs"].return_value = [] mock_container["wait"].return_value = {"StatusCode": 0} @@ -277,9 +275,7 @@ def test_run(self, mock): b"This is a unittest - line 2", ) - with patch.multiple( - Container, logs=DEFAULT, wait=DEFAULT, autospec=True - ) as mock_container: + with patch.multiple(Container, logs=DEFAULT, wait=DEFAULT, autospec=True) as mock_container: mock_container["wait"].return_value = {"StatusCode": 0} with self.subTest("Results not streamed"): diff --git a/podman/tests/unit/test_manifests.py b/podman/tests/unit/test_manifests.py index 98d5292b..f0f8cda6 100644 --- a/podman/tests/unit/test_manifests.py +++ b/podman/tests/unit/test_manifests.py @@ -1,7 +1,7 @@ import unittest from podman import PodmanClient, tests -from podman.domain.manifests import ManifestsManager, Manifest +from podman.domain.manifests import Manifest, ManifestsManager class ManifestTestCase(unittest.TestCase): @@ -9,11 +9,7 @@ def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) - - def tearDown(self) -> None: - super().tearDown() - - self.client.close() + self.addCleanup(self.client.close) def test_podmanclient(self): manager = self.client.manifests @@ -24,13 +20,8 @@ def test_list(self): self.client.manifests.list() def test_name(self): - with self.assertRaises(ValueError): - manifest = Manifest(attrs={"names": ""}) - _ = manifest.name - - with self.assertRaises(ValueError): - manifest = Manifest() - _ = manifest.name + manifest = Manifest() + self.assertIsNone(manifest.name) if __name__ == '__main__': diff --git a/podman/tests/unit/test_network.py b/podman/tests/unit/test_network.py index 012502de..b5dfb06b 100644 --- a/podman/tests/unit/test_network.py +++ b/podman/tests/unit/test_network.py @@ -28,29 +28,29 @@ "Labels": {}, } -FIRST_NETWORK_LIBPOD = [ - { - "cniVersion": "0.4.0", - "name": "podman", - "plugins": [ - { - "bridge": "cni-podman0", - "hairpinMode": True, - "ipMasq": True, - "ipam": { - "ranges": [[{"gateway": "10.88.0.1", "subnet": "10.88.0.0/16"}]], - "routes": [{"dst": "0.0.0.0/0"}], - "type": "host-local", - }, - "isGateway": True, - "type": "bridge", +FIRST_NETWORK_LIBPOD = { + "name": "podman", + "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", + "driver": "bridge", + "network_interface": "libpod_veth0", + "created": "2022-01-28T09:18:37.491308364-07:00", + "subnets": [ + { + "subnet": "10.11.12.0/24", + "gateway": "10.11.12.1", + "lease_range": { + "start_ip": "10.11.12.1", + "end_ip": "10.11.12.63", }, - {"capabilities": {"portMappings": True}, "type": "portmap"}, - {"type": "firewall"}, - {"type": "tuning"}, - ], - } -] + } + ], + "ipv6_enabled": False, + "internal": False, + "dns_enabled": False, + "labels": {}, + "options": {}, + "ipam_options": {}, +} class NetworkTestCase(unittest.TestCase): @@ -58,11 +58,7 @@ def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) - - def tearDown(self) -> None: - super().tearDown() - - self.client.close() + self.addCleanup(self.client.close) def test_id(self): expected = {"Id": "1cf06390-709d-4ffa-a054-c3083abe367c"} @@ -84,7 +80,7 @@ def test_name(self): @requests_mock.Mocker() def test_remove(self, mock): adapter = mock.delete( - tests.COMPATIBLE_URL + "/networks/podman?force=True", + tests.LIBPOD_URL + "/networks/podman?force=True", status_code=204, json={"Name": "podman", "Err": None}, ) @@ -96,7 +92,7 @@ def test_remove(self, mock): @requests_mock.Mocker() def test_connect(self, mock): - adapter = mock.post(tests.COMPATIBLE_URL + "/networks/podman/connect") + adapter = mock.post(tests.LIBPOD_URL + "/networks/podman/connect") net = Network(attrs=FIRST_NETWORK, client=self.client.api) net.connect( @@ -120,7 +116,7 @@ def test_connect(self, mock): @requests_mock.Mocker() def test_disconnect(self, mock): - adapter = mock.post(tests.COMPATIBLE_URL + "/networks/podman/disconnect") + adapter = mock.post(tests.LIBPOD_URL + "/networks/podman/disconnect") net = Network(attrs=FIRST_NETWORK, client=self.client.api) net.disconnect("podman_ctnr", force=True) diff --git a/podman/tests/unit/test_networksmanager.py b/podman/tests/unit/test_networksmanager.py index 6032b25a..1219bb54 100644 --- a/podman/tests/unit/test_networksmanager.py +++ b/podman/tests/unit/test_networksmanager.py @@ -3,7 +3,6 @@ import requests_mock from podman import PodmanClient, tests -from podman.domain.ipam import IPAMConfig, IPAMPool from podman.domain.networks import Network from podman.domain.networks_manager import NetworksManager @@ -51,53 +50,53 @@ "Labels": {}, } -FIRST_NETWORK_LIBPOD = [ - { - "cniVersion": "0.4.0", - "name": "podman", - "plugins": [ - { - "bridge": "cni-podman0", - "hairpinMode": True, - "ipMasq": True, - "ipam": { - "ranges": [[{"gateway": "10.88.0.1", "subnet": "10.88.0.0/16"}]], - "routes": [{"dst": "0.0.0.0/0"}], - "type": "host-local", - }, - "isGateway": True, - "type": "bridge", +FIRST_NETWORK_LIBPOD = { + "name": "podman", + "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", + "driver": "bridge", + "network_interface": "libpod_veth0", + "created": "2022-01-28T09:18:37.491308364-07:00", + "subnets": [ + { + "subnet": "10.11.12.0/24", + "gateway": "10.11.12.1", + "lease_range": { + "start_ip": "10.11.12.1", + "end_ip": "10.11.12.63", }, - {"capabilities": {"portMappings": True}, "type": "portmap"}, - {"type": "firewall"}, - {"type": "tuning"}, - ], - } -] + } + ], + "ipv6_enabled": False, + "internal": False, + "dns_enabled": False, + "labels": {}, + "options": {}, + "ipam_options": {}, +} -SECOND_NETWORK_LIBPOD = [ - { - "cniVersion": "0.4.0", - "name": "database", - "plugins": [ - { - "bridge": "cni-podman0", - "hairpinMode": True, - "ipMasq": True, - "ipam": { - "ranges": [[{"gateway": "10.88.0.1", "subnet": "10.88.0.0/16"}]], - "routes": [{"dst": "0.0.0.0/0"}], - "type": "host-local", - }, - "isGateway": True, - "type": "bridge", +SECOND_NETWORK_LIBPOD = { + "name": "database", + "id": "3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8", + "created": "2021-03-01T09:18:37.491308364-07:00", + "driver": "bridge", + "network_interface": "libpod_veth1", + "subnets": [ + { + "subnet": "10.11.12.0/24", + "gateway": "10.11.12.1", + "lease_range": { + "start_ip": "10.11.12.1", + "end_ip": "10.11.12.63", }, - {"capabilities": {"portMappings": True}, "type": "portmap"}, - {"type": "firewall"}, - {"type": "tuning"}, - ], - } -] + } + ], + "ipv6_enabled": False, + "internal": False, + "dns_enabled": False, + "labels": {}, + "options": {}, + "ipam_options": {}, +} class NetworksManagerTestCase(unittest.TestCase): @@ -112,11 +111,7 @@ def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) - - def tearDown(self) -> None: - super().tearDown() - - self.client.close() + self.addCleanup(self.client.close) def test_podmanclient(self): manager = self.client.networks @@ -124,10 +119,7 @@ def test_podmanclient(self): @requests_mock.Mocker() def test_get(self, mock): - mock.get( - tests.COMPATIBLE_URL + "/networks/podman", - json=FIRST_NETWORK, - ) + mock.get(tests.LIBPOD_URL + "/networks/podman", json=FIRST_NETWORK) actual = self.client.networks.get("podman") self.assertIsInstance(actual, Network) @@ -135,47 +127,14 @@ def test_get(self, mock): actual.id, "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9" ) - @requests_mock.Mocker() - def test_get_libpod(self, mock): - mock.get( - tests.LIBPOD_URL + "/networks/podman/json", - json=FIRST_NETWORK_LIBPOD, - ) - - actual = self.client.networks.get("podman", compatible=False) - self.assertIsInstance(actual, Network) - self.assertEqual(actual.attrs["name"], "podman") - - @requests_mock.Mocker() - def test_list(self, mock): - mock.get( - tests.COMPATIBLE_URL + "/networks", - json=[FIRST_NETWORK, SECOND_NETWORK], - ) - - actual = self.client.networks.list() - self.assertEqual(len(actual), 2) - - self.assertIsInstance(actual[0], Network) - self.assertEqual( - actual[0].id, "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9" - ) - self.assertEqual(actual[0].attrs["Name"], "podman") - - self.assertIsInstance(actual[1], Network) - self.assertEqual( - actual[1].id, "3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8" - ) - self.assertEqual(actual[1].name, "database") - @requests_mock.Mocker() def test_list_libpod(self, mock): mock.get( tests.LIBPOD_URL + "/networks/json", - json=FIRST_NETWORK_LIBPOD + SECOND_NETWORK_LIBPOD, + json=[FIRST_NETWORK_LIBPOD, SECOND_NETWORK_LIBPOD], ) - actual = self.client.networks.list(compatible=False) + actual = self.client.networks.list() self.assertEqual(len(actual), 2) self.assertIsInstance(actual[0], Network) @@ -191,68 +150,33 @@ def test_list_libpod(self, mock): self.assertEqual(actual[1].name, "database") @requests_mock.Mocker() - def test_create(self, mock): - adapter = mock.post( - tests.LIBPOD_URL + "/networks/create?name=podman", - json={ - "Filename": "/home/developer/.config/cni/net.d/podman.conflist", - }, - ) - mock.get( - tests.COMPATIBLE_URL + "/networks/podman", - json=FIRST_NETWORK, - ) + def test_create_libpod(self, mock): + adapter = mock.post(tests.LIBPOD_URL + "/networks/create", json=FIRST_NETWORK_LIBPOD) - pool = IPAMPool(subnet="172.16.0.0/12", iprange="172.16.0.0/16", gateway="172.31.255.254") - ipam = IPAMConfig(pool_configs=[pool]) - - network = self.client.networks.create( - "podman", disabled_dns=True, enable_ipv6=False, ipam=ipam - ) + network = self.client.networks.create("podman", dns_enabled=True, enable_ipv6=True) self.assertIsInstance(network, Network) self.assertEqual(adapter.call_count, 1) self.assertDictEqual( adapter.last_request.json(), { - 'DisabledDNS': True, - 'Gateway': '172.31.255.254', - 'IPv6': False, - 'Range': {'IP': '172.16.0.0', 'Mask': "//8AAA=="}, - 'Subnet': {'IP': '172.16.0.0', 'Mask': "//AAAA=="}, + "name": "podman", + "ipv6_enabled": True, + "dns_enabled": True, }, ) - self.assertEqual(network.name, "podman") - @requests_mock.Mocker() def test_create_defaults(self, mock): - adapter = mock.post( - tests.LIBPOD_URL + "/networks/create?name=podman", - json={ - "Filename": "/home/developer/.config/cni/net.d/podman.conflist", - }, - ) - mock.get( - tests.COMPATIBLE_URL + "/networks/podman", - json=FIRST_NETWORK, - ) + adapter = mock.post(tests.LIBPOD_URL + "/networks/create", json=FIRST_NETWORK_LIBPOD) network = self.client.networks.create("podman") self.assertEqual(adapter.call_count, 1) - self.assertEqual(network.name, "podman") - self.assertEqual(len(adapter.last_request.json()), 0) - - @requests_mock.Mocker() - def test_prune(self, mock): - mock.post( - tests.COMPATIBLE_URL + "/networks/prune", - json={"NetworksDeleted": ["podman", "database"]}, + self.assertDictEqual( + adapter.last_request.json(), + {"name": "podman"}, ) - actual = self.client.networks.prune() - self.assertListEqual(actual["NetworksDeleted"], ["podman", "database"]) - @requests_mock.Mocker() def test_prune_libpod(self, mock): mock.post( @@ -263,7 +187,7 @@ def test_prune_libpod(self, mock): ], ) - actual = self.client.networks.prune(compatible=False) + actual = self.client.networks.prune() self.assertListEqual(actual["NetworksDeleted"], ["podman", "database"]) diff --git a/podman/tests/unit/test_volumesmanager.py b/podman/tests/unit/test_volumesmanager.py index 25af0189..6650929b 100644 --- a/podman/tests/unit/test_volumesmanager.py +++ b/podman/tests/unit/test_volumesmanager.py @@ -38,11 +38,7 @@ def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) - - def tearDown(self) -> None: - super().tearDown() - - self.client.close() + self.addCleanup(self.client.close) def test_podmanclient(self): manager = self.client.volumes diff --git a/podman/version.py b/podman/version.py index f4cbf9bf..1cc66575 100644 --- a/podman/version.py +++ b/podman/version.py @@ -1,4 +1,4 @@ """Version of PodmanPy.""" -__version__ = "3.2.1" +__version__ = "4.0.0" __compatible_version__ = "1.40" diff --git a/pyproject.toml b/pyproject.toml index a656fede..b0af397e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,12 +19,20 @@ exclude = ''' profile = "black" line_length = 100 [build-system] +# Any changes should be copied into requirements.txt, setup.cfg, and/or test-requirements.txt requires = [ + "pyxdg>=0.26", "requests>=2.24", + "setuptools>=46.4", + "sphinx", "toml>=0.10.2", "urllib3>=1.24.2", - "pyxdg>=0.26", - "setuptools>=46.4", "wheel", ] build-backend = "setuptools.build_meta" +[tool.pytest.ini_options] +log_cli = true +log_cli_level = "DEBUG" +log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" +log_cli_date_format = "%Y-%m-%d %H:%M:%S" + diff --git a/requirements.txt b/requirements.txt index 7d2af098..5713ef5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ +# Any changes should be copied into pyproject.toml +pyxdg>=0.26 requests>=2.24 +setuptools +sphinx toml>=0.10.2 urllib3>=1.24.2 -pyxdg>=0.26 -sphinx wheel -setuptools diff --git a/setup.cfg b/setup.cfg index f2c3d1f5..d0cd807c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,11 +31,12 @@ keywords = podman, libpod include_package_data = True python_requires = >=3.6 test_suite = +# Any changes should be copied into pyproject.toml install_requires = + pyxdg>=0.26 requests>=2.24 toml>=0.10.2 urllib3>=1.24.2 - pyxdg>=0.26 # typing_extensions are included for RHEL 8.5 # typing_extensions;python_version<'3.8' diff --git a/setup.py b/setup.py index 86d48173..01e2dd7a 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,10 @@ -import setuptools - import fnmatch + +import setuptools from setuptools import find_packages from setuptools.command.build_py import build_py as build_py_orig excluded = [ - "podman/api_connection.py", - "podman/containers/*", - "podman/images/*", - "podman/manifests/*", - "podman/networks/*", - "podman/pods/*", - "podman/system/*", - "podman/system/*", "podman/tests/*", ] diff --git a/test-requirements.txt b/test-requirements.txt index d5b54f4d..62d3e8e1 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,9 @@ +# Any changes should be copied into pyproject.toml -r requirements.txt black coverage fixtures~=3.0.0 -pytest pylint +pytest requests-mock +tox diff --git a/tox.ini b/tox.ini index 007f01fd..8ade9859 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.2.0 -envlist = py36,py38,py39,py310,pylint,coverage +envlist = pylint,coverage,py36,py38,py39,py310 ignore_basepython_conflict = true [testenv] @@ -8,7 +8,11 @@ basepython = python3 usedevelop = True install_command = pip install {opts} {packages} deps = -r{toxinidir}/test-requirements.txt -commands = pytest +commands = pytest {posargs} +setenv = + PODMAN_LOG_LEVEL = {env:PODMAN_LOG_LEVEL:INFO} + PODMAN_BINARY = {env:PODMAN_BINARY:podman} + DEBUG = {env:DEBUG:0} [testenv:venv] commands = {posargs}