def prune( self, filters: Optional[Dict[str, str]] = None # pylint: disable=unused-argument ) -> Dict[str, Any]: """Delete unused volumes. Args: filters: Criteria for selecting volumes to delete. Ignored. Raises: APIError when service reports error """ response = self.client.post("/volumes/prune") data = response.json() if response.status_code != 200: raise APIError(data["cause"], response=response, explanation=data["message"]) volumes: List[str] = list() space_reclaimed = 0 for item in data: if "Err" in item: raise APIError( item["Err"], response=response, explanation=f"""Failed to prune volume '{item.get("Id")}'""", ) volumes.append(item.get("Id")) space_reclaimed += item["Size"] return {"VolumesDeleted": volumes, "SpaceReclaimed": space_reclaimed}
def prune(self, filters: Optional[Dict[str, str]] = None) -> Dict[str, Any]: """Delete unused Pods. Returns: Dictionary Keys: - PodsDeleted (List[str]): List of pod ids deleted. - SpaceReclaimed (int): Always zero. Raises: APIError when service reports error """ response = self.client.post( "/pods/prune", params={"filters": api.prepare_filters(filters)}) body = response.json() if response.status_code != requests.codes.okay: raise APIError(body["cause"], response=response, explanation=body["message"]) deleted: List[str] = list() for item in body: if item["Err"] is not None: raise APIError( item["Err"], response=response, explanation=f"""Failed to prune network '{item["Id"]}'""", ) deleted.append(item["Id"]) return {"PodsDeleted": deleted, "SpaceReclaimed": 0}
def prune(self, filters: Mapping[str, str] = None) -> Dict[str, Any]: """Delete stopped containers. Args: filters: Dict of criteria for determining containers to remove. Available keys are: - until (str): Delete containers before this time - label (List[str]): Labels associated with containers Returns: List of deleted container id's and the freed disk space in bytes. Raises: APIError: If service reports an error """ params = dict() if filters is not None: params = {"filters", api.format_filters(filters)} response = self.client.post("/containers/prune", params=params) body = response.json() if response.status_code != 200: raise APIError(body["cause"], response=response, explanation=body["message"]) results = {"ContainersDeleted": [], "SpaceReclaimed": 0} for entry in body: if entry.get("error", None) is not None: raise APIError(entry["error"], response=response, explanation=entry["error"]) results["ContainersDeleted"].append(entry["id"]) results["SpaceReclaimed"] += entry["space"] return results
def prune(self, filters: Mapping[str, str] = None) -> Dict[str, Any]: """Delete stopped containers. Args: filters: Criteria for determining containers to remove. Available keys are: - until (str): Delete containers before this time - label (List[str]): Labels associated with containers Returns: Keys: - ContainersDeleted (List[str]): Id's of deleted containers. - SpaceReclaimed (int): Amount of disk space reclaimed in bytes. Raises: APIError: If service reports an error """ params = {"filters": api.prepare_filters(filters)} response = self.client.post("/containers/prune", params=params) body = response.json() if response.status_code != requests.codes.okay: raise APIError(body["cause"], response=response, explanation=body["message"]) results = {"ContainersDeleted": [], "SpaceReclaimed": 0} for entry in body: if entry.get("error", None) is not None: raise APIError(entry["error"], response=response, explanation=entry["error"]) results["ContainersDeleted"].append(entry["Id"]) results["SpaceReclaimed"] += entry["Size"] return results
def create(self, name: Optional[str] = None, **kwargs) -> Volume: """Create an Volume. Args: name: Name given to new volume Keyword Args: driver (str): Volume driver to use driver_opts (Dict[str, str]): Options to use with driver labels (Dict[str, str]): Labels to apply to volume Raises: APIError when service reports error """ data = { "Driver": kwargs.get("driver", None), "Labels": kwargs.get("labels", None), "Name": name, "Options": kwargs.get("driver_opts"), } # Strip out any keys without a value data = {k: v for (k, v) in data.items() if v is not None} contents = json.dumps(data) response = self.client.post( "/volumes/create", data=contents, headers={"Content-Type": "application/json"}, ) data = response.json() if response.status_code == 201: return self.prepare_model(data) raise APIError(data["cause"], response=response, explanation=data["message"])
def disconnect(self, container: Union[str, Container], **kwargs) -> None: """Disconnect given container from this network. Args: container: To remove from this Network Keyword Args: force (bool): Force operation 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) if response.status_code == 200: return data = response.json() raise APIError(data["cause"], response=response, explanation=data["message"])
def top(self, **kwargs) -> Union[Iterator[Dict[str, Any]], Dict[str, Any]]: """Report on running processes in the container. Keyword Args: ps_args (str): When given, arguments will be passed to ps stream (bool): When True, repeatedly return results. Default: False Raises: NotFound: when the container no longer exists APIError: when the service reports an error """ params = { "ps_args": kwargs.get("ps_args"), "stream": kwargs.get("stream", False), } response = self.client.get(f"/containers/{self.id}/top", params=params) if response.status_code != requests.codes.okay: body = response.json() if response.status_code == requests.codes.not_found: raise NotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"]) if params["stream"]: self._top_helper(response) return response.json()
def wait(self, **kwargs) -> Dict[str, Any]: """Block until container enters given state. Keyword Args: condition (str): Container state on which to release, values: not-running (default), next-exit or removed. timeout (int): Number of seconds to wait for container to stop. Returns: API response as a dict, including the container's exit code under the key StatusCode. Raises: ReadTimeoutError: If the timeout is exceeded. APIError: If the service returns as error. """ params = {"condition": kwargs.get("condition", None)} response = self.client.post(f"/containers/{self.id}/wait", params=params) if response.status_code == 204: return body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"])
def export(self, chunk_size: int = api.DEFAULT_CHUNK_SIZE) -> Iterator[bytes]: """Download container's filesystem contents as a tar archive. Args: chunk_size: <= number of bytes to return for each iteration of the generator. Yields: tarball in size/chunk_size chunks Raises: NotFound when container has been removed from service APIError when service reports an error """ response = self.client.get(f"/containers/{self.id}/export", stream=True) if response.status_code != 200: body = response.json() if response.status_code == 404: raise NotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"]) for out in response.iter_content(chunk_size=chunk_size): yield out
def stop(self, **kwargs) -> None: """Stop container. Keyword Args: all (bool): When True, stop all containers. Default: False (Podman only) ignore (bool): When True, ignore error if container already stopped (Podman only) timeout (int): Number of seconds to wait on container to stop before killing it. """ params = {"all": kwargs.get("all"), "timeout": kwargs.get("timeout")} connection_timeout: float = api.DEFAULT_TIMEOUT if params.get("timeout"): connection_timeout += float(params["timeout"]) response = self.client.post(f"/containers/{self.id}/stop", params=params, timeout=connection_timeout) response.raise_for_status() if response.status_code == requests.codes.no_content: return if response.status_code == requests.codes.not_modified: if kwargs.get("ignore", False): return body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"])
def get(self, network_id: str, *_, **kwargs) -> Network: # pylint: disable=arguments-differ """Return information for network network_id. Args: network_id: Network name or id. Keyword Args: compatible (bool): Should compatible API be used. Default: True Raises: NotFound: Network does not exist. APIError: 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) body = response.json() if response.status_code != 200: if response.status_code == 404: raise NotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"]) if not compatible: body = body[0] return self.prepare_model(body)
def list(self, *_, **kwargs) -> List[Volume]: """Report on volumes. Keyword Args: filters (Dict[str, str]): criteria to filter Volume list - driver (str): filter volumes by their driver - label (Dict[str, str]): filter by label and/or value - name (str): filter by volume's name """ filters = api.prepare_filters(kwargs.get("filters")) response = self.client.get("/volumes", params={"filters": filters}) if response.status_code == requests.codes.not_found: return [] data = response.json() if response.status_code != requests.codes.okay: raise APIError(data["cause"], response=response, explanation=data["message"]) volumes: List[Volume] = list() for item in data: volumes.append(self.prepare_model(item)) return volumes
def remove(self, name: Union[Volume, str], force: Optional[bool] = None) -> None: """Delete a volume. Args: name: Identifier for Volume to be deleted. force: When true, force deletion of in-use volume Raises: APIError: when service reports an error Notes: Podman only. """ if isinstance(name, Volume): name = name.name response = self.client.delete(f"/volumes/{name}", params={"force": force}) if response.status_code == requests.codes.no_content: return data = response.json() if response.status_code == requests.codes.not_found: raise NotFound(data["cause"], response=response, explanation=data["message"]) raise APIError(data["cause"], response=response, explanation=data["message"])
def stats(self, **kwargs) -> Union[Sequence[Dict[str, bytes]], bytes]: """Return statistics for container. Keyword Args: decode (bool): If True and stream is True, stream will be decoded into dict's. Default: False. stream (bool): Stream statistics until cancelled. Default: True. Raises: APIError when service reports an error """ # FIXME Errors in stream are not handled, need content and json to read Errors. stream = kwargs.get("stream", True) decode = kwargs.get("decode", False) params = { "containers": self.id, "stream": stream, } response = self.client.get("/containers/stats", params=params) if response.status_code != requests.codes.okay: body = response.json() if response.status_code == requests.codes.not_found: raise NotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"]) if stream: return self._stats_helper(decode, response.iter_lines()) with io.StringIO() as buffer: for entry in response.text: buffer.writer(json.dumps(entry) + "\n") return buffer.getvalue()
def wait(self, **kwargs) -> Dict[str, Any]: """Block until the container enters given state. Keyword Args: condition (str): Container state on which to release, values: not-running (default), next-exit or removed. timeout (int): Number of seconds to wait for the container to stop. Returns: Keys: - StatusCode (int): Container's exit code - Error["Message"] (str): Error message from container Raises: NotFound: When Container not found ReadTimeoutError: When timeout is exceeded APIError: When service returns an error """ condition = kwargs.get("condition") if isinstance(condition, str): condition = [condition] response = self.client.post(f"/containers/{self.id}/wait", params={"condition": condition}) body = response.json() if response.status_code == requests.codes.okay: return body if response.status_code == requests.codes.not_found: raise NotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def get_archive( self, path: str, chunk_size: int = api.DEFAULT_CHUNK_SIZE ) -> Tuple[Iterable, Dict[str, Any]]: """Download a file or folder from the container's filesystem. Args: path: Path to file or folder. chunk_size: <= number of bytes to return for each iteration of the generator. Returns: First item is a raw tar data stream. Second item is a dict containing stat information on the specified path. """ response = self.client.get(f"/containers/{self.id}/archive", params={"path": [path]}) if response.status_code != 200: body = response.json() if response.status_code == 404: raise NotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"]) stat = response.headers.get('x-docker-container-path-stat', None) stat = api.decode_header(stat) return response.iter_content(chunk_size=chunk_size), stat
def stop(self, **kwargs): """Stop container. Keyword Args: all (bool): When True, stop all containers. Default: False (Podman only) ignore (bool): When True, ignore error if container already stopped (Podman only) timeout (int): Number of seconds to wait on container to stop before killing it. """ connection_timeout = api.DEFAULT_TIMEOUT params = {} if "all" in kwargs: params["all"] = kwargs["all"] if "timeout" in kwargs: params["timeout"] = kwargs["timeout"] connection_timeout += float(kwargs["timeout"]) response = self.client.post(f"/containers/{self.id}/stop", params=params, timeout=connection_timeout) if response.status_code == 204: return if response.status_code == 304: if kwargs.get("ignore", False): return body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"])
def list(self, **kwargs) -> List[Pod]: """Report on pods. Keyword Args: filters (Mapping[str, str]): Criteria for listing pods. Available filters: - ctr-ids (List[str]): List of container ids to filter by. - ctr-names (List[str]): List of container names to filter by. - ctr-number (List[int]): list pods with given number of containers. - ctr-status (List[str]): List pods with containers in given state. Legal values are: "created", "running", "paused", "stopped", "exited", "unknown" - id (str) - List pod with this id. - name (str) - List pod with this name. - status (List[str]): List pods in given state. Legal values are: "created", "running", "paused", "stopped", "exited", "unknown" - label (List[str]): List pods with given labels. - network (List[str]): List pods associated with given Network Ids (not Names). Raises: APIError: Error returned by service. """ params = {"filters": api.prepare_filters(kwargs.get("filters"))} response = self.client.get("/pods/json", params=params) body = response.json() if response.status_code != requests.codes.okay: raise APIError(body["cause"], response=response, explanation=body["message"]) pods: List[Pod] = list() for item in body: pods.append(self.prepare_model(attrs=item)) return pods
def create(self, name: Optional[str] = None, **kwargs) -> Volume: """Create a Volume. Args: name: Name given to new volume Keyword Args: driver (str): Volume driver to use driver_opts (Dict[str, str]): Options to use with driver labels (Dict[str, str]): Labels to apply to volume Raises: APIError when service reports error """ data = { "Driver": kwargs.get("driver"), "Labels": kwargs.get("labels"), "Name": name, "Options": kwargs.get("driver_opts"), } response = self.client.post( "/volumes/create", data=api.prepare_body(data), headers={"Content-Type": "application/json"}, ) data = response.json() if response.status_code == requests.codes.created: return self.prepare_model(attrs=data) raise APIError(data["cause"], response=response, explanation=data["message"])
def stats(self, **kwargs) -> Dict[str, Any]: """Resource usage statistics for the containers in pods. Keyword Args: all (bool): Provide statistics for all running pods. name (Union[str, List[str]]): Pods to include in report. Raises: NotFound when pod not found. APIError when service reports an error. """ if "all" in kwargs and "name" in kwargs: raise ValueError( "Keywords 'all' and 'name' are mutually exclusive.") params = { "all": kwargs.get("all"), "namesOrIDs": kwargs.get("name"), } response = self.client.get("/pods/stats", params=params) body = response.json() if response.status_code == requests.codes.okay: return body if response.status_code == requests.codes.not_found: raise NotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def remove(self, force: Optional[bool] = None, **kwargs) -> None: """Remove this network. Args: force: Remove network and any associated networks Keyword Args: compatible (bool): Should compatible API be used. Default: True Raises: APIError when Podman service reports an error """ compatible = kwargs.get("compatible", True) response = self.client.delete(f"/networks/{self.name}", params={"force": force}, compatible=compatible) if (response.status_code == requests.codes.no_content or response.status_code == requests.codes.okay): return body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"])
def kill(self, signal: Union[str, int, None] = None) -> None: """Send signal to container. """ response = self.client.post(f"/containers/{self.id}/kill", params={"signal": signal}) if response.status_code == requests.codes.no_content: return body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"])
def unpause(self) -> None: """Unpause processes in container.""" response = self.client.post(f"/containers/{self.id}/unpause") if response.status_code == requests.codes.no_content: return body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"])
def info(self, *_, **__) -> Dict[str, Any]: """Returns information on Podman service.""" response = self.client.get("/system/info") body = response.json() if response.status_code == 200: return body raise APIError(body["cause"], response=response, explanation=body["message"])
def prune(self, filters: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]: """Delete unused Networks. 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 Notes: SpaceReclaimed always reported as 0 """ compatible = kwargs.get("compatible", True) response = self.client.post("/networks/prune", filters=api.prepare_filters(filters), compatible=compatible) body = response.json() if response.status_code != requests.codes.okay: raise APIError(body["cause"], response=response, explanation=body["message"]) if compatible: return body deleted = list() for item in body: if item["Error"] is not None: raise APIError( item["Error"], response=response, explanation=f"""Failed to prune network '{item["Name"]}'""", ) deleted.append(item["Name"]) return {"NetworksDeleted": deleted, "SpaceReclaimed": 0}
def pause(self) -> None: """Pause processes within the container.""" response = self.client.post(f"/containers/{self.id}/pause") if response.status_code == requests.codes.no_content: return body = response.json() if response.status_code == requests.codes.not_found: raise NotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def prune(self, filters: Optional[Dict[str, str]] = None): """Delete unused Pods.""" response = self.client.post( "/pods/prune", params={"filters": api.format_filters(filters)}) if response.status_code == 200: return body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"])
def list(self, **kwargs) -> List[Container]: """Report on containers. Keyword Args: all: If False, only show running containers. Default: False. since: Show containers created after container name or id given. before: Show containers created before container name or id given. limit: Show last N created containers. filters: Filter container reported. Available filters: - exited (int): Only containers with specified exit code - status (str): One of restarting, running, paused, exited - label (Union[str, List[str]]): Format either "key", "key=value" or a list of such. - id (str): The id of the container. - name (str): The name of the container. - ancestor (str): Filter by container ancestor. Format of <image-name>[:tag], <image-id>, or <image@digest>. - before (str): Only containers created before a particular container. Give the container name or id. - since (str): Only containers created after a particular container. Give container name or id. sparse: Ignored ignore_removed: If True, ignore failures due to missing containers. Raises: APIError: If service returns an error. """ params = { "all": kwargs.get("all"), "filters": kwargs.get("filters", dict()), "limit": kwargs.get("limit"), } if "before" in kwargs: params["filters"]["before"] = kwargs.get("before") if "since" in kwargs: params["filters"]["since"] = kwargs.get("since") # filters formatted last because some kwargs may need to be mapped into filters params["filters"] = api.prepare_filters(params["filters"]) response = self.client.get("/containers/json", params=params) body = response.json() if response.status_code != requests.codes.okay: raise APIError(body["cause"], response=response, explanation=body["message"]) containers: List[Container] = list() for item in body: containers.append(self.prepare_model(attrs=item)) return containers
def df(self) -> Dict[str, Any]: # pylint: disable=invalid-name """Disk usage by Podman resources. Returns: dict: Keyed by resource categories and their data usage. """ response = self.client.get("/system/df") body = response.json() if response.status_code == 200: return body raise APIError(body["cause"], response=response, explanation=body["message"])
def list(self, **kwargs) -> List[Network]: """Report on networks. Keyword Args: names (List[str]): List of names to filter by. ids (List[str]): List of ids to filter by. filters (Mapping[str,str]): Criteria for listing networks. Available filters: - driver="bridge": Matches a network's driver. Only "bridge" is supported. - label=(Union[str, List[str]]): format either "key", "key=value" or a list of such. - type=(str): Filters networks by type, legal values are: - "custom" - "builtin" - plugin=(List[str]]): Matches CNI plugins included in a network, legal values are (Podman only): - bridge - portmap - firewall - tuning - dnsname - macvlan greedy (bool): Fetch more details for each network individually. You might want this to get the containers attached to them. (ignored) Raises: APIError: Error returned by service. """ compatible = kwargs.get("compatible", True) filters = kwargs.get("filters", dict()) 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) body = response.json() if response.status_code != requests.codes.okay: raise APIError(body["cause"], response=response, explanation=body["message"]) nets: List[Network] = list() for item in body: nets.append(self.prepare_model(item)) return nets