diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index 9c388c43..8ef538a9 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -282,7 +282,7 @@ def __prepareContainerConfig(self): self.enableExegolResources() if ParametersManager().log: self.enableShellLogging(ParametersManager().log_method, - UserConfig().shell_logging_compress ^ ParametersManager().log_compress) + UserConfig().shell_logging_compress ^ ParametersManager().log_compress) if ParametersManager().workspace_path: if ParametersManager().mount_current_dir: logger.warning(f'Workspace conflict detected (-cwd cannot be use with -w). Using: {ParametersManager().workspace_path}') @@ -936,11 +936,15 @@ def __removeSysctl(self, sysctl_key: str): return False def getNetwork(self) -> (str, str): - """Network mode, docker term getter""" + """First Network getter for docker API on container creation""" if len(self.__networks) > 0: return self.__networks[0].getNetworkConfig() return None, None + def getNetworks(self): + """Networks getter""" + return self.__networks + def setExtraHost(self, host: str, ip: str): """Add or update an extra host to resolv inside the container.""" self.__extra_host[host] = ip diff --git a/exegol/model/ExegolContainer.py b/exegol/model/ExegolContainer.py index d6de7d2c..d0e3818f 100644 --- a/exegol/model/ExegolContainer.py +++ b/exegol/model/ExegolContainer.py @@ -218,8 +218,13 @@ def remove(self): self.__container.remove() logger.success(f"Container {self.name} successfully removed.") except NotFound: - logger.error( - f"The container {self.name} has already been removed (probably created as a temporary container).") + logger.error(f"The container {self.name} has already been removed (probably created as a temporary container).") + nets = self.config.getNetworks() + # Must be imported locally to avoid circular importation + from exegol.utils.DockerUtils import DockerUtils + for net in nets: + if net.shouldBeRemoved(): + DockerUtils.removeNetwork(net.getNetworkName()) def __removeVolume(self): """Remove private workspace volume directory if exist""" diff --git a/exegol/model/ExegolNetwork.py b/exegol/model/ExegolNetwork.py index 9df025b0..ee798614 100644 --- a/exegol/model/ExegolNetwork.py +++ b/exegol/model/ExegolNetwork.py @@ -1,8 +1,6 @@ from enum import Enum from typing import Optional, Union, List -from docker.types import EndpointConfig - class DockerDrivers(Enum): """Enum for Docker driver type""" @@ -21,13 +19,17 @@ class ExegolNetworkMode(Enum): class ExegolNetwork: + DEFAULT_DOCKER_NETWORK = [d.value for d in DockerDrivers] + + __DEFAULT_NETWORK_DRIVER = DockerDrivers.Bridge + def __init__(self, net_mode: ExegolNetworkMode = ExegolNetworkMode.host, net_name: Optional[str] = None): self.__net_mode: ExegolNetworkMode = net_mode self.__net_name: str = net_name if net_name is not None else net_mode.value try: self.__docker_net_mode: DockerDrivers = DockerDrivers(self.__net_name) except ValueError: - self.__docker_net_mode = DockerDrivers.Bridge + self.__docker_net_mode = self.__DEFAULT_NETWORK_DRIVER @classmethod def instance_network(cls, mode: Union[ExegolNetworkMode, str], container_name: str): @@ -60,11 +62,17 @@ def getNetworkConfig(self) -> (str, str): def getNetworkMode(self) -> ExegolNetworkMode: return self.__net_mode + def getNetworkName(self): + return self.__net_name + def getTextNetworkMode(self) -> str: if self.__net_mode is ExegolNetworkMode.attached: return self.__net_name return self.__net_mode.name + def shouldBeRemoved(self): + return self.__net_mode == ExegolNetworkMode.nat + def __repr__(self): repr_str = self.__net_mode.value if self.__net_mode in [ExegolNetworkMode.nat, ExegolNetworkMode.attached]: diff --git a/exegol/utils/DockerUtils.py b/exegol/utils/DockerUtils.py index 30ee85da..de617824 100644 --- a/exegol/utils/DockerUtils.py +++ b/exegol/utils/DockerUtils.py @@ -7,6 +7,7 @@ from docker import DockerClient from docker.errors import APIError, DockerException, NotFound, ImageNotFound from docker.models.images import Image +from docker.models.networks import Network from docker.models.volumes import Volume from requests import ReadTimeout @@ -20,6 +21,7 @@ from exegol.model.ExegolContainer import ExegolContainer from exegol.model.ExegolContainerTemplate import ExegolContainerTemplate from exegol.model.ExegolImage import ExegolImage +from exegol.model.ExegolNetwork import ExegolNetwork from exegol.model.MetaImages import MetaImages from exegol.utils.ExeLog import logger, console, ExeLog from exegol.utils.WebUtils import WebUtils @@ -132,6 +134,10 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False docker_args["network_disabled"] = True else: docker_args["network"], docker_args["network_driver_opt"] = model.config.getNetwork() + if not cls.networkExist(docker_args["network"]): + if not cls.createNetwork(network_name=docker_args["network"], driver=docker_args["network_driver_opt"]): + logger.critical("Unable to create the dedicated network for the new container. Aborting.") + # Handle temporary arguments if temporary: # Only the 'run' function support the "remove" parameter @@ -158,6 +164,11 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False logger.debug("Container removed") except Exception: pass + try: + if cls.removeNetwork(docker_args["network"]): + logger.debug("Network removed") + except Exception: + pass logger.critical("Error while creating exegol container. Exiting.") # Not reachable, critical logging will exit return # type: ignore @@ -256,6 +267,103 @@ def __loadDockerVolume(cls, volume_path: str, volume_name: str) -> Volume: return None # type: ignore return volume + # # # Network Section # # # + + @classmethod + def listAttachableNetworks(cls) -> List[Network]: + """List every non-default networks""" + networks = [] + try: + networks = cls.__client.networks.list() + except APIError as e: + raise e + except ReadTimeout: + logger.critical("Received a timeout error, Docker is busy... Unable to enumerate volume, retry later.") + for net in networks.copy(): + if net.name in ExegolNetwork.DEFAULT_DOCKER_NETWORK: + networks.remove(net) + return networks + + @classmethod + def listExegolNetworks(cls) -> List[Network]: + """List every exegol networks""" + try: + return cls.__client.networks.list(filters={"label": "source=exegol"}) + except APIError as e: + raise e + except ReadTimeout: + logger.critical("Received a timeout error, Docker is busy... Unable to enumerate volume, retry later.") + + @classmethod + def getNetwork(cls, network_name: str, exegol_only: bool = False) -> Optional[Network]: + """Find a specific network""" + networks: List[Network] = [] + filter = {} + if exegol_only: + filter["label"] = "source=exegol" + try: + networks = cls.__client.networks.list(names=network_name, filters=filter) + except APIError as e: + raise e + except ReadTimeout: + logger.critical("Received a timeout error, Docker is busy... Unable to enumerate volume, retry later.") + for net in networks: + # Search for an exact match + if net.name == network_name: + return net + return None + + @classmethod + def networkExist(cls, network_name: str) -> bool: + """Return True is the supplied network name exist""" + return cls.getNetwork(network_name) is not None + + @classmethod + def createNetwork(cls, network_name: str, driver: str) -> bool: + """Create a new exegol network""" + # ip_pool = IPAMPool() + # config = IPAMConfig(pool_configs=[ip_pool]) + # TODO use custom network range + try: + network: Network = cls.__client.networks.create(name=network_name, driver=driver, labels={"source": "exegol"}, check_duplicate=True) # ipam=config + return True + except APIError as e: + if e.status_code == 409: + logger.error("This network already exist.") + return False + raise e + except ReadTimeout: + logger.critical("Received a timeout error, Docker is busy... Unable to enumerate volume, retry later.") + return False + + @classmethod + def removeNetwork(cls, network_name: Optional[str] = None, network: Optional[Network] = None) -> bool: + """Remove exegol network""" + if network is None: + if network_name is None: + raise ValueError("One of the parameter must be supplied.") + elif network_name in ExegolNetwork.DEFAULT_DOCKER_NETWORK: + # Default docker driver cannot be deleted + return False + network = cls.getNetwork(network_name, exegol_only=True) + + if network is not None: + if network.name in ExegolNetwork.DEFAULT_DOCKER_NETWORK: + # Default docker driver cannot be deleted + return False + try: + network.remove() + logger.success(f"Dedicated network successfully removed.") + return True + except NotFound: + logger.verbose(f"The dedicated network {network.name} was already removed.") + except APIError as e: + logger.error(f"The associated dedicated network cannot be automatically removed. " + f"You have to delete it manually ({network.name}). Error: {e.explanation}") + else: + logger.info(f"The network {network_name} will not be deleted. Only exegol network can be automatically deleted.") + return False + # # # Image Section # # # @classmethod @@ -589,8 +697,8 @@ def __remove_image(cls, image_name: str) -> bool: else: logger.debug(f"Unexpected error after timeout: {err}") except ReadTimeout: - wait_time = wait_time + wait_time*i - logger.info(f"Docker timeout again ({i+1}/{max_retry}). Next retry in {wait_time} seconds...") + wait_time = wait_time + wait_time * i + logger.info(f"Docker timeout again ({i + 1}/{max_retry}). Next retry in {wait_time} seconds...") sleep(wait_time) # Wait x seconds before retry logger.error(f"The deletion of the image '{image_name}' has timeout, the deletion may be incomplete.") return False