From efdfcec15390cbb8c111c863db0d502dc61d0e03 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sat, 6 May 2023 12:07:34 +0200 Subject: [PATCH 01/44] Bump dockerpy>=6.1.0 and support for deterministic MAC Addrs (#137) --- src/Kathara/manager/docker/DockerMachine.py | 14 +++++++++++-- src/Kathara/model/Machine.py | 22 ++++++++++++++++++++- src/requirements.txt | 2 +- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Kathara/manager/docker/DockerMachine.py b/src/Kathara/manager/docker/DockerMachine.py index f2b235d9..78a843d7 100644 --- a/src/Kathara/manager/docker/DockerMachine.py +++ b/src/Kathara/manager/docker/DockerMachine.py @@ -268,6 +268,10 @@ def create(self, machine: Machine) -> None: privileged=privileged, network=first_network.name if first_network else None, network_mode="bridge" if first_network else "none", + network_driver_opt={ + 'kathara.machine': machine.name, + 'kathara.iface': 0 + } if first_network else None, environment=machine.meta['envs'], sysctls=sysctl_parameters, mem_limit=memory, @@ -315,7 +319,10 @@ def connect_to_link(machine: Machine, link: Link) -> None: if link.api_object.name not in attached_networks: try: - link.api_object.connect(machine.api_object) + link.api_object.connect( + machine.api_object, + driver_opt={'kathara.machine': machine.name, 'kathara.iface': machine.get_interface_by_link(link)} + ) except APIError as e: if e.response.status_code == 500 and \ ("network does not exist" in e.explanation or "endpoint does not exist" in e.explanation): @@ -379,7 +386,10 @@ def start(self, machine: Machine) -> None: ) ) try: - machine_link.api_object.connect(machine.api_object) + machine_link.api_object.connect( + machine.api_object, + driver_opt={'kathara.machine': machine.name, 'kathara.iface': iface_num} + ) except APIError as e: if e.response.status_code == 500 and \ ("network does not exist" in e.explanation or "endpoint does not exist" in e.explanation): diff --git a/src/Kathara/model/Machine.py b/src/Kathara/model/Machine.py index c4b5f19e..bc390432 100644 --- a/src/Kathara/model/Machine.py +++ b/src/Kathara/model/Machine.py @@ -1,7 +1,6 @@ import collections import logging import re -import tempfile from io import BytesIO from typing import Dict, Any, Tuple, Optional, List, OrderedDict, TextIO, Union, BinaryIO @@ -129,6 +128,27 @@ def remove_interface(self, link: 'LinkPackage.Link') -> None: ) link.machines.pop(self.name) + def get_interface_by_link(self, link: 'LinkPackage.Link') -> int: + """Get the interface number associated to the specified collision domain. + + Args: + link (Kathara.model.Link): The Kathara collision domain to search. + + Returns: + int: The interface number associated to the collision domain. + + Raises: + MachineCollisionDomainConflictError: If the device is not connected to the collision domain. + + """ + for (number, machine_link) in self.interfaces.items(): + if machine_link == link: + return number + + raise MachineCollisionDomainError( + f"Device `{self.name}` is not connected to collision domain `{link.name}`." + ) + def add_meta(self, name: str, value: Any) -> None: """Add a meta property to the device. diff --git a/src/requirements.txt b/src/requirements.txt index feebbf0d..57d9bda8 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,5 +1,5 @@ binaryornot>=0.4.4; -docker>=6.0.1; +docker>=6.1.0; kubernetes>=23.3.0; requests>=2.22.0; coloredlogs>=10.0; From 16b62595db064f096e0f26d2c03b94e7d53d994b Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sat, 6 May 2023 13:32:31 +0200 Subject: [PATCH 02/44] Cast iface_idx to string --- src/Kathara/manager/docker/DockerMachine.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Kathara/manager/docker/DockerMachine.py b/src/Kathara/manager/docker/DockerMachine.py index 78a843d7..a7510735 100644 --- a/src/Kathara/manager/docker/DockerMachine.py +++ b/src/Kathara/manager/docker/DockerMachine.py @@ -270,7 +270,7 @@ def create(self, machine: Machine) -> None: network_mode="bridge" if first_network else "none", network_driver_opt={ 'kathara.machine': machine.name, - 'kathara.iface': 0 + 'kathara.iface': "0" } if first_network else None, environment=machine.meta['envs'], sysctls=sysctl_parameters, @@ -321,7 +321,8 @@ def connect_to_link(machine: Machine, link: Link) -> None: try: link.api_object.connect( machine.api_object, - driver_opt={'kathara.machine': machine.name, 'kathara.iface': machine.get_interface_by_link(link)} + driver_opt={'kathara.machine': machine.name, + 'kathara.iface': str(machine.get_interface_by_link(link))} ) except APIError as e: if e.response.status_code == 500 and \ @@ -388,7 +389,7 @@ def start(self, machine: Machine) -> None: try: machine_link.api_object.connect( machine.api_object, - driver_opt={'kathara.machine': machine.name, 'kathara.iface': iface_num} + driver_opt={'kathara.machine': machine.name, 'kathara.iface': str(iface_num)} ) except APIError as e: if e.response.status_code == 500 and \ From d04f1c02ae2a4bbc46e94c57d5cb53be40d69797 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Tue, 5 Dec 2023 10:38:01 +0100 Subject: [PATCH 03/44] PrivilegeHandler raises/drops privileges once (even if called N times) --- src/Kathara/auth/PrivilegeHandler.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Kathara/auth/PrivilegeHandler.py b/src/Kathara/auth/PrivilegeHandler.py index b8b0353c..981c96cf 100644 --- a/src/Kathara/auth/PrivilegeHandler.py +++ b/src/Kathara/auth/PrivilegeHandler.py @@ -7,7 +7,7 @@ class PrivilegeHandler(object): - __slots__ = ['user_uid', 'user_gid', 'effective_user_uid', 'effective_user_gid'] + __slots__ = ['user_uid', 'user_gid', 'effective_user_uid', 'effective_user_gid', '_ref'] __instance: PrivilegeHandler = None @@ -31,10 +31,20 @@ def __init__(self) -> None: except AttributeError: pass + self._ref = 0 + PrivilegeHandler.__instance = self def drop_privileges(self) -> None: - logging.debug("Dropping privileges to UID=%d and GID=%d..." % (self.user_uid, self.user_gid)) + logging.debug("Called `drop_privileges`...") + + self._ref -= 1 + logging.debug(f"Reference count is now {self._ref}.") + if self._ref > 0: + logging.debug("Reference count > 0, exiting.") + return + + logging.debug(f"Dropping privileges to UID={self.user_uid} and GID={self.user_gid}...") try: os.setuid(self.user_uid) @@ -47,7 +57,15 @@ def drop_privileges(self) -> None: pass def raise_privileges(self) -> None: - logging.debug("Raising privileges to UID=%d and GID=%d..." % (self.effective_user_uid, self.effective_user_gid)) + logging.debug("Called `raise_privileges`...") + + self._ref += 1 + logging.debug(f"Reference count is now {self._ref}.") + if self._ref > 1: + logging.debug("Reference count > 1, exiting.") + return + + logging.debug(f"Raising privileges to UID={self.effective_user_uid} and GID={self.effective_user_gid}...") try: os.setuid(self.effective_user_uid) From d9f2845828b7ba95af55ca4eedde11aee03bf282 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Tue, 5 Dec 2023 17:51:45 +0100 Subject: [PATCH 04/44] Fix get_link_api_object by name label --- src/Kathara/manager/docker/DockerLink.py | 10 +++++----- src/Kathara/manager/docker/DockerMachine.py | 6 +++--- src/Kathara/manager/kubernetes/KubernetesLink.py | 2 +- src/Kathara/manager/kubernetes/KubernetesMachine.py | 2 +- tests/manager/docker/docker_link_test.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Kathara/manager/docker/DockerLink.py b/src/Kathara/manager/docker/DockerLink.py index c6a29290..b91f7eaa 100644 --- a/src/Kathara/manager/docker/DockerLink.py +++ b/src/Kathara/manager/docker/DockerLink.py @@ -95,13 +95,13 @@ def create(self, link: Link) -> None: return # If a network with the same name exists, return it instead of creating a new one. - link_name = self.get_network_name(link.name) - networks = self.get_links_api_objects_by_filters(link_name=link_name) + networks = self.get_links_api_objects_by_filters(link_name=link.name) if networks: link.api_object = networks.pop() else: network_ipam_config = docker.types.IPAMConfig(driver='null') + link_name = self.get_network_name(link.name) user_label = "shared_cd" if Setting.get_instance().shared_cd else utils.get_current_user_name() link.api_object = self.client.networks.create( name=link_name, @@ -211,11 +211,11 @@ def get_links_api_objects_by_filters(self, lab_hash: str = None, link_name: str """ filters = {"label": ["app=kathara"]} if user: - filters["label"].append("user=%s" % user) + filters["label"].append(f"user={user}") if lab_hash: - filters["label"].append("lab_hash=%s" % lab_hash) + filters["label"].append(f"lab_hash={lab_hash}") if link_name: - filters["name"] = link_name + filters["label"].append(f"name={link_name}") return self.client.networks.list(filters=filters) diff --git a/src/Kathara/manager/docker/DockerMachine.py b/src/Kathara/manager/docker/DockerMachine.py index 3508f2e5..255d86ff 100644 --- a/src/Kathara/manager/docker/DockerMachine.py +++ b/src/Kathara/manager/docker/DockerMachine.py @@ -818,11 +818,11 @@ def get_machines_api_objects_by_filters(self, lab_hash: str = None, machine_name """ filters = {"label": ["app=kathara"]} if user: - filters["label"].append("user=%s" % user) + filters["label"].append(f"user={user}") if lab_hash: - filters["label"].append("lab_hash=%s" % lab_hash) + filters["label"].append(f"lab_hash={lab_hash}") if machine_name: - filters["label"].append("name=%s" % machine_name) + filters["label"].append(f"name={machine_name}") return self.client.containers.list(all=True, filters=filters) diff --git a/src/Kathara/manager/kubernetes/KubernetesLink.py b/src/Kathara/manager/kubernetes/KubernetesLink.py index 26bf3c19..a4cedd29 100644 --- a/src/Kathara/manager/kubernetes/KubernetesLink.py +++ b/src/Kathara/manager/kubernetes/KubernetesLink.py @@ -197,7 +197,7 @@ def get_links_api_objects_by_filters(self, lab_hash: str = None, link_name: str """ filters = ["app=kathara"] if link_name: - filters.append("name=%s" % link_name) + filters.append(f"name={link_name}") # Get all Kathara namespaces if lab_hash is None namespaces = list(map(lambda x: x.metadata.name, self.kubernetes_namespace.get_all())) \ diff --git a/src/Kathara/manager/kubernetes/KubernetesMachine.py b/src/Kathara/manager/kubernetes/KubernetesMachine.py index affb2ebf..e9862b25 100644 --- a/src/Kathara/manager/kubernetes/KubernetesMachine.py +++ b/src/Kathara/manager/kubernetes/KubernetesMachine.py @@ -772,7 +772,7 @@ def get_machines_api_objects_by_filters(self, lab_hash: str = None, machine_name """ filters = ["app=kathara"] if machine_name: - filters.append("name=%s" % machine_name) + filters.append(f"name={machine_name}") # Get all Kathara namespaces if lab_hash is None namespaces = list(map(lambda x: x.metadata.name, self.kubernetes_namespace.get_all())) \ diff --git a/tests/manager/docker/docker_link_test.py b/tests/manager/docker/docker_link_test.py index 40bceb7f..3a5cb55f 100644 --- a/tests/manager/docker/docker_link_test.py +++ b/tests/manager/docker/docker_link_test.py @@ -194,7 +194,7 @@ def test_undeploy_link(mock_undeploy_link, docker_link, docker_network): @mock.patch("docker.models.networks.list") def test_get_links_by_filters(mock_network_list, docker_link): docker_link.get_links_api_objects_by_filters("lab_hash_value", "link_name_value", "user_name_value") - filters = {"label": ["app=kathara", "lab_hash=lab_hash_value", "user=user_name_value"], "name": "link_name_value"} + filters = {"label": ["app=kathara", "lab_hash=lab_hash_value", "user=user_name_value", "name=link_name_value"]} mock_network_list.called_once_with(filters=filters) @@ -215,7 +215,7 @@ def test_get_links_by_filters_only_lab_hash(mock_network_list, docker_link): @mock.patch("docker.models.networks.list") def test_get_links_by_filters_only_link_name(mock_network_list, docker_link): docker_link.get_links_api_objects_by_filters(None, "link_name_value") - filters = {"label": ["app=kathara"], "name": "link_name_value"} + filters = {"label": ["app=kathara", "name=link_name_value"]} mock_network_list.called_once_with(filters=filters) From 4430fa538274da250cff3342ac4c65f8ac3595ae Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Tue, 5 Dec 2023 18:11:39 +0100 Subject: [PATCH 05/44] Fix privileges reference count --- src/Kathara/auth/PrivilegeHandler.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Kathara/auth/PrivilegeHandler.py b/src/Kathara/auth/PrivilegeHandler.py index 981c96cf..3a29ab5f 100644 --- a/src/Kathara/auth/PrivilegeHandler.py +++ b/src/Kathara/auth/PrivilegeHandler.py @@ -38,10 +38,15 @@ def __init__(self) -> None: def drop_privileges(self) -> None: logging.debug("Called `drop_privileges`...") - self._ref -= 1 - logging.debug(f"Reference count is now {self._ref}.") - if self._ref > 0: - logging.debug("Reference count > 0, exiting.") + logging.debug(f"Reference count is {self._ref}.") + if self._ref > 1: + self._ref -= 1 + logging.debug(f"Reference count is {self._ref}, exiting.") + return + + if self._ref <= 0: + self._ref = 0 + logging.debug(f"Reference count is {self._ref}, exiting.") return logging.debug(f"Dropping privileges to UID={self.user_uid} and GID={self.user_gid}...") @@ -56,6 +61,9 @@ def drop_privileges(self) -> None: except OSError: pass + self._ref = 0 + logging.debug(f"Reference count is reset to 0.") + def raise_privileges(self) -> None: logging.debug("Called `raise_privileges`...") From 6291e618f117dc5e6f2c823c5f9ba51572b9312b Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Tue, 5 Dec 2023 18:18:27 +0100 Subject: [PATCH 06/44] Fix privileged decorator in DockerManager --- src/Kathara/manager/docker/DockerManager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Kathara/manager/docker/DockerManager.py b/src/Kathara/manager/docker/DockerManager.py index 62b18294..a82003a3 100644 --- a/src/Kathara/manager/docker/DockerManager.py +++ b/src/Kathara/manager/docker/DockerManager.py @@ -416,6 +416,7 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na raise MachineNotFoundError(f"Device `{machine_name}` not found.") + @privileged def get_machines_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ List[docker.models.containers.Container]: """Return API objects of running devices. @@ -434,6 +435,7 @@ def get_machines_api_objects(self, lab_hash: str = None, lab_name: str = None, a return self.docker_machine.get_machines_api_objects_by_filters(lab_hash=lab_hash, user=user_name) + @privileged def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> docker.models.networks.Network: """Return the corresponding API object of a collision domain in a network scenario. @@ -468,6 +470,7 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st raise LinkNotFoundError(f"Collision Domain `{link_name}` not found.") + @privileged def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ List[docker.models.networks.Network]: """Return API objects of collision domains in a network scenario. @@ -659,6 +662,7 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None, yield machine_stats + @privileged def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: str = None, all_users: bool = False) -> Generator[Dict[str, DockerLinkStats], None, None]: """Return information about deployed Docker networks. @@ -679,6 +683,7 @@ def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: return self.docker_link.get_links_stats(lab_hash=lab_hash, link_name=link_name, user=user_name) + @privileged def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ Generator[DockerLinkStats, None, None]: """Return information of the specified deployed network in a specified network scenario. From 8e6ee5908b441a63521c5faebbd0433011041422 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Wed, 13 Dec 2023 15:53:29 +0100 Subject: [PATCH 07/44] Add `lab` parameter to all `Manager` methods (#254) --- src/Kathara/foundation/manager/IManager.py | 137 +++++---- src/Kathara/manager/Kathara.py | 157 +++++++---- src/Kathara/manager/docker/DockerManager.py | 230 ++++++++++------ .../manager/kubernetes/KubernetesManager.py | 245 ++++++++++------- tests/manager/docker/docker_manager_test.py | 260 ++++++++++++++++-- .../kubernetes/kubernetes_manager_test.py | 121 +++++++- 6 files changed, 829 insertions(+), 321 deletions(-) diff --git a/src/Kathara/foundation/manager/IManager.py b/src/Kathara/foundation/manager/IManager.py index 15f41703..5a226e41 100644 --- a/src/Kathara/foundation/manager/IManager.py +++ b/src/Kathara/foundation/manager/IManager.py @@ -157,13 +157,18 @@ def wipe(self, all_users: bool = False) -> None: @abstractmethod def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, - shell: str = None, logs: bool = False, wait: Union[bool, Tuple[int, float]] = True) -> None: + lab: Optional[Lab] = None, shell: str = None, logs: bool = False, + wait: Union[bool, Tuple[int, float]] = True) -> None: """Connect to a device in a running network scenario, using the specified shell. Args: machine_name (str): The name of the device to connect. - lab_hash (str): The hash of the network scenario where the device is deployed. - lab_name (str): The name of the network scenario where the device is deployed. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. shell (str): The name of the shell to use for connecting. logs (bool): If True, print startup logs on stdout. wait (Union[bool, Tuple[int, float]]): If True, wait indefinitely until the end of the startup commands @@ -181,15 +186,19 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam @abstractmethod def exec(self, machine_name: str, command: Union[List[str], str], lab_hash: Optional[str] = None, - lab_name: Optional[str] = None, wait: Union[bool, Tuple[int, float]] = False) \ + lab_name: Optional[str] = None, lab: Optional[Lab] = None, wait: Union[bool, Tuple[int, float]] = False) \ -> Generator[Tuple[bytes, bytes], None, None]: """Exec a command on a device in a running network scenario. Args: machine_name (str): The name of the device to connect. command (Union[List[str], str]): The command to exec on the device. - lab_hash (Optional[str]): The hash of the network scenario where the device is deployed. - lab_name (Optional[str]): The name of the network scenario where the device is deployed. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. wait (Union[bool, Tuple[int, float]]): If True, wait indefinitely until the end of the startup commands execution before executing the command. If a tuple is provided, the first value indicates the number of retries before stopping waiting and the second value indicates the time interval to wait @@ -218,16 +227,18 @@ def copy_files(self, machine: Machine, guest_to_host: Dict[str, io.IOBase]) -> N raise NotImplementedError("You must implement `copy_files` method.") @abstractmethod - def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_name: str = None, - all_users: bool = False) -> Any: + def get_machine_api_object(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> Any: """Return the corresponding API object of a running device in a network scenario. Args: machine_name (str): The name of the device. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, return information about devices of all users. Returns: @@ -240,13 +251,17 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na raise NotImplementedError("You must implement `get_machine_api_object` method.") @abstractmethod - def get_machines_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) \ - -> List[Any]: + def get_machines_api_objects(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> List[Any]: """Return API objects of running devices. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. all_users (bool): If True, return information about devices of all users. Returns: @@ -255,16 +270,18 @@ def get_machines_api_objects(self, lab_hash: str = None, lab_name: str = None, a raise NotImplementedError("You must implement `get_machines_api_objects` method.") @abstractmethod - def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: str = None, - all_users: bool = False) -> Any: + def get_link_api_object(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> Any: """Return the corresponding API object of a collision domain in a network scenario. Args: link_name (str): The name of the collision domain. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, return information about collision domains of all users. Returns: @@ -277,12 +294,17 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st raise NotImplementedError("You must implement `get_link_api_object` method.") @abstractmethod - def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> List[Any]: + def get_links_api_objects(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> List[Any]: """Return API objects of collision domains in a network scenario. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. all_users (bool): If True, return information about collision domains of all users. Returns: @@ -291,12 +313,14 @@ def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_ raise NotImplementedError("You must implement `get_links_api_objects` method.") @abstractmethod - def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab: + def get_lab_from_api(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None) -> Lab: """Return the network scenario (specified by the hash or name), building it from API objects. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name. If None, lab_name should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash. If None, lab_hash should be set. Returns: Lab: The built network scenario. @@ -316,13 +340,18 @@ def update_lab_from_api(self, lab: Lab) -> None: raise NotImplementedError("You must implement `update_lab_from_api` method.") @abstractmethod - def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine_name: str = None, - all_users: bool = False) -> Generator[Dict[str, IMachineStats], None, None]: + def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, machine_name: str = None, all_users: bool = False) \ + -> Generator[Dict[str, IMachineStats], None, None]: """Return information about the running devices. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. machine_name (str): If specified return all the devices with machine_name. all_users (bool): If True, return information about the device of all users. @@ -333,16 +362,18 @@ def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine raise NotImplementedError("You must implement `get_machines_stats` method.") @abstractmethod - def get_machine_stats(self, machine_name: str, lab_hash: str = None, - lab_name: str = None, all_users: bool = False) -> Generator[IMachineStats, None, None]: + def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> Generator[IMachineStats, None, None]: """Return information of the specified device in a specified network scenario. Args: machine_name (str): The device name. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, search the device among all the users devices. Returns: @@ -354,13 +385,17 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None, raise NotImplementedError("You must implement `get_machine_stats` method.") @abstractmethod - def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: str = None, - all_users: bool = False) -> Generator[Dict[str, ILinkStats], None, None]: + def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, lab: Optional[Lab] = None, + link_name: str = None, all_users: bool = False) -> Generator[Dict[str, ILinkStats], None, None]: """Return information about deployed networks. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. link_name (str): If specified return all the networks with link_name. all_users (bool): If True, return information about the networks of all users. @@ -371,17 +406,19 @@ def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: raise NotImplementedError("You must implement `get_links_stats` method.") @abstractmethod - def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ - Generator[ILinkStats, None, None]: + def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> Generator[ILinkStats, None, None]: """Return information of the specified deployed network in a specified network scenario. Args: - link_name (str): If specified return all the networks with link_name. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. - If None, lab_hash should be set. - all_users (bool): If True, return information about the networks of all users. + link_name (str): If specified return all the networks with link_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. + all_users (bool): If True, return information about the networks of all users. Returns: Generator[Dict[str, ILinkStats], None, None]: A generator containing dicts that has API Object diff --git a/src/Kathara/manager/Kathara.py b/src/Kathara/manager/Kathara.py index f86412bd..3d1193e3 100644 --- a/src/Kathara/manager/Kathara.py +++ b/src/Kathara/manager/Kathara.py @@ -183,13 +183,18 @@ def wipe(self, all_users: bool = False) -> None: self.manager.wipe(all_users) def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, - shell: str = None, logs: bool = False, wait: Union[bool, Tuple[int, float]] = True) -> None: + lab: Optional[Lab] = None, shell: str = None, logs: bool = False, + wait: Union[bool, Tuple[int, float]] = True) -> None: """Connect to a device in a running network scenario, using the specified shell. Args: machine_name (str): The name of the device to connect. - lab_hash (str): The hash of the network scenario where the device is deployed. - lab_name (str): The name of the network scenario where the device is deployed. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. shell (str): The name of the shell to use for connecting. logs (bool): If True, print startup logs on stdout. wait (Union[bool, Tuple[int, float]]): If True, wait indefinitely until the end of the startup commands @@ -203,18 +208,22 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam Raises: InvocationError: If a running network scenario hash or name is not specified. """ - self.manager.connect_tty(machine_name, lab_hash, lab_name, shell, logs, wait) + self.manager.connect_tty(machine_name, lab_hash, lab_name, lab, shell, logs, wait) def exec(self, machine_name: str, command: Union[List[str], str], lab_hash: Optional[str] = None, - lab_name: Optional[str] = None, wait: Union[bool, Tuple[int, float]] = False) \ + lab_name: Optional[str] = None, lab: Optional[Lab] = None, wait: Union[bool, Tuple[int, float]] = False) \ -> Generator[Tuple[bytes, bytes], None, None]: """Exec a command on a device in a running network scenario. Args: machine_name (str): The name of the device to connect. command (Union[List[str], str]): The command to exec on the device. - lab_hash (Optional[str]): The hash of the network scenario where the device is deployed. - lab_name (Optional[str]): The name of the network scenario where the device is deployed. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. wait (Union[bool, Tuple[int, float]]): If True, wait indefinitely until the end of the startup commands execution before executing the command. If a tuple is provided, the first value indicates the number of retries before stopping waiting and the second value indicates the time interval to wait @@ -226,7 +235,7 @@ def exec(self, machine_name: str, command: Union[List[str], str], lab_hash: Opti Raises: InvocationError: If a running network scenario hash or name is not specified. """ - return self.manager.exec(machine_name, command, lab_hash, lab_name, wait) + return self.manager.exec(machine_name, command, lab_hash, lab_name, lab, wait) def copy_files(self, machine: Machine, guest_to_host: Dict[str, io.IOBase]) -> None: """Copy files on a running device in the specified paths. @@ -241,16 +250,18 @@ def copy_files(self, machine: Machine, guest_to_host: Dict[str, io.IOBase]) -> N """ self.manager.copy_files(machine, guest_to_host) - def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_name: str = None, - all_users: bool = False) -> Any: + def get_machine_api_object(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> Any: """Return the corresponding API object of a running device in a network scenario. Args: machine_name (str): The name of the device. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, return information about devices of all users. Returns: @@ -260,32 +271,38 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na InvocationError: If a running network scenario hash or name is not specified. MachineNotFoundError: If the specified device is not found. """ - return self.manager.get_machine_api_object(machine_name, lab_hash, lab_name, all_users) + return self.manager.get_machine_api_object(machine_name, lab_hash, lab_name, lab, all_users) - def get_machines_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) \ - -> List[Any]: + def get_machines_api_objects(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> List[Any]: """Return API objects of running devices. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. all_users (bool): If True, return information about devices of all users. Returns: List[Any]: API objects of devices, specific for the current manager. """ - return self.manager.get_machines_api_objects(lab_hash, lab_name, all_users) + return self.manager.get_machines_api_objects(lab_hash, lab_name, lab, all_users) - def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: str = None, - all_users: bool = False) -> Any: + def get_link_api_object(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> Any: """Return the corresponding API object of a collision domain in a network scenario. Args: link_name (str): The name of the collision domain. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, return information about collision domains of all users. Returns: @@ -295,27 +312,34 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st InvocationError: If a running network scenario hash or name is not specified. LinkNotFoundError: If the collision domain is not found. """ - return self.manager.get_link_api_object(link_name, lab_hash, lab_name, all_users) + return self.manager.get_link_api_object(link_name, lab_hash, lab_name, lab, all_users) - def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> List[Any]: + def get_links_api_objects(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> List[Any]: """Return API objects of collision domains in a network scenario. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. all_users (bool): If True, return information about collision domains of all users. Returns: List[Any]: API objects of collision domains, specific for the current manager. """ - return self.manager.get_links_api_objects(lab_hash, lab_name, all_users) + return self.manager.get_links_api_objects(lab_hash, lab_name, lab, all_users) - def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab: + def get_lab_from_api(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None) -> Lab: """Return the network scenario (specified by the hash or name), building it from API objects. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name. If None, lab_name should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash. If None, lab_hash should be set. Returns: Lab: The built network scenario. @@ -333,13 +357,18 @@ def update_lab_from_api(self, lab: Lab) -> None: """ self.manager.update_lab_from_api(lab) - def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine_name: str = None, - all_users: bool = False) -> Generator[Dict[str, IMachineStats], None, None]: + def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, machine_name: str = None, all_users: bool = False) \ + -> Generator[Dict[str, IMachineStats], None, None]: """Return information about the running devices. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. machine_name (str): If specified return all the devices with machine_name. all_users (bool): If True, return information about the device of all users. @@ -347,18 +376,20 @@ def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine Generator[Dict[str, IMachineStats], None, None]: A generator containing dicts that has API Object identifier as keys and IMachineStats objects as values. """ - return self.manager.get_machines_stats(lab_hash, lab_name, machine_name, all_users) + return self.manager.get_machines_stats(lab_hash, lab_name, lab, machine_name, all_users) - def get_machine_stats(self, machine_name: str, lab_hash: str = None, - lab_name: str = None, all_users: bool = False) -> Generator[IMachineStats, None, None]: + def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> Generator[IMachineStats, None, None]: """Return information of the specified device in a specified network scenario. Args: machine_name (str): The device name. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, search the device among all the users devices. Returns: @@ -367,15 +398,19 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None, Raises: InvocationError: If a running network scenario hash or name is not specified. """ - return self.manager.get_machine_stats(machine_name, lab_hash, lab_name, all_users) + return self.manager.get_machine_stats(machine_name, lab_hash, lab_name, lab, all_users) - def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: str = None, - all_users: bool = False) -> Generator[Dict[str, ILinkStats], None, None]: + def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, lab: Optional[Lab] = None, + link_name: str = None, all_users: bool = False) -> Generator[Dict[str, ILinkStats], None, None]: """Return information about deployed networks. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. link_name (str): If specified return all the networks with link_name. all_users (bool): If True, return information about the networks of all users. @@ -383,19 +418,21 @@ def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: Generator[Dict[str, ILinkStats], None, None]: A generator containing dicts that has API Object identifier as keys and ILinksStats objects as values. """ - return self.manager.get_links_stats(lab_hash, lab_name, link_name, all_users) + return self.manager.get_links_stats(lab_hash, lab_name, lab, link_name, all_users) - def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ - Generator[ILinkStats, None, None]: + def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> Generator[ILinkStats, None, None]: """Return information of the specified deployed network in a specified network scenario. Args: - link_name (str): If specified return all the networks with link_name. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. - If None, lab_hash should be set. - all_users (bool): If True, return information about the networks of all users. + link_name (str): If specified return all the networks with link_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. + all_users (bool): If True, return information about the networks of all users. Returns: Generator[Dict[str, ILinkStats], None, None]: A generator containing dicts that has API Object @@ -404,7 +441,7 @@ def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = N Raises: InvocationError: If a running network scenario hash or name is not specified. """ - return self.manager.get_link_stats(link_name, lab_hash, lab_name, all_users) + return self.manager.get_link_stats(link_name, lab_hash, lab_name, lab, all_users) def check_image(self, image_name: str) -> None: """Check if the specified image is valid. diff --git a/src/Kathara/manager/docker/DockerManager.py b/src/Kathara/manager/docker/DockerManager.py index 62b18294..2700a7e8 100644 --- a/src/Kathara/manager/docker/DockerManager.py +++ b/src/Kathara/manager/docker/DockerManager.py @@ -295,13 +295,18 @@ def wipe(self, all_users: bool = False) -> None: @privileged def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, - shell: str = None, logs: bool = False, wait: Union[bool, Tuple[int, float]] = True) -> None: + lab: Optional[Lab] = None, shell: str = None, logs: bool = False, + wait: Union[bool, Tuple[int, float]] = True) -> None: """Connect to a device in a running network scenario, using the specified shell. Args: machine_name (str): The name of the device to connect. - lab_hash (str): The hash of the network scenario where the device is deployed. - lab_name (str): The name of the network scenario where the device is deployed. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. shell (str): The name of the shell to use for connecting. logs (bool): If True, print startup logs on stdout. wait (Union[bool, Tuple[int, float]]): If True, wait indefinitely until the end of the startup commands @@ -315,10 +320,12 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) user_name = utils.get_current_user_name() @@ -333,15 +340,19 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam @privileged def exec(self, machine_name: str, command: Union[List[str], str], lab_hash: Optional[str] = None, - lab_name: Optional[str] = None, wait: Union[bool, Tuple[int, float]] = False) \ + lab_name: Optional[str] = None, lab: Optional[Lab] = None, wait: Union[bool, Tuple[int, float]] = False) \ -> Generator[Tuple[bytes, bytes], None, None]: """Exec a command on a device in a running network scenario. Args: machine_name (str): The name of the device to connect. command (Union[List[str], str]): The command to exec on the device. - lab_hash (Optional[str]): The hash of the network scenario where the device is deployed. - lab_name (Optional[str]): The name of the network scenario where the device is deployed. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. wait (Union[bool, Tuple[int, float]]): If True, wait indefinitely until the end of the startup commands execution before executing the command. If a tuple is provided, the first value indicates the number of retries before stopping waiting and the second value indicates the time interval to wait @@ -353,13 +364,15 @@ def exec(self, machine_name: str, command: Union[List[str], str], lab_hash: Opti Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - user_name = utils.get_current_user_name() - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) + user_name = utils.get_current_user_name() return self.docker_machine.exec(lab_hash, machine_name, command, user=user_name, tty=False, wait=wait) @privileged @@ -382,16 +395,19 @@ def copy_files(self, machine: Machine, guest_to_host: Dict[str, io.IOBase]) -> N ) @privileged - def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_name: str = None, - all_users: bool = False) -> docker.models.containers.Container: + def get_machine_api_object(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) \ + -> docker.models.containers.Container: """Return the corresponding API object of a running device in a network scenario. Args: machine_name (str): The name of the device. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, return information about devices of all users. Returns: @@ -401,13 +417,15 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na InvocationError: If a running network scenario hash or name is not specified. MachineNotFoundError: If the specified device is not found. """ - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - user_name = utils.get_current_user_name() if not all_users else None - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) + user_name = utils.get_current_user_name() if not all_users else None containers = self.docker_machine.get_machines_api_objects_by_filters( lab_hash=lab_hash, machine_name=machine_name, user=user_name ) @@ -416,34 +434,45 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na raise MachineNotFoundError(f"Device `{machine_name}` not found.") - def get_machines_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ - List[docker.models.containers.Container]: + @privileged + def get_machines_api_objects(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) \ + -> List[docker.models.containers.Container]: """Return API objects of running devices. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. all_users (bool): If True, return information about devices of all users. Returns: List[docker.models.containers.Container]: Docker API objects of devices. """ - user_name = utils.get_current_user_name() if not all_users else None - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) + user_name = utils.get_current_user_name() if not all_users else None return self.docker_machine.get_machines_api_objects_by_filters(lab_hash=lab_hash, user=user_name) - def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: str = None, - all_users: bool = False) -> docker.models.networks.Network: + @privileged + def get_link_api_object(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> docker.models.networks.Network: """Return the corresponding API object of a collision domain in a network scenario. Args: link_name (str): The name of the collision domain. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, return information about collision domains of all users. Returns: @@ -453,13 +482,15 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st InvocationError: If a running network scenario hash or name is not specified. LinkNotFoundError: If the collision domain is not found. """ - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - user_name = utils.get_current_user_name() if not all_users else None - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) + user_name = utils.get_current_user_name() if not all_users else None networks = self.docker_link.get_links_api_objects_by_filters( lab_hash=lab_hash, link_name=link_name, user=user_name ) @@ -468,31 +499,41 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st raise LinkNotFoundError(f"Collision Domain `{link_name}` not found.") - def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ - List[docker.models.networks.Network]: + @privileged + def get_links_api_objects(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) \ + -> List[docker.models.networks.Network]: """Return API objects of collision domains in a network scenario. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. all_users (bool): If True, return information about collision domains of all users. Returns: List[docker.models.networks.Network]: Docker API objects of networks. """ - user_name = utils.get_current_user_name() if not all_users else None - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) + user_name = utils.get_current_user_name() if not all_users else None return self.docker_link.get_links_api_objects_by_filters(lab_hash=lab_hash, user=user_name) @privileged - def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab: + def get_lab_from_api(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None) -> Lab: """Return the network scenario (specified by the hash or name), building it from API objects. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name. If None, lab_name should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash. If None, lab_hash should be set. Returns: Lab: The built network scenario. @@ -607,13 +648,18 @@ def update_lab_from_api(self, lab: Lab) -> None: device.remove_interface(link) @privileged - def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine_name: str = None, - all_users: bool = False) -> Generator[Dict[str, DockerMachineStats], None, None]: + def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, machine_name: str = None, all_users: bool = False) \ + -> Generator[Dict[str, DockerMachineStats], None, None]: """Return information about the running devices. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. machine_name (str): If specified return all the devices with machine_name. all_users (bool): If True, return information about the device of all users. @@ -621,24 +667,29 @@ def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine Generator[Dict[str, DockerMachineStats], None, None]: A generator containing dicts that has API Object identifier as keys and DockerMachineStats objects as values. """ - user_name = utils.get_current_user_name() if not all_users else None - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) + user_name = utils.get_current_user_name() if not all_users else None return self.docker_machine.get_machines_stats(lab_hash=lab_hash, machine_name=machine_name, user=user_name) @privileged - def get_machine_stats(self, machine_name: str, lab_hash: str = None, - lab_name: str = None, all_users: bool = False) -> Generator[DockerMachineStats, None, None]: + def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) \ + -> Generator[DockerMachineStats, None, None]: """Return information of the specified device in a specified network scenario. - Args: + Args: machine_name (str): The device name. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, search the device among all the users devices. Returns: @@ -648,10 +699,12 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None, Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) machines_stats = self.get_machines_stats(lab_hash=lab_hash, machine_name=machine_name, all_users=all_users) @@ -659,13 +712,19 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None, yield machine_stats - def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: str = None, - all_users: bool = False) -> Generator[Dict[str, DockerLinkStats], None, None]: - """Return information about deployed Docker networks. + @privileged + def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, lab: Optional[Lab] = None, + link_name: str = None, all_users: bool = False) \ + -> Generator[Dict[str, DockerLinkStats], None, None]: + """Return information about deployed networks. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. link_name (str): If specified return all the networks with link_name. all_users (bool): If True, return information about the networks of all users. @@ -673,23 +732,28 @@ def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: Generator[Dict[str, DockerLinkStats], None, None]: A generator containing dicts that has API Object identifier as keys and DockerLinksStats objects as values. """ - user_name = utils.get_current_user_name() if not all_users else None - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) + user_name = utils.get_current_user_name() if not all_users else None return self.docker_link.get_links_stats(lab_hash=lab_hash, link_name=link_name, user=user_name) - def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ - Generator[DockerLinkStats, None, None]: + @privileged + def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> Generator[DockerLinkStats, None, None]: """Return information of the specified deployed network in a specified network scenario. Args: - link_name (str): The link name. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. - If None, lab_hash should be set. - all_users (bool): If True, search the network among all the users networks. + link_name (str): If specified return all the networks with link_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. + all_users (bool): If True, return information about the networks of all users. Returns: Generator[DockerLinkStats, None, None]: A generator containing DockerLinkStats objects with the network @@ -698,10 +762,12 @@ def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = N Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) links_stats = self.get_links_stats(lab_hash=lab_hash, link_name=link_name, all_users=all_users) diff --git a/src/Kathara/manager/kubernetes/KubernetesManager.py b/src/Kathara/manager/kubernetes/KubernetesManager.py index 9fe81efa..36123378 100644 --- a/src/Kathara/manager/kubernetes/KubernetesManager.py +++ b/src/Kathara/manager/kubernetes/KubernetesManager.py @@ -301,19 +301,24 @@ def wipe(self, all_users: bool = False) -> None: self.k8s_namespace.wipe() def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, - shell: str = None, logs: bool = False, wait: Union[bool, Tuple[int, float]] = True) -> None: + lab: Optional[Lab] = None, shell: str = None, logs: bool = False, + wait: Union[bool, Tuple[int, float]] = True) -> None: """Connect to a device in a running network scenario, using the specified shell. Args: machine_name (str): The name of the device to connect. - lab_hash (str): The hash of the network scenario where the device is deployed. - lab_name (str): The name of the network scenario where the device is deployed. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. shell (str): The name of the shell to use for connecting. logs (bool): If True, print startup logs on stdout. wait (Union[bool, Tuple[int, float]]): If True, wait indefinitely until the end of the startup commands execution before connecting. If a tuple is provided, the first value indicates the number of retries before stopping waiting and the second value indicates the time interval to wait for each retry. - Default is True. No effect on Kubernetes. + Default is True. Returns: None @@ -321,10 +326,12 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) lab_hash = lab_hash.lower() @@ -336,19 +343,23 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam ) def exec(self, machine_name: str, command: Union[List[str], str], lab_hash: Optional[str] = None, - lab_name: Optional[str] = None, wait: Union[bool, Tuple[int, float]] = False) \ + lab_name: Optional[str] = None, lab: Optional[Lab] = None, wait: Union[bool, Tuple[int, float]] = False) \ -> Generator[Tuple[bytes, bytes], None, None]: """Exec a command on a device in a running network scenario. Args: machine_name (str): The name of the device to connect. command (Union[List[str], str]): The command to exec on the device. - lab_hash (Optional[str]): The hash of the network scenario where the device is deployed. - lab_name (Optional[str]): The name of the network scenario where the device is deployed. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. wait (Union[bool, Tuple[int, float]]): If True, wait indefinitely until the end of the startup commands execution before executing the command. If a tuple is provided, the first value indicates the number of retries before stopping waiting and the second value indicates the time interval to wait - for each retry. Default is False. No effect on Kubernetes. + for each retry. Default is False. Returns: Generator[Tuple[bytes, bytes]]: A generator of tuples containing the stdout and stderr in bytes. @@ -356,15 +367,17 @@ def exec(self, machine_name: str, command: Union[List[str], str], lab_hash: Opti Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") + + if lab: + lab_hash = lab.hash + elif lab_name: + lab_hash = utils.generate_urlsafe_hash(lab_name) if wait: logging.warning("Wait option has no effect on Megalos.") - if lab_name: - lab_hash = utils.generate_urlsafe_hash(lab_name) - lab_hash = lab_hash.lower() return self.k8s_machine.exec(lab_hash, machine_name, command, stderr=True, tty=False, is_stream=True) @@ -384,16 +397,18 @@ def copy_files(self, machine: Machine, guest_to_host: Dict[str, io.IOBase]) -> N self.k8s_machine.copy_files(machine.api_object, path="/", tar_data=tar_data) - def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_name: str = None, - all_users: bool = False) -> client.V1Pod: + def get_machine_api_object(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> client.V1Pod: """Return the corresponding API object of a running device in a network scenario. Args: machine_name (str): The name of the device. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, return information about devices of all users. Returns: @@ -406,10 +421,12 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na if all_users: logging.warning("User-specific options have no effect on Megalos.") - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) lab_hash = lab_hash.lower() @@ -420,13 +437,17 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na raise MachineNotFoundError(f"Device {machine_name} not found.") - def get_machines_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ - List[client.V1Pod]: + def get_machines_api_objects(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> List[client.V1Pod]: """Return API objects of running devices. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. all_users (bool): If True, return information about devices of all users. Returns: @@ -435,23 +456,27 @@ def get_machines_api_objects(self, lab_hash: str = None, lab_name: str = None, a if all_users: logging.warning("User-specific options have no effect on Megalos.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) lab_hash = lab_hash.lower() if lab_hash else None return self.k8s_machine.get_machines_api_objects_by_filters(lab_hash=lab_hash) - def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: str = None, - all_users: bool = False) -> Any: + def get_link_api_object(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> Any: """Return the corresponding API object of a collision domain in a network scenario. Args: link_name (str): The name of the collision domain. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, return information about collision domains of all users. Returns: @@ -464,10 +489,12 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st if all_users: logging.warning("User-specific options have no effect on Megalos.") - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) lab_hash = lab_hash.lower() @@ -478,13 +505,17 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st raise LinkNotFoundError(f"Collision Domain {link_name} not found.") - def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ - List[Any]: + def get_links_api_objects(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) -> List[Any]: """Return API objects of collision domains in a network scenario. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. all_users (bool): If True, return information about collision domains of all users. Returns: @@ -493,37 +524,15 @@ def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_ if all_users: logging.warning("User-specific options have no effect on Megalos.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) lab_hash = lab_hash.lower() if lab_hash else None return self.k8s_link.get_links_api_objects_by_filters(lab_hash=lab_hash) - def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine_name: str = None, - all_users: bool = False) -> Generator[Dict[str, KubernetesMachineStats], None, None]: - """Return information about the running devices. - - Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. - machine_name (str): If specified return all the devices with machine_name. - all_users (bool): If True, return information about the device of all users. - - Returns: - Generator[Dict[str, KubernetesMachineStats], None, None]: A generator containing dicts that has API Object - identifier as keys and KubernetesMachineStats objects as values. - """ - if all_users: - logging.warning("User-specific options have no effect on Megalos.") - - if lab_name: - lab_hash = utils.generate_urlsafe_hash(lab_name) - - lab_hash = lab_hash.lower() if lab_hash else None - - return self.k8s_machine.get_machines_stats(lab_hash=lab_hash, machine_name=machine_name) - def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab: """Return the network scenario (specified by the hash or name), building it from API objects. @@ -597,16 +606,50 @@ def update_lab_from_api(self, lab: Lab) -> None: """ raise NotSupportedError("Unable to update a running network scenario.") - def get_machine_stats(self, machine_name: str, lab_hash: str = None, lab_name: str = None, - all_users: bool = False) -> Generator[KubernetesMachineStats, None, None]: + def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, machine_name: str = None, all_users: bool = False) \ + -> Generator[Dict[str, KubernetesMachineStats], None, None]: + """Return information about the running devices. + + Args: + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. + machine_name (str): If specified return all the devices with machine_name. + all_users (bool): If True, return information about the device of all users. + + Returns: + Generator[Dict[str, KubernetesMachineStats], None, None]: A generator containing dicts that has API Object + identifier as keys and KubernetesMachineStats objects as values. + """ + if all_users: + logging.warning("User-specific options have no effect on Megalos.") + + if lab: + lab_hash = lab.hash + elif lab_name: + lab_hash = utils.generate_urlsafe_hash(lab_name) + + lab_hash = lab_hash.lower() if lab_hash else None + + return self.k8s_machine.get_machines_stats(lab_hash=lab_hash, machine_name=machine_name) + + def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) \ + -> Generator[KubernetesMachineStats, None, None]: """Return information of the specified device in a specified network scenario. Args: machine_name (str): The device name. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. - If None, lab_hash should be set. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. all_users (bool): If True, search the device among all the users devices. Returns: @@ -615,10 +658,12 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None, lab_name: s Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) lab_hash = lab_hash.lower() @@ -628,13 +673,18 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None, lab_name: s yield machine_stats - def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: str = None, - all_users: bool = False) -> Generator[Dict[str, KubernetesLinkStats], None, None]: - """Return information about deployed Kubernetes networks. + def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, lab: Optional[Lab] = None, + link_name: str = None, all_users: bool = False) \ + -> Generator[Dict[str, KubernetesLinkStats], None, None]: + """Return information about deployed networks. Args: - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. link_name (str): If specified return all the networks with link_name. all_users (bool): If True, return information about the networks of all users. @@ -645,24 +695,29 @@ def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: if all_users: logging.warning("User-specific options have no effect on Megalos.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) lab_hash = lab_hash.lower() if lab_hash else None return self.k8s_link.get_links_stats(lab_hash=lab_hash, link_name=link_name) - def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \ - Generator[KubernetesLinkStats, None, None]: + def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, + lab: Optional[Lab] = None, all_users: bool = False) \ + -> Generator[KubernetesLinkStats, None, None]: """Return information of the specified deployed network in a specified network scenario. Args: - link_name (str): The link name. - lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name. - If None, lab_name should be set. - lab_name (str): The name of the network scenario. Can be used as an alternative to lab_hash. - If None, lab_hash should be set. - all_users (bool): If True, search the network among all the users networks. + link_name (str): If specified return all the networks with link_name. + lab_hash (Optional[str]): The hash of the network scenario. + Can be used as an alternative to lab_name and lab. If None, lab_name or lab should be set. + lab_name (Optional[str]): The name of the network scenario. + Can be used as an alternative to lab_hash and lab. If None, lab_hash or lab should be set. + lab (Optional[Kathara.model.Lab]): The network scenario object. + Can be used as an alternative to lab_hash and lab_name. If None, lab_hash or lab_name should be set. + all_users (bool): If True, return information about the networks of all users. Returns: Generator[KubernetesLinkStats, None, None]: A generator containing KubernetesLinkStats objects with @@ -671,10 +726,12 @@ def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = N Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name: - raise InvocationError("You must specify a running network scenario hash or name.") + if not lab_hash and not lab_name and not lab: + raise InvocationError("You must specify a running network scenario hash, name or object.") - if lab_name: + if lab: + lab_hash = lab.hash + elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) lab_hash = lab_hash.lower() diff --git a/tests/manager/docker/docker_manager_test.py b/tests/manager/docker/docker_manager_test.py index a65c7cd7..1d9388a5 100644 --- a/tests/manager/docker/docker_manager_test.py +++ b/tests/manager/docker/docker_manager_test.py @@ -548,6 +548,23 @@ def test_connect_tty_lab_name(mock_connect, mock_get_current_user_name, docker_m wait=True) +@mock.patch("src.Kathara.utils.get_current_user_name") +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.connect") +def test_connect_tty_lab_obj(mock_connect, mock_get_current_user_name, docker_manager, default_device, + two_device_scenario): + mock_get_current_user_name.return_value = "kathara_user" + + docker_manager.connect_tty(default_device.name, + lab=two_device_scenario) + + mock_connect.assert_called_once_with(lab_hash=two_device_scenario.hash, + machine_name=default_device.name, + user="kathara_user", + shell=None, + logs=False, + wait=True) + + @mock.patch("src.Kathara.utils.get_current_user_name") @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.connect") def test_connect_tty_with_custom_shell(mock_connect, mock_get_current_user_name, docker_manager, default_device): @@ -630,6 +647,23 @@ def test_exec_lab_name(mock_exec, mock_get_current_user_name, docker_manager, de ) +@mock.patch("src.Kathara.utils.get_current_user_name") +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.exec") +def test_exec_lab_obj(mock_exec, mock_get_current_user_name, docker_manager, default_device, two_device_scenario): + mock_get_current_user_name.return_value = "kathara_user" + + docker_manager.exec(default_device.name, ["test", "command"], lab=two_device_scenario) + + mock_exec.assert_called_once_with( + two_device_scenario.hash, + default_device.name, + ["test", "command"], + user="kathara_user", + tty=False, + wait=False + ) + + @mock.patch("src.Kathara.utils.get_current_user_name") @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.exec") def test_exec_wait(mock_exec, mock_get_current_user_name, docker_manager, default_device): @@ -699,8 +733,28 @@ def test_get_machine_api_object_lab_name_no_user(mock_get_machines_api_objects, machine_name="test_device", user=None) +@mock.patch("src.Kathara.utils.get_current_user_name") +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters") +def test_get_machine_api_object_lab_obj_user(mock_get_machines_api_objects, mock_get_current_user_name, + docker_manager, default_device, two_device_scenario): + mock_get_machines_api_objects.return_value = [default_device.api_object] + mock_get_current_user_name.return_value = "kathara_user" + docker_manager.get_machine_api_object("test_device", lab=two_device_scenario, all_users=False) + mock_get_machines_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash, + machine_name="test_device", user="kathara_user") + + +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters") +def test_get_machine_api_object_lab_obj_no_user(mock_get_machines_api_objects, docker_manager, default_device, + two_device_scenario): + mock_get_machines_api_objects.return_value = [default_device.api_object] + docker_manager.get_machine_api_object("test_device", lab=two_device_scenario, all_users=True) + mock_get_machines_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash, + machine_name="test_device", user=None) + + @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters") -def test_get_machine_api_object_no_name_no_hash(mock_get_machines_api_objects, docker_manager, default_device): +def test_get_machine_api_object_invocation_error(mock_get_machines_api_objects, docker_manager, default_device): with pytest.raises(InvocationError): docker_manager.get_machine_api_object("test_device", all_users=True) assert not mock_get_machines_api_objects.called @@ -753,8 +807,27 @@ def test_get_machines_api_objects_lab_name_no_user(mock_get_machines_api_objects mock_get_machines_api_objects.assert_called_once_with(lab_hash=generate_urlsafe_hash("lab_name"), user=None) +@mock.patch("src.Kathara.utils.get_current_user_name") +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters") +def test_get_machines_api_objects_lab_obj_user(mock_get_machines_api_objects, mock_get_current_user_name, + docker_manager, default_device, two_device_scenario): + mock_get_machines_api_objects.return_value = [default_device.api_object] + mock_get_current_user_name.return_value = "kathara_user" + docker_manager.get_machines_api_objects(lab=two_device_scenario, all_users=False) + mock_get_machines_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash, + user="kathara_user") + + @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters") -def test_get_machines_api_objects_no_name_no_hash(mock_get_machines_api_objects, docker_manager): +def test_get_machines_api_objects_lab_obj_no_user(mock_get_machines_api_objects, docker_manager, default_device, + two_device_scenario): + mock_get_machines_api_objects.return_value = [default_device.api_object] + docker_manager.get_machines_api_objects(lab=two_device_scenario, all_users=True) + mock_get_machines_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash, user=None) + + +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters") +def test_get_machines_api_objects_no_labs(mock_get_machines_api_objects, docker_manager): docker_manager.get_machines_api_objects(all_users=True) mock_get_machines_api_objects.assert_called_once_with(lab_hash=None, user=None) @@ -810,8 +883,28 @@ def test_get_link_api_object_lab_name_no_user(mock_get_links_api_objects, link_name="test_link", user=None) +@mock.patch("src.Kathara.utils.get_current_user_name") @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters") -def test_get_link_api_object_no_name_no_hash(mock_get_links_api_objects, docker_manager): +def test_get_link_api_object_lab_obj_user(mock_get_links_api_objects, mock_get_current_user_name, + docker_manager, docker_network, two_device_scenario): + mock_get_links_api_objects.return_value = [docker_network] + mock_get_current_user_name.return_value = "kathara_user" + docker_manager.get_link_api_object("test_link", lab=two_device_scenario, all_users=False) + mock_get_links_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash, + link_name="test_link", user="kathara_user") + + +@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters") +def test_get_link_api_object_lab_obj_no_user(mock_get_links_api_objects, docker_manager, docker_network, + two_device_scenario): + mock_get_links_api_objects.return_value = [docker_network] + docker_manager.get_link_api_object("test_link", lab=two_device_scenario, all_users=True) + mock_get_links_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash, + link_name="test_link", user=None) + + +@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters") +def test_get_link_api_object_invocation_error(mock_get_links_api_objects, docker_manager): with pytest.raises(InvocationError): docker_manager.get_link_api_object("test_link", all_users=True) assert not mock_get_links_api_objects.called @@ -868,8 +961,28 @@ def test_get_links_api_objects_lab_name_no_user(mock_get_links_api_objects, user=None) +@mock.patch("src.Kathara.utils.get_current_user_name") +@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters") +def test_get_links_api_objects_lab_obj_user(mock_get_links_api_objects, mock_get_current_user_name, + docker_manager, docker_network, two_device_scenario): + mock_get_links_api_objects.return_value = [docker_network] + mock_get_current_user_name.return_value = "kathara_user" + docker_manager.get_links_api_objects(lab=two_device_scenario, all_users=False) + mock_get_links_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash, + user="kathara_user") + + +@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters") +def test_get_links_api_objects_lab_obj_no_user(mock_get_links_api_objects, docker_manager, docker_network, + two_device_scenario): + mock_get_links_api_objects.return_value = [docker_network] + docker_manager.get_links_api_objects(lab=two_device_scenario, all_users=True) + mock_get_links_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash, + user=None) + + @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters") -def test_get_links_api_objects_no_name_no_hash(mock_get_links_api_objects, docker_manager): +def test_get_links_api_objects_no_labs(mock_get_links_api_objects, docker_manager): docker_manager.get_links_api_objects(all_users=True) mock_get_links_api_objects.assert_called_once_with(lab_hash=None, user=None) @@ -1030,6 +1143,14 @@ def test_get_machines_stats_lab_hash_no_user(mock_get_machines_stats, docker_man user=None) +@mock.patch("src.Kathara.utils.get_current_user_name") +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_stats") +def test_get_machines_stats_lab_hash_user(mock_get_machines_stats, mock_get_current_user_name, docker_manager): + mock_get_current_user_name.return_value = "kathara-user" + docker_manager.get_machines_stats(lab_hash="lab_hash") + mock_get_machines_stats.assert_called_once_with(lab_hash="lab_hash", machine_name=None, user="kathara-user") + + @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_stats") def test_get_machines_stats_lab_name_no_user(mock_get_machines_stats, docker_manager): docker_manager.get_machines_stats(lab_name="lab_name", all_users=True) @@ -1039,14 +1160,32 @@ def test_get_machines_stats_lab_name_no_user(mock_get_machines_stats, docker_man @mock.patch("src.Kathara.utils.get_current_user_name") @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_stats") -def test_get_machines_stats_lab_hash_user(mock_get_machines_stats, mock_get_current_user_name, docker_manager): +def test_get_machines_stats_lab_name_user(mock_get_machines_stats, mock_get_current_user_name, docker_manager): mock_get_current_user_name.return_value = "kathara-user" - docker_manager.get_machines_stats(lab_hash="lab_hash") - mock_get_machines_stats.assert_called_once_with(lab_hash="lab_hash", machine_name=None, user="kathara-user") + docker_manager.get_machines_stats(lab_name="lab_name") + mock_get_machines_stats.assert_called_once_with(lab_hash=generate_urlsafe_hash("lab_name"), + machine_name=None, user="kathara-user") @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_stats") -def test_get_machines_stats_no_name_no_hash(mock_get_machines_stats, docker_manager): +def test_get_machines_stats_lab_obj_no_user(mock_get_machines_stats, docker_manager, two_device_scenario): + docker_manager.get_machines_stats(lab=two_device_scenario, all_users=True) + mock_get_machines_stats.assert_called_once_with(lab_hash=two_device_scenario.hash, machine_name=None, + user=None) + + +@mock.patch("src.Kathara.utils.get_current_user_name") +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_stats") +def test_get_machines_stats_lab_obj_user(mock_get_machines_stats, mock_get_current_user_name, docker_manager, + two_device_scenario): + mock_get_current_user_name.return_value = "kathara-user" + docker_manager.get_machines_stats(lab=two_device_scenario) + mock_get_machines_stats.assert_called_once_with(lab_hash=two_device_scenario.hash, + machine_name=None, user="kathara-user") + + +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_stats") +def test_get_machines_stats_no_labs(mock_get_machines_stats, docker_manager): docker_manager.get_machines_stats(all_users=True) mock_get_machines_stats.assert_called_once_with(lab_hash=None, machine_name=None, user=None) @@ -1062,6 +1201,15 @@ def test_get_machine_stats_lab_hash_no_user(mock_get_machines_stats, default_dev all_users=True) +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_stats") +def test_get_machine_stats_lab_hash_user(mock_get_machines_stats, default_device, docker_manager): + mock_get_machines_stats.return_value = iter([{"test_device": DockerMachineStats(default_device.api_object)}]) + next(docker_manager.get_machine_stats(machine_name="test_device", lab_hash="lab_hash")) + mock_get_machines_stats.assert_called_once_with(lab_hash="lab_hash", + machine_name="test_device", + all_users=False) + + @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_stats") def test_get_machine_stats_lab_name_no_user(mock_get_machines_stats, default_device, docker_manager): mock_get_machines_stats.return_value = iter([{"test_device": DockerMachineStats(default_device.api_object)}]) @@ -1072,10 +1220,29 @@ def test_get_machine_stats_lab_name_no_user(mock_get_machines_stats, default_dev @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_stats") -def test_get_machine_stats_lab_hash_user(mock_get_machines_stats, default_device, docker_manager): +def test_get_machine_stats_lab_name_user(mock_get_machines_stats, default_device, docker_manager): mock_get_machines_stats.return_value = iter([{"test_device": DockerMachineStats(default_device.api_object)}]) - next(docker_manager.get_machine_stats(machine_name="test_device", lab_hash="lab_hash")) - mock_get_machines_stats.assert_called_once_with(lab_hash="lab_hash", + next(docker_manager.get_machine_stats(machine_name="test_device", lab_name="lab_name")) + mock_get_machines_stats.assert_called_once_with(lab_hash=generate_urlsafe_hash("lab_name"), + machine_name="test_device", + all_users=False) + + +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_stats") +def test_get_machine_stats_lab_obj_no_user(mock_get_machines_stats, default_device, docker_manager, + two_device_scenario): + mock_get_machines_stats.return_value = iter([{"test_device": DockerMachineStats(default_device.api_object)}]) + next(docker_manager.get_machine_stats(machine_name="test_device", lab=two_device_scenario, all_users=True)) + mock_get_machines_stats.assert_called_once_with(lab_hash=two_device_scenario.hash, + machine_name="test_device", + all_users=True) + + +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_stats") +def test_get_machine_stats_lab_obj_user(mock_get_machines_stats, default_device, docker_manager, two_device_scenario): + mock_get_machines_stats.return_value = iter([{"test_device": DockerMachineStats(default_device.api_object)}]) + next(docker_manager.get_machine_stats(machine_name="test_device", lab=two_device_scenario)) + mock_get_machines_stats.assert_called_once_with(lab_hash=two_device_scenario.hash, machine_name="test_device", all_users=False) @@ -1096,32 +1263,49 @@ def test_get_links_stats_lab_hash_no_user(mock_get_links_stats, docker_manager): mock_get_links_stats.assert_called_once_with(lab_hash="lab_hash", link_name=None, user=None) +@mock.patch("src.Kathara.utils.get_current_user_name") +@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_stats") +def test_get_links_stats_lab_hash_user(mock_get_links_stats, mock_get_current_user_name, docker_manager): + mock_get_current_user_name.return_value = "kathara-user" + docker_manager.get_links_stats(lab_hash="lab_hash") + mock_get_links_stats.assert_called_once_with(lab_hash="lab_hash", link_name=None, user="kathara-user") + + @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_stats") def test_get_links_stats_lab_name_no_user(mock_get_links_stats, docker_manager): docker_manager.get_links_stats(lab_name="lab_name", all_users=True) mock_get_links_stats.assert_called_once_with(lab_hash=generate_urlsafe_hash("lab_name"), link_name=None, user=None) +@mock.patch("src.Kathara.utils.get_current_user_name") @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_stats") -def test_get_links_stats_no_hash_no_user(mock_get_links_stats, docker_manager): - docker_manager.get_links_stats(all_users=True) - mock_get_links_stats.assert_called_once_with(lab_hash=None, link_name=None, user=None) +def test_get_links_stats_lab_name_user(mock_get_links_stats, mock_get_current_user_name, docker_manager): + mock_get_current_user_name.return_value = "kathara-user" + docker_manager.get_links_stats(lab_name="lab_name") + mock_get_links_stats.assert_called_once_with(lab_hash=generate_urlsafe_hash("lab_name"), + link_name=None, user="kathara-user") -@mock.patch("src.Kathara.utils.get_current_user_name") @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_stats") -def test_get_links_stats_no_hash_user(mock_get_links_stats, mock_get_current_user_name, docker_manager): - mock_get_current_user_name.return_value = "kathara-user" - docker_manager.get_links_stats() - mock_get_links_stats.assert_called_once_with(lab_hash=None, link_name=None, user="kathara-user") +def test_get_links_stats_lab_obj_no_user(mock_get_links_stats, docker_manager, two_device_scenario): + docker_manager.get_links_stats(lab=two_device_scenario, all_users=True) + mock_get_links_stats.assert_called_once_with(lab_hash=two_device_scenario.hash, link_name=None, user=None) @mock.patch("src.Kathara.utils.get_current_user_name") @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_stats") -def test_get_links_stats_lab_hash_user(mock_get_links_stats, mock_get_current_user_name, docker_manager): +def test_get_links_stats_lab_obj_user(mock_get_links_stats, mock_get_current_user_name, docker_manager, + two_device_scenario): mock_get_current_user_name.return_value = "kathara-user" - docker_manager.get_links_stats(lab_hash="lab_hash") - mock_get_links_stats.assert_called_once_with(lab_hash="lab_hash", link_name=None, user="kathara-user") + docker_manager.get_links_stats(lab=two_device_scenario) + mock_get_links_stats.assert_called_once_with(lab_hash=two_device_scenario.hash, + link_name=None, user="kathara-user") + + +@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_stats") +def test_get_links_stats_no_labs(mock_get_links_stats, docker_manager): + docker_manager.get_links_stats(all_users=True) + mock_get_links_stats.assert_called_once_with(lab_hash=None, link_name=None, user=None) # @@ -1134,6 +1318,13 @@ def test_get_link_stats_lab_hash_no_user(mock_get_links_stats, docker_network, d mock_get_links_stats.assert_called_once_with(lab_hash="lab_hash", link_name="test_network", all_users=True) +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_stats") +def test_get_link_stats_lab_hash_user(mock_get_links_stats, docker_network, docker_manager): + mock_get_links_stats.return_value = iter([{"test_network": DockerLinkStats(docker_network)}]) + next(docker_manager.get_link_stats(link_name="test_network", lab_hash="lab_hash")) + mock_get_links_stats.assert_called_once_with(lab_hash="lab_hash", link_name="test_network", all_users=False) + + @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_stats") def test_get_link_stats_lab_name_no_user(mock_get_links_stats, docker_network, docker_manager): mock_get_links_stats.return_value = iter([{"test_network": DockerLinkStats(docker_network)}]) @@ -1143,14 +1334,31 @@ def test_get_link_stats_lab_name_no_user(mock_get_links_stats, docker_network, d @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_stats") -def test_get_link_stats_lab_hash_user(mock_get_links_stats, docker_network, docker_manager): +def test_get_link_stats_lab_name_user(mock_get_links_stats, docker_network, docker_manager): mock_get_links_stats.return_value = iter([{"test_network": DockerLinkStats(docker_network)}]) - next(docker_manager.get_link_stats(link_name="test_network", lab_hash="lab_hash")) - mock_get_links_stats.assert_called_once_with(lab_hash="lab_hash", link_name="test_network", all_users=False) + next(docker_manager.get_link_stats(link_name="test_network", lab_name="lab_name")) + mock_get_links_stats.assert_called_once_with(lab_hash=generate_urlsafe_hash("lab_name"), + link_name="test_network", all_users=False) + + +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_stats") +def test_get_link_stats_lab_obj_no_user(mock_get_links_stats, docker_network, docker_manager, two_device_scenario): + mock_get_links_stats.return_value = iter([{"test_network": DockerLinkStats(docker_network)}]) + next(docker_manager.get_link_stats(link_name="test_network", lab=two_device_scenario, all_users=True)) + mock_get_links_stats.assert_called_once_with(lab_hash=two_device_scenario.hash, + link_name="test_network", all_users=True) + + +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_stats") +def test_get_link_stats_lab_obj_user(mock_get_links_stats, docker_network, docker_manager, two_device_scenario): + mock_get_links_stats.return_value = iter([{"test_network": DockerLinkStats(docker_network)}]) + next(docker_manager.get_link_stats(link_name="test_network", lab=two_device_scenario)) + mock_get_links_stats.assert_called_once_with(lab_hash=two_device_scenario.hash, + link_name="test_network", all_users=False) @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_stats") -def test_get_link_stats_no_lab_hash_and_no_name(mock_get_links_stats, docker_network, docker_manager): +def test_get_link_stats_invocation_error(mock_get_links_stats, docker_network, docker_manager): mock_get_links_stats.return_value = iter([{"test_network": DockerLinkStats(docker_network)}]) with pytest.raises(InvocationError): next(docker_manager.get_link_stats(link_name="test_network")) diff --git a/tests/manager/kubernetes/kubernetes_manager_test.py b/tests/manager/kubernetes/kubernetes_manager_test.py index d614ec47..b84c95af 100644 --- a/tests/manager/kubernetes/kubernetes_manager_test.py +++ b/tests/manager/kubernetes/kubernetes_manager_test.py @@ -477,6 +477,18 @@ def test_connect_tty_lab_name(mock_connect, kubernetes_manager, default_device): logs=False) +@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.connect") +def test_connect_tty_lab_obj(mock_connect, kubernetes_manager, default_device, + two_device_scenario): + kubernetes_manager.connect_tty(default_device.name, + lab=two_device_scenario) + + mock_connect.assert_called_once_with(lab_hash=two_device_scenario.hash.lower(), + machine_name=default_device.name, + shell=None, + logs=False) + + @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.connect") def test_connect_tty_custom_shell(mock_connect, kubernetes_manager, default_device): kubernetes_manager.connect_tty(default_device.name, @@ -540,6 +552,20 @@ def test_exec_lab_name(mock_exec, kubernetes_manager, default_device): ) +@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.exec") +def test_exec_lab_obj(mock_exec, kubernetes_manager, default_device, two_device_scenario): + kubernetes_manager.exec(default_device.name, ["test", "command"], lab=two_device_scenario) + + mock_exec.assert_called_once_with( + two_device_scenario.hash.lower(), + default_device.name, + ["test", "command"], + stderr=True, + tty=False, + is_stream=True + ) + + @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.exec") def test_exec_wait(mock_exec, kubernetes_manager, default_device): kubernetes_manager.exec(default_device.name, ["test", "command"], lab_hash=default_device.lab.hash, wait=True) @@ -582,6 +608,16 @@ def test_get_machine_api_object_lab_name(mock_get_machines_api_objects, kubernet lab_hash=generate_urlsafe_hash("lab_name").lower()) +@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") +def test_get_machine_api_object_lab_obj(mock_get_machines_api_objects, kubernetes_manager, default_device, + two_device_scenario): + default_device.api_object.name = "default_device" + mock_get_machines_api_objects.return_value = [default_device.api_object] + kubernetes_manager.get_machine_api_object(machine_name="default_device", lab=two_device_scenario) + mock_get_machines_api_objects.assert_called_once_with(machine_name="default_device", + lab_hash=two_device_scenario.hash.lower()) + + @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") def test_get_machine_api_object_lab_hash_and_name(mock_get_machines_api_objects, kubernetes_manager, default_device): default_device.api_object.name = "default_device" @@ -592,7 +628,18 @@ def test_get_machine_api_object_lab_hash_and_name(mock_get_machines_api_objects, @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") -def test_get_machine_api_object_no_hash_no_name(mock_get_machines_api_objects, kubernetes_manager, default_device): +def test_get_machine_api_object_lab_hash_and_name_and_obj(mock_get_machines_api_objects, kubernetes_manager, + default_device, two_device_scenario): + default_device.api_object.name = "default_device" + mock_get_machines_api_objects.return_value = [default_device.api_object] + kubernetes_manager.get_machine_api_object(machine_name="default_device", lab_name="lab_name", + lab_hash="lab_hash", lab=two_device_scenario) + mock_get_machines_api_objects.assert_called_once_with(machine_name="default_device", + lab_hash=two_device_scenario.hash.lower()) + + +@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") +def test_get_machine_api_object_invocation_error(mock_get_machines_api_objects, kubernetes_manager, default_device): default_device.api_object.name = "default_device" mock_get_machines_api_objects.return_value = [default_device.api_object] with pytest.raises(InvocationError): @@ -625,6 +672,14 @@ def test_get_machines_api_objects_lab_name(mock_get_machines_api_objects, kubern mock_get_machines_api_objects.assert_called_once_with(lab_hash=generate_urlsafe_hash("lab_name").lower()) +@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") +def test_get_machines_api_objects_lab_obj(mock_get_machines_api_objects, kubernetes_manager, default_device, + two_device_scenario): + mock_get_machines_api_objects.return_value = [default_device.api_object] + kubernetes_manager.get_machines_api_objects(lab=two_device_scenario) + mock_get_machines_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash.lower()) + + # # TEST: get_links_api_objects # @@ -642,6 +697,14 @@ def test_get_links_api_objects_lab_name(mock_get_links_api_objects, kubernetes_m mock_get_links_api_objects.assert_called_once_with(lab_hash=generate_urlsafe_hash("lab_name").lower()) +@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters") +def test_get_links_api_objects_lab_obj(mock_get_links_api_objects, kubernetes_manager, kubernetes_network, + two_device_scenario): + mock_get_links_api_objects.return_value = [kubernetes_network] + kubernetes_manager.get_links_api_objects(lab=two_device_scenario) + mock_get_links_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash.lower()) + + # # TEST: get_link_api_object # @@ -661,15 +724,26 @@ def test_get_link_api_object_lab_name(mock_get_links_api_objects, kubernetes_man @mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters") -def test_get_link_api_object_lab_hash_and_name(mock_get_links_api_objects, kubernetes_manager, kubernetes_network): +def test_get_link_api_object_lab_obj(mock_get_links_api_objects, kubernetes_manager, kubernetes_network, + two_device_scenario): mock_get_links_api_objects.return_value = [kubernetes_network] - kubernetes_manager.get_link_api_object(link_name="test_network", lab_name="lab_name", lab_hash="lab_hash") + kubernetes_manager.get_link_api_object(link_name="test_network", lab=two_device_scenario) mock_get_links_api_objects.assert_called_once_with(link_name="test_network", - lab_hash=generate_urlsafe_hash("lab_name").lower()) + lab_hash=two_device_scenario.hash.lower()) + + +@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters") +def test_get_link_api_object_lab_hash_and_name_and_obj(mock_get_links_api_objects, kubernetes_manager, + kubernetes_network, two_device_scenario): + mock_get_links_api_objects.return_value = [kubernetes_network] + kubernetes_manager.get_link_api_object(link_name="test_network", lab_name="lab_name", lab_hash="lab_hash", + lab=two_device_scenario) + mock_get_links_api_objects.assert_called_once_with(link_name="test_network", + lab_hash=two_device_scenario.hash.lower()) @mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters") -def test_get_link_api_object_no_hash_no_name(mock_get_links_api_objects, kubernetes_manager, kubernetes_network): +def test_get_link_api_object_invocation_error(mock_get_links_api_objects, kubernetes_manager, kubernetes_network): mock_get_links_api_objects.return_value = [kubernetes_network] with pytest.raises(InvocationError): kubernetes_manager.get_link_api_object(link_name="test_network") @@ -792,7 +866,14 @@ def test_get_machines_stats_lab_name(mock_get_machines_stats, kubernetes_manager @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_stats") -def test_get_machines_stats_no_hash_no_name(mock_get_machines_stats, kubernetes_manager): +def test_get_machines_stats_lab_obj(mock_get_machines_stats, kubernetes_manager, two_device_scenario): + kubernetes_manager.get_machines_stats(lab=two_device_scenario) + mock_get_machines_stats.assert_called_once_with(lab_hash=two_device_scenario.hash.lower(), + machine_name=None) + + +@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_stats") +def test_get_machines_stats_no_labs(mock_get_machines_stats, kubernetes_manager): kubernetes_manager.get_machines_stats(all_users=True) mock_get_machines_stats.assert_called_once_with(lab_hash=None, machine_name=None) @@ -816,7 +897,15 @@ def test_get_machine_stats_lab_name(mock_get_machines_stats, default_device, kub @mock.patch("src.Kathara.manager.kubernetes.KubernetesManager.KubernetesManager.get_machines_stats") -def test_get_machine_stats_no_hash_no_name(mock_get_machines_stats, kubernetes_manager): +def test_get_machine_stats_lab_obj(mock_get_machines_stats, default_device, kubernetes_manager, two_device_scenario): + mock_get_machines_stats.return_value = iter([{"test_device": KubernetesMachineStats(default_device.api_object)}]) + next(kubernetes_manager.get_machine_stats(machine_name="test_device", lab=two_device_scenario)) + mock_get_machines_stats.assert_called_once_with(lab_hash=two_device_scenario.hash.lower(), + machine_name="test_device") + + +@mock.patch("src.Kathara.manager.kubernetes.KubernetesManager.KubernetesManager.get_machines_stats") +def test_get_machine_stats_invocation_error(mock_get_machines_stats, kubernetes_manager): mock_get_machines_stats.return_value = iter([]) with pytest.raises(InvocationError): next(kubernetes_manager.get_machine_stats(machine_name="test_device")) @@ -839,7 +928,13 @@ def test_get_links_stats_lab_name(mock_get_links_stats, kubernetes_manager): @mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_stats") -def test_get_links_stats_no_lab_hash(mock_get_links_stats, kubernetes_manager): +def test_get_links_stats_lab_obj(mock_get_links_stats, kubernetes_manager, two_device_scenario): + kubernetes_manager.get_links_stats(lab=two_device_scenario) + mock_get_links_stats.assert_called_once_with(lab_hash=two_device_scenario.hash.lower(), link_name=None) + + +@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_stats") +def test_get_links_stats_no_labs(mock_get_links_stats, kubernetes_manager): kubernetes_manager.get_links_stats() mock_get_links_stats.assert_called_once_with(lab_hash=None, link_name=None) @@ -863,7 +958,15 @@ def test_get_link_stats_lab_name(mock_get_links_stats, kubernetes_network, kuber @mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_stats") -def test_get_link_stats_no_lab_hash_and_no_name(mock_get_links_stats, kubernetes_manager): +def test_get_link_stats_lab_obj(mock_get_links_stats, kubernetes_network, kubernetes_manager, two_device_scenario): + mock_get_links_stats.return_value = iter([{"test_network": KubernetesLinkStats(kubernetes_network)}]) + next(kubernetes_manager.get_links_stats(link_name="test_network", lab=two_device_scenario)) + mock_get_links_stats.assert_called_once_with(lab_hash=two_device_scenario.hash.lower(), + link_name="test_network") + + +@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_stats") +def test_get_link_stats_invocation_error(mock_get_links_stats, kubernetes_manager): with pytest.raises(InvocationError): next(kubernetes_manager.get_link_stats(link_name="test_network")) assert not mock_get_links_stats.called From 7943db975dfb0d30a08ded704951ac3026540652 Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Fri, 15 Dec 2023 13:13:06 +0100 Subject: [PATCH 08/44] Add `MachineNotRunningError` --- src/Kathara/exceptions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Kathara/exceptions.py b/src/Kathara/exceptions.py index cd56dbf7..778e7c15 100644 --- a/src/Kathara/exceptions.py +++ b/src/Kathara/exceptions.py @@ -95,6 +95,11 @@ class MachineNotFoundError(Exception): pass +class MachineNotRunningError(Exception): + def __init__(self, machine_name: str) -> None: + super().__init__(f"Device `{machine_name}` is not running.") + + class MachineNotReadyError(Exception): def __init__(self, machine_name: str) -> None: super().__init__(f"Device `{machine_name}` is not ready.") From 7f34553eebce4aec085b3a56e52bf75431f80278 Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Fri, 15 Dec 2023 13:16:39 +0100 Subject: [PATCH 09/44] Fix `lconfig` error handling (#252) --- src/Kathara/cli/command/LconfigCommand.py | 2 +- src/Kathara/manager/docker/DockerMachine.py | 5 ++++- src/Kathara/parser/netkit/DepParser.py | 2 +- tests/cli/lconfig_command_test.py | 14 ++++++++++++++ tests/manager/docker/docker_manager_test.py | 10 +++++++++- tests/parser/lab_parser_test.py | 5 ++--- 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Kathara/cli/command/LconfigCommand.py b/src/Kathara/cli/command/LconfigCommand.py index 42246e4d..e925aaf4 100644 --- a/src/Kathara/cli/command/LconfigCommand.py +++ b/src/Kathara/cli/command/LconfigCommand.py @@ -74,7 +74,7 @@ def run(self, current_path: str, argv: List[str]) -> None: Kathara.get_instance().update_lab_from_api(lab) machine_name = args['name'] - device = lab.get_or_new_machine(machine_name) + device = lab.get_machine(machine_name) if args['to_add']: for cd in args['to_add']: diff --git a/src/Kathara/manager/docker/DockerMachine.py b/src/Kathara/manager/docker/DockerMachine.py index 3508f2e5..71f2be1e 100644 --- a/src/Kathara/manager/docker/DockerMachine.py +++ b/src/Kathara/manager/docker/DockerMachine.py @@ -17,7 +17,7 @@ from ... import utils from ...event.EventDispatcher import EventDispatcher from ...exceptions import MountDeniedError, MachineAlreadyExistsError, MachineNotFoundError, DockerPluginError, \ - MachineBinaryError + MachineBinaryError, MachineNotRunningError from ...model.Lab import Lab from ...model.Link import Link, BRIDGE_LINK_NAME from ...model.Machine import Machine, MACHINE_CAPABILITIES @@ -317,6 +317,9 @@ def connect_to_link(machine: Machine, link: Link) -> None: DockerPluginError: If Kathara has been left in an inconsistent state. APIError: If the Docker APIs return an error. """ + if not machine.api_object: + raise MachineNotRunningError(machine.name) + machine.api_object.reload() attached_networks = machine.api_object.attrs["NetworkSettings"]["Networks"] diff --git a/src/Kathara/parser/netkit/DepParser.py b/src/Kathara/parser/netkit/DepParser.py index b574243e..3213da39 100644 --- a/src/Kathara/parser/netkit/DepParser.py +++ b/src/Kathara/parser/netkit/DepParser.py @@ -39,7 +39,7 @@ def parse(path: str) -> Optional[List[str]]: dependencies = {} - # Reads lab.dep in memory so it is faster. + # Reads lab.dep in memory, so it is faster. try: with open(lab_dep_path, 'r') as dep_file: dep_mem_file = mmap.mmap(dep_file.fileno(), 0, access=mmap.ACCESS_READ) diff --git a/tests/cli/lconfig_command_test.py b/tests/cli/lconfig_command_test.py index 4b2fa8ee..2ea8d411 100644 --- a/tests/cli/lconfig_command_test.py +++ b/tests/cli/lconfig_command_test.py @@ -10,6 +10,7 @@ from src.Kathara.model.Machine import Machine from src.Kathara.model.Link import Link from src.Kathara.model.Lab import Lab +from src.Kathara.exceptions import MachineNotFoundError @pytest.fixture() @@ -127,6 +128,19 @@ def test_run_remove_two_links(mock_parse_lab, mock_docker_manager, mock_manager_ test_lab.get_or_new_link('B')) +@mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") +@mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") +def test_run_machine_not_found_error(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): + mock_parse_lab.return_value = test_lab + mock_manager_get_instance.return_value = mock_docker_manager + command = LconfigCommand() + with pytest.raises(MachineNotFoundError): + command.run('.', ['-n', 'pc10', '--add', 'A']) + mock_parse_lab.assert_called_once_with(os.getcwd()) + mock_docker_manager.update_lab_from_api.assert_called_once_with(test_lab) + + def test_run_system_exit_error(): command = LconfigCommand() with pytest.raises(SystemExit): diff --git a/tests/manager/docker/docker_manager_test.py b/tests/manager/docker/docker_manager_test.py index a65c7cd7..bf0b798c 100644 --- a/tests/manager/docker/docker_manager_test.py +++ b/tests/manager/docker/docker_manager_test.py @@ -13,7 +13,8 @@ from src.Kathara.utils import generate_urlsafe_hash from src.Kathara.manager.docker.stats.DockerLinkStats import DockerLinkStats from src.Kathara.manager.docker.stats.DockerMachineStats import DockerMachineStats -from src.Kathara.exceptions import MachineNotFoundError, LabNotFoundError, InvocationError, LinkNotFoundError +from src.Kathara.exceptions import MachineNotFoundError, LabNotFoundError, InvocationError, LinkNotFoundError, \ + MachineNotRunningError # @@ -292,6 +293,13 @@ def test_connect_machine_to_link_no_link_lab(docker_manager, default_device, def docker_manager.connect_machine_to_link(default_device, default_link) +def test_connect_machine_to_link_machine_not_running_error(docker_manager, default_device, default_link): + default_device.api_object = None + + with pytest.raises(MachineNotRunningError): + docker_manager.connect_machine_to_link(default_device, default_link) + + # # TEST: disconnect_machine_from_link # diff --git a/tests/parser/lab_parser_test.py b/tests/parser/lab_parser_test.py index e1ef9db6..659c63fa 100644 --- a/tests/parser/lab_parser_test.py +++ b/tests/parser/lab_parser_test.py @@ -2,11 +2,10 @@ import pytest -from src.Kathara.exceptions import MachineCollisionDomainError -from src.Kathara.parser.netkit.LabParser import LabParser - sys.path.insert(0, './') +from src.Kathara.exceptions import MachineCollisionDomainError +from src.Kathara.parser.netkit.LabParser import LabParser def test_one_device(): lab = LabParser.parse("tests/parser/labconf/one_device") From 1ee82da6299acf122a086801c7aecc9ed54af233 Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Sun, 17 Dec 2023 12:11:40 +0100 Subject: [PATCH 10/44] Wait shutdown commands (#255) --- src/Kathara/manager/docker/DockerMachine.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Kathara/manager/docker/DockerMachine.py b/src/Kathara/manager/docker/DockerMachine.py index 3508f2e5..0fbf6421 100644 --- a/src/Kathara/manager/docker/DockerMachine.py +++ b/src/Kathara/manager/docker/DockerMachine.py @@ -900,15 +900,16 @@ def _delete_machine(self, container: docker.models.containers.Container) -> None # Build the shutdown command string shutdown_commands_string = "; ".join(SHUTDOWN_COMMANDS).format(machine_name=container.labels["name"]) + logging.debug(f"Executing shutdown commands on `{container.labels['name']}`: {shutdown_commands_string}") # Execute the shutdown commands inside the container (only if it's running) if container.status == "running": try: self._exec_run(container, cmd=[container.labels['shell'], '-c', shutdown_commands_string], - stdout=False, + stdout=True, stderr=False, privileged=True, - detach=True + detach=False ) except MachineBinaryError as e: logging.warning(f"Shell `{e.binary}` not found in " From 967d04440f35bd1343b814131106824ab6b3ea7d Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Mon, 18 Dec 2023 11:36:50 +0100 Subject: [PATCH 11/44] Fix `lconfig` error handling (#252) --- src/Kathara/manager/docker/DockerMachine.py | 5 +---- src/Kathara/manager/docker/DockerManager.py | 6 +++++- tests/manager/docker/docker_manager_test.py | 8 ++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Kathara/manager/docker/DockerMachine.py b/src/Kathara/manager/docker/DockerMachine.py index 71f2be1e..3508f2e5 100644 --- a/src/Kathara/manager/docker/DockerMachine.py +++ b/src/Kathara/manager/docker/DockerMachine.py @@ -17,7 +17,7 @@ from ... import utils from ...event.EventDispatcher import EventDispatcher from ...exceptions import MountDeniedError, MachineAlreadyExistsError, MachineNotFoundError, DockerPluginError, \ - MachineBinaryError, MachineNotRunningError + MachineBinaryError from ...model.Lab import Lab from ...model.Link import Link, BRIDGE_LINK_NAME from ...model.Machine import Machine, MACHINE_CAPABILITIES @@ -317,9 +317,6 @@ def connect_to_link(machine: Machine, link: Link) -> None: DockerPluginError: If Kathara has been left in an inconsistent state. APIError: If the Docker APIs return an error. """ - if not machine.api_object: - raise MachineNotRunningError(machine.name) - machine.api_object.reload() attached_networks = machine.api_object.attrs["NetworkSettings"]["Networks"] diff --git a/src/Kathara/manager/docker/DockerManager.py b/src/Kathara/manager/docker/DockerManager.py index 62b18294..c16182b1 100644 --- a/src/Kathara/manager/docker/DockerManager.py +++ b/src/Kathara/manager/docker/DockerManager.py @@ -16,7 +16,7 @@ from ... import utils from ...decorators import privileged from ...exceptions import DockerDaemonConnectionError, LinkNotFoundError, MachineCollisionDomainError, \ - InvocationError, LabNotFoundError + InvocationError, LabNotFoundError, MachineNotRunningError from ...exceptions import MachineNotFoundError from ...foundation.manager.IManager import IManager from ...model.Lab import Lab @@ -151,12 +151,16 @@ def connect_machine_to_link(self, machine: Machine, link: Link) -> None: Raises: LabNotFoundError: If the device specified is not associated to any network scenario. + MachineNotRunningError: If the specified device is not running. LabNotFoundError: If the collision domain is not associated to any network scenario. MachineCollisionDomainConflictError: If the device is already connected to the collision domain. """ if not machine.lab: raise LabNotFoundError("Device `%s` is not associated to a network scenario." % machine.name) + if not machine.api_object or machine.api_object.status != "running": + raise MachineNotRunningError(machine.name) + if not link.lab: raise LabNotFoundError(f"Collision domain `{link.name}` is not associated to a network scenario.") diff --git a/tests/manager/docker/docker_manager_test.py b/tests/manager/docker/docker_manager_test.py index bf0b798c..1ea402b1 100644 --- a/tests/manager/docker/docker_manager_test.py +++ b/tests/manager/docker/docker_manager_test.py @@ -52,6 +52,7 @@ def default_device(mock_docker_container): device.add_meta("image", "kathara/test") device.add_meta("bridged", False) device.api_object = mock_docker_container + mock_docker_container.status = "running" return device @@ -300,6 +301,13 @@ def test_connect_machine_to_link_machine_not_running_error(docker_manager, defau docker_manager.connect_machine_to_link(default_device, default_link) +def test_connect_machine_to_link_machine_exited_error(docker_manager, default_device, default_link): + default_device.api_object.status = "exited" + + with pytest.raises(MachineNotRunningError): + docker_manager.connect_machine_to_link(default_device, default_link) + + # # TEST: disconnect_machine_from_link # From c6d4432eb32258b5ab8cc5b47e303383217e2b8d Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Mon, 18 Dec 2023 17:28:16 +0100 Subject: [PATCH 12/44] Add parameter to specify MAC Address (#137) --- src/Kathara/cli/command/VconfigCommand.py | 24 +++--- src/Kathara/exceptions.py | 8 ++ src/Kathara/foundation/manager/IManager.py | 3 +- src/Kathara/manager/Kathara.py | 5 +- src/Kathara/manager/docker/DockerMachine.py | 57 ++++++++------ src/Kathara/manager/docker/DockerManager.py | 15 ++-- .../manager/kubernetes/KubernetesMachine.py | 4 +- .../manager/kubernetes/KubernetesManager.py | 7 +- src/Kathara/model/Interface.py | 34 ++++++++ src/Kathara/model/Lab.py | 45 ++++++----- src/Kathara/model/Machine.py | 52 +++++-------- src/Kathara/parser/netkit/LabParser.py | 12 ++- src/Kathara/utils.py | 17 +++- src/requirements.txt | 2 +- tests/manager/docker/docker_machine_test.py | 15 ++-- tests/manager/docker/docker_manager_test.py | 25 +++--- tests/model/lab_test.py | 78 ++++++++++--------- tests/model/machine_test.py | 32 ++++---- tests/parser/lab_parser_test.py | 31 ++++++-- tests/parser/labconf/mac_address/lab.conf | 3 + .../parser/labconf/mac_address_error/lab.conf | 3 + .../labconf/mac_address_parse_error/lab.conf | 3 + 22 files changed, 296 insertions(+), 179 deletions(-) create mode 100644 src/Kathara/model/Interface.py create mode 100644 tests/parser/labconf/mac_address/lab.conf create mode 100644 tests/parser/labconf/mac_address_error/lab.conf create mode 100644 tests/parser/labconf/mac_address_parse_error/lab.conf diff --git a/src/Kathara/cli/command/VconfigCommand.py b/src/Kathara/cli/command/VconfigCommand.py index af9c8a0b..2b069e04 100644 --- a/src/Kathara/cli/command/VconfigCommand.py +++ b/src/Kathara/cli/command/VconfigCommand.py @@ -7,6 +7,7 @@ from ...manager.Kathara import Kathara from ...model.Lab import Lab from ...strings import strings, wiki_description +from ...utils import parse_cd_mac_address class VconfigCommand(Command): @@ -37,7 +38,7 @@ def __init__(self) -> None: group.add_argument( '--add', - type=alphanumeric, + type=str, dest='to_add', metavar='CD', nargs='+', @@ -63,19 +64,22 @@ def run(self, current_path: str, argv: List[str]) -> None: device.api_object = Kathara.get_instance().get_machine_api_object(machine_name, lab_name=lab.name) if args['to_add']: - for cd in args['to_add']: + for cd_to_add in args['to_add']: + cd_name, mac_address = parse_cd_mac_address(cd_to_add) logging.info( - "Adding interface to device `%s` on collision domain `%s`..." % (machine_name, cd) + f"Adding interface to device `{machine_name}` on collision domain `{cd_name}`" + + (f" with MAC Address {mac_address}" if mac_address else "") + + f"..." ) - link = lab.get_or_new_link(cd) - Kathara.get_instance().connect_machine_to_link(device, link) + link = lab.get_or_new_link(cd_name) + Kathara.get_instance().connect_machine_to_link(device, link, mac_address=mac_address) if args['to_remove']: - for cd in args['to_remove']: + for cd_to_remove in args['to_remove']: logging.info( - "Removing interface on collision domain `%s` from device `%s`..." % (cd, machine_name) + "Removing interface on collision domain `%s` from device `%s`..." % (cd_to_remove, machine_name) ) - (_, link) = lab.connect_machine_to_link(machine_name, cd) - link.api_object = Kathara.get_instance().get_link_api_object(cd, lab_name=lab.name) + (_, interface) = lab.connect_machine_to_link(machine_name, cd_to_remove) + interface.link.api_object = Kathara.get_instance().get_link_api_object(cd_to_remove, lab_name=lab.name) - Kathara.get_instance().disconnect_machine_from_link(device, link) + Kathara.get_instance().disconnect_machine_from_link(device, interface.link) diff --git a/src/Kathara/exceptions.py b/src/Kathara/exceptions.py index cd56dbf7..d2807005 100644 --- a/src/Kathara/exceptions.py +++ b/src/Kathara/exceptions.py @@ -111,6 +111,14 @@ def __str__(self): return f"Binary `{self.binary}` not found in device `{self.machine_name}`." +# Interface Exceptions +class InterfaceMacAddressError(Exception): + def __init__(self, mac_address: str, interface_num: int, machine_name: str) -> None: + super().__init__( + f"MAC address {mac_address} on interface `{interface_num}` of device `{machine_name}` is invalid." + ) + + # Link Exceptions class LinkNotFoundError(Exception): pass diff --git a/src/Kathara/foundation/manager/IManager.py b/src/Kathara/foundation/manager/IManager.py index 15f41703..88d8e698 100644 --- a/src/Kathara/foundation/manager/IManager.py +++ b/src/Kathara/foundation/manager/IManager.py @@ -56,12 +56,13 @@ def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None: raise NotImplementedError("You must implement `deploy_lab` method.") @abstractmethod - def connect_machine_to_link(self, machine: Machine, link: Link) -> None: + def connect_machine_to_link(self, machine: Machine, link: Link, mac_address: Optional[str] = None) -> None: """Connect a Kathara device to a collision domain. Args: machine (Kathara.model.Machine): A Kathara machine object. link (Kathara.model.Link): A Kathara collision domain object. + mac_address (Optional[str]): The MAC address to assign to the interface. Returns: None diff --git a/src/Kathara/manager/Kathara.py b/src/Kathara/manager/Kathara.py index f86412bd..143f0c2a 100644 --- a/src/Kathara/manager/Kathara.py +++ b/src/Kathara/manager/Kathara.py @@ -87,12 +87,13 @@ def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None: """ self.manager.deploy_lab(lab, selected_machines) - def connect_machine_to_link(self, machine: Machine, link: Link) -> None: + def connect_machine_to_link(self, machine: Machine, link: Link, mac_address: Optional[str] = None) -> None: """Connect a Kathara device to a collision domain. Args: machine (Kathara.model.Machine): A Kathara machine object. link (Kathara.model.Link): A Kathara collision domain object. + mac_address (Optional[str]): The MAC address to assign to the interface. Returns: None @@ -102,7 +103,7 @@ def connect_machine_to_link(self, machine: Machine, link: Link) -> None: LabNotFoundError: If the collision domain is not associated to any network scenario. MachineCollisionDomainConflictError: If the device is already connected to the collision domain. """ - self.manager.connect_machine_to_link(machine, link) + self.manager.connect_machine_to_link(machine, link, mac_address=mac_address) def disconnect_machine_from_link(self, machine: Machine, link: Link) -> None: """Disconnect a Kathara device from a collision domain. diff --git a/src/Kathara/manager/docker/DockerMachine.py b/src/Kathara/manager/docker/DockerMachine.py index 1f9c7e67..9222b2b3 100644 --- a/src/Kathara/manager/docker/DockerMachine.py +++ b/src/Kathara/manager/docker/DockerMachine.py @@ -18,6 +18,7 @@ from ...event.EventDispatcher import EventDispatcher from ...exceptions import MountDeniedError, MachineAlreadyExistsError, MachineNotFoundError, DockerPluginError, \ MachineBinaryError +from ...model.Interface import Interface from ...model.Lab import Lab from ...model.Link import Link, BRIDGE_LINK_NAME from ...model.Machine import Machine, MACHINE_CAPABILITIES @@ -219,19 +220,21 @@ def create(self, machine: Machine) -> None: # Get the first network object, if defined. # This should be used in container create function first_network = None + first_machine_iface = None if machine.interfaces: - first_network = machine.interfaces[0].api_object + first_machine_iface = machine.interfaces[0] + first_network = first_machine_iface.link.api_object # If no interfaces are declared in machine, but bridged mode is required, get bridge as first link. # Flag that bridged is already connected (because there's another check in `start`). - if first_network is None and machine.is_bridged(): + if first_machine_iface is None and machine.is_bridged(): first_network = machine.lab.get_or_new_link(BRIDGE_LINK_NAME).api_object machine.add_meta("bridge_connected", True) # Sysctl params to pass to the container creation sysctl_parameters = {RP_FILTER_NAMESPACE % x: 0 for x in ["all", "default", "lo"]} - if first_network: + if first_machine_iface: sysctl_parameters[RP_FILTER_NAMESPACE % "eth0"] = 0 sysctl_parameters["net.ipv4.ip_forward"] = 1 @@ -264,6 +267,17 @@ def create(self, machine: Machine) -> None: privileged = False logging.warning("Privileged flag is ignored with a remote Docker connection.") + networking_config = None + if first_machine_iface: + driver_opt = {'kathara.mac_addr': first_machine_iface.mac_address} \ + if first_machine_iface.mac_address else None + + networking_config = { + first_network.name: self.client.api.create_endpoint_config( + driver_opt=driver_opt + ) + } + container_name = self.get_container_name(machine.name, machine.lab.hash) try: @@ -274,10 +288,7 @@ def create(self, machine: Machine) -> None: privileged=privileged, network=first_network.name if first_network else None, network_mode="bridge" if first_network else "none", - network_driver_opt={ - 'kathara.machine': machine.name, - 'kathara.iface': "0" - } if first_network else None, + networking_config=networking_config, environment=machine.meta['envs'], sysctls=sysctl_parameters, mem_limit=memory, @@ -307,12 +318,12 @@ def create(self, machine: Machine) -> None: machine.api_object = machine_container @staticmethod - def connect_to_link(machine: Machine, link: Link) -> None: + def connect_interface(machine: Machine, interface: Interface) -> None: """Connect the Docker container representing the machine to a specified collision domain. Args: - machine (Kathara.model.Machine): A Kathara device. - link (Kathara.model.Link): A Kathara collision domain object. + machine (Kathara.model.Machine.Machine): A Kathara device. + interface (Kathara.model.Interface.Interface): A Kathara interface object. Returns: None @@ -324,12 +335,13 @@ def connect_to_link(machine: Machine, link: Link) -> None: machine.api_object.reload() attached_networks = machine.api_object.attrs["NetworkSettings"]["Networks"] - if link.api_object.name not in attached_networks: + if interface.link.api_object.name not in attached_networks: try: - link.api_object.connect( + driver_opt = {'kathara.mac_addr': interface.mac_address} if interface.mac_address else None + + interface.link.api_object.connect( machine.api_object, - driver_opt={'kathara.machine': machine.name, - 'kathara.iface': str(machine.get_interface_by_link(link))} + driver_opt=driver_opt ) except APIError as e: if e.response.status_code == 500 and \ @@ -388,16 +400,17 @@ def start(self, machine: Machine) -> None: # Connect the container to its networks (starting from the second, the first is already connected in `create`) # This should be done after the container start because Docker causes a non-deterministic order when attaching # networks before container startup. - for (iface_num, machine_link) in islice(machine.interfaces.items(), 1, None): - logging.debug("Connecting device `%s` to collision domain `%s` on interface %d..." % (machine.name, - machine_link.name, - iface_num - ) - ) + for (iface_num, machine_iface) in islice(machine.interfaces.items(), 1, None): + logging.debug( + f"Connecting device `{machine.name}` to collision domain `{machine_iface.link.name}` " + f"on interface {iface_num}..." + ) try: - machine_link.api_object.connect( + driver_opt = {'kathara.mac_addr': machine_iface.mac_address} if machine_iface.mac_address else None + + machine_iface.link.api_object.connect( machine.api_object, - driver_opt={'kathara.machine': machine.name, 'kathara.iface': str(iface_num)} + driver_opt=driver_opt ) except APIError as e: if e.response.status_code == 500 and \ diff --git a/src/Kathara/manager/docker/DockerManager.py b/src/Kathara/manager/docker/DockerManager.py index 62b18294..16fbd8cb 100644 --- a/src/Kathara/manager/docker/DockerManager.py +++ b/src/Kathara/manager/docker/DockerManager.py @@ -89,7 +89,7 @@ def deploy_machine(self, machine: Machine) -> None: if not machine.lab: raise LabNotFoundError("Device `%s` is not associated to a network scenario." % machine.name) - self.docker_link.deploy_links(machine.lab, selected_links={x.name for x in machine.interfaces.values()}) + self.docker_link.deploy_links(machine.lab, selected_links={x.link.name for x in machine.interfaces.values()}) self.docker_machine.deploy_machines(machine.lab, selected_machines={machine.name}) @privileged @@ -139,12 +139,13 @@ def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None: self.docker_machine.deploy_machines(lab, selected_machines=selected_machines) @privileged - def connect_machine_to_link(self, machine: Machine, link: Link) -> None: - """Connect a Kathara device to a collision domain. + def connect_machine_to_link(self, machine: Machine, link: Link, mac_address: Optional[str] = None) -> None: + """Create a new interface and connect a Kathara device to a collision domain. Args: machine (Kathara.model.Machine): A Kathara machine object. link (Kathara.model.Link): A Kathara collision domain object. + mac_address (Optional[str]): The MAC address to assign to the interface. Returns: None @@ -165,10 +166,10 @@ def connect_machine_to_link(self, machine: Machine, link: Link) -> None: f"Device `{machine.name}` is already connected to collision domain `{link.name}`." ) - machine.add_interface(link) + interface = machine.add_interface(link, mac_address=mac_address) self.deploy_link(link) - self.docker_machine.connect_to_link(machine, link) + self.docker_machine.connect_interface(machine, interface) @privileged def disconnect_machine_from_link(self, machine: Machine, link: Link) -> None: @@ -219,7 +220,7 @@ def undeploy_machine(self, machine: Machine) -> None: raise LabNotFoundError(f"Device `{machine.name}` is not associated to a network scenario.") self.docker_machine.undeploy(machine.lab.hash, selected_machines={machine.name}) - self.docker_link.undeploy(machine.lab.hash, selected_links={x.name for x in machine.interfaces.values()}) + self.docker_link.undeploy(machine.lab.hash, selected_links={x.link.name for x in machine.interfaces.values()}) @privileged def undeploy_link(self, link: Link) -> None: @@ -584,7 +585,7 @@ def update_lab_from_api(self, lab: Lab) -> None: device.api_object = container # Collision domains declared in the network scenario - static_links = set(device.interfaces.values()) + static_links = set([x.link for x in device.interfaces.values()]) # Collision domains currently attached to the device current_links = set( map(lambda x: lab.get_or_new_link(deployed_networks[x].attrs["Labels"]["name"]), diff --git a/src/Kathara/manager/kubernetes/KubernetesMachine.py b/src/Kathara/manager/kubernetes/KubernetesMachine.py index affb2ebf..2070b11e 100644 --- a/src/Kathara/manager/kubernetes/KubernetesMachine.py +++ b/src/Kathara/manager/kubernetes/KubernetesMachine.py @@ -377,9 +377,9 @@ def _build_definition(self, machine: Machine, config_map: client.V1ConfigMap) -> pod_annotations = {} network_interfaces = [] - for (idx, machine_link) in machine.interfaces.items(): + for (idx, interface) in machine.interfaces.items(): network_interfaces.append({ - "name": machine_link.api_object["metadata"]["name"], + "name": interface.link.api_object["metadata"]["name"], "namespace": machine.lab.hash, "interface": "net%d" % idx }) diff --git a/src/Kathara/manager/kubernetes/KubernetesManager.py b/src/Kathara/manager/kubernetes/KubernetesManager.py index 9fe81efa..85884124 100644 --- a/src/Kathara/manager/kubernetes/KubernetesManager.py +++ b/src/Kathara/manager/kubernetes/KubernetesManager.py @@ -52,7 +52,7 @@ def deploy_machine(self, machine: Machine) -> None: machine.lab.hash = machine.lab.hash.lower() self.k8s_namespace.create(machine.lab) - self.k8s_link.deploy_links(machine.lab, selected_links={x.name for x in machine.interfaces.values()}) + self.k8s_link.deploy_links(machine.lab, selected_links={x.link.name for x in machine.interfaces.values()}) self.k8s_machine.deploy_machines(machine.lab, selected_machines={machine.name}) def deploy_link(self, link: Link) -> None: @@ -113,12 +113,13 @@ def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None: else: raise e - def connect_machine_to_link(self, machine: Machine, link: Link) -> None: + def connect_machine_to_link(self, machine: Machine, link: Link, mac_address: Optional[str] = None) -> None: """Connect a Kathara device to a collision domain. Args: machine (Kathara.model.Machine): A Kathara machine object. link (Kathara.model.Link): A Kathara collision domain object. + mac_address (Optional[str]): The MAC address to assign to the interface. Returns: None @@ -173,7 +174,7 @@ def undeploy_machine(self, machine: Machine) -> None: running_networks.update([net['name'] for net in network_annotation]) # Difference between all networks of the machine to undeploy, and attached networks are the ones to delete - machine_networks = {self.k8s_link.get_network_name(x.name) for x in machine.interfaces.values()} + machine_networks = {self.k8s_link.get_network_name(x.link.name) for x in machine.interfaces.values()} networks_to_delete = machine_networks - running_networks self.k8s_machine.undeploy(machine.lab.hash, selected_machines={machine.name}) diff --git a/src/Kathara/model/Interface.py b/src/Kathara/model/Interface.py new file mode 100644 index 00000000..eb401308 --- /dev/null +++ b/src/Kathara/model/Interface.py @@ -0,0 +1,34 @@ +import re +from typing import Optional + +from . import Link as LinkPackage +from . import Machine as MachinePackage +from ..exceptions import InterfaceMacAddressError + +MAC_ADDRESS_REGEX = re.compile(r"^[0-9a-f]{2}([:]?)[0-9a-f]{2}(\1[0-9a-f]{2}){4}$") + + +class Interface(object): + """Interface object associated to a Machine network interface. + + Attributes: + machine (Kathara.model.Machine.Machine): The machine associated to this interface. + link (Kathara.model.Link.Link): The collision domain associated to this interface. + num (int): The interface number. + mac_address (Optional[str]): The MAC address of the interface. If None, a generated MAC address + is associated when the Machine is started. + """ + __slots__ = ['machine', 'link', 'num', 'mac_address'] + + def __init__(self, machine: 'MachinePackage.Machine', link: 'LinkPackage.Link', + num: int, mac_address: Optional[str] = None) -> None: + self.machine: 'MachinePackage.Machine' = machine + self.link: 'LinkPackage.Link' = link + self.num: int = num + self.mac_address: Optional[str] = mac_address + + if self.mac_address and not MAC_ADDRESS_REGEX.match(self.mac_address): + raise InterfaceMacAddressError(self.mac_address, self.num, machine.name) + + def __repr__(self) -> str: + return "Interface(%s, %d, %s)" % (self.machine.name, self.num, self.mac_address) diff --git a/src/Kathara/model/Lab.py b/src/Kathara/model/Lab.py index 83005fee..4caab092 100644 --- a/src/Kathara/model/Lab.py +++ b/src/Kathara/model/Lab.py @@ -5,9 +5,10 @@ from fs import open_fs from fs.base import FS +from . import Interface as InterfacePackage +from . import Link as LinkPackage from . import Machine as MachinePackage from .ExternalLink import ExternalLink -from .Link import Link from .. import utils from ..exceptions import LinkNotFoundError, MachineNotFoundError, MachineAlreadyExistsError, LinkAlreadyExistsError from ..foundation.model.FilesystemMixin import FilesystemMixin @@ -58,7 +59,7 @@ def __init__(self, name: Optional[str], path: Optional[str] = None) -> None: self.web: Optional[str] = None self.machines: Dict[str, 'MachinePackage.Machine'] = {} - self.links: Dict[str, Link] = {} + self.links: Dict[str, 'LinkPackage.Link'] = {} self.general_options: Dict[str, Any] = {} @@ -82,8 +83,9 @@ def name(self, value): self._name = value self.hash = utils.generate_urlsafe_hash(value) - def connect_machine_to_link(self, machine_name: str, link_name: str, machine_iface_number: int = None) \ - -> Tuple['MachinePackage.Machine', Link]: + def connect_machine_to_link(self, machine_name: str, link_name: str, + machine_iface_number: int = None, mac_address: Optional[str] = None) \ + -> Tuple['MachinePackage.Machine', 'InterfacePackage.Interface']: """Connect the specified device to the specified collision domain. Args: @@ -91,10 +93,11 @@ def connect_machine_to_link(self, machine_name: str, link_name: str, machine_ifa link_name (str): The collision domain name. machine_iface_number (int): The number of the device interface to connect. If it is None, the first free number is used. + mac_address (Optional[str]): The MAC address to assign to the interface. Returns: - Tuple[Kathara.model.Machine, Kathara.model.Link]: A tuple containing the Kathara device and collision domain - specified by their names. + Tuple[Kathara.model.Machine.Machine, Kathara.model.Interface.Interface]: A tuple containing the Kathara + device and the interface object specified by their names. Raises: Exception: If an already used interface number is specified. @@ -102,12 +105,13 @@ def connect_machine_to_link(self, machine_name: str, link_name: str, machine_ifa machine = self.get_or_new_machine(machine_name) link = self.get_or_new_link(link_name) - machine.add_interface(link, number=machine_iface_number) + interface = machine.add_interface(link, number=machine_iface_number, mac_address=mac_address) - return machine, link + return machine, interface - def connect_machine_obj_to_link(self, machine: 'MachinePackage.Machine', - link_name: str, machine_iface_number: int = None) -> Tuple[Link, Optional[int]]: + def connect_machine_obj_to_link(self, machine: 'MachinePackage.Machine', link_name: str, + machine_iface_number: int = None, mac_address: Optional[str] = None) \ + -> 'InterfacePackage.Interface': """Connect the specified device object to the specified collision domain. Args: @@ -117,17 +121,16 @@ def connect_machine_obj_to_link(self, machine: 'MachinePackage.Machine', number is used. Returns: - Tuple[Kathara.model.Link, Optional[int]]: A tuple containing the collision domain and - the assigned interface number (if machine_iface_number is None). + Kathara.model.Interface.Interface: The interface object associated to the new interface.. Raises: Exception: If an already used interface number is specified. """ link = self.get_or_new_link(link_name) - machine_iface_number = machine.add_interface(link, number=machine_iface_number) + interface = machine.add_interface(link, number=machine_iface_number, mac_address=mac_address) - return link, machine_iface_number + return interface def assign_meta_to_machine(self, machine_name: str, meta_name: str, meta_value: str) -> Optional[Any]: """Assign meta information to the specified device. @@ -192,7 +195,7 @@ def get_links_from_machines(self, machines: Union[List[str], Set[str]]) -> Set[s # Get only selected machines Link objects. selected_links = set(chain.from_iterable([machine.interfaces.values() for machine in machines])) - selected_links = {link.name for link in selected_links} + selected_links = {interface.link.name for interface in selected_links} return selected_links @@ -214,7 +217,7 @@ def get_links_from_machine_objs(self, # Get only selected machines Link objects. selected_links = set(chain.from_iterable([machine.interfaces.values() for machine in machines])) - selected_links = {link.name for link in selected_links} + selected_links = {interface.link.name for interface in selected_links} return selected_links @@ -291,7 +294,7 @@ def get_or_new_machine(self, name: str, **kwargs: Dict[str, Any]) -> 'MachinePac return self.machines[name] - def get_link(self, name: str) -> Link: + def get_link(self, name: str) -> 'LinkPackage.Link': """Get the specified collision domain. Args: @@ -308,7 +311,7 @@ def get_link(self, name: str) -> Link: return self.links[name] - def new_link(self, name: str) -> Link: + def new_link(self, name: str) -> 'LinkPackage.Link': """Create the collision domain and add it to the collision domains list. Args: @@ -323,11 +326,11 @@ def new_link(self, name: str) -> Link: if name in self.links: raise LinkAlreadyExistsError(f"Collision domain {name} is already the network scenario.") - self.links[name] = Link(self, name) + self.links[name] = LinkPackage.Link(self, name) return self.links[name] - def get_or_new_link(self, name: str) -> Link: + def get_or_new_link(self, name: str) -> 'LinkPackage.Link': """Get the specified collision domain. If it not exists, create and add it to the collision domains list. Args: @@ -337,7 +340,7 @@ def get_or_new_link(self, name: str) -> Link: Kathara.model.Link: A Kathara collision domain. """ if name not in self.links: - self.links[name] = Link(self, name) + self.links[name] = LinkPackage.Link(self, name) return self.links[name] diff --git a/src/Kathara/model/Machine.py b/src/Kathara/model/Machine.py index 58cbed27..1f4fde26 100644 --- a/src/Kathara/model/Machine.py +++ b/src/Kathara/model/Machine.py @@ -13,9 +13,9 @@ from fs.tempfs import TempFS from fs.walk import Walker +from . import Interface as InterfacePackage from . import Lab as LabPackage from . import Link as LinkPackage -from .Link import Link from .. import utils from ..exceptions import NonSequentialMachineInterfaceError, MachineOptionError, MachineCollisionDomainError from ..foundation.model.FilesystemMixin import FilesystemMixin @@ -61,7 +61,7 @@ def __init__(self, lab: 'LabPackage.Lab', name: str, **kwargs) -> None: self.lab: LabPackage.Lab = lab self.name: str = name - self.interfaces: OrderedDict[int, Link] = collections.OrderedDict() + self.interfaces: OrderedDict[int, 'InterfacePackage.Interface'] = collections.OrderedDict() self.meta: Dict[str, Any] = { 'exec_commands': [], @@ -77,24 +77,25 @@ def __init__(self, lab: 'LabPackage.Lab', name: str, **kwargs) -> None: self.update_meta(kwargs) - def add_interface(self, link: 'LinkPackage.Link', number: int = None) -> Optional[int]: + def add_interface(self, link: 'LinkPackage.Link', number: int = None, mac_address: str = None) \ + -> 'InterfacePackage.Interface': """Add an interface to the device attached to the specified collision domain. Args: link (Kathara.model.Link): The Kathara collision domain to attach. number (int): The number of the new interface. If it is None, the first free number is selected. + mac_address (str): The MAC address of the interface. If None, a generated MAC address + is associated when the Machine is started. Returns: - Optional[int]: The number of the assigned interface if not passed, else None. + Interface: The object associated to this interface. Raises: MachineCollisionDomainConflictError: If the interface number specified is already used on the device. MachineCollisionDomainConflictError: If the device is already connected to the collision domain. """ - had_number = True if number is None: number = len(self.interfaces.keys()) - had_number = False if number in self.interfaces: raise MachineCollisionDomainError(f"Interface {number} already set on device `{self.name}`.") @@ -104,10 +105,11 @@ def add_interface(self, link: 'LinkPackage.Link', number: int = None) -> Optiona f"Device `{self.name}` is already connected to collision domain `{link.name}`." ) - self.interfaces[number] = link + interface = InterfacePackage.Interface(self, link, number, mac_address) + self.interfaces[number] = interface link.machines[self.name] = self - return number if not had_number else None + return interface def remove_interface(self, link: 'LinkPackage.Link') -> None: """Disconnect the device from the specified collision domain. @@ -127,31 +129,13 @@ def remove_interface(self, link: 'LinkPackage.Link') -> None: ) self.interfaces = collections.OrderedDict( - map(lambda x: x if x[1] is not None and x[1].name != link.name else (x[0], None), self.interfaces.items()) + map( + lambda x: x if x[1] is not None and x[1].link.name != link.name else (x[0], None), + self.interfaces.items() + ) ) link.machines.pop(self.name) - def get_interface_by_link(self, link: 'LinkPackage.Link') -> int: - """Get the interface number associated to the specified collision domain. - - Args: - link (Kathara.model.Link): The Kathara collision domain to search. - - Returns: - int: The interface number associated to the collision domain. - - Raises: - MachineCollisionDomainConflictError: If the device is not connected to the collision domain. - - """ - for (number, machine_link) in self.interfaces.items(): - if machine_link == link: - return number - - raise MachineCollisionDomainError( - f"Device `{self.name}` is not connected to collision domain `{link.name}`." - ) - def add_meta(self, name: str, value: Any) -> None: """Add a meta property to the device. @@ -692,9 +676,11 @@ def __str__(self) -> str: if self.interfaces: formatted_machine += "\nInterfaces: " - for (iface_num, link) in self.interfaces.items(): - if link: - formatted_machine += f"\n\t- {iface_num}: {link.name}" + for (iface_num, interface) in self.interfaces.items(): + if interface: + formatted_machine += f"\n\t- {iface_num}: {interface.link.name}" + if interface.mac_address: + formatted_machine += f" ({interface.mac_address})" formatted_machine += f"\nBridged Connection: {self.meta['bridged']}" diff --git a/src/Kathara/parser/netkit/LabParser.py b/src/Kathara/parser/netkit/LabParser.py index 9b4fd6d1..48ca83e2 100644 --- a/src/Kathara/parser/netkit/LabParser.py +++ b/src/Kathara/parser/netkit/LabParser.py @@ -4,7 +4,7 @@ import re from ...model.Lab import Lab, LAB_METADATA -from ...utils import RESERVED_MACHINE_NAMES +from ...utils import parse_cd_mac_address, RESERVED_MACHINE_NAMES class LabParser(object): @@ -58,8 +58,14 @@ def parse(path: str) -> Lab: # It's an interface, handle it. interface_number = int(arg) - if re.search(r"^\w+$", value): - lab.connect_machine_to_link(key, value, machine_iface_number=interface_number) + try: + cd_name, mac_address = parse_cd_mac_address(value) + except SyntaxError as e: + raise SyntaxError(f"In lab.conf - Line {line_number}: {str(e)}") + + if re.search(r"^\w+$", cd_name): + lab.connect_machine_to_link(key, cd_name, + machine_iface_number=interface_number, mac_address=mac_address) else: raise SyntaxError(f"In lab.conf - Line {line_number}: " f"Collision domain `{value}` contains non-alphanumeric characters.") diff --git a/src/Kathara/utils.py b/src/Kathara/utils.py index b8904bb2..71867ec3 100644 --- a/src/Kathara/utils.py +++ b/src/Kathara/utils.py @@ -16,7 +16,7 @@ from platform import node, machine from sys import platform as _platform from types import ModuleType -from typing import Any, Optional, Match, Generator, List, Callable, Union, Dict, Iterable +from typing import Any, Optional, Match, Generator, List, Callable, Union, Dict, Iterable, Tuple from binaryornot.check import is_binary from slug import slug @@ -306,3 +306,18 @@ def pack_files_for_tar(guest_to_host: Dict) -> bytes: tar_data = temp_file.read() return tar_data + + +def parse_cd_mac_address(value) -> Tuple[str, str]: + if '/' in value: + parts = [x for x in value.split('/') if x] + + if len(parts) != 2: + raise SyntaxError(f"Invalid interface definition: `{value}`.") + + cd_name = parts[0] + mac_address = parts[1] + else: + (cd_name, mac_address) = value, None + + return cd_name, mac_address diff --git a/src/requirements.txt b/src/requirements.txt index 07b26ce7..486999fe 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,5 +1,5 @@ binaryornot>=0.4.4; -docker>=6.1.0; +docker>=7.0.0; kubernetes>=23.3.0; requests>=2.22.0; coloredlogs>=10.0; diff --git a/tests/manager/docker/docker_machine_test.py b/tests/manager/docker/docker_machine_test.py index ce8efabf..9d6e05f0 100644 --- a/tests/manager/docker/docker_machine_test.py +++ b/tests/manager/docker/docker_machine_test.py @@ -87,6 +87,7 @@ def test_create(mock_get_current_user_name, mock_setting_get_instance, mock_copy privileged=False, network=None, network_mode='none', + networking_config=None, sysctls={'net.ipv4.conf.all.rp_filter': 0, 'net.ipv4.conf.default.rp_filter': 0, 'net.ipv4.conf.lo.rp_filter': 0, @@ -137,6 +138,7 @@ def test_create_ipv6(mock_get_current_user_name, mock_setting_get_instance, mock privileged=False, network=None, network_mode='none', + networking_config=None, sysctls={'net.ipv4.conf.all.rp_filter': 0, 'net.ipv4.conf.default.rp_filter': 0, 'net.ipv4.conf.lo.rp_filter': 0, @@ -193,6 +195,7 @@ def test_create_privileged(mock_get_current_user_name, mock_setting_get_instance privileged=True, network=None, network_mode='none', + networking_config=None, sysctls={'net.ipv4.conf.all.rp_filter': 0, 'net.ipv4.conf.default.rp_filter': 0, 'net.ipv4.conf.lo.rp_filter': 0, @@ -318,26 +321,26 @@ def test_connect_to_link(docker_machine, default_device, default_link, default_l default_link.api_object.name = "A" default_device.add_interface(default_link) - default_device.add_interface(default_link_b) + interface = default_device.add_interface(default_link_b) - docker_machine.connect_to_link(default_device, default_link_b) + docker_machine.connect_interface(default_device, interface) assert not default_link.api_object.connect.called default_link_b.api_object.connect.assert_called_once() def test_connect_to_link_plugin_error_network(default_device, default_link, docker_machine): - default_device.add_interface(default_link) + interface = default_device.add_interface(default_link) default_link.api_object.connect.side_effect = DockerPluginError("network does not exists") with pytest.raises(DockerPluginError): - docker_machine.connect_to_link(default_device, default_link) + docker_machine.connect_interface(default_device, interface) def test_connect_to_link_plugin_error_endpoint(default_device, default_link, docker_machine): - default_device.add_interface(default_link) + interface = default_device.add_interface(default_link) default_link.api_object.connect.side_effect = DockerPluginError("endpoint does not exists") with pytest.raises(DockerPluginError): - docker_machine.connect_to_link(default_device, default_link) + docker_machine.connect_interface(default_device, interface) # diff --git a/tests/manager/docker/docker_manager_test.py b/tests/manager/docker/docker_manager_test.py index a65c7cd7..8552a433 100644 --- a/tests/manager/docker/docker_manager_test.py +++ b/tests/manager/docker/docker_manager_test.py @@ -10,6 +10,7 @@ from src.Kathara.model.Lab import Lab from src.Kathara.model.Machine import Machine from src.Kathara.model.Link import Link +from src.Kathara.model.Interface import Interface from src.Kathara.utils import generate_urlsafe_hash from src.Kathara.manager.docker.stats.DockerLinkStats import DockerLinkStats from src.Kathara.manager.docker.stats.DockerMachineStats import DockerMachineStats @@ -251,31 +252,35 @@ def test_deploy_link_no_lab(docker_manager, default_link): # TEST: connect_machine_to_link # @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.deploy_link") -@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.connect_to_link") -def test_connect_machine_to_link_one_link(mock_connect_to_link_machine, mock_deploy_link, docker_manager, +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.connect_interface") +def test_connect_machine_to_link_one_link(mock_connect_interface_machine, mock_deploy_link, docker_manager, default_device, default_link): docker_manager.connect_machine_to_link(default_device, default_link) + interface = default_device.interfaces[0] + mock_deploy_link.assert_called_once_with(default_link) - mock_connect_to_link_machine.assert_called_once_with(default_device, default_link) + mock_connect_interface_machine.assert_called_once_with(default_device, interface) @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.deploy_link") -@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.connect_to_link") -def test_connect_machine_to_link_two_links(mock_connect_to_link_machine, mock_deploy_link, docker_manager, +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.connect_interface") +def test_connect_machine_to_link_two_links(mock_connect_interface_machine, mock_deploy_link, docker_manager, default_device, default_link, default_link_b): docker_manager.connect_machine_to_link(default_device, default_link) + interface_a = default_device.interfaces[0] mock_deploy_link.assert_called_with(default_link) - mock_connect_to_link_machine.assert_called_with(default_device, default_link) + mock_connect_interface_machine.assert_called_with(default_device, interface_a) docker_manager.connect_machine_to_link(default_device, default_link_b) + interface_b = default_device.interfaces[1] mock_deploy_link.assert_called_with(default_link_b) - mock_connect_to_link_machine.assert_called_with(default_device, default_link_b) + mock_connect_interface_machine.assert_called_with(default_device, interface_b) assert mock_deploy_link.call_count == 2 - assert mock_connect_to_link_machine.call_count == 2 + assert mock_connect_interface_machine.call_count == 2 def test_connect_machine_to_link_no_machine_lab(docker_manager, default_device, default_link): @@ -364,7 +369,7 @@ def test_undeploy_machine(mock_machine_undeploy, mock_link_undeploy, docker_mana @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.undeploy") def test_undeploy_machine_two_machines(mock_machine_undeploy, mock_link_undeploy, docker_manager, two_device_scenario): device_1 = two_device_scenario.get_or_new_machine('pc1') - device_1_links = {x.name for x in device_1.interfaces.values()} + device_1_links = {x.link.name for x in device_1.interfaces.values()} docker_manager.undeploy_machine(device_1) @@ -968,7 +973,7 @@ def test_update_lab_from_api_add_link(mock_get_links_api_objects, mock_get_machi lab = Lab("test") device = lab.get_or_new_machine(docker_container.labels["name"]) link = Link(lab, docker_network.attrs["Labels"]["name"]) - device.add_interface(link) + interface = device.add_interface(link) mock_get_machines_api_objects.return_value = [docker_container] mock_get_links_api_objects.return_value = [docker_network, docker_network_b] docker_container.attrs["NetworkSettings"]["Networks"] = ["kathara_user_hash_test_network", diff --git a/tests/model/lab_test.py b/tests/model/lab_test.py index 234bc90f..95e3e59c 100644 --- a/tests/model/lab_test.py +++ b/tests/model/lab_test.py @@ -159,110 +159,116 @@ def test_get_or_new_link_two_cd(default_scenario: Lab): def test_connect_one_machine_to_link(default_scenario: Lab): - result_1 = default_scenario.connect_machine_to_link("pc1", "A") + (machine, interface) = default_scenario.connect_machine_to_link("pc1", "A") assert len(default_scenario.machines) == 1 assert default_scenario.machines['pc1'] assert len(default_scenario.links) == 1 assert default_scenario.links['A'] - assert default_scenario.machines['pc1'].interfaces[0].name == 'A' - assert result_1 == (default_scenario.machines['pc1'], default_scenario.links['A']) + assert default_scenario.machines['pc1'].interfaces[0].link.name == 'A' + assert machine == default_scenario.machines['pc1'] + assert interface == default_scenario.machines['pc1'].interfaces[0] def test_connect_two_machine_to_link(default_scenario: Lab): - result_1 = default_scenario.connect_machine_to_link("pc1", "A") + (machine_a, interface_a) = default_scenario.connect_machine_to_link("pc1", "A") assert len(default_scenario.machines) == 1 assert default_scenario.machines['pc1'] assert len(default_scenario.links) == 1 assert default_scenario.links['A'] - result_2 = default_scenario.connect_machine_to_link("pc2", "A") + (machine_b, interface_b) = default_scenario.connect_machine_to_link("pc2", "A") assert len(default_scenario.machines) == 2 assert default_scenario.machines['pc2'] assert len(default_scenario.links) == 1 assert default_scenario.links['A'] - assert default_scenario.machines['pc1'].interfaces[0].name == 'A' - assert default_scenario.machines['pc2'].interfaces[0].name == 'A' - assert result_1 == (default_scenario.machines['pc1'], default_scenario.links['A']) - assert result_2 == (default_scenario.machines['pc2'], default_scenario.links['A']) + assert default_scenario.machines['pc1'].interfaces[0].link.name == 'A' + assert default_scenario.machines['pc2'].interfaces[0].link.name == 'A' + assert machine_a == default_scenario.machines['pc1'] + assert interface_a == default_scenario.machines['pc1'].interfaces[0] + assert machine_b == default_scenario.machines['pc2'] + assert interface_b == default_scenario.machines['pc2'].interfaces[0] def test_connect_machine_to_two_links(default_scenario: Lab): - result_1 = default_scenario.connect_machine_to_link("pc1", "A") - result_2 = default_scenario.connect_machine_to_link("pc1", "B") + (machine_a1, interface_a1) = default_scenario.connect_machine_to_link("pc1", "A") + (machine_a2, interface_a2) = default_scenario.connect_machine_to_link("pc1", "B") assert len(default_scenario.machines) == 1 assert default_scenario.machines['pc1'] assert len(default_scenario.links) == 2 assert default_scenario.links['A'] assert default_scenario.links['B'] - assert default_scenario.machines['pc1'].interfaces[0].name == 'A' - assert default_scenario.machines['pc1'].interfaces[1].name == 'B' - assert result_1 == (default_scenario.machines['pc1'], default_scenario.links['A']) - assert result_2 == (default_scenario.machines['pc1'], default_scenario.links['B']) + assert default_scenario.machines['pc1'].interfaces[0].link.name == 'A' + assert default_scenario.machines['pc1'].interfaces[1].link.name == 'B' + assert machine_a1 == default_scenario.machines['pc1'] + assert interface_a1 == default_scenario.machines['pc1'].interfaces[0] + assert machine_a2 == default_scenario.machines['pc1'] + assert interface_a2 == default_scenario.machines['pc1'].interfaces[1] def test_connect_machine_to_link_iface_numbers(default_scenario: Lab): - result_1 = default_scenario.connect_machine_to_link("pc1", "A", machine_iface_number=2) + (machine_a, interface_a) = default_scenario.connect_machine_to_link("pc1", "A", + machine_iface_number=2) assert len(default_scenario.machines) == 1 assert default_scenario.machines['pc1'] assert len(default_scenario.links) == 1 assert default_scenario.links['A'] - assert default_scenario.machines['pc1'].interfaces[2].name == 'A' - assert result_1 == (default_scenario.machines['pc1'], default_scenario.links['A']) + assert machine_a == default_scenario.machines['pc1'] + assert interface_a == default_scenario.machines['pc1'].interfaces[2] def test_connect_one_machine_obj_to_link(default_scenario: Lab): pc1 = default_scenario.new_machine("pc1") - result_1 = default_scenario.connect_machine_obj_to_link(pc1, "A") + interface = default_scenario.connect_machine_obj_to_link(pc1, "A") assert len(default_scenario.machines) == 1 assert default_scenario.machines['pc1'] assert len(default_scenario.links) == 1 assert default_scenario.links['A'] - assert default_scenario.machines['pc1'].interfaces[0].name == 'A' - assert result_1 == (default_scenario.links['A'], 0) + assert default_scenario.machines['pc1'].interfaces[0].link.name == 'A' + assert interface == default_scenario.machines['pc1'].interfaces[0] def test_connect_two_machine_obj_to_link(default_scenario: Lab): pc1 = default_scenario.new_machine("pc1") - result_1 = default_scenario.connect_machine_obj_to_link(pc1, "A") + interface_a = default_scenario.connect_machine_obj_to_link(pc1, "A") assert len(default_scenario.machines) == 1 assert default_scenario.machines['pc1'] assert len(default_scenario.links) == 1 assert default_scenario.links['A'] pc2 = default_scenario.new_machine("pc2") - result_2 = default_scenario.connect_machine_obj_to_link(pc2, "A") + interface_b = default_scenario.connect_machine_obj_to_link(pc2, "A") assert len(default_scenario.machines) == 2 assert default_scenario.machines['pc2'] assert len(default_scenario.links) == 1 assert default_scenario.links['A'] - assert default_scenario.machines['pc1'].interfaces[0].name == 'A' - assert default_scenario.machines['pc2'].interfaces[0].name == 'A' - assert result_1 == (default_scenario.links['A'], 0) - assert result_2 == (default_scenario.links['A'], 0) + assert default_scenario.machines['pc1'].interfaces[0].link.name == 'A' + assert default_scenario.machines['pc2'].interfaces[0].link.name == 'A' + assert interface_a == default_scenario.machines['pc1'].interfaces[0] + assert interface_b == default_scenario.machines['pc2'].interfaces[0] def test_connect_machine_obj_to_two_links(default_scenario: Lab): pc1 = default_scenario.new_machine("pc1") - result_1 = default_scenario.connect_machine_obj_to_link(pc1, "A") - result_2 = default_scenario.connect_machine_obj_to_link(pc1, "B") + interface_a = default_scenario.connect_machine_obj_to_link(pc1, "A") + interface_b = default_scenario.connect_machine_obj_to_link(pc1, "B") assert len(default_scenario.machines) == 1 assert default_scenario.machines['pc1'] assert len(default_scenario.links) == 2 assert default_scenario.links['A'] assert default_scenario.links['B'] - assert default_scenario.machines['pc1'].interfaces[0].name == 'A' - assert default_scenario.machines['pc1'].interfaces[1].name == 'B' - assert result_1 == (default_scenario.links['A'], 0) - assert result_2 == (default_scenario.links['B'], 1) + assert default_scenario.machines['pc1'].interfaces[0].link.name == 'A' + assert default_scenario.machines['pc1'].interfaces[1].link.name == 'B' + assert interface_a == default_scenario.machines['pc1'].interfaces[0] + assert interface_b == default_scenario.machines['pc1'].interfaces[1] def test_connect_machine_obj_to_link_iface_numbers(default_scenario: Lab): pc1 = default_scenario.new_machine("pc1") - result_1 = default_scenario.connect_machine_obj_to_link(pc1, "A", machine_iface_number=2) + interface = default_scenario.connect_machine_obj_to_link(pc1, "A", machine_iface_number=2) assert len(default_scenario.machines) == 1 assert default_scenario.machines['pc1'] assert len(default_scenario.links) == 1 assert default_scenario.links['A'] - assert default_scenario.machines['pc1'].interfaces[2].name == 'A' - assert result_1 == (default_scenario.links['A'], None) + assert default_scenario.machines['pc1'].interfaces[2].link.name == 'A' + assert interface == default_scenario.machines['pc1'].interfaces[2] def test_assign_meta_to_machine(default_scenario: Lab): diff --git a/tests/model/machine_test.py b/tests/model/machine_test.py index 360a75b4..aeda34aa 100644 --- a/tests/model/machine_test.py +++ b/tests/model/machine_test.py @@ -39,17 +39,17 @@ def test_default_device_parameters(default_device: Machine): # TEST: add_interface # def test_add_interface(default_device: Machine): - result = default_device.add_interface(Link(default_device.lab, "A")) + interface = default_device.add_interface(Link(default_device.lab, "A")) assert len(default_device.interfaces) == 1 - assert default_device.interfaces[0].name == "A" - assert result == 0 + assert default_device.interfaces[0].link.name == "A" + assert interface == default_device.interfaces[0] def test_add_interface_with_number(default_device: Machine): - result = default_device.add_interface(Link(default_device.lab, "A"), number=2) + interface = default_device.add_interface(Link(default_device.lab, "A"), number=2) assert len(default_device.interfaces) == 1 - assert default_device.interfaces[2].name == "A" - assert result is None + assert default_device.interfaces[2].link.name == "A" + assert interface == default_device.interfaces[2] def test_add_interface_exception(default_device: Machine): @@ -73,7 +73,7 @@ def test_remove_interface(default_device: Machine): link = Link(default_device.lab, "A") default_device.add_interface(link) assert len(default_device.interfaces) == 1 - assert default_device.interfaces[0].name == "A" + assert default_device.interfaces[0].link.name == "A" assert default_device.name in link.machines default_device.remove_interface(link) assert len(default_device.interfaces) == 1 @@ -89,14 +89,14 @@ def test_remove_interface_one(default_device: Machine): default_device.add_interface(link_b) default_device.add_interface(link_c) assert len(default_device.interfaces) == 3 - assert default_device.interfaces[0].name == "A" - assert default_device.interfaces[1].name == "B" - assert default_device.interfaces[2].name == "C" + assert default_device.interfaces[0].link.name == "A" + assert default_device.interfaces[1].link.name == "B" + assert default_device.interfaces[2].link.name == "C" default_device.remove_interface(link_a) assert len(default_device.interfaces) == 3 assert default_device.interfaces[0] is None - assert default_device.interfaces[1].name == "B" - assert default_device.interfaces[2].name == "C" + assert default_device.interfaces[1].link.name == "B" + assert default_device.interfaces[2].link.name == "C" assert default_device.name not in link_a.machines @@ -110,8 +110,8 @@ def test_add_remove_add_interface(default_device: Machine): link = Link(default_device.lab, "A") default_device.add_interface(link) default_device.remove_interface(link) - default_device.add_interface(link) - assert default_device.interfaces[1] == link + interface = default_device.add_interface(link) + assert default_device.interfaces[1] == interface def test_add_remove_three_interfaces(default_device: Machine): @@ -126,8 +126,8 @@ def test_add_remove_three_interfaces(default_device: Machine): default_device.remove_interface(link_c) - default_device.add_interface(link_d) - assert default_device.interfaces[3] == link_d + interface = default_device.add_interface(link_d) + assert default_device.interfaces[3] == interface # diff --git a/tests/parser/lab_parser_test.py b/tests/parser/lab_parser_test.py index e1ef9db6..3941695a 100644 --- a/tests/parser/lab_parser_test.py +++ b/tests/parser/lab_parser_test.py @@ -2,6 +2,7 @@ import pytest +from src.Kathara.exceptions import InterfaceMacAddressError from src.Kathara.exceptions import MachineCollisionDomainError from src.Kathara.parser.netkit.LabParser import LabParser @@ -14,8 +15,8 @@ def test_one_device(): assert len(lab.links) == 2 assert lab.machines['pc1'] assert len(lab.machines['pc1'].interfaces) == 2 - assert lab.machines['pc1'].interfaces[0].name == 'A' - assert lab.machines['pc1'].interfaces[1].name == 'B' + assert lab.machines['pc1'].interfaces[0].link.name == 'A' + assert lab.machines['pc1'].interfaces[1].link.name == 'B' assert lab.machines['pc1'].meta['privileged'] assert (1000, 'udp') in lab.machines['pc1'].meta['ports'] assert lab.machines['pc1'].meta['ports'][(1000, 'udp')] == 2000 @@ -56,10 +57,10 @@ def test_two_device_one_cd(): assert len(lab.links) == 1 assert lab.machines['pc1'] assert len(lab.machines['pc1'].interfaces) == 1 - assert lab.machines['pc1'].interfaces[0].name == 'A' + assert lab.machines['pc1'].interfaces[0].link.name == 'A' assert lab.machines['pc2'] assert len(lab.machines['pc2'].interfaces) == 1 - assert lab.machines['pc2'].interfaces[0].name == 'A' + assert lab.machines['pc2'].interfaces[0].link.name == 'A' def test_inline_comment(): @@ -68,7 +69,7 @@ def test_inline_comment(): assert len(lab.links) == 1 assert lab.machines['pc1'] assert len(lab.machines['pc1'].interfaces) == 1 - assert lab.machines['pc1'].interfaces[0].name == 'A' + assert lab.machines['pc1'].interfaces[0].link.name == 'A' def test_inline_comment_error(): @@ -84,3 +85,23 @@ def test_unmatched_quotes(): def test_unclosed_quotes(): with pytest.raises(SyntaxError): LabParser.parse("tests/parser/labconf/unclosed_quotes") + + +def test_mac_address(): + lab = LabParser.parse("tests/parser/labconf/mac_address") + + assert lab.machines["pc1"].interfaces[0].mac_address == "00:00:00:00:00:01" + assert lab.machines["pc1"].interfaces[0].link.name == "A" + + assert lab.machines["pc2"].interfaces[0].mac_address is None + assert lab.machines["pc2"].interfaces[0].link.name == "A" + + +def test_mac_address_error(): + with pytest.raises(InterfaceMacAddressError): + LabParser.parse("tests/parser/labconf/mac_address_error") + + +def test_mac_address_parse_error(): + with pytest.raises(SyntaxError): + LabParser.parse("tests/parser/labconf/mac_address_parse_error") diff --git a/tests/parser/labconf/mac_address/lab.conf b/tests/parser/labconf/mac_address/lab.conf new file mode 100644 index 00000000..17e1ae26 --- /dev/null +++ b/tests/parser/labconf/mac_address/lab.conf @@ -0,0 +1,3 @@ +pc1[0]="A/00:00:00:00:00:01" + +pc2[0]="A" \ No newline at end of file diff --git a/tests/parser/labconf/mac_address_error/lab.conf b/tests/parser/labconf/mac_address_error/lab.conf new file mode 100644 index 00000000..12621ee4 --- /dev/null +++ b/tests/parser/labconf/mac_address_error/lab.conf @@ -0,0 +1,3 @@ +pc1[0]="A/00:00:00:00:00" + +pc2[0]="A" \ No newline at end of file diff --git a/tests/parser/labconf/mac_address_parse_error/lab.conf b/tests/parser/labconf/mac_address_parse_error/lab.conf new file mode 100644 index 00000000..93f38ae7 --- /dev/null +++ b/tests/parser/labconf/mac_address_parse_error/lab.conf @@ -0,0 +1,3 @@ +pc1[0]="A/0/0" + +pc2[0]="A" \ No newline at end of file From 07eb98dda9be2fc32ebefeeba3f2122c48a80b49 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Mon, 18 Dec 2023 17:51:38 +0100 Subject: [PATCH 13/44] Fix lconfig and vconfig (#137) Co-Authored-By: Tommaso Caiazzi --- src/Kathara/cli/command/LconfigCommand.py | 18 +++++++----- src/Kathara/cli/command/VconfigCommand.py | 2 +- tests/cli/lconfig_command_test.py | 35 +++++++++++++++++------ tests/cli/vconfig_command_test.py | 15 ++++++++-- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/Kathara/cli/command/LconfigCommand.py b/src/Kathara/cli/command/LconfigCommand.py index e925aaf4..d77fa0a8 100644 --- a/src/Kathara/cli/command/LconfigCommand.py +++ b/src/Kathara/cli/command/LconfigCommand.py @@ -2,8 +2,8 @@ import logging from typing import List -from ... import utils from ..ui.utils import alphanumeric +from ... import utils from ...foundation.cli.command.Command import Command from ...manager.Kathara import Kathara from ...model.Lab import Lab @@ -77,15 +77,19 @@ def run(self, current_path: str, argv: List[str]) -> None: device = lab.get_machine(machine_name) if args['to_add']: - for cd in args['to_add']: + for cd_to_add in args['to_add']: + cd_name, mac_address = utils.parse_cd_mac_address(cd_to_add) logging.info( - "Adding interface to device `%s` on collision domain `%s`..." % (machine_name, cd) + f"Adding interface to device `{machine_name}` on collision domain `{cd_name}`" + + (f" with MAC Address {mac_address}" if mac_address else "") + + f"..." ) - Kathara.get_instance().connect_machine_to_link(device, lab.get_or_new_link(cd)) + link = lab.get_or_new_link(cd_name) + Kathara.get_instance().connect_machine_to_link(device, link, mac_address=mac_address) if args['to_remove']: - for cd in args['to_remove']: + for cd_to_remove in args['to_remove']: logging.info( - "Removing interface on collision domain `%s` from device `%s`..." % (cd, machine_name) + "Removing interface on collision domain `%s` from device `%s`..." % (cd_to_remove, machine_name) ) - Kathara.get_instance().disconnect_machine_from_link(device, lab.get_or_new_link(cd)) + Kathara.get_instance().disconnect_machine_from_link(device, lab.get_link(cd_to_remove)) diff --git a/src/Kathara/cli/command/VconfigCommand.py b/src/Kathara/cli/command/VconfigCommand.py index 2b069e04..5077bfde 100644 --- a/src/Kathara/cli/command/VconfigCommand.py +++ b/src/Kathara/cli/command/VconfigCommand.py @@ -60,7 +60,7 @@ def run(self, current_path: str, argv: List[str]) -> None: lab = Lab("kathara_vlab") machine_name = args['name'] - device = lab.get_or_new_machine(machine_name) + device = lab.get_machine(machine_name) device.api_object = Kathara.get_instance().get_machine_api_object(machine_name, lab_name=lab.name) if args['to_add']: diff --git a/tests/cli/lconfig_command_test.py b/tests/cli/lconfig_command_test.py index 2ea8d411..3d3a568a 100644 --- a/tests/cli/lconfig_command_test.py +++ b/tests/cli/lconfig_command_test.py @@ -28,44 +28,63 @@ def test_lab(): @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") @mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") def test_run_add_link(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): + test_lab.connect_machine_to_link("pc1", "A") + mock_parse_lab.return_value = test_lab mock_manager_get_instance.return_value = mock_docker_manager command = LconfigCommand() command.run('.', ['-n', 'pc1', '--add', 'A']) mock_parse_lab.assert_called_once_with(os.getcwd()) mock_docker_manager.update_lab_from_api.assert_called_once_with(test_lab) - mock_docker_manager.connect_machine_to_link.assert_called_once_with(test_lab.get_or_new_machine('pc1'), - test_lab.get_or_new_link('A')) + mock_docker_manager.connect_machine_to_link.assert_called_once_with( + test_lab.get_machine('pc1'), + test_lab.get_machine("pc1").interfaces[0].link, + mac_address=None + ) @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") @mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") def test_run_add_link_with_directory(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): + test_lab.connect_machine_to_link("pc1", "A") + mock_parse_lab.return_value = test_lab mock_manager_get_instance.return_value = mock_docker_manager command = LconfigCommand() command.run('.', ['-d', os.path.join('/test', 'path'), '-n', 'pc1', '--add', 'A']) mock_parse_lab.assert_called_once_with(os.path.abspath(os.path.join('/test', 'path'))) mock_docker_manager.update_lab_from_api.assert_called_once_with(test_lab) - mock_docker_manager.connect_machine_to_link.assert_called_once_with(test_lab.get_or_new_machine('pc1'), - test_lab.get_or_new_link('A')) + mock_docker_manager.connect_machine_to_link.assert_called_once_with( + test_lab.get_machine('pc1'), + test_lab.get_machine("pc1").interfaces[0].link, + mac_address=None + ) @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") @mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") def test_run_add_two_links(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): + test_lab.connect_machine_to_link("pc1", "A") + test_lab.connect_machine_to_link("pc1", "B") + mock_parse_lab.return_value = test_lab mock_manager_get_instance.return_value = mock_docker_manager command = LconfigCommand() command.run('.', ['-n', 'pc1', '--add', 'A', 'B']) mock_parse_lab.assert_called_once_with(os.getcwd()) mock_docker_manager.update_lab_from_api.assert_called_once_with(test_lab) - mock_docker_manager.connect_machine_to_link.assert_any_call(test_lab.get_or_new_machine('pc1'), - test_lab.get_or_new_link('A')) - mock_docker_manager.connect_machine_to_link.assert_any_call(test_lab.get_or_new_machine('pc1'), - test_lab.get_or_new_link('B')) + mock_docker_manager.connect_machine_to_link.assert_any_call( + test_lab.get_machine('pc1'), + test_lab.get_machine("pc1").interfaces[0].link, + mac_address=None + ) + mock_docker_manager.connect_machine_to_link.assert_any_call( + test_lab.get_machine('pc1'), + test_lab.get_machine("pc1").interfaces[1].link, + mac_address=None + ) @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") diff --git a/tests/cli/vconfig_command_test.py b/tests/cli/vconfig_command_test.py index 6b83782b..2f5574d9 100644 --- a/tests/cli/vconfig_command_test.py +++ b/tests/cli/vconfig_command_test.py @@ -4,21 +4,30 @@ sys.path.insert(0, './') from src.Kathara.cli.command.VconfigCommand import VconfigCommand +from src.Kathara.model.Lab import Lab +@mock.patch("src.Kathara.model.Lab.Lab.get_machine") @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") -def test_run_add_link(mock_docker_manager, mock_manager_get_instance): +def test_run_add_link(mock_docker_manager, mock_manager_get_instance, mock_get_machine): + lab = Lab('kathara_vlab') + pc1 = lab.new_machine("pc1") + mock_get_machine.return_value = pc1 mock_manager_get_instance.return_value = mock_docker_manager command = VconfigCommand() command.run('.', ['-n', 'pc1', '--add', 'A']) mock_docker_manager.get_machine_api_object.assert_called_once_with('pc1', lab_name='kathara_vlab') mock_docker_manager.connect_machine_to_link.assert_called_once() - +@mock.patch("src.Kathara.model.Lab.Lab.get_machine") @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") -def test_run_remove_link(mock_docker_manager, mock_manager_get_instance): +def test_run_remove_link(mock_docker_manager, mock_manager_get_instance, mock_get_machine): + lab = Lab('kathara_vlab') + pc1 = lab.new_machine("pc1") + lab.new_link("A") + mock_get_machine.return_value = pc1 mock_manager_get_instance.return_value = mock_docker_manager command = VconfigCommand() command.run('.', ['-n', 'pc1', '--rm', 'A']) From d0c2438559f1892d8e43bd6181cc56b91daecf81 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Tue, 19 Dec 2023 10:47:22 +0100 Subject: [PATCH 14/44] Add tests + Add MAC Address in reconstructed lab (#137) --- src/Kathara/cli/command/VconfigCommand.py | 2 +- src/Kathara/manager/Kathara.py | 2 +- src/Kathara/manager/docker/DockerManager.py | 31 ++- src/Kathara/model/Machine.py | 2 +- tests/cli/vconfig_command_test.py | 1 + tests/manager/docker/docker_machine_test.py | 217 +++++++++++++++++++- tests/manager/docker/docker_manager_test.py | 62 +++++- tests/model/interface_test.py | 84 ++++++++ tests/model/lab_test.py | 54 +++++ tests/model/machine_test.py | 17 ++ 10 files changed, 448 insertions(+), 24 deletions(-) create mode 100644 tests/model/interface_test.py diff --git a/src/Kathara/cli/command/VconfigCommand.py b/src/Kathara/cli/command/VconfigCommand.py index 5077bfde..a2fb6806 100644 --- a/src/Kathara/cli/command/VconfigCommand.py +++ b/src/Kathara/cli/command/VconfigCommand.py @@ -60,7 +60,7 @@ def run(self, current_path: str, argv: List[str]) -> None: lab = Lab("kathara_vlab") machine_name = args['name'] - device = lab.get_machine(machine_name) + device = lab.new_machine(machine_name) device.api_object = Kathara.get_instance().get_machine_api_object(machine_name, lab_name=lab.name) if args['to_add']: diff --git a/src/Kathara/manager/Kathara.py b/src/Kathara/manager/Kathara.py index 143f0c2a..549458ce 100644 --- a/src/Kathara/manager/Kathara.py +++ b/src/Kathara/manager/Kathara.py @@ -103,7 +103,7 @@ def connect_machine_to_link(self, machine: Machine, link: Link, mac_address: Opt LabNotFoundError: If the collision domain is not associated to any network scenario. MachineCollisionDomainConflictError: If the device is already connected to the collision domain. """ - self.manager.connect_machine_to_link(machine, link, mac_address=mac_address) + self.manager.connect_machine_to_link(machine, link, mac_address) def disconnect_machine_from_link(self, machine: Machine, link: Link) -> None: """Disconnect a Kathara device from a collision domain. diff --git a/src/Kathara/manager/docker/DockerManager.py b/src/Kathara/manager/docker/DockerManager.py index bac2007c..224968f4 100644 --- a/src/Kathara/manager/docker/DockerManager.py +++ b/src/Kathara/manager/docker/DockerManager.py @@ -552,7 +552,7 @@ def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab: device.meta["sysctls"] = container.attrs["HostConfig"]["Sysctls"] if "none" not in container.attrs["NetworkSettings"]["Networks"]: - for network_name in container.attrs["NetworkSettings"]["Networks"]: + for network_name, network_options in container.attrs["NetworkSettings"]["Networks"].items(): if network_name == "bridge": device.add_meta("bridged", True) continue @@ -560,7 +560,13 @@ def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab: network = lab_networks[network_name] link = reconstructed_lab.get_or_new_link(network.attrs["Labels"]["name"]) link.api_object = network - device.add_interface(link) + + iface_mac_addr = None + if network_options["DriverOpts"] is not None: + if "kathara.mac_addr" in network_options["DriverOpts"]: + iface_mac_addr = network_options["DriverOpts"]["kathara.mac_addr"] + + device.add_interface(link, mac_address=iface_mac_addr) return reconstructed_lab @@ -590,11 +596,15 @@ def update_lab_from_api(self, lab: Lab) -> None: # Collision domains declared in the network scenario static_links = set([x.link for x in device.interfaces.values()]) + # Interfaces currently attached to the device + current_ifaces = [ + (lab.get_or_new_link(deployed_networks[name].attrs["Labels"]["name"]), options) + for name, options in container.attrs["NetworkSettings"]["Networks"].items() + if name != "bridge" + ] + # Collision domains currently attached to the device - current_links = set( - map(lambda x: lab.get_or_new_link(deployed_networks[x].attrs["Labels"]["name"]), - filter(lambda x: x != "bridge", container.attrs["NetworkSettings"]["Networks"])) - ) + current_links = set(map(lambda x: x[0], current_ifaces)) # Collision domains attached at runtime to the device dynamic_links = current_links - static_links # Static collision domains detached at runtime from the device @@ -604,9 +614,16 @@ def update_lab_from_api(self, lab: Lab) -> None: if link.name in deployed_networks_by_link_name: link.api_object = deployed_networks_by_link_name[link.name] + current_ifaces = dict([(x[0].name, x[1]) for x in current_ifaces]) for link in dynamic_links: link.api_object = deployed_networks_by_link_name[link.name] - device.add_interface(link) + iface_options = current_ifaces[link.name] + iface_mac_addr = None + if iface_options["DriverOpts"] is not None: + if "kathara.mac_addr" in iface_options["DriverOpts"]: + iface_mac_addr = iface_options["DriverOpts"]["kathara.mac_addr"] + + device.add_interface(link, mac_address=iface_mac_addr) for link in deleted_links: device.remove_interface(link) diff --git a/src/Kathara/model/Machine.py b/src/Kathara/model/Machine.py index 1f4fde26..78827aa4 100644 --- a/src/Kathara/model/Machine.py +++ b/src/Kathara/model/Machine.py @@ -680,7 +680,7 @@ def __str__(self) -> str: if interface: formatted_machine += f"\n\t- {iface_num}: {interface.link.name}" if interface.mac_address: - formatted_machine += f" ({interface.mac_address})" + formatted_machine += f" (MAC Address: {interface.mac_address})" formatted_machine += f"\nBridged Connection: {self.meta['bridged']}" diff --git a/tests/cli/vconfig_command_test.py b/tests/cli/vconfig_command_test.py index 2f5574d9..8e4cd804 100644 --- a/tests/cli/vconfig_command_test.py +++ b/tests/cli/vconfig_command_test.py @@ -20,6 +20,7 @@ def test_run_add_link(mock_docker_manager, mock_manager_get_instance, mock_get_m mock_docker_manager.get_machine_api_object.assert_called_once_with('pc1', lab_name='kathara_vlab') mock_docker_manager.connect_machine_to_link.assert_called_once() + @mock.patch("src.Kathara.model.Lab.Lab.get_machine") @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") diff --git a/tests/manager/docker/docker_machine_test.py b/tests/manager/docker/docker_machine_test.py index 9d6e05f0..f4336508 100644 --- a/tests/manager/docker/docker_machine_test.py +++ b/tests/manager/docker/docker_machine_test.py @@ -55,6 +55,14 @@ def default_link_b(default_device): return link +@pytest.fixture() +def default_link_c(default_device): + link = Link(default_device.lab, "C") + link.api_object = Mock() + link.api_object.connect = Mock(return_value=True) + return link + + # # TEST: create # @@ -220,6 +228,137 @@ def test_create_privileged(mock_get_current_user_name, mock_setting_get_instance assert not mock_copy_files.called +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters") +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.copy_files") +@mock.patch("src.Kathara.setting.Setting.Setting.get_instance") +@mock.patch("src.Kathara.utils.get_current_user_name") +def test_create_interface(mock_get_current_user_name, mock_setting_get_instance, mock_copy_files, + mock_get_machines_api_objects_by_filters, docker_machine, default_device): + class LinkApiObj: + def __init__(self, name): + self.name = name + + link = Link(default_device.lab, "A") + link.api_object = LinkApiObj("link_a") + + default_device.add_interface(link, 0) + + docker_machine.client.api.create_endpoint_config.return_value = {} + mock_get_machines_api_objects_by_filters.return_value = [] + mock_get_current_user_name.return_value = "test-user" + + setting_mock = Mock() + setting_mock.configure_mock(**{ + 'shared_cd': False, + 'device_prefix': 'dev_prefix', + "device_shell": '/bin/bash', + 'enable_ipv6': False, + 'remote_url': None, + 'hosthome_mount': False, + 'shared_mount': False + }) + mock_setting_get_instance.return_value = setting_mock + docker_machine.create(default_device) + docker_machine.client.containers.create.assert_called_once_with( + image='kathara/test', + name='dev_prefix_test-user_test_device_9pe3y6IDMwx4PfOPu5mbNg', + hostname='test_device', + cap_add=['NET_ADMIN', 'NET_RAW', 'NET_BROADCAST', 'NET_BIND_SERVICE', 'SYS_ADMIN'], + privileged=False, + network="link_a", + network_mode='bridge', + networking_config={"link_a": {}}, + sysctls={'net.ipv4.conf.all.rp_filter': 0, + 'net.ipv4.conf.default.rp_filter': 0, + 'net.ipv4.conf.lo.rp_filter': 0, + 'net.ipv4.conf.eth0.rp_filter': 0, + 'net.ipv4.ip_forward': 1, + 'net.ipv4.icmp_ratelimit': 0, + }, + environment={}, + mem_limit='64m', + nano_cpus=2000000000, + ports=None, + tty=True, + stdin_open=True, + detach=True, + volumes={}, + labels={'name': 'test_device', 'lab_hash': '9pe3y6IDMwx4PfOPu5mbNg', 'user': 'test-user', 'app': 'kathara', + 'shell': '/bin/bash'} + ) + + assert not mock_copy_files.called + + +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters") +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.copy_files") +@mock.patch("src.Kathara.setting.Setting.Setting.get_instance") +@mock.patch("src.Kathara.utils.get_current_user_name") +def test_create_interface_mac_addr(mock_get_current_user_name, mock_setting_get_instance, mock_copy_files, + mock_get_machines_api_objects_by_filters, docker_machine, default_device): + class LinkApiObj: + def __init__(self, name): + self.name = name + + link = Link(default_device.lab, "A") + link.api_object = LinkApiObj("link_a") + + expected_mac_addr = "00:00:00:00:ff:ff" + default_device.add_interface(link, 0, expected_mac_addr) + + docker_machine.client.api.create_endpoint_config.return_value = { + 'driver_opt': {'kathara.mac_addr': expected_mac_addr} + } + mock_get_machines_api_objects_by_filters.return_value = [] + mock_get_current_user_name.return_value = "test-user" + + setting_mock = Mock() + setting_mock.configure_mock(**{ + 'shared_cd': False, + 'device_prefix': 'dev_prefix', + "device_shell": '/bin/bash', + 'enable_ipv6': False, + 'remote_url': None, + 'hosthome_mount': False, + 'shared_mount': False + }) + mock_setting_get_instance.return_value = setting_mock + docker_machine.create(default_device) + + docker_machine.client.api.create_endpoint_config.assert_called_once_with( + driver_opt={'kathara.mac_addr': expected_mac_addr} + ) + docker_machine.client.containers.create.assert_called_once_with( + image='kathara/test', + name='dev_prefix_test-user_test_device_9pe3y6IDMwx4PfOPu5mbNg', + hostname='test_device', + cap_add=['NET_ADMIN', 'NET_RAW', 'NET_BROADCAST', 'NET_BIND_SERVICE', 'SYS_ADMIN'], + privileged=False, + network="link_a", + network_mode='bridge', + networking_config={"link_a": {'driver_opt': {'kathara.mac_addr': expected_mac_addr}}}, + sysctls={'net.ipv4.conf.all.rp_filter': 0, + 'net.ipv4.conf.default.rp_filter': 0, + 'net.ipv4.conf.lo.rp_filter': 0, + 'net.ipv4.conf.eth0.rp_filter': 0, + 'net.ipv4.ip_forward': 1, + 'net.ipv4.icmp_ratelimit': 0, + }, + environment={}, + mem_limit='64m', + nano_cpus=2000000000, + ports=None, + tty=True, + stdin_open=True, + detach=True, + volumes={}, + labels={'name': 'test_device', 'lab_hash': '9pe3y6IDMwx4PfOPu5mbNg', 'user': 'test-user', 'app': 'kathara', + 'shell': '/bin/bash'} + ) + + assert not mock_copy_files.called + + # # TEST: start # @@ -242,6 +381,58 @@ def test_start(docker_machine, default_device, default_link, default_link_b): default_link_b.api_object.connect.assert_called_once() +def test_start_one_mac_addr(docker_machine, default_device, default_link, default_link_b): + expected_mac_addr = "00:00:00:00:ee:ee" + + default_device.add_interface(default_link) + default_device.add_interface(default_link_b, mac_address=expected_mac_addr) + default_device.add_meta("num_terms", 3) + docker_machine.client.api.exec_create.return_value = {"Id": "1234"} + docker_machine.client.api.exec_start.return_value = ("cmd_stdout", "cmd_stderr") + docker_machine.client.api.exec_inspect.return_value = {"ExitCode": 0} + default_device.api_object.start.return_value = True + + docker_machine.start(default_device) + + default_device.api_object.start.assert_called_once() + docker_machine.client.api.exec_create.assert_called_once() + docker_machine.client.api.exec_start.assert_called_once() + docker_machine.client.api.exec_inspect.assert_called_once() + default_link_b.api_object.connect.assert_called_once_with( + default_device.api_object, + driver_opt={'kathara.mac_addr': expected_mac_addr} + ) + + +def test_start_two_mac_addr(docker_machine, default_device, default_link, default_link_b, default_link_c): + expected_mac_addr_1 = "00:00:00:00:ee:ee" + expected_mac_addr_2 = "00:00:00:00:ee:ee" + + default_device.add_interface(default_link) + default_device.add_interface(default_link_b, mac_address=expected_mac_addr_1) + default_device.add_interface(default_link_c, mac_address=expected_mac_addr_2) + default_device.add_meta("num_terms", 3) + docker_machine.client.api.exec_create.return_value = {"Id": "1234"} + docker_machine.client.api.exec_start.return_value = ("cmd_stdout", "cmd_stderr") + docker_machine.client.api.exec_inspect.return_value = {"ExitCode": 0} + default_device.api_object.start.return_value = True + + docker_machine.start(default_device) + + default_device.api_object.start.assert_called_once() + docker_machine.client.api.exec_create.assert_called_once() + docker_machine.client.api.exec_start.assert_called_once() + docker_machine.client.api.exec_inspect.assert_called_once() + default_link_b.api_object.connect.assert_called_once_with( + default_device.api_object, + driver_opt={'kathara.mac_addr': expected_mac_addr_1} + ) + default_link_c.api_object.connect.assert_called_once_with( + default_device.api_object, + driver_opt={'kathara.mac_addr': expected_mac_addr_2} + ) + + def test_start_plugin_error_endpoint_start(default_device, docker_machine): default_device.api_object.start.side_effect = DockerPluginError("endpoint does not exists") with pytest.raises(DockerPluginError): @@ -313,9 +504,9 @@ def test_deploy_machines(mock_deploy_and_start, mock_setting_get_instance, docke # -# TEST: connect_to_link +# TEST: connect_interface # -def test_connect_to_link(docker_machine, default_device, default_link, default_link_b): +def test_connect_interface(docker_machine, default_device, default_link, default_link_b): default_device.api_object.attrs["NetworkSettings"] = {} default_device.api_object.attrs["NetworkSettings"]["Networks"] = ["A"] default_link.api_object.name = "A" @@ -329,14 +520,32 @@ def test_connect_to_link(docker_machine, default_device, default_link, default_l default_link_b.api_object.connect.assert_called_once() -def test_connect_to_link_plugin_error_network(default_device, default_link, docker_machine): +def test_connect_interface_mac_addr(docker_machine, default_device, default_link, default_link_b): + default_device.api_object.attrs["NetworkSettings"] = {} + default_device.api_object.attrs["NetworkSettings"]["Networks"] = ["A"] + default_link.api_object.name = "A" + + expected_mac_addr = "00:00:00:00:ff:ff" + default_device.add_interface(default_link) + interface = default_device.add_interface(default_link_b, mac_address=expected_mac_addr) + + docker_machine.connect_interface(default_device, interface) + + assert not default_link.api_object.connect.called + default_link_b.api_object.connect.assert_called_once_with( + default_device.api_object, + driver_opt={'kathara.mac_addr': expected_mac_addr} + ) + + +def test_connect_interface_plugin_error_network(default_device, default_link, docker_machine): interface = default_device.add_interface(default_link) default_link.api_object.connect.side_effect = DockerPluginError("network does not exists") with pytest.raises(DockerPluginError): docker_machine.connect_interface(default_device, interface) -def test_connect_to_link_plugin_error_endpoint(default_device, default_link, docker_machine): +def test_connect_interface_plugin_error_endpoint(default_device, default_link, docker_machine): interface = default_device.add_interface(default_link) default_link.api_object.connect.side_effect = DockerPluginError("endpoint does not exists") with pytest.raises(DockerPluginError): diff --git a/tests/manager/docker/docker_manager_test.py b/tests/manager/docker/docker_manager_test.py index 382ce1c3..473e9800 100644 --- a/tests/manager/docker/docker_manager_test.py +++ b/tests/manager/docker/docker_manager_test.py @@ -10,7 +10,6 @@ from src.Kathara.model.Lab import Lab from src.Kathara.model.Machine import Machine from src.Kathara.model.Link import Link -from src.Kathara.model.Interface import Interface from src.Kathara.utils import generate_urlsafe_hash from src.Kathara.manager.docker.stats.DockerLinkStats import DockerLinkStats from src.Kathara.manager.docker.stats.DockerMachineStats import DockerMachineStats @@ -139,8 +138,11 @@ def docker_container(mock_container): "Networks": { "kathara_user_hash_test_network": { "Links": None, + "DriverOpts": { + "kathara.mac_addr": "00:00:00:00:00:01" + } }, - "bridge": {} + "bridge": {}, } }} @@ -285,6 +287,20 @@ def test_connect_machine_to_link_two_links(mock_connect_interface_machine, mock_ assert mock_connect_interface_machine.call_count == 2 +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.deploy_link") +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.connect_interface") +def test_connect_machine_to_link_mac_address(mock_connect_interface_machine, mock_deploy_link, docker_manager, + default_device, default_link): + expected_mac_address = "00:00:00:00:ff:ff" + docker_manager.connect_machine_to_link(default_device, default_link, mac_address=expected_mac_address) + + interface = default_device.interfaces[0] + + assert interface.mac_address == expected_mac_address + mock_deploy_link.assert_called_once_with(default_link) + mock_connect_interface_machine.assert_called_once_with(default_device, interface) + + def test_connect_machine_to_link_no_machine_lab(docker_manager, default_device, default_link): default_device.lab = None @@ -924,6 +940,8 @@ def test_get_lab_from_api_lab_name_all_info(mock_get_links_api_objects, mock_get assert reconstructed_device.meta["envs"]["test"] == "path" assert reconstructed_device.meta["ports"][(3000, "udp")] == 55 assert reconstructed_device.meta["sysctls"]["sysctl.test"] == "0" + assert reconstructed_device.meta["bridged"] + assert reconstructed_device.interfaces[0].mac_address == "00:00:00:00:00:01" assert len(lab.links) == 1 assert docker_network.attrs["Labels"]["name"] in lab.links @@ -949,6 +967,7 @@ def test_get_lab_from_api_lab_hash_all_info(mock_get_links_api_objects, mock_get assert reconstructed_device.meta["ports"][(3000, "udp")] == 55 assert reconstructed_device.meta["sysctls"]["sysctl.test"] == "0" assert reconstructed_device.meta["bridged"] + assert reconstructed_device.interfaces[0].mac_address == "00:00:00:00:00:01" assert len(lab.links) == 1 assert docker_network.attrs["Labels"]["name"] in lab.links @@ -988,18 +1007,31 @@ def test_update_lab_from_api_add_link(mock_get_links_api_objects, mock_get_machi docker_network, docker_network_b, docker_manager): lab = Lab("test") device = lab.get_or_new_machine(docker_container.labels["name"]) - link = Link(lab, docker_network.attrs["Labels"]["name"]) - interface = device.add_interface(link) + link = lab.new_link(docker_network.attrs["Labels"]["name"]) + device.add_interface(link) mock_get_machines_api_objects.return_value = [docker_container] mock_get_links_api_objects.return_value = [docker_network, docker_network_b] - docker_container.attrs["NetworkSettings"]["Networks"] = ["kathara_user_hash_test_network", - "kathara_user_hash_test_network_b"] + docker_container.attrs["NetworkSettings"]["Networks"] = { + "kathara_user_hash_test_network": { + "Links": None, + "DriverOpts": None + }, + "kathara_user_hash_test_network_b": { + "Links": None, + "DriverOpts": { + "kathara.mac_addr": "00:00:00:00:00:02" + } + } + } + docker_manager.update_lab_from_api(lab) assert len(lab.machines) == 1 assert docker_container.labels["name"] in lab.machines assert len(lab.links) == 2 assert docker_network.attrs["Labels"]["name"] in lab.links assert docker_network_b.attrs["Labels"]["name"] in lab.links + assert lab.machines[docker_container.labels["name"]].interfaces[0].mac_address is None + assert lab.machines[docker_container.labels["name"]].interfaces[1].mac_address == "00:00:00:00:00:02" @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_api_objects") @@ -1008,16 +1040,18 @@ def test_update_lab_from_api_remove_link(mock_get_links_api_objects, mock_get_ma docker_network, docker_manager): lab = Lab("test") device = lab.get_or_new_machine(docker_container.labels["name"]) - link = Link(lab, docker_network.attrs["Labels"]["name"]) + link = lab.new_link(docker_network.attrs["Labels"]["name"]) device.add_interface(link) mock_get_machines_api_objects.return_value = [docker_container] mock_get_links_api_objects.return_value = [] - docker_container.attrs["NetworkSettings"]["Networks"] = [] + docker_container.attrs["NetworkSettings"]["Networks"] = {} docker_manager.update_lab_from_api(lab) assert len(lab.machines) == 1 assert docker_container.labels["name"] in lab.machines - assert len(lab.links) == 0 + assert len(device.interfaces) == 1 + assert device.interfaces[0] is None + assert docker_container.labels["name"] not in link.machines @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_api_objects") @@ -1032,11 +1066,19 @@ def test_update_lab_from_api_add_remove_link(mock_get_links_api_objects, mock_ge mock_get_machines_api_objects.return_value = [docker_container] mock_get_links_api_objects.return_value = [docker_network_b] - docker_container.attrs["NetworkSettings"]["Networks"] = ["kathara_user_hash_test_network_b"] + docker_container.attrs["NetworkSettings"]["Networks"] = { + "kathara_user_hash_test_network_b": { + "Links": None, + "DriverOpts": { + "kathara.mac_addr": "00:00:00:00:00:02" + } + } + } docker_manager.update_lab_from_api(lab) assert len(lab.machines) == 1 assert docker_container.labels["name"] in lab.machines + assert lab.machines[docker_container.labels["name"]].interfaces[1].mac_address == "00:00:00:00:00:02" assert len(lab.links) == 1 assert docker_network_b.attrs["Labels"]["name"] in lab.links diff --git a/tests/model/interface_test.py b/tests/model/interface_test.py new file mode 100644 index 00000000..521d098d --- /dev/null +++ b/tests/model/interface_test.py @@ -0,0 +1,84 @@ +import sys + +import pytest + +sys.path.insert(0, './') + +from src.Kathara.exceptions import InterfaceMacAddressError +from src.Kathara.model.Machine import Machine +from src.Kathara.model.Lab import Lab +from src.Kathara.model.Interface import Interface +from src.Kathara.model.Link import Link + +sys.path.insert(0, './src') + + +@pytest.fixture() +def default_device(): + return Machine(Lab("test_lab"), "test_machine") + + +@pytest.fixture() +def default_link(default_device): + return Link(default_device.lab, "A") + + +def test_normal_iface(default_device: Machine, default_link: Link): + interface = Interface(default_device, default_link, 0) + + assert interface.machine == default_device + assert interface.link == default_link + assert interface.num == 0 + assert interface.mac_address is None + + +def test_iface_num(default_device: Machine, default_link: Link): + interface = Interface(default_device, default_link, 2) + + assert interface.machine == default_device + assert interface.link == default_link + assert interface.num == 2 + assert interface.mac_address is None + + +def test_iface_mac_address(default_device: Machine, default_link: Link): + interface = Interface(default_device, default_link, 0, "00:00:00:00:00:01") + + assert interface.machine == default_device + assert interface.link == default_link + assert interface.num == 0 + assert interface.mac_address == "00:00:00:00:00:01" + + +def test_iface_num_and_mac_address(default_device: Machine, default_link: Link): + interface = Interface(default_device, default_link, 2, "00:00:00:00:00:01") + + assert interface.machine == default_device + assert interface.link == default_link + assert interface.num == 2 + assert interface.mac_address == "00:00:00:00:00:01" + + +def test_iface_mac_address_error_short(default_device: Machine, default_link: Link): + with pytest.raises(InterfaceMacAddressError): + Interface(default_device, default_link, 0, "00:00:00:00:00") + + +def test_iface_mac_address_error_long(default_device: Machine, default_link: Link): + with pytest.raises(InterfaceMacAddressError): + Interface(default_device, default_link, 0, "00:00:00:00:00:00:00") + + +def test_iface_mac_address_error_invalid_sep(default_device: Machine, default_link: Link): + with pytest.raises(InterfaceMacAddressError): + Interface(default_device, default_link, 0, "00:00-00:00:00:00:00") + + +def test_iface_mac_address_invalid_val(default_device: Machine, default_link: Link): + with pytest.raises(InterfaceMacAddressError): + Interface(default_device, default_link, 0, "gg:00:00:00:00:00:00") + + +def test_iface_mac_address_invalid_format(default_device: Machine, default_link: Link): + with pytest.raises(InterfaceMacAddressError): + Interface(default_device, default_link, 0, "f:00:00:00:00:00:00") diff --git a/tests/model/lab_test.py b/tests/model/lab_test.py index 95e3e59c..54f9ccea 100644 --- a/tests/model/lab_test.py +++ b/tests/model/lab_test.py @@ -215,6 +215,32 @@ def test_connect_machine_to_link_iface_numbers(default_scenario: Lab): assert interface_a == default_scenario.machines['pc1'].interfaces[2] +def test_connect_machine_to_link_mac_address(default_scenario: Lab): + (machine_a, interface_a) = default_scenario.connect_machine_to_link( + "pc1", "A", mac_address="00:00:00:00:00:01" + ) + assert len(default_scenario.machines) == 1 + assert default_scenario.machines['pc1'] + assert len(default_scenario.links) == 1 + assert default_scenario.links['A'] + assert machine_a == default_scenario.machines['pc1'] + assert interface_a == default_scenario.machines['pc1'].interfaces[0] + assert interface_a.mac_address == "00:00:00:00:00:01" + + +def test_connect_machine_to_link_iface_num_mac_address(default_scenario: Lab): + (machine_a, interface_a) = default_scenario.connect_machine_to_link( + "pc1", "A", machine_iface_number=2, mac_address="00:00:00:00:00:01" + ) + assert len(default_scenario.machines) == 1 + assert default_scenario.machines['pc1'] + assert len(default_scenario.links) == 1 + assert default_scenario.links['A'] + assert machine_a == default_scenario.machines['pc1'] + assert interface_a == default_scenario.machines['pc1'].interfaces[2] + assert interface_a.mac_address == "00:00:00:00:00:01" + + def test_connect_one_machine_obj_to_link(default_scenario: Lab): pc1 = default_scenario.new_machine("pc1") interface = default_scenario.connect_machine_obj_to_link(pc1, "A") @@ -271,6 +297,34 @@ def test_connect_machine_obj_to_link_iface_numbers(default_scenario: Lab): assert interface == default_scenario.machines['pc1'].interfaces[2] +def test_connect_machine_obj_to_link_mac_address(default_scenario: Lab): + pc1 = default_scenario.new_machine("pc1") + interface_a = default_scenario.connect_machine_obj_to_link( + pc1, "A", mac_address="00:00:00:00:00:01" + ) + assert len(default_scenario.machines) == 1 + assert default_scenario.machines['pc1'] + assert len(default_scenario.links) == 1 + assert default_scenario.links['A'] + assert pc1 == default_scenario.machines['pc1'] + assert interface_a == default_scenario.machines['pc1'].interfaces[0] + assert interface_a.mac_address == "00:00:00:00:00:01" + + +def test_connect_machine_obj_to_link_iface_num_mac_address(default_scenario: Lab): + pc1 = default_scenario.new_machine("pc1") + interface_a = default_scenario.connect_machine_obj_to_link( + pc1, "A", machine_iface_number=2, mac_address="00:00:00:00:00:01" + ) + assert len(default_scenario.machines) == 1 + assert default_scenario.machines['pc1'] + assert len(default_scenario.links) == 1 + assert default_scenario.links['A'] + assert pc1 == default_scenario.machines['pc1'] + assert interface_a == default_scenario.machines['pc1'].interfaces[2] + assert interface_a.mac_address == "00:00:00:00:00:01" + + def test_assign_meta_to_machine(default_scenario: Lab): default_scenario.get_or_new_machine("pc1") result = default_scenario.assign_meta_to_machine("pc1", "test_meta", "test_value") diff --git a/tests/model/machine_test.py b/tests/model/machine_test.py index aeda34aa..691fa7c8 100644 --- a/tests/model/machine_test.py +++ b/tests/model/machine_test.py @@ -52,6 +52,23 @@ def test_add_interface_with_number(default_device: Machine): assert interface == default_device.interfaces[2] +def test_add_interface_with_mac_address(default_device: Machine): + interface = default_device.add_interface(Link(default_device.lab, "A"), mac_address="00:00:00:00:00:01") + assert len(default_device.interfaces) == 1 + assert default_device.interfaces[0].link.name == "A" + assert interface == default_device.interfaces[0] + assert interface.mac_address == "00:00:00:00:00:01" + + +def test_add_interface_with_number_and_mac_address(default_device: Machine): + interface = default_device.add_interface(Link(default_device.lab, "A"), + number=2, mac_address="00:00:00:00:00:01") + assert len(default_device.interfaces) == 1 + assert default_device.interfaces[2].link.name == "A" + assert interface == default_device.interfaces[2] + assert interface.mac_address == "00:00:00:00:00:01" + + def test_add_interface_exception(default_device: Machine): default_device.add_interface(Link(default_device.lab, "A")) with pytest.raises(MachineCollisionDomainError): From dfcf0a51e99dd0d8947abd7416b8a1eb73ccd784 Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Tue, 19 Dec 2023 14:45:42 +0100 Subject: [PATCH 15/44] Update man pages (#137) --- docs/Makefile | 3 +- docs/kathara-lab.conf.5.ronn | 93 ++++++++++++++++++++++-------------- docs/kathara-lconfig.1.ronn | 25 ++++++---- docs/kathara-vconfig.1.ronn | 23 +++++---- 4 files changed, 89 insertions(+), 55 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 34897db2..f2a4b7c3 100755 --- a/docs/Makefile +++ b/docs/Makefile @@ -11,7 +11,8 @@ build-%: docker-build-image docker-build-image: cp ../scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template ../scripts/Linux-Deb/Docker-Linux-Build/Dockerfile - sed -i -e "s|__DISTRO__|focal|g" ../scripts/Linux-Deb/Docker-Linux-Build/Dockerfile + sed -i -e "s|__DISTRO__|jammy|g" ../scripts/Linux-Deb/Docker-Linux-Build/Dockerfile + sed -i -e "s|__NOKOGIRI__||g" ../scripts/Linux-Deb/Docker-Linux-Build/Dockerfile cd ../scripts/Linux-Deb/Docker-Linux-Build/ && docker build -t kathara/linux-build-deb . rm -f ../scripts/Linux-Deb/Docker-Linux-Build/Dockerfile diff --git a/docs/kathara-lab.conf.5.ronn b/docs/kathara-lab.conf.5.ronn index 721af546..5c3112af 100644 --- a/docs/kathara-lab.conf.5.ronn +++ b/docs/kathara-lab.conf.5.ronn @@ -8,41 +8,59 @@ The main network scenario configuration file. In this file you can specify the n This file is a list of `device[arg]=value` assignments, where `arg` can be an integer value or the name of an option (described below). -If `arg` is an integer value, then `value` is the name of the collision domain to which interface eth`arg` of device `device` must be connected (note that the name of the collision domain must not contain spaces (" "), commas (",") and dots ("."). For example, `pc1[0]=CD1` means that interface eth0 of device pc1 will be connected to collision domain CD1. +In order to establish a uniform convention, comment lines should always start with a hash character (`#`). -If `arg` is an option name, then `device` will be launched with option `arg` set to value `value`. +## DEVICE INTERFACES -In order to establish a uniform convention, comment lines should always start with a hash character (`#`). +If `arg` is an integer value, then `value` is the name of the collision domain to which interface eth`arg` of device `device` must be connected. The syntax is as follows: + +- `arg`: An integer value representing the interface number (e.g., 0). +- `CD1`: The name of the collision domain to which the specified interface must be connected. Note that the name of the collision domain must not contain spaces (" "), commas (",") and dots ("."). +- `/MAC_ADDR`: An optional parameter to specify the MAC address of the interface of `device` (MAC address must be in the format `XX:XX:XX:XX:XX:XX`). If `MAC_ADDR` is not provided, Kathará will assign a random one. + +### EXAMPLES + +1. Connect eth0 of `pc1` to collision domain `CD1` with a random MAC address: + ``` + pc1[0]="CD1" + ``` + +2. Connect eth1 of `pc1` to collision domain `CD2` with the specified MAC address: + ``` + pc1[1]="CD2/02:42:ac:11:00:02" + ``` ## DEVICE OPTIONS +If `arg` is an option name, then `device` will be launched with option `arg` set to value `value`. + * `image` (string): - Docker image used for this device. + Docker image used for this device. * `mem` (string): - Set the amount of available RAM inside the device. If you set this option, the minimum allowed value is 4m (4 megabyte). + Set the amount of available RAM inside the device. If you set this option, the minimum allowed value is 4m (4 megabyte). - This option takes a positive integer, followed by a suffix of "b", "k", "m", "g", to indicate bytes, kilobytes, megabytes, or gigabytes. + This option takes a positive integer, followed by a suffix of "b", "k", "m", "g", to indicate bytes, kilobytes, megabytes, or gigabytes. * `cpus` (float): - Limit the amount of CPU available for this device. + Limit the amount of CPU available for this device. - This option takes a positive float, ranging from 0 to max number of host logical CPUs. For instance, if the host device has two CPUs and you set `device[cpus]=1.5`, the device is guaranteed at most one and a half of the CPUs. + This option takes a positive float, ranging from 0 to max number of host logical CPUs. For instance, if the host device has two CPUs and you set `device[cpus]=1.5`, the device is guaranteed at most one and a half of the CPUs. * `port` (string): - Map localhost port HOST to the internal port GUEST of the device for the specified PROTOCOL. The syntax is [HOST:]GUEST[/PROTOCOL]. + Map localhost port HOST to the internal port GUEST of the device for the specified PROTOCOL. The syntax is [HOST:]GUEST[/PROTOCOL]. - If HOST port is not specified, default is 3000. If PROTOCOL is not specified, default is `tcp`. Supported PROTOCOL values are: tcp, udp, or sctp. - For instance, with this command you can map host's port 8080 to device's port 80 with TCP protocol: `device[port]="8080:80/tcp"`. + If HOST port is not specified, default is 3000. If PROTOCOL is not specified, default is `tcp`. Supported PROTOCOL values are: tcp, udp, or sctp. + For instance, with this command you can map host's port 8080 to device's port 80 with TCP protocol: `device[port]="8080:80/tcp"`. * `bridged` (boolean): - Connect the device to the host network by adding an additional network interface. This interface will be connected to the host network through a NAT connection. + Connect the device to the host network by adding an additional network interface. This interface will be connected to the host network through a NAT connection. * `ipv6` (boolean): - Enable or disable IPv6 on this device. + Enable or disable IPv6 on this device. * `exec` (string): - Run a specific shell command inside the device during the startup phase. + Run a specific shell command inside the device during the startup phase. * `sysctl` (string): Set a sysctl option for this device. Only the `net.` namespace is allowed to be set. Can be set multiple times per device, each will add a new entry (unless the same config item is used again). @@ -51,10 +69,10 @@ In order to establish a uniform convention, comment lines should always start wi Set an environment variable for the device. Can be set multiple times per device, each will add a new entry (unless the same variable is used again). The format is: ENV_NAME=ENV_VALUE. * `shell` (string): - Use the specified shell to connect to the device, e.g., when `kathara connect` is called. + Use the specified shell to connect to the device, e.g., when `kathara connect` is called. * `num_terms` (integer): - Choose the number of terminals to open for this device. + Choose the number of terminals to open for this device. ## NETWORK SCENARIO META INFORMATION @@ -69,32 +87,33 @@ It is also possible to provide descriptive information about a network scenario ## EXAMPLE - LAB_NAME="Example" - LAB_DESCRIPTION="A simple example of lab.conf" - LAB_VERSION=1.0 - LAB_AUTHOR="Kathara Authors" - LAB_EMAIL=contact@kathara.org - LAB_WEB=http://www.kathara.org/ +Example of a `lab.conf`(5) file. - r1[0]="A" - r1[1]="B" - r1[port]="32000" - r1[image]="namespace/image_name" - r1[sysctl]="net.ipv6.conf.all.forwarding=1" + LAB_NAME="Example" + LAB_DESCRIPTION="A simple example of lab.conf" + LAB_VERSION=1.0 + LAB_AUTHOR="Kathara Authors" + LAB_EMAIL=contact@kathara.org + LAB_WEB=http://www.kathara.org/ - r2[0]="C" - r2[1]="B" - r2[port]="2000:500/udp" - r2[exec]="echo Hi" + r1[0]="A" + r1[1]="B/02:42:ac:11:00:02" # Specify the MAC address assigned to interface eth1 of r1 + r1[port]="32000" + r1[image]="namespace/image_name" + r1[sysctl]="net.ipv6.conf.all.forwarding=1" - pc1[0]="A" - pc1[bridged]="true" + r2[0]="C" + r2[1]="B" + r2[port]="2000:500/udp" + r2[exec]="echo Hi" - pc2[0]="C" - pc2[mem]="128m" - pc2[shell]="/bin/sh" + pc1[0]="A" + pc1[bridged]="true" + + pc2[0]="C" + pc2[mem]="128m" + pc2[shell]="/bin/sh" -Example of a `lab.conf`(5) file. m4_include(footer.txt) diff --git a/docs/kathara-lconfig.1.ronn b/docs/kathara-lconfig.1.ronn index 9e3ec86f..bbe31744 100644 --- a/docs/kathara-lconfig.1.ronn +++ b/docs/kathara-lconfig.1.ronn @@ -24,25 +24,32 @@ Manage the network interfaces of a running Kathara device in a network scenario. * `-n` , `--name` : Name of the device to configure. -* `--add` [ ...]: - Specify the collision domain to add. - - Equip the device with an additional network interface attached to a (virtual) collision domain whose name is . The number of the resulting network interface is generated incrementing the number of the last network interface used by the device. +* `--add` [ ...]: + Specify the collision domain to be connected to the device: + + `CD`: The name of the collision domain to which the specified interface must be connected. Note that the name of the collision domain must not contain spaces (" "), commas (",") and dots ("."). + + `/MAC_ADDR`: An optional parameter to specify the MAC address of the interface of `pc1` (MAC address must be in the format `XX:XX:XX:XX:XX:XX`). If `MAC_ADDR` is not provided, Kathará will assign a random one. + + Equip the device with an additional network interface attached to a (virtual) collision domain whose name is . + The number of the resulting network interface is generated incrementing the number of the last network interface used by the device. * `--rm` [ ...]: - Specify the collision domain to remove. + Specify the collision domain to be disconnected from the device. Disconnect the device from the collision domain whose name is and remove the corresponding interface. ## EXAMPLES +Connect `pc1` to collision domain `X` and `Y` (with random MAC addresses): + kathara lconfig -d path/to/network_scenario -n pc1 --add X Y -Two new interfaces will be added to the device pc1 in the network scenario located in "path/to/network_scenario": the first one will be attached to the collision domain named X, while the other one to the collision domain named Y. Both the interfaces will have to be configured by hand inside the device (for example, by using ifconfig). - - kathara vconfig -n pc1 --rm X +Connect `pc1` to collision domain `X` with the specified MAC address: + + kathara lconfig -d path/to/network_scenario -n pc1 --add X/00:00:00:00:00:01 -pc1, in the network scenario located in "path/to/network_scenario", will be disconnected from the collision domain named X and the corresponding network interface will be removed. +Disconnect `pc1` from collision domain `X` and remove the corresponding interface: + + kathara lconfig -d path/to/network_scenario -n pc1 --rm X m4_include(footer.txt) diff --git a/docs/kathara-vconfig.1.ronn b/docs/kathara-vconfig.1.ronn index 3d54987c..8c7d2612 100644 --- a/docs/kathara-vconfig.1.ronn +++ b/docs/kathara-vconfig.1.ronn @@ -18,26 +18,33 @@ Manage the network interfaces of a running Kathara device. The affected device i * `-n` , `--name` : Name of the device to manage. -* `--add` [ ...]: - Specify the collision domain to add. - - Equip the device with an additional network interface attached to a (virtual) collision domain whose name is . The number of the resulting network interface is generated incrementing the number of the last network interface used by the device. +* `--add` [ ...]: + Specify the collision domain to be connected to the device: + + `CD`: The name of the collision domain to which the specified interface must be connected. Note that the name of the collision domain must not contain spaces (" "), commas (",") and dots ("."). + + `/MAC_ADDR`: An optional parameter to specify the MAC address of the interface of `pc1` (MAC address must be in the format `XX:XX:XX:XX:XX:XX`). If `MAC_ADDR` is not provided, Kathará will assign a random one. + + Equip the device with an additional network interface attached to a (virtual) collision domain whose name is . + The number of the resulting network interface is generated incrementing the number of the last network interface used by the device. * `--rm` [ ...]: - Specify the collision domain to remove. + Specify the collision domain to be disconnected from the device. Disconnect the device from the collision domain whose name is and remove the corresponding interface. ## EXAMPLES +Connect `pc1` to collision domain `X` and `Y` (with random MAC addresses): + kathara vconfig -n pc1 --add X Y -Two new interfaces will be added to the device pc1: the first one will be attached to the collision domain named X, while the other one to the collision domain named Y. Both the interfaces will have to be configured by hand inside the device (for example, by using ifconfig). +Connect `pc1` to collision domain `X` with the specified MAC address: + + kathara vconfig -n pc1 --add X/00:00:00:00:00:01 +Disconnect `pc1` from collision domain `X` and remove the corresponding interface: + kathara vconfig -n pc1 --rm X -pc1 will be disconnected from the collision domain named X and the corresponding network interface will be removed. - m4_include(footer.txt) ## SEE ALSO From aafb77ef3536cb9f294148a524fba0c14806c1ec Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Tue, 19 Dec 2023 14:47:20 +0100 Subject: [PATCH 16/44] Fix `lconfig` parser (#137) --- src/Kathara/cli/command/LconfigCommand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kathara/cli/command/LconfigCommand.py b/src/Kathara/cli/command/LconfigCommand.py index d77fa0a8..8f65c985 100644 --- a/src/Kathara/cli/command/LconfigCommand.py +++ b/src/Kathara/cli/command/LconfigCommand.py @@ -45,7 +45,7 @@ def __init__(self) -> None: group.add_argument( '--add', - type=alphanumeric, + type=str, dest='to_add', metavar='CD', nargs='+', From efc28cc2062968008e01fd0f25ab23ca69f60cfc Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 19 Dec 2023 15:14:54 +0100 Subject: [PATCH 17/44] Fix debian build. Bump Python to 3.11 --- .../Docker-Linux-Build/Dockerfile_template | 41 ++++++++++--------- scripts/Linux-Deb/Makefile | 6 ++- scripts/Linux-Deb/debian/control | 8 ++-- scripts/Linux-Deb/debian/rules | 6 +-- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template b/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template index b553e81f..1515853d 100644 --- a/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template +++ b/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template @@ -9,29 +9,30 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone ARG DEBIAN_FRONTEND="noninteractive" RUN apt update && \ - apt upgrade -y && \ + apt upgrade -y && \ apt install -y \ - apt-utils \ - debhelper \ - libxml2-dev \ - zlib1g-dev \ - build-essential \ - lintian \ - devscripts \ - git \ - ruby-full \ - bash-completion \ - software-properties-common \ - patchelf - - + apt-utils \ + bash-completion \ + build-essential \ + debhelper \ + devscripts \ + git \ + libxml2-dev \ + libffi-dev \ + lintian \ + patchelf \ + ruby-full \ + software-properties-common \ + zlib1g-dev + + RUN add-apt-repository ppa:deadsnakes/ppa \ && DEBIAN_FRONTEND=noninteractive apt install -y \ - python3.10 \ - python3.10-dev \ - python3.10-venv \ - python3-pip \ - python3.10-distutils + python3.11 \ + python3.11-dev \ + python3.11-venv \ + python3-pip \ + python3.11-distutils __NOKOGIRI__ RUN gem install ronn-ng diff --git a/scripts/Linux-Deb/Makefile b/scripts/Linux-Deb/Makefile index 1959de7f..37a24f3a 100644 --- a/scripts/Linux-Deb/Makefile +++ b/scripts/Linux-Deb/Makefile @@ -51,7 +51,7 @@ copy-debian-folder: sed -i -e 's/__VERSION__/$(VERSION)/g' Output/kathara-$(VERSION)/debian/changelog venv: - python3.10 -m venv /root/py-build-env + python3.11 -m venv /root/py-build-env download-pip: copy-debian-folder mkdir Output/kathara-$(VERSION)/debian/pythonLibs_amd64 @@ -61,14 +61,18 @@ download-pip: copy-debian-folder /root/py-build-env/bin/pip install --upgrade setuptools cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/py-build-env/bin/pip download --platform manylinux2014_x86_64 --only-binary=:all: flit_core cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/py-build-env/bin/pip download --platform manylinux2014_x86_64 --only-binary=:all: wheel + cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/py-build-env/bin/pip download --platform manylinux2014_x86_64 --only-binary=:all: zstandard cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/py-build-env/bin/pip download --no-binary=:all: -r ../../requirements-no-binary.txt cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/py-build-env/bin/pip download --platform manylinux2014_x86_64 --platform x86_64 --only-binary=:all: -r ../../requirements-binary.txt cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/py-build-env/bin/pip download --no-binary=:all: nuitka cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/py-build-env/bin/pip download --platform manylinux2014_aarch64 --only-binary=:all: flit_core cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/py-build-env/bin/pip download --platform manylinux2014_aarch64 --only-binary=:all: wheel + cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/py-build-env/bin/pip download --platform manylinux2014_aarch64 --only-binary=:all: zstandard cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/py-build-env/bin/pip download --no-binary=:all: -r ../../requirements-no-binary.txt cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/py-build-env/bin/pip download --platform manylinux2014_aarch64 --platform arm64 --only-binary=:all: -r ../../requirements-binary.txt cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/py-build-env/bin/pip download --no-binary=:all: nuitka + rm Output/kathara-$(VERSION)/debian/pythonLibs_arm64/zstandard*.tar.gz + rm Output/kathara-$(VERSION)/debian/pythonLibs_amd64/zstandard*.tar.gz rm -Rf Output/kathara-$(VERSION)/requirements-binary.txt Output/kathara-$(VERSION)/requirements-no-binary.txt ls Output/kathara-$(VERSION)/debian/pythonLibs_amd64/ | awk '{print "debian/pythonLibs_amd64/"$$0}' > Output/kathara-$(VERSION)/debian/source/include-binaries ls Output/kathara-$(VERSION)/debian/pythonLibs_arm64/ | awk '{print "debian/pythonLibs_arm64/"$$0}' >> Output/kathara-$(VERSION)/debian/source/include-binaries diff --git a/scripts/Linux-Deb/debian/control b/scripts/Linux-Deb/debian/control index 63ee2ab6..9428d8c7 100644 --- a/scripts/Linux-Deb/debian/control +++ b/scripts/Linux-Deb/debian/control @@ -4,11 +4,11 @@ Section: misc Priority: optional Standards-Version: 3.9.8 Build-Depends: debhelper (>= 10), - python3.10 (>= 3.10.0), + python3.11 (>= 3.11.0), python3-pip (>= 9.0.0), - python3.10-dev (>= 3.10.0), - python3.10-venv (>= 3.10.0), - python3.10-distutils (>= 3.10.0) | python3-distutils (>= 3.8.0), + python3.11-dev (>= 3.11.0), + python3.11-venv (>= 3.11.0), + python3.11-distutils (>= 3.11.0) | python3-distutils (>= 3.8.0), patchelf, bash-completion, zlib1g-dev diff --git a/scripts/Linux-Deb/debian/rules b/scripts/Linux-Deb/debian/rules index 85c072fc..645ad796 100755 --- a/scripts/Linux-Deb/debian/rules +++ b/scripts/Linux-Deb/debian/rules @@ -11,11 +11,11 @@ override_dh_auto_clean: find . | grep -E "(__pycache__|\.pyc|\.pyo)$$" | xargs rm -rf override_dh_auto_configure: - python3.10 -m venv $(CURDIR)/venv - $(CURDIR)/venv/bin/pip3.10 install --no-index --find-links $(CURDIR)/debian/pythonLibs_$(ARCHITECTURE)/ $(CURDIR)/debian/pythonLibs_$(ARCHITECTURE)/* + python3.11 -m venv $(CURDIR)/venv + $(CURDIR)/venv/bin/pip3.11 install --no-index --find-links $(CURDIR)/debian/pythonLibs_$(ARCHITECTURE)/ $(CURDIR)/debian/pythonLibs_$(ARCHITECTURE)/* override_dh_auto_build: - $(CURDIR)/venv/bin/python3.10 -m nuitka --lto=no --show-progress --plugin-enable=pylint-warnings \ + $(CURDIR)/venv/bin/python3.11 -m nuitka --lto=no --show-progress --plugin-enable=pylint-warnings \ --plugin-enable=multiprocessing --follow-imports --standalone --include-plugin-directory=Kathara --output-filename=kathara kathara.py override_dh_auto_install: From 332cc11896327e9742b70aa41c2c90b81b9074ac Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Tue, 19 Dec 2023 15:20:46 +0100 Subject: [PATCH 18/44] Update copyright in man pages --- docs/footer.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/footer.txt b/docs/footer.txt index ebb0ee60..d8c5ddb4 100644 --- a/docs/footer.txt +++ b/docs/footer.txt @@ -18,5 +18,5 @@ People involved also include: ## COPYRIGHT -Copyright © 2017-2021 License GPLv3+: GNU GPL version 3 or later . +Copyright © 2017-2023 License GPLv3+: GNU GPL version 3 or later . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. From a0d66fdd5a623c8505292a4c625600f601c12214 Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Tue, 19 Dec 2023 15:50:00 +0100 Subject: [PATCH 19/44] Fix `vstart` command (#137) --- src/Kathara/cli/command/VstartCommand.py | 15 +++++++++------ src/Kathara/cli/ui/utils.py | 14 +++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Kathara/cli/command/VstartCommand.py b/src/Kathara/cli/command/VstartCommand.py index 00d934c8..46f57766 100644 --- a/src/Kathara/cli/command/VstartCommand.py +++ b/src/Kathara/cli/command/VstartCommand.py @@ -3,7 +3,7 @@ import sys from typing import List -from ..ui.utils import format_headers, colon_separated +from ..ui.utils import format_headers, interface_cd_mac from ... import utils from ...exceptions import PrivilegeError from ...foundation.cli.command.Command import Command @@ -69,9 +69,9 @@ def __init__(self) -> None: ) self.parser.add_argument( '--eth', - type=colon_separated, + type=interface_cd_mac, dest='eths', - metavar='N:CD', + metavar='N:CD/MAC', nargs='+', required=False, help='Set a specific interface on a collision domain.' @@ -193,11 +193,14 @@ def run(self, current_path: str, argv: List[str]) -> None: device = lab.get_or_new_machine(name, **args) if args['eths']: - for iface_number, cd in args['eths']: + for iface_number, cd, mac_address in args['eths']: try: - lab.connect_machine_to_link(device.name, cd, machine_iface_number=int(iface_number)) + lab.connect_machine_to_link(device.name, cd, + machine_iface_number=int(iface_number), + mac_address=mac_address) except ValueError: - raise SyntaxError("Interface number in `--eth %s:%s` is not a number." % (iface_number, cd)) + s = f"{cd}/{mac_address}" if mac_address else f"{cd}" + raise SyntaxError(f"Interface number in `--eth {iface_number}:{s}` is not a number.") lab.check_integrity() diff --git a/src/Kathara/cli/ui/utils.py b/src/Kathara/cli/ui/utils.py index 2240ee85..a382e811 100644 --- a/src/Kathara/cli/ui/utils.py +++ b/src/Kathara/cli/ui/utils.py @@ -153,15 +153,19 @@ def osx_connect() -> None: # Types for argparse def alphanumeric(value, pat=re.compile(r"^\w+$")): if not pat.match(value): - raise argparse.ArgumentTypeError("invalid alphanumeric value") + raise argparse.ArgumentTypeError("Invalid alphanumeric value") return value -def colon_separated(value): +def interface_cd_mac(value): + n, cd, mac = None, None, None try: - (v1, v2) = value.split(':') + parts = value.split('/') + (n, cd) = parts[0].split(':') + if len(parts) == 2: + mac = parts[1] except ValueError: - raise argparse.ArgumentTypeError("invalid colon-separated value: %s" % value) + raise argparse.ArgumentTypeError("Invalid interface definition: %s" % value) - return v1, v2 + return n, cd, mac \ No newline at end of file From 9d0aa011d11ffc985d9a030a3e16aabd732b928d Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Tue, 19 Dec 2023 15:50:34 +0100 Subject: [PATCH 20/44] Fix MAC address regex (#137) --- src/Kathara/model/Interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kathara/model/Interface.py b/src/Kathara/model/Interface.py index eb401308..3ab55d16 100644 --- a/src/Kathara/model/Interface.py +++ b/src/Kathara/model/Interface.py @@ -5,7 +5,7 @@ from . import Machine as MachinePackage from ..exceptions import InterfaceMacAddressError -MAC_ADDRESS_REGEX = re.compile(r"^[0-9a-f]{2}([:]?)[0-9a-f]{2}(\1[0-9a-f]{2}){4}$") +MAC_ADDRESS_REGEX = re.compile(r"^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$") class Interface(object): From 3ded2b783e5c1d1975cbefae9ae9b0a8a02377fc Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Tue, 19 Dec 2023 17:11:59 +0100 Subject: [PATCH 21/44] Fix `vstart` command test (#137) --- tests/cli/vstart_command_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/cli/vstart_command_test.py b/tests/cli/vstart_command_test.py index 4558db0c..e1c29d9e 100644 --- a/tests/cli/vstart_command_test.py +++ b/tests/cli/vstart_command_test.py @@ -194,7 +194,7 @@ def test_run_with_one_interface(mock_docker_manager, mock_manager_get_instance, mock_manager_get_instance.return_value = mock_docker_manager mock_setting_get_instance.return_value = mock_setting mock_get_or_new_machine.return_value = Machine(test_lab, 'pc1') - default_device_args['eths'] = [('0', 'A')] + default_device_args['eths'] = [('0', 'A', None)] command = VstartCommand() with mock.patch.object(Lab, "add_option") as mock_add_option: command.run('.', ['-n', 'pc1', '--eth', '0:A']) @@ -205,7 +205,7 @@ def test_run_with_one_interface(mock_docker_manager, mock_manager_get_instance, mock_add_option.assert_any_call('shared_mount', False) mock_add_option.assert_any_call('privileged_machines', None) mock_get_or_new_machine.assert_called_once_with('pc1', **default_device_args) - mock_connect_machine_to_link.assert_called_once_with('pc1', 'A', machine_iface_number=0) + mock_connect_machine_to_link.assert_called_once_with('pc1', 'A', machine_iface_number=0, mac_address=None) mock_docker_manager.deploy_lab.assert_called_once() @@ -220,7 +220,7 @@ def test_run_with_two_interfaces(mock_docker_manager, mock_manager_get_instance, mock_manager_get_instance.return_value = mock_docker_manager mock_setting_get_instance.return_value = mock_setting mock_get_or_new_machine.return_value = Machine(test_lab, 'pc1') - default_device_args['eths'] = [('0', 'A'), ('1', 'B')] + default_device_args['eths'] = [('0', 'A', None), ('1', 'B', None)] command = VstartCommand() with mock.patch.object(Lab, "add_option") as mock_add_option: command.run('.', ['-n', 'pc1', '--eth', '0:A', '1:B']) @@ -231,8 +231,8 @@ def test_run_with_two_interfaces(mock_docker_manager, mock_manager_get_instance, mock_add_option.assert_any_call('shared_mount', False) mock_add_option.assert_any_call('privileged_machines', None) mock_get_or_new_machine.assert_called_once_with('pc1', **default_device_args) - mock_connect_machine_to_link.assert_any_call('pc1', 'A', machine_iface_number=0) - mock_connect_machine_to_link.assert_any_call('pc1', 'B', machine_iface_number=1) + mock_connect_machine_to_link.assert_any_call('pc1', 'A', machine_iface_number=0, mac_address=None) + mock_connect_machine_to_link.assert_any_call('pc1', 'B', machine_iface_number=1, mac_address=None) mock_docker_manager.deploy_lab.assert_called_once() From 5b929e18131c4c838ccead8f67450965f160ad93 Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Wed, 20 Dec 2023 19:47:33 +0100 Subject: [PATCH 22/44] Fix `vconfig` and `lconfig` commands (#137) --- src/Kathara/cli/command/LconfigCommand.py | 16 ++-- src/Kathara/cli/command/VconfigCommand.py | 13 ++- src/Kathara/cli/ui/utils.py | 7 +- tests/cli/lconfig_command_test.py | 98 ++++++++++++++++++++--- tests/cli/vconfig_command_test.py | 39 ++++++++- 5 files changed, 142 insertions(+), 31 deletions(-) diff --git a/src/Kathara/cli/command/LconfigCommand.py b/src/Kathara/cli/command/LconfigCommand.py index 8f65c985..a7656c15 100644 --- a/src/Kathara/cli/command/LconfigCommand.py +++ b/src/Kathara/cli/command/LconfigCommand.py @@ -2,11 +2,10 @@ import logging from typing import List -from ..ui.utils import alphanumeric +from ..ui.utils import alphanumeric, cd_mac_address from ... import utils from ...foundation.cli.command.Command import Command from ...manager.Kathara import Kathara -from ...model.Lab import Lab from ...parser.netkit.LabParser import LabParser from ...strings import strings, wiki_description @@ -45,9 +44,9 @@ def __init__(self) -> None: group.add_argument( '--add', - type=str, + type=cd_mac_address, dest='to_add', - metavar='CD', + metavar='CD/MAC', nargs='+', help='Specify the collision domain to add.' ) @@ -66,10 +65,8 @@ def run(self, current_path: str, argv: List[str]) -> None: lab_path = args['directory'].replace('"', '').replace("'", '') if args['directory'] else current_path lab_path = utils.get_absolute_path(lab_path) - try: - lab = LabParser.parse(lab_path) - except (Exception, IOError): - lab = Lab(None, path=lab_path) + + lab = LabParser.parse(lab_path) Kathara.get_instance().update_lab_from_api(lab) @@ -77,8 +74,7 @@ def run(self, current_path: str, argv: List[str]) -> None: device = lab.get_machine(machine_name) if args['to_add']: - for cd_to_add in args['to_add']: - cd_name, mac_address = utils.parse_cd_mac_address(cd_to_add) + for cd_name, mac_address in args['to_add']: logging.info( f"Adding interface to device `{machine_name}` on collision domain `{cd_name}`" + (f" with MAC Address {mac_address}" if mac_address else "") + diff --git a/src/Kathara/cli/command/VconfigCommand.py b/src/Kathara/cli/command/VconfigCommand.py index a2fb6806..1abc9399 100644 --- a/src/Kathara/cli/command/VconfigCommand.py +++ b/src/Kathara/cli/command/VconfigCommand.py @@ -2,12 +2,11 @@ import logging from typing import List -from ..ui.utils import alphanumeric +from ..ui.utils import alphanumeric, cd_mac_address from ...foundation.cli.command.Command import Command from ...manager.Kathara import Kathara from ...model.Lab import Lab from ...strings import strings, wiki_description -from ...utils import parse_cd_mac_address class VconfigCommand(Command): @@ -38,9 +37,9 @@ def __init__(self) -> None: group.add_argument( '--add', - type=str, + type=cd_mac_address, dest='to_add', - metavar='CD', + metavar='CD/MAC', nargs='+', help='Specify the collision domain to add.' ) @@ -58,14 +57,14 @@ def run(self, current_path: str, argv: List[str]) -> None: args = self.get_args() lab = Lab("kathara_vlab") + Kathara.get_instance().update_lab_from_api(lab) machine_name = args['name'] - device = lab.new_machine(machine_name) + device = lab.get_machine(machine_name) device.api_object = Kathara.get_instance().get_machine_api_object(machine_name, lab_name=lab.name) if args['to_add']: - for cd_to_add in args['to_add']: - cd_name, mac_address = parse_cd_mac_address(cd_to_add) + for cd_name, mac_address in args['to_add']: logging.info( f"Adding interface to device `{machine_name}` on collision domain `{cd_name}`" + (f" with MAC Address {mac_address}" if mac_address else "") + diff --git a/src/Kathara/cli/ui/utils.py b/src/Kathara/cli/ui/utils.py index a382e811..dbea7521 100644 --- a/src/Kathara/cli/ui/utils.py +++ b/src/Kathara/cli/ui/utils.py @@ -13,6 +13,7 @@ from ...foundation.manager.stats.IMachineStats import IMachineStats from ...setting.Setting import Setting from ...trdparty.consolemenu import PromptUtils, Screen +from ...utils import parse_cd_mac_address FORBIDDEN_TABLE_COLUMNS = ["container_name"] @@ -168,4 +169,8 @@ def interface_cd_mac(value): except ValueError: raise argparse.ArgumentTypeError("Invalid interface definition: %s" % value) - return n, cd, mac \ No newline at end of file + return n, cd, mac + + +def cd_mac_address(value): + return parse_cd_mac_address(value) diff --git a/tests/cli/lconfig_command_test.py b/tests/cli/lconfig_command_test.py index 3d3a568a..75a220cc 100644 --- a/tests/cli/lconfig_command_test.py +++ b/tests/cli/lconfig_command_test.py @@ -27,7 +27,7 @@ def test_lab(): @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") @mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") -def test_run_add_link(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): +def test_run_add_interface(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): test_lab.connect_machine_to_link("pc1", "A") mock_parse_lab.return_value = test_lab @@ -46,7 +46,40 @@ def test_run_add_link(mock_parse_lab, mock_docker_manager, mock_manager_get_inst @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") @mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") -def test_run_add_link_with_directory(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): +def test_run_add_interface_with_mac_address(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): + test_lab.connect_machine_to_link("pc1", "A") + + mock_parse_lab.return_value = test_lab + mock_manager_get_instance.return_value = mock_docker_manager + command = LconfigCommand() + command.run('.', ['-n', 'pc1', '--add', 'A/00:00:00:00:00:01']) + mock_parse_lab.assert_called_once_with(os.getcwd()) + mock_docker_manager.update_lab_from_api.assert_called_once_with(test_lab) + mock_docker_manager.connect_machine_to_link.assert_called_once_with( + test_lab.get_machine('pc1'), + test_lab.get_machine('pc1').interfaces[0].link, + mac_address='00:00:00:00:00:01' + ) + + +@mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") +@mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") +def test_run_add_interface_invalid_interface_definition(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, + test_lab): + test_lab.connect_machine_to_link("pc1", "A") + + mock_parse_lab.return_value = test_lab + mock_manager_get_instance.return_value = mock_docker_manager + command = LconfigCommand() + with pytest.raises(SyntaxError): + command.run('.', ['-n', 'pc1', '--add', 'B/00:/00:00:00:00:01']) + + +@mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") +@mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") +def test_run_add_interface_with_directory(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): test_lab.connect_machine_to_link("pc1", "A") mock_parse_lab.return_value = test_lab @@ -65,7 +98,7 @@ def test_run_add_link_with_directory(mock_parse_lab, mock_docker_manager, mock_m @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") @mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") -def test_run_add_two_links(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): +def test_run_add_two_interfaces(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): test_lab.connect_machine_to_link("pc1", "A") test_lab.connect_machine_to_link("pc1", "B") @@ -90,7 +123,52 @@ def test_run_add_two_links(mock_parse_lab, mock_docker_manager, mock_manager_get @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") @mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") -def test_run_remove_link(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): +def test_run_add_two_interfaces_one_mac_address(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, + test_lab): + test_lab.connect_machine_to_link("pc1", "A") + test_lab.connect_machine_to_link("pc1", "B") + + mock_parse_lab.return_value = test_lab + mock_manager_get_instance.return_value = mock_docker_manager + command = LconfigCommand() + command.run('.', ['-n', 'pc1', '--add', 'A', 'B/00:00:00:00:00:01']) + mock_parse_lab.assert_called_once_with(os.getcwd()) + mock_docker_manager.update_lab_from_api.assert_called_once_with(test_lab) + mock_docker_manager.connect_machine_to_link.assert_any_call( + test_lab.get_machine('pc1'), + test_lab.get_machine("pc1").interfaces[0].link, + mac_address=None + ) + mock_docker_manager.connect_machine_to_link.assert_any_call( + test_lab.get_machine('pc1'), + test_lab.get_machine("pc1").interfaces[1].link, + mac_address='00:00:00:00:00:01' + ) + + +@mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") +@mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") +def test_run_add_interface_with_directory(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): + test_lab.connect_machine_to_link("pc1", "A") + + mock_parse_lab.return_value = test_lab + mock_manager_get_instance.return_value = mock_docker_manager + command = LconfigCommand() + command.run('.', ['-d', os.path.join('/test', 'path'), '-n', 'pc1', '--add', 'A']) + mock_parse_lab.assert_called_once_with(os.path.abspath(os.path.join('/test', 'path'))) + mock_docker_manager.update_lab_from_api.assert_called_once_with(test_lab) + mock_docker_manager.connect_machine_to_link.assert_called_once_with( + test_lab.get_machine('pc1'), + test_lab.get_machine("pc1").interfaces[0].link, + mac_address=None + ) + + +@mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") +@mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") +def test_run_remove_interface(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): mock_parse_lab.return_value = test_lab mock_manager_get_instance.return_value = mock_docker_manager command = LconfigCommand() @@ -104,8 +182,9 @@ def test_run_remove_link(mock_parse_lab, mock_docker_manager, mock_manager_get_i @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") @mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") -def test_run_remove_link_with_directory_absolute_path(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, - test_lab): +def test_run_remove_interface_with_directory_absolute_path(mock_parse_lab, mock_docker_manager, + mock_manager_get_instance, + test_lab): mock_parse_lab.return_value = test_lab mock_manager_get_instance.return_value = mock_docker_manager command = LconfigCommand() @@ -119,8 +198,9 @@ def test_run_remove_link_with_directory_absolute_path(mock_parse_lab, mock_docke @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") @mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") -def test_run_remove_link_with_directory_relative_path(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, - test_lab): +def test_run_remove_interface_with_directory_relative_path(mock_parse_lab, mock_docker_manager, + mock_manager_get_instance, + test_lab): mock_parse_lab.return_value = test_lab mock_manager_get_instance.return_value = mock_docker_manager command = LconfigCommand() @@ -134,7 +214,7 @@ def test_run_remove_link_with_directory_relative_path(mock_parse_lab, mock_docke @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") @mock.patch("src.Kathara.parser.netkit.LabParser.LabParser.parse") -def test_run_remove_two_links(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): +def test_run_remove_two_interfaces(mock_parse_lab, mock_docker_manager, mock_manager_get_instance, test_lab): mock_parse_lab.return_value = test_lab mock_manager_get_instance.return_value = mock_docker_manager command = LconfigCommand() diff --git a/tests/cli/vconfig_command_test.py b/tests/cli/vconfig_command_test.py index 8e4cd804..b48a187e 100644 --- a/tests/cli/vconfig_command_test.py +++ b/tests/cli/vconfig_command_test.py @@ -1,30 +1,61 @@ import sys from unittest import mock +import pytest + sys.path.insert(0, './') from src.Kathara.cli.command.VconfigCommand import VconfigCommand from src.Kathara.model.Lab import Lab - +@mock.patch("src.Kathara.model.Lab.Lab.get_or_new_link") @mock.patch("src.Kathara.model.Lab.Lab.get_machine") @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") -def test_run_add_link(mock_docker_manager, mock_manager_get_instance, mock_get_machine): +def test_run_add_interface(mock_docker_manager, mock_manager_get_instance, mock_get_machine, mock_get_or_new_link): lab = Lab('kathara_vlab') pc1 = lab.new_machine("pc1") + link_a = lab.get_or_new_link("A") + mock_get_or_new_link.return_value = link_a mock_get_machine.return_value = pc1 mock_manager_get_instance.return_value = mock_docker_manager command = VconfigCommand() command.run('.', ['-n', 'pc1', '--add', 'A']) mock_docker_manager.get_machine_api_object.assert_called_once_with('pc1', lab_name='kathara_vlab') - mock_docker_manager.connect_machine_to_link.assert_called_once() + mock_docker_manager.connect_machine_to_link.assert_called_once_with(pc1, link_a, mac_address=None) + + +@mock.patch("src.Kathara.model.Lab.Lab.get_or_new_link") +@mock.patch("src.Kathara.model.Lab.Lab.get_machine") +@mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") +def test_run_add_interface_with_mac_address(mock_docker_manager, mock_manager_get_instance, mock_get_machine, + mock_get_or_new_link): + lab = Lab('kathara_vlab') + pc1 = lab.new_machine("pc1") + link_a = lab.get_or_new_link("A") + mock_get_machine.return_value = pc1 + mock_get_or_new_link.return_value = link_a + mock_manager_get_instance.return_value = mock_docker_manager + command = VconfigCommand() + command.run('.', ['-n', 'pc1', '--add', 'A/00:00:00:00:00:01']) + mock_docker_manager.get_machine_api_object.assert_called_once_with('pc1', lab_name='kathara_vlab') + mock_docker_manager.connect_machine_to_link.assert_called_once_with(pc1, link_a, mac_address='00:00:00:00:00:01') + + +@mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") +def test_run_add_interface_syntax_error(mock_docker_manager, mock_manager_get_instance): + mock_manager_get_instance.return_value = mock_docker_manager + command = VconfigCommand() + with pytest.raises(SyntaxError): + command.run('.', ['-n', 'pc1', '--add', 'A/00/:00:00:00:00:01']) @mock.patch("src.Kathara.model.Lab.Lab.get_machine") @mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") -def test_run_remove_link(mock_docker_manager, mock_manager_get_instance, mock_get_machine): +def test_run_remove_interface(mock_docker_manager, mock_manager_get_instance, mock_get_machine): lab = Lab('kathara_vlab') pc1 = lab.new_machine("pc1") lab.new_link("A") From 5b2c51b1ef821bfb10f6c74410d0d4d7ba5fa6d5 Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Wed, 20 Dec 2023 19:48:35 +0100 Subject: [PATCH 23/44] Add `vstart` command test with MAC address (#137) --- tests/cli/vstart_command_test.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/cli/vstart_command_test.py b/tests/cli/vstart_command_test.py index e1c29d9e..6cd43f54 100644 --- a/tests/cli/vstart_command_test.py +++ b/tests/cli/vstart_command_test.py @@ -209,6 +209,34 @@ def test_run_with_one_interface(mock_docker_manager, mock_manager_get_instance, mock_docker_manager.deploy_lab.assert_called_once() +@mock.patch("src.Kathara.model.Lab.Lab.connect_machine_to_link") +@mock.patch("src.Kathara.model.Lab.Lab.get_or_new_machine") +@mock.patch("src.Kathara.setting.Setting.Setting.get_instance") +@mock.patch("src.Kathara.manager.Kathara.Kathara.get_instance") +@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager") +def test_run_with_one_interface_and_mac_address(mock_docker_manager, mock_manager_get_instance, + mock_setting_get_instance, + mock_get_or_new_machine, mock_connect_machine_to_link, test_lab, + mock_setting, default_device_args): + mock_manager_get_instance.return_value = mock_docker_manager + mock_setting_get_instance.return_value = mock_setting + mock_get_or_new_machine.return_value = Machine(test_lab, 'pc1') + default_device_args['eths'] = [('0', 'A', '00:00:00:00:00:01')] + command = VstartCommand() + with mock.patch.object(Lab, "add_option") as mock_add_option: + command.run('.', ['-n', 'pc1', '--eth', '0:A/00:00:00:00:00:01']) + assert mock_setting.open_terminals + assert mock_setting.terminal == '/usr/bin/xterm' + assert mock_setting.device_shell == '/usr/bin/bash' + mock_add_option.assert_any_call('hosthome_mount', None) + mock_add_option.assert_any_call('shared_mount', False) + mock_add_option.assert_any_call('privileged_machines', None) + mock_get_or_new_machine.assert_called_once_with('pc1', **default_device_args) + mock_connect_machine_to_link.assert_called_once_with('pc1', 'A', machine_iface_number=0, + mac_address='00:00:00:00:00:01') + mock_docker_manager.deploy_lab.assert_called_once() + + @mock.patch("src.Kathara.model.Lab.Lab.connect_machine_to_link") @mock.patch("src.Kathara.model.Lab.Lab.get_or_new_machine") @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") From fdbc515893ae306b67f862c278f4e2253cc2b11e Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Thu, 21 Dec 2023 18:49:57 +0100 Subject: [PATCH 24/44] Add `lab_hash` to Docker networks names (#256) This commit adds the `lab_hash` to Docker networks names and adds an option in the Shared Collision Domain of the `DockerSettingsAddon` to share collision domains between network scenarios of the same user or between different users. --- .../cli/ui/setting/DockerOptionsHandler.py | 31 +++--- src/Kathara/cli/ui/setting/utils.py | 7 ++ src/Kathara/manager/docker/DockerLink.py | 25 +++-- src/Kathara/manager/docker/DockerManager.py | 15 ++- .../setting/addon/DockerSettingsAddon.py | 9 +- src/Kathara/types.py | 25 +++++ tests/manager/docker/docker_link_test.py | 97 ++++++++++++++++--- tests/types_test.py | 10 ++ 8 files changed, 177 insertions(+), 42 deletions(-) create mode 100644 src/Kathara/types.py create mode 100644 tests/types_test.py diff --git a/src/Kathara/cli/ui/setting/DockerOptionsHandler.py b/src/Kathara/cli/ui/setting/DockerOptionsHandler.py index 5e62c795..e93083db 100644 --- a/src/Kathara/cli/ui/setting/DockerOptionsHandler.py +++ b/src/Kathara/cli/ui/setting/DockerOptionsHandler.py @@ -4,6 +4,7 @@ from ....trdparty.consolemenu import * from ....trdparty.consolemenu.items import * from ....trdparty.consolemenu.validators.regex import RegexValidator +from ....types import SharedCollisionDomainsOption from ....utils import exec_by_platform @@ -130,28 +131,36 @@ def add_items(self, current_menu: ConsoleMenu, menu_formatter: MenuFormatBuilder image_update_policy_item = SubmenuItem(image_update_policy_string, image_update_policy_menu, current_menu) - # Shared Links Option - shared_cd_string = "Enable Shared Collision Domains between users" + # Shared Collision Domains Option + shared_cd_string = "Enable Shared Collision Domains" shared_cd_menu = SelectionMenu(strings=[], title=shared_cd_string, - subtitle=setting_utils.current_bool("shared_cd"), - prologue_text="""This option allows to connect devices of different users to """ - """the same collision domains. + subtitle=setting_utils.current_enum("shared_cd", + SharedCollisionDomainsOption.to_string + ), + prologue_text="""This option allows sharing collision domains between """ + """network scenarios and users. - Default is %s.""" % - setting_utils.format_bool(DEFAULTS['shared_cd']), + Default is: %s.""" % DEFAULTS['shared_cd'], formatter=menu_formatter ) - shared_cd_menu.append_item(FunctionItem(text="Yes", + shared_cd_menu.append_item(FunctionItem(text="Share collision domains between network scenarios", function=setting_utils.update_setting_value, - args=["shared_cd", True], + args=["shared_cd", SharedCollisionDomainsOption.LABS], should_exit=True ) ) - shared_cd_menu.append_item(FunctionItem(text="No", + shared_cd_menu.append_item(FunctionItem(text="Share collision domains between users", function=setting_utils.update_setting_value, - args=["shared_cd", False], + args=["shared_cd", SharedCollisionDomainsOption.USERS], + should_exit=True + ) + ) + + shared_cd_menu.append_item(FunctionItem(text="Do not share collision domains", + function=setting_utils.update_setting_value, + args=["shared_cd", SharedCollisionDomainsOption.NOT_SHARED], should_exit=True ) ) diff --git a/src/Kathara/cli/ui/setting/utils.py b/src/Kathara/cli/ui/setting/utils.py index 7896e48d..cacf7f2e 100644 --- a/src/Kathara/cli/ui/setting/utils.py +++ b/src/Kathara/cli/ui/setting/utils.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Optional, Callable, Any, Tuple, List from ....exceptions import SettingsError @@ -34,6 +35,12 @@ def current_string(attribute_name: str, text: Optional[str] = None) -> Callable[ ")" if text else "" ) +def current_enum(attribute_name: str, to_string: Callable[[int], str], text: Optional[str] = None) -> Callable[[], str]: + return lambda: "%sCurrent: %s%s" % (text + " (" if text else "", + to_string(getattr(Setting.get_instance(), attribute_name)), + ")" if text else "" + ) + def update_setting_value(attribute_name: str, value: Any, stdout: bool = True) -> None: reload = False diff --git a/src/Kathara/manager/docker/DockerLink.py b/src/Kathara/manager/docker/DockerLink.py index c6a29290..a20a3d59 100644 --- a/src/Kathara/manager/docker/DockerLink.py +++ b/src/Kathara/manager/docker/DockerLink.py @@ -20,6 +20,7 @@ from ...model.Link import BRIDGE_LINK_NAME, Link from ...os.Networking import Networking from ...setting.Setting import Setting +from ...types import SharedCollisionDomainsOption class DockerLink(object): @@ -95,21 +96,23 @@ def create(self, link: Link) -> None: return # If a network with the same name exists, return it instead of creating a new one. - link_name = self.get_network_name(link.name) + link_name = self.get_network_name(link) networks = self.get_links_api_objects_by_filters(link_name=link_name) if networks: link.api_object = networks.pop() else: network_ipam_config = docker.types.IPAMConfig(driver='null') - user_label = "shared_cd" if Setting.get_instance().shared_cd else utils.get_current_user_name() + user_label = "shared_cd" if Setting.get_instance().shared_cd == SharedCollisionDomainsOption.USERS \ + else utils.get_current_user_name() link.api_object = self.client.networks.create( name=link_name, driver=f"{Setting.get_instance().network_plugin}:{utils.get_architecture()}", check_duplicate=True, ipam=network_ipam_config, labels={ - "lab_hash": link.lab.hash, + "lab_hash": link.lab.hash if + Setting.get_instance().shared_cd == SharedCollisionDomainsOption.NOT_SHARED else None, "name": link.name, "user": user_label, "app": "kathara", @@ -161,7 +164,7 @@ def wipe(self, user: str = None) -> None: Returns: None """ - user_label = "shared_cd" if Setting.get_instance().shared_cd else user + user_label = "shared_cd" if Setting.get_instance().shared_cd == SharedCollisionDomainsOption.USERS else user networks = self.get_links_api_objects_by_filters(user=user_label) for item in networks: item.reload() @@ -350,18 +353,22 @@ def _get_bridge_name(network: docker.models.networks.Network) -> str: Returns: str: The name of the Docker bridge in the format "kt-". """ - return "kt-%s" % network.id[:12] + return f"kt-{network.id[:12]}" @staticmethod - def get_network_name(name: str) -> str: + def get_network_name(link: Link) -> str: """Return the name of a Docker network. Args: - name (str): The name of a Kathara collision domain. + link (Kathara.model.Link): A Kathara collision domain. Returns: str: The name of the Docker network in the format "|net_prefix|_|username_prefix|_|name|". If shared collision domains, the format is: "|net_prefix|_|lab_hash|". """ - username_prefix = "_%s" % utils.get_current_user_name() if not Setting.get_instance().shared_cd else "" - return "%s%s_%s" % (Setting.get_instance().net_prefix, username_prefix, name) + if Setting.get_instance().shared_cd == SharedCollisionDomainsOption.LABS: + return f"{Setting.get_instance().net_prefix}_{utils.get_current_user_name()}_{link.name}" + elif Setting.get_instance().shared_cd == SharedCollisionDomainsOption.USERS: + return f"{Setting.get_instance().net_prefix}_{link.name}" + elif Setting.get_instance().shared_cd == SharedCollisionDomainsOption.NOT_SHARED: + return f"{Setting.get_instance().net_prefix}_{utils.get_current_user_name()}_{link.lab.hash}_{link.name}" diff --git a/src/Kathara/manager/docker/DockerManager.py b/src/Kathara/manager/docker/DockerManager.py index 224968f4..fcd894ce 100644 --- a/src/Kathara/manager/docker/DockerManager.py +++ b/src/Kathara/manager/docker/DockerManager.py @@ -23,6 +23,7 @@ from ...model.Link import Link from ...model.Machine import Machine from ...setting.Setting import Setting +from ...types import SharedCollisionDomainsOption from ...utils import pack_files_for_tar, import_pywintypes pywintypes = import_pywintypes() @@ -516,7 +517,11 @@ def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab: lab_containers = self.get_machines_api_objects(lab_hash=reconstructed_lab.hash) lab_networks = dict( - map(lambda x: (x.name, x), self.get_links_api_objects(lab_hash=reconstructed_lab.hash)) + map(lambda x: (x.name, x), self.get_links_api_objects( + lab_hash=reconstructed_lab.hash \ + if Setting.get_instance().shared_cd == SharedCollisionDomainsOption.NOT_SHARED else None, + all_users=Setting.get_instance().shared_cd == SharedCollisionDomainsOption.USERS + )) ) for container in lab_containers: @@ -580,13 +585,17 @@ def update_lab_from_api(self, lab: Lab) -> None: running_containers = self.get_machines_api_objects(lab_hash=lab.hash) deployed_networks = dict( - map(lambda x: (x.name, x), self.get_links_api_objects(lab_hash=lab.hash)) + map(lambda x: (x.name, x), self.get_links_api_objects( + lab_hash=lab.hash \ + if Setting.get_instance().shared_cd == SharedCollisionDomainsOption.NOT_SHARED else None, + all_users=Setting.get_instance().shared_cd == SharedCollisionDomainsOption.USERS + )) ) for network in deployed_networks.values(): network.reload() deployed_networks_by_link_name = dict( - map(lambda x: (x.attrs["Labels"]["name"], x), self.get_links_api_objects(lab_hash=lab.hash)) + map(lambda x: (x.attrs["Labels"]["name"], x), deployed_networks.values()) ) for container in running_containers: diff --git a/src/Kathara/setting/addon/DockerSettingsAddon.py b/src/Kathara/setting/addon/DockerSettingsAddon.py index 9da87411..f91fd5c4 100644 --- a/src/Kathara/setting/addon/DockerSettingsAddon.py +++ b/src/Kathara/setting/addon/DockerSettingsAddon.py @@ -1,12 +1,13 @@ from typing import Optional, Dict, Any from ...foundation.setting.SettingsAddon import SettingsAddon +from ...types import SharedCollisionDomainsOption DEFAULTS = { "hosthome_mount": False, "shared_mount": True, "image_update_policy": "Prompt", - "shared_cd": False, + "shared_cd": SharedCollisionDomainsOption.NOT_SHARED, "remote_url": None, "cert_path": None, "network_plugin": "kathara/katharanp_vde" @@ -14,14 +15,14 @@ class DockerSettingsAddon(SettingsAddon): - __slots__ = ['hosthome_mount', 'shared_mount', 'image_update_policy', 'shared_cd', 'remote_url', 'cert_path', - 'network_plugin'] + __slots__ = ['hosthome_mount', 'shared_mount', 'image_update_policy', 'shared_cd', + 'remote_url', 'cert_path', 'network_plugin'] def __init__(self) -> None: self.hosthome_mount: bool = False self.shared_mount: bool = True self.image_update_policy: str = 'Prompt' - self.shared_cd: bool = False + self.shared_cd: int = SharedCollisionDomainsOption.NOT_SHARED self.remote_url: Optional[str] = None self.cert_path: Optional[str] = None self.network_plugin: Optional[str] = "kathara/katharanp_vde" diff --git a/src/Kathara/types.py b/src/Kathara/types.py new file mode 100644 index 00000000..f831b721 --- /dev/null +++ b/src/Kathara/types.py @@ -0,0 +1,25 @@ +from enum import IntEnum + + +class SharedCollisionDomainsOption(IntEnum): + """Enum representing options for shared collision domains option. + + Attributes: + NOT_SHARED (int): Represents the option for not sharing collision domains (value: 1). + LABS (int): Represents the option for sharing collision domains among network scenarios of the same user + (value: 2). + USERS (int): Represents the option for sharing collision domains among network scenarios of different0 users + (value: 3). + """ + NOT_SHARED = 1 + LABS = 2 + USERS = 3 + + @staticmethod + def to_string(value): + if value == 1: + return "Not shared" + elif value == 2: + return "Share collision domain between network scenarios" + elif value == 3: + return "Share collision domain between users" diff --git a/tests/manager/docker/docker_link_test.py b/tests/manager/docker/docker_link_test.py index 40bceb7f..c2d7426e 100644 --- a/tests/manager/docker/docker_link_test.py +++ b/tests/manager/docker/docker_link_test.py @@ -8,9 +8,11 @@ sys.path.insert(0, './') from src.Kathara.model.Lab import Lab +from src.Kathara.model.Link import BRIDGE_LINK_NAME from src.Kathara.manager.docker.DockerLink import DockerLink from src.Kathara import utils from src.Kathara.exceptions import LinkNotFoundError +from src.Kathara.types import SharedCollisionDomainsOption # @@ -19,7 +21,17 @@ @pytest.fixture() def default_link(): from src.Kathara.model.Link import Link - return Link(Lab("default_scenario"), "A") + lab = Lab("default_scenario") + lab.hash = "lab-hash" + return Link(lab, "A") + + +@pytest.fixture() +def bridged_link(): + from src.Kathara.model.Link import Link + lab = Lab("default_scenario") + lab.hash = "lab-hash" + return Link(lab, BRIDGE_LINK_NAME) @pytest.fixture() @@ -47,32 +59,47 @@ def docker_link(mock_docker_client, mock_docker_plugin): # @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @mock.patch("src.Kathara.utils.get_current_user_name") -def test_get_network_name(mock_get_current_user_name, mock_setting_get_instance): +def test_get_network_name(mock_get_current_user_name, mock_setting_get_instance, default_link): mock_get_current_user_name.return_value = 'user' setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cd': SharedCollisionDomainsOption.NOT_SHARED, 'net_prefix': 'kathara', 'remote_url': None, }) mock_setting_get_instance.return_value = setting_mock - link_name = DockerLink.get_network_name("A") + link_name = DockerLink.get_network_name(default_link) + assert link_name == "kathara_user_lab-hash_A" + + +@mock.patch("src.Kathara.setting.Setting.Setting.get_instance") +@mock.patch("src.Kathara.utils.get_current_user_name") +def test_get_network_name_shared_cd_between_labs(mock_get_current_user_name, mock_setting_get_instance, default_link): + mock_get_current_user_name.return_value = 'user' + setting_mock = Mock() + setting_mock.configure_mock(**{ + 'shared_cd': SharedCollisionDomainsOption.LABS, + 'net_prefix': 'kathara', + 'remote_url': None + }) + mock_setting_get_instance.return_value = setting_mock + link_name = DockerLink.get_network_name(default_link) assert link_name == "kathara_user_A" @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @mock.patch("src.Kathara.utils.get_current_user_name") -def test_get_network_name_shared_cd(mock_get_current_user_name, mock_setting_get_instance): +def test_get_network_name_shared_cd_between_users(mock_get_current_user_name, mock_setting_get_instance, default_link): mock_get_current_user_name.return_value = 'user' setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': True, - 'net_prefix': 'CUSTOM_PREFIX', + 'shared_cd': SharedCollisionDomainsOption.USERS, + 'net_prefix': 'kathara', 'remote_url': None }) mock_setting_get_instance.return_value = setting_mock - link_name = DockerLink.get_network_name("A") - assert link_name == "CUSTOM_PREFIX_A" + link_name = DockerLink.get_network_name(default_link) + assert link_name == "kathara_A" # @@ -86,7 +113,7 @@ def test_create(mock_get_current_user_name, mock_setting_get_instance, docker_li mock_get_current_user_name.return_value = 'user' setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cd': SharedCollisionDomainsOption.NOT_SHARED, 'net_prefix': 'kathara', 'remote_url': None, 'network_plugin': 'kathara/katharanp' @@ -94,12 +121,12 @@ def test_create(mock_get_current_user_name, mock_setting_get_instance, docker_li mock_setting_get_instance.return_value = setting_mock docker_link.create(default_link) docker_link.client.networks.create.assert_called_once_with( - name="kathara_user_A", + name="kathara_user_lab-hash_A", driver=f"{setting_mock.network_plugin}:{utils.get_architecture()}", check_duplicate=True, ipam=docker.types.IPAMConfig(driver='null'), labels={ - "lab_hash": utils.generate_urlsafe_hash("default_scenario"), + "lab_hash": default_link.lab.hash, "name": "A", "user": "user", "app": "kathara", @@ -108,15 +135,20 @@ def test_create(mock_get_current_user_name, mock_setting_get_instance, docker_li ) +def test_create_bridge_link(docker_link, bridged_link): + assert not docker_link.create(bridged_link) + + @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @mock.patch("src.Kathara.utils.get_current_user_name") -def test_create_shared_cd(mock_get_current_user_name, mock_setting_get_instance, docker_link, default_link): +def test_create_shared_cd_between_users(mock_get_current_user_name, mock_setting_get_instance, docker_link, + default_link): docker_link.client.networks.list.return_value = [] mock_get_current_user_name.return_value = 'user' setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': True, + 'shared_cd': SharedCollisionDomainsOption.USERS, 'net_prefix': 'kathara', 'remote_url': None, 'network_plugin': 'kathara/katharanp' @@ -129,7 +161,7 @@ def test_create_shared_cd(mock_get_current_user_name, mock_setting_get_instance, check_duplicate=True, ipam=docker.types.IPAMConfig(driver='null'), labels={ - "lab_hash": utils.generate_urlsafe_hash("default_scenario"), + "lab_hash": None, "name": "A", "user": "shared_cd", "app": "kathara", @@ -138,6 +170,37 @@ def test_create_shared_cd(mock_get_current_user_name, mock_setting_get_instance, ) +@mock.patch("src.Kathara.setting.Setting.Setting.get_instance") +@mock.patch("src.Kathara.utils.get_current_user_name") +def test_create_shared_cd_between_labs(mock_get_current_user_name, mock_setting_get_instance, docker_link, + default_link): + docker_link.client.networks.list.return_value = [] + + mock_get_current_user_name.return_value = 'user' + setting_mock = Mock() + setting_mock.configure_mock(**{ + 'shared_cd': SharedCollisionDomainsOption.LABS, + 'net_prefix': 'kathara', + 'remote_url': None, + 'network_plugin': 'kathara/katharanp' + }) + mock_setting_get_instance.return_value = setting_mock + docker_link.create(default_link) + docker_link.client.networks.create.assert_called_once_with( + name="kathara_user_A", + driver=f"{setting_mock.network_plugin}:{utils.get_architecture()}", + check_duplicate=True, + ipam=docker.types.IPAMConfig(driver='null'), + labels={ + "lab_hash": None, + "name": "A", + "user": "user", + "app": "kathara", + "external": "" + } + ) + + # # TEST: _deploy_link # @@ -147,6 +210,10 @@ def test_deploy_link(mock_create, docker_link, default_link): mock_create.called_once_with(default_link) +def test_deploy_link_bridge(docker_link, bridged_link): + assert not docker_link._deploy_link((bridged_link.name, bridged_link)) + + # # TEST: deploy_links # diff --git a/tests/types_test.py b/tests/types_test.py new file mode 100644 index 00000000..342ef404 --- /dev/null +++ b/tests/types_test.py @@ -0,0 +1,10 @@ +import sys + +sys.path.insert(0, './') + +from src.Kathara.types import SharedCollisionDomainsOption + +def test_shared_collision_domains_option_to_string(): + assert SharedCollisionDomainsOption.to_string(1) == 'Not shared' + assert SharedCollisionDomainsOption.to_string(2) == 'Share collision domain between network scenarios' + assert SharedCollisionDomainsOption.to_string(3) == 'Share collision domain between users' From 83c6b34c7c27d2bbb819dc1c4a2df7383dff741b Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sat, 23 Dec 2023 15:09:33 +0100 Subject: [PATCH 25/44] Fix shared CD Setting name + Minor (#256) --- docs/kathara.conf.5.ronn | 8 +- src/Kathara/cli/command/LconfigCommand.py | 4 +- src/Kathara/cli/command/VconfigCommand.py | 4 +- .../cli/ui/setting/CommonOptionsHandler.py | 491 ++++++++++-------- .../cli/ui/setting/DockerOptionsHandler.py | 409 ++++++++------- .../ui/setting/KubernetesOptionsHandler.py | 218 ++++---- .../cli/ui/setting/SettingsMenuFactory.py | 3 +- src/Kathara/cli/ui/setting/utils.py | 1 + src/Kathara/cli/ui/utils.py | 15 +- src/Kathara/manager/docker/DockerLink.py | 13 +- src/Kathara/manager/docker/DockerManager.py | 8 +- .../setting/addon/DockerSettingsAddon.py | 8 +- src/Kathara/types.py | 20 +- tests/manager/docker/docker_link_test.py | 20 +- tests/manager/docker/docker_machine_test.py | 34 +- tests/manager/docker/docker_manager_test.py | 29 +- tests/types_test.py | 7 +- 17 files changed, 721 insertions(+), 571 deletions(-) diff --git a/docs/kathara.conf.5.ronn b/docs/kathara.conf.5.ronn index 6bb7eb44..1f5b0140 100644 --- a/docs/kathara.conf.5.ronn +++ b/docs/kathara.conf.5.ronn @@ -97,10 +97,10 @@ Each Manager specifies additional parameters which are used only when the Manage Default to `Prompt`. -* `shared_cd` (boolean): - This parameter allows to connect devices of different users to the same collision domains. +* `shared_cds` (integer): + This parameter allows to connect devices of different network scenarios and users to the same collision domains. - Default to `false`. + Default to `1` (enum value for `Not Shared`). * `remote_url` (string): This parameter specifies a Remote Docker daemon URL to connect to, instead of a local one. @@ -154,7 +154,7 @@ Each Manager specifies additional parameters which are used only when the Manage "hosthome_mount": false, "shared_mount": true, "image_update_policy": "Prompt", - "shared_cd": false, + "shared_cds": 1, "remote_url": null, "cert_path": null, "network_plugin": "kathara/katharanp_vde" diff --git a/src/Kathara/cli/command/LconfigCommand.py b/src/Kathara/cli/command/LconfigCommand.py index a7656c15..38ec2301 100644 --- a/src/Kathara/cli/command/LconfigCommand.py +++ b/src/Kathara/cli/command/LconfigCommand.py @@ -2,7 +2,7 @@ import logging from typing import List -from ..ui.utils import alphanumeric, cd_mac_address +from ..ui.utils import alphanumeric, cd_mac from ... import utils from ...foundation.cli.command.Command import Command from ...manager.Kathara import Kathara @@ -44,7 +44,7 @@ def __init__(self) -> None: group.add_argument( '--add', - type=cd_mac_address, + type=cd_mac, dest='to_add', metavar='CD/MAC', nargs='+', diff --git a/src/Kathara/cli/command/VconfigCommand.py b/src/Kathara/cli/command/VconfigCommand.py index 1abc9399..4ca9bb4c 100644 --- a/src/Kathara/cli/command/VconfigCommand.py +++ b/src/Kathara/cli/command/VconfigCommand.py @@ -2,7 +2,7 @@ import logging from typing import List -from ..ui.utils import alphanumeric, cd_mac_address +from ..ui.utils import alphanumeric, cd_mac from ...foundation.cli.command.Command import Command from ...manager.Kathara import Kathara from ...model.Lab import Lab @@ -37,7 +37,7 @@ def __init__(self) -> None: group.add_argument( '--add', - type=cd_mac_address, + type=cd_mac, dest='to_add', metavar='CD/MAC', nargs='+', diff --git a/src/Kathara/cli/ui/setting/CommonOptionsHandler.py b/src/Kathara/cli/ui/setting/CommonOptionsHandler.py index 491d1c6d..d7330740 100644 --- a/src/Kathara/cli/ui/setting/CommonOptionsHandler.py +++ b/src/Kathara/cli/ui/setting/CommonOptionsHandler.py @@ -22,115 +22,134 @@ def add_items(self, current_menu: ConsoleMenu, menu_formatter: MenuFormatBuilder manager_type = Setting.get_instance().manager_type choose_manager_string = "Choose default manager" - manager_menu = SelectionMenu(strings=[], - title=choose_manager_string, - subtitle=lambda: "Current: %s" % managers[manager_type], - prologue_text="""Manager is the Engine used to run Kathara labs. - Default is `%s`.""" % managers[DEFAULTS['manager_type']], - formatter=menu_formatter - ) + manager_menu = SelectionMenu( + strings=[], + title=choose_manager_string, + subtitle=lambda: "Current: %s" % managers[manager_type], + prologue_text="""Manager is the Engine used to run Kathara labs. + + Default is `%s`.""" % managers[DEFAULTS['manager_type']], + formatter=menu_formatter + ) for name, formatted_name in managers.items(): - manager_menu.append_item(FunctionItem(text=formatted_name, - function=self.update_manager_value, - args=[current_menu, name], - should_exit=True - ) - ) + manager_menu.append_item( + FunctionItem( + text=formatted_name, + function=self.update_manager_value, + args=[current_menu, name], + should_exit=True + ) + ) manager_item = SubmenuItem(choose_manager_string, manager_menu, current_menu) # Image Selection Submenu image_string = "Choose default image" - select_image_menu = SelectionMenu(strings=[], - title=image_string, - subtitle=setting_utils.current_string("image"), - prologue_text="""Default Docker image when you start a network scenario or """ - """a single Kathara device. - Default is `%s`.""" % DEFAULTS['image'], - formatter=menu_formatter - ) + select_image_menu = SelectionMenu( + strings=[], + title=image_string, + subtitle=setting_utils.current_string("image"), + prologue_text="""Default Docker image when you start a network scenario or a single Kathara device. + + Default is `%s`.""" % DEFAULTS['image'], + formatter=menu_formatter + ) try: for image_name in DockerHubApi.get_tagged_images(): - select_image_menu.append_item(FunctionItem(text=image_name, - function=setting_utils.update_setting_value, - args=['image', image_name], - should_exit=True - ) - ) + select_image_menu.append_item( + FunctionItem( + text=image_name, + function=setting_utils.update_setting_value, + args=['image', image_name], + should_exit=True + ) + ) except HTTPConnectionError: pass - select_image_menu.append_item(FunctionItem(text="Choose another image", - function=setting_utils.read_value, - args=['image', - ImageValidator(), - 'Write the name of a Docker image available on Docker Hub:', - None - ], - should_exit=True - ) - ) + select_image_menu.append_item( + FunctionItem( + text="Choose another image", + function=setting_utils.read_value, + args=['image', + ImageValidator(), + 'Write the name of a Docker image available on Docker Hub:', + None + ], + should_exit=True + ) + ) submenu_item = SubmenuItem(image_string, select_image_menu, current_menu) # Open Terminals Option open_terminals_string = "Automatically open terminals on startup" - open_terminals_menu = SelectionMenu(strings=[], - title=open_terminals_string, - subtitle=setting_utils.current_bool("open_terminals"), - prologue_text="""Determines if the device terminal should be opened """ - """when starting it. - Default is %s.""" - % setting_utils.format_bool(DEFAULTS['open_terminals']), - formatter=menu_formatter - ) - - open_terminals_menu.append_item(FunctionItem(text="Yes", - function=setting_utils.update_setting_value, - args=['open_terminals', True], - should_exit=True - ) - ) - open_terminals_menu.append_item(FunctionItem(text="No", - function=setting_utils.update_setting_value, - args=['open_terminals', False], - should_exit=True - ) - ) + open_terminals_menu = SelectionMenu( + strings=[], + title=open_terminals_string, + subtitle=setting_utils.current_bool("open_terminals"), + prologue_text="""Determines if the device terminal should be opened when starting it. + + Default is %s.""" + % setting_utils.format_bool(DEFAULTS['open_terminals']), + formatter=menu_formatter + ) + + open_terminals_menu.append_item( + FunctionItem( + text="Yes", + function=setting_utils.update_setting_value, + args=['open_terminals', True], + should_exit=True + ) + ) + open_terminals_menu.append_item( + FunctionItem( + text="No", + function=setting_utils.update_setting_value, + args=['open_terminals', False], + should_exit=True + ) + ) open_terminals_item = SubmenuItem(open_terminals_string, open_terminals_menu, current_menu) # Machine Shell Submenu machine_shell_string = "Choose device shell to be used" - machine_shell_menu = SelectionMenu(strings=[], - title=machine_shell_string, - subtitle=setting_utils.current_string("device_shell"), - formatter=menu_formatter, - prologue_text="""The shell to use inside the device. - **The application must be correctly installed in the Docker - image used for the device!** - Default is `%s`, but it depends on the used Docker image. - """ % DEFAULTS['device_shell'] - ) + machine_shell_menu = SelectionMenu( + strings=[], + title=machine_shell_string, + subtitle=setting_utils.current_string("device_shell"), + formatter=menu_formatter, + prologue_text="""The shell to use inside the device. + + **The application must be correctly installed in the Docker image used for the device!** + + Default is `%s`, but it depends on the used Docker image.""" % DEFAULTS['device_shell'] + ) for shell in SHELLS_HINT: - machine_shell_menu.append_item(FunctionItem(text=shell, - function=setting_utils.update_setting_value, - args=["device_shell", shell], - should_exit=True - ) - ) - machine_shell_menu.append_item(FunctionItem(text="Choose another shell", - function=setting_utils.read_value, - args=['device_shell', - RegexValidator(r"^(\w|/)+$"), - 'Write the name of a shell:', - 'Shell name is not valid!' - ], - should_exit=True - ) - ) + machine_shell_menu.append_item( + FunctionItem( + text=shell, + function=setting_utils.update_setting_value, + args=["device_shell", shell], + should_exit=True + ) + ) + machine_shell_menu.append_item( + FunctionItem( + text="Choose another shell", + function=setting_utils.read_value, + args=['device_shell', + RegexValidator(r"^(\w|/)+$"), + 'Write the name of a shell:', + 'Shell name is not valid!' + ], + should_exit=True + ) + ) machine_shell_item = SubmenuItem(machine_shell_string, machine_shell_menu, current_menu) @@ -138,69 +157,81 @@ def add_items(self, current_menu: ConsoleMenu, menu_formatter: MenuFormatBuilder # Linux Version def terminal_emulator_menu_linux(): terminal_string = "Choose terminal emulator to be used" - terminal_menu = SelectionMenu(strings=[], - title=terminal_string, - subtitle=setting_utils.current_string("terminal"), - formatter=menu_formatter, - prologue_text="""Terminal emulator application to be used for device """ - """terminals. - **The application must be correctly installed in """ - """the host system!** - Default is `%s`.""" % DEFAULTS['terminal'] - ) - - terminal_menu.append_item(FunctionItem(text="/usr/bin/xterm", - function=setting_utils.update_setting_value, - args=["terminal", "/usr/bin/xterm"], - should_exit=True - ) - ) - terminal_menu.append_item(FunctionItem(text="TMUX", - function=setting_utils.update_setting_value, - args=["terminal", "TMUX"], - should_exit=True - ) - ) - terminal_menu.append_item(FunctionItem(text="Choose another terminal emulator", - function=setting_utils.read_value, - args=['terminal', - TerminalValidator(), - 'Write the path of a terminal emulator:', - 'Terminal emulator is not valid! Install it before using it.' - ], - should_exit=True - ) - ) + terminal_menu = SelectionMenu( + strings=[], + title=terminal_string, + subtitle=setting_utils.current_string("terminal"), + formatter=menu_formatter, + prologue_text="""Terminal emulator application to be used for device terminals. + + **The application must be correctly installed in the host system!** + + Default is `%s`.""" % DEFAULTS['terminal'] + ) + + terminal_menu.append_item( + FunctionItem( + text="/usr/bin/xterm", + function=setting_utils.update_setting_value, + args=["terminal", "/usr/bin/xterm"], + should_exit=True + ) + ) + terminal_menu.append_item( + FunctionItem( + text="TMUX", + function=setting_utils.update_setting_value, + args=["terminal", "TMUX"], + should_exit=True + ) + ) + terminal_menu.append_item( + FunctionItem( + text="Choose another terminal emulator", + function=setting_utils.read_value, + args=['terminal', + TerminalValidator(), + 'Write the path of a terminal emulator:', + 'Terminal emulator is not valid! Install it before using it.' + ], + should_exit=True + ) + ) return SubmenuItem(terminal_string, terminal_menu, current_menu) # macOS Version def terminal_emulator_menu_osx(): terminal_string = "Choose terminal emulator to be used" - terminal_menu = SelectionMenu(strings=[], - title=terminal_string, - subtitle=setting_utils.current_string("terminal"), - formatter=menu_formatter, - prologue_text="""Terminal emulator application to be used for device """ - """terminals. - **The application must be correctly installed in """ - """the host system!** - Default is `Terminal`.""" - ) + terminal_menu = SelectionMenu( + strings=[], + title=terminal_string, + subtitle=setting_utils.current_string("terminal"), + formatter=menu_formatter, + prologue_text="""Terminal emulator application to be used for device terminals. + + **The application must be correctly installed in the host system!** + + Default is `Terminal`.""" + ) for terminal in TERMINALS_OSX: - terminal_menu.append_item(FunctionItem(text=terminal, - function=setting_utils.update_setting_value, - args=["terminal", terminal], - should_exit=True - ) - ) - terminal_menu.append_item(FunctionItem(text="TMUX", - function=setting_utils.update_setting_value, - args=["terminal", "TMUX"], - should_exit=True - ) - ) + terminal_menu.append_item( + FunctionItem( + text=terminal, + function=setting_utils.update_setting_value, + args=["terminal", terminal], + should_exit=True + ) + ) + terminal_menu.append_item( + FunctionItem( + text="TMUX", + function=setting_utils.update_setting_value, + args=["terminal", "TMUX"], + should_exit=True + ) + ) return SubmenuItem(terminal_string, terminal_menu, current_menu) @@ -208,108 +239,124 @@ def terminal_emulator_menu_osx(): # Prefixes Submenu prefixes_string = "Choose Kathara prefixes" - prefixes_menu = SelectionMenu(strings=[], - title=prefixes_string, - formatter=menu_formatter, - prologue_text="""Prefixes assigned to the network and device names when deployed. - Default is `%s` and `%s`.""" % (DEFAULTS['net_prefix'], - DEFAULTS['device_prefix']) - ) - - net_prefix_item = FunctionItem(text=setting_utils.current_string("net_prefix", - text="Insert Kathara networks prefix"), - function=setting_utils.read_value, - args=['net_prefix', - RegexValidator(r"^[a-z]+_?[a-z_]+$"), - 'Write a Kathara networks prefix:', - 'Network Prefix must only contain lowercase letters and underscore.' - ], - should_exit=True - ) + prefixes_menu = SelectionMenu( + strings=[], + title=prefixes_string, + formatter=menu_formatter, + prologue_text="""Prefixes assigned to the network and device names when deployed. + + Defaults are `%s` and `%s`.""" % (DEFAULTS['net_prefix'], DEFAULTS['device_prefix']) + ) + + net_prefix_item = FunctionItem( + text=setting_utils.current_string("net_prefix", text="Insert Kathara networks prefix"), + function=setting_utils.read_value, + args=['net_prefix', + RegexValidator(r"^[a-z]+_?[a-z_]+$"), + 'Write a Kathara networks prefix:', + 'Network Prefix must only contain lowercase letters and underscore.' + ], + should_exit=True + ) prefixes_menu.append_item(net_prefix_item) - machine_prefix_item = FunctionItem(text=setting_utils.current_string("device_prefix", - text="Insert Kathara devices prefix"), - function=setting_utils.read_value, - args=['device_prefix', - RegexValidator(r"^[a-z]+_?[a-z_]+$"), - 'Write a Kathara devices prefix:', - 'Device Prefix must only contain lowercase letters and underscore.' - ], - should_exit=True - ) + machine_prefix_item = FunctionItem( + text=setting_utils.current_string("device_prefix", text="Insert Kathara devices prefix"), + function=setting_utils.read_value, + args=['device_prefix', + RegexValidator(r"^[a-z]+_?[a-z_]+$"), + 'Write a Kathara devices prefix:', + 'Device Prefix must only contain lowercase letters and underscore.' + ], + should_exit=True + ) prefixes_menu.append_item(machine_prefix_item) prefixes_item = SubmenuItem(prefixes_string, prefixes_menu, current_menu) # Debug Level Submenu debug_level_string = "Choose logging level to be used" - debug_level_menu = SelectionMenu(strings=[], - title=debug_level_string, - subtitle=setting_utils.current_string("debug_level"), - formatter=menu_formatter, - prologue_text="""Logging level of Kathara messages. - Default is `%s`.""" % DEFAULTS['debug_level'] - ) + debug_level_menu = SelectionMenu( + strings=[], + title=debug_level_string, + subtitle=setting_utils.current_string("debug_level"), + formatter=menu_formatter, + prologue_text="""Logging level of Kathara messages. + + Default is `%s`.""" % DEFAULTS['debug_level'] + ) for debug_level in AVAILABLE_DEBUG_LEVELS: - debug_level_menu.append_item(FunctionItem(text=debug_level, - function=setting_utils.update_setting_value, - args=["debug_level", debug_level], - should_exit=True - ) - ) + debug_level_menu.append_item( + FunctionItem( + text=debug_level, + function=setting_utils.update_setting_value, + args=["debug_level", debug_level], + should_exit=True + ) + ) debug_level_item = SubmenuItem(debug_level_string, debug_level_menu, current_menu) # Print Startup Logs Option print_startup_log_string = "Print Startup Logs on device startup" - print_startup_log_menu = SelectionMenu(strings=[], - title=open_terminals_string, - subtitle=setting_utils.current_bool("print_startup_log"), - formatter=menu_formatter, - prologue_text="""When opening a device terminal, print its startup log. - Default is %s.""" % - setting_utils.format_bool(DEFAULTS['print_startup_log']) - ) - - print_startup_log_menu.append_item(FunctionItem(text="Yes", - function=setting_utils.update_setting_value, - args=['print_startup_log', True], - should_exit=True - ) - ) - print_startup_log_menu.append_item(FunctionItem(text="No", - function=setting_utils.update_setting_value, - args=['print_startup_log', False], - should_exit=True - ) - ) + print_startup_log_menu = SelectionMenu( + strings=[], + title=open_terminals_string, + subtitle=setting_utils.current_bool("print_startup_log"), + formatter=menu_formatter, + prologue_text="""When opening a device terminal, print its startup log. + + Default is %s.""" % setting_utils.format_bool(DEFAULTS['print_startup_log']) + ) + + print_startup_log_menu.append_item( + FunctionItem( + text="Yes", + function=setting_utils.update_setting_value, + args=['print_startup_log', True], + should_exit=True + ) + ) + print_startup_log_menu.append_item( + FunctionItem( + text="No", + function=setting_utils.update_setting_value, + args=['print_startup_log', False], + should_exit=True + ) + ) print_startup_log_item = SubmenuItem(print_startup_log_string, print_startup_log_menu, current_menu) # Enable IPv6 Option enable_ipv6_string = "Enable IPv6" - enable_ipv6_menu = SelectionMenu(strings=[], - title=enable_ipv6_string, - subtitle=setting_utils.current_bool("enable_ipv6"), - formatter=menu_formatter, - prologue_text="""This option enables IPv6 inside the devices. - Default is %s.""" % - setting_utils.format_bool(DEFAULTS['enable_ipv6'])) - - enable_ipv6_menu.append_item(FunctionItem(text="Yes", - function=setting_utils.update_setting_value, - args=['enable_ipv6', True], - should_exit=True - ) - ) - enable_ipv6_menu.append_item(FunctionItem(text="No", - function=setting_utils.update_setting_value, - args=['enable_ipv6', False], - should_exit=True - ) - ) + enable_ipv6_menu = SelectionMenu( + strings=[], + title=enable_ipv6_string, + subtitle=setting_utils.current_bool("enable_ipv6"), + formatter=menu_formatter, + prologue_text="""This option enables IPv6 inside the devices. + + Default is %s.""" % setting_utils.format_bool(DEFAULTS['enable_ipv6']) + ) + + enable_ipv6_menu.append_item( + FunctionItem( + text="Yes", + function=setting_utils.update_setting_value, + args=['enable_ipv6', True], + should_exit=True + ) + ) + enable_ipv6_menu.append_item( + FunctionItem( + text="No", + function=setting_utils.update_setting_value, + args=['enable_ipv6', False], + should_exit=True + ) + ) enable_ipv6_item = SubmenuItem(enable_ipv6_string, enable_ipv6_menu, current_menu) diff --git a/src/Kathara/cli/ui/setting/DockerOptionsHandler.py b/src/Kathara/cli/ui/setting/DockerOptionsHandler.py index e93083db..1e9f9f91 100644 --- a/src/Kathara/cli/ui/setting/DockerOptionsHandler.py +++ b/src/Kathara/cli/ui/setting/DockerOptionsHandler.py @@ -12,226 +12,259 @@ class DockerOptionsHandler(OptionsHandler): def add_items(self, current_menu: ConsoleMenu, menu_formatter: MenuFormatBuilder) -> None: # Network Plugin Option network_plugin_string = "Choose Docker Network Plugin version" - network_plugin_menu = SelectionMenu(strings=[], - title=network_plugin_string, - subtitle=setting_utils.current_string("network_plugin"), - prologue_text="""Choose Docker Network Plugin version used to create""" - """ collision domains.\n""" - """`kathara/katharanp` plugin is based on Linux bridges.\n""" - """`kathara/katharanp_vde` plugin is based on VDE switches. - - Default is `%s`.""" % - DEFAULTS['network_plugin'], - formatter=menu_formatter - ) - - network_plugin_menu.append_item(FunctionItem(text="kathara/katharanp", - function=setting_utils.update_setting_value, - args=["network_plugin", "kathara/katharanp"], - should_exit=True - ) - ) - network_plugin_menu.append_item(FunctionItem(text="kathara/katharanp_vde", - function=setting_utils.update_setting_value, - args=["network_plugin", "kathara/katharanp_vde"], - should_exit=True - ) - ) + network_plugin_menu = SelectionMenu( + strings=[], + title=network_plugin_string, + subtitle=setting_utils.current_string("network_plugin"), + prologue_text="""Choose Docker Network Plugin version for collision domains. + + `kathara/katharanp` plugin is based on Linux bridges. + + `kathara/katharanp_vde` plugin is based on VDE switches. + + Default is `%s`.""" % + DEFAULTS['network_plugin'], + formatter=menu_formatter + ) + + network_plugin_menu.append_item( + FunctionItem( + text="kathara/katharanp", + function=setting_utils.update_setting_value, + args=["network_plugin", "kathara/katharanp"], + should_exit=True + ) + ) + network_plugin_menu.append_item( + FunctionItem( + text="kathara/katharanp_vde", + function=setting_utils.update_setting_value, + args=["network_plugin", "kathara/katharanp_vde"], + should_exit=True + ) + ) network_plugin_item = SubmenuItem(network_plugin_string, network_plugin_menu, current_menu) # Hosthome Mount Option hosthome_string = "Automatically mount /hosthome on startup" - hosthome_menu = SelectionMenu(strings=[], - title=hosthome_string, - subtitle=setting_utils.current_bool("hosthome_mount"), - prologue_text="""The home directory of the current user is made available for """ - """reading/writing inside the device under the special """ - """directory `/hosthome`. - - Default is %s.""" % - setting_utils.format_bool(DEFAULTS['hosthome_mount']), - formatter=menu_formatter - ) - - hosthome_menu.append_item(FunctionItem(text="Yes", - function=setting_utils.update_setting_value, - args=["hosthome_mount", True], - should_exit=True - ) - ) - hosthome_menu.append_item(FunctionItem(text="No", - function=setting_utils.update_setting_value, - args=["hosthome_mount", False], - should_exit=True - ) - ) + hosthome_menu = SelectionMenu( + strings=[], + title=hosthome_string, + subtitle=setting_utils.current_bool("hosthome_mount"), + prologue_text="""The home directory of the current user is made available for """ + """reading/writing inside the device under the special directory `/hosthome`. + + Default is %s.""" % + setting_utils.format_bool(DEFAULTS['hosthome_mount']), + formatter=menu_formatter + ) + + hosthome_menu.append_item( + FunctionItem( + text="Yes", + function=setting_utils.update_setting_value, + args=["hosthome_mount", True], + should_exit=True + ) + ) + hosthome_menu.append_item( + FunctionItem( + text="No", + function=setting_utils.update_setting_value, + args=["hosthome_mount", False], + should_exit=True + ) + ) hosthome_item = SubmenuItem(hosthome_string, hosthome_menu, current_menu) # Shared Mount Option shared_string = "Automatically mount /shared on startup" - shared_menu = SelectionMenu(strings=[], - title=shared_string, - subtitle=setting_utils.current_bool("shared_mount"), - prologue_text="""The shared directory inside the network scenario folder is """ - """made available for reading/writing inside the device """ - """under the special directory `/shared`. - - Default is %s.""" % - setting_utils.format_bool(DEFAULTS['shared_mount']), - formatter=menu_formatter - ) - - shared_menu.append_item(FunctionItem(text="Yes", - function=setting_utils.update_setting_value, - args=["shared_mount", True], - should_exit=True - ) - ) - shared_menu.append_item(FunctionItem(text="No", - function=setting_utils.update_setting_value, - args=["shared_mount", False], - should_exit=True - ) - ) + shared_menu = SelectionMenu( + strings=[], + title=shared_string, + subtitle=setting_utils.current_bool("shared_mount"), + prologue_text="""The shared directory inside the network scenario folder is """ + """made available for reading/writing inside the device under the special directory `/shared`. + + Default is %s.""" % + setting_utils.format_bool(DEFAULTS['shared_mount']), + formatter=menu_formatter + ) + + shared_menu.append_item( + FunctionItem( + text="Yes", + function=setting_utils.update_setting_value, + args=["shared_mount", True], + should_exit=True + ) + ) + shared_menu.append_item( + FunctionItem( + text="No", + function=setting_utils.update_setting_value, + args=["shared_mount", False], + should_exit=True + ) + ) shared_item = SubmenuItem(shared_string, shared_menu, current_menu) # Image Update Policy Option image_update_policy_string = "Docker Image Update Policy" - image_update_policy_menu = SelectionMenu(strings=[], - title=image_update_policy_string, - subtitle=setting_utils.current_string("image_update_policy"), - prologue_text="""Choose the policy when a Docker image update """ - """is available for a running device. - - Default is %s.""" % DEFAULTS['image_update_policy'], - formatter=menu_formatter - ) - - image_update_policy_menu.append_item(FunctionItem(text="Prompt", - function=setting_utils.update_setting_value, - args=["image_update_policy", "Prompt"], - should_exit=True - ) - ) - image_update_policy_menu.append_item(FunctionItem(text="Always", - function=setting_utils.update_setting_value, - args=["image_update_policy", "Always"], - should_exit=True - ) - ) - image_update_policy_menu.append_item(FunctionItem(text="Never", - function=setting_utils.update_setting_value, - args=["image_update_policy", "Never"], - should_exit=True - ) - ) + image_update_policy_menu = SelectionMenu( + strings=[], + title=image_update_policy_string, + subtitle=setting_utils.current_string("image_update_policy"), + prologue_text="""Choose the policy when a Docker image update is available for a running device. + + \tDefault is %s.""" % DEFAULTS['image_update_policy'], + formatter=menu_formatter + ) + + image_update_policy_menu.append_item( + FunctionItem( + text="Prompt", + function=setting_utils.update_setting_value, + args=["image_update_policy", "Prompt"], + should_exit=True + ) + ) + image_update_policy_menu.append_item( + FunctionItem( + text="Always", + function=setting_utils.update_setting_value, + args=["image_update_policy", "Always"], + should_exit=True + ) + ) + image_update_policy_menu.append_item( + FunctionItem( + text="Never", + function=setting_utils.update_setting_value, + args=["image_update_policy", "Never"], + should_exit=True + ) + ) image_update_policy_item = SubmenuItem(image_update_policy_string, image_update_policy_menu, current_menu) # Shared Collision Domains Option - shared_cd_string = "Enable Shared Collision Domains" - shared_cd_menu = SelectionMenu(strings=[], - title=shared_cd_string, - subtitle=setting_utils.current_enum("shared_cd", - SharedCollisionDomainsOption.to_string - ), - prologue_text="""This option allows sharing collision domains between """ - """network scenarios and users. - - Default is: %s.""" % DEFAULTS['shared_cd'], - formatter=menu_formatter - ) - - shared_cd_menu.append_item(FunctionItem(text="Share collision domains between network scenarios", - function=setting_utils.update_setting_value, - args=["shared_cd", SharedCollisionDomainsOption.LABS], - should_exit=True - ) - ) - shared_cd_menu.append_item(FunctionItem(text="Share collision domains between users", - function=setting_utils.update_setting_value, - args=["shared_cd", SharedCollisionDomainsOption.USERS], - should_exit=True - ) - ) - - shared_cd_menu.append_item(FunctionItem(text="Do not share collision domains", - function=setting_utils.update_setting_value, - args=["shared_cd", SharedCollisionDomainsOption.NOT_SHARED], - should_exit=True - ) - ) - - shared_cd_item = SubmenuItem(shared_cd_string, shared_cd_menu, current_menu) + shared_cds_string = "Enable Shared Collision Domains" + shared_cds_menu = SelectionMenu( + strings=[], + title=shared_cds_string, + subtitle=setting_utils.current_enum("shared_cds", SharedCollisionDomainsOption.to_string), + prologue_text="""This option allows sharing collision domains between network scenarios and users. + + Default is: %s.""" % SharedCollisionDomainsOption.to_string(DEFAULTS['shared_cds']), + formatter=menu_formatter + ) + + shared_cds_menu.append_item( + FunctionItem( + text="Share collision domains between network scenarios", + function=setting_utils.update_setting_value, + args=["shared_cds", SharedCollisionDomainsOption.LABS], + should_exit=True + ) + ) + shared_cds_menu.append_item( + FunctionItem( + text="Share collision domains between users", + function=setting_utils.update_setting_value, + args=["shared_cds", SharedCollisionDomainsOption.USERS], + should_exit=True + ) + ) + + shared_cds_menu.append_item( + FunctionItem( + text="Do not share collision domains", + function=setting_utils.update_setting_value, + args=["shareds_cd", SharedCollisionDomainsOption.NOT_SHARED], + should_exit=True + ) + ) + + shared_cds_item = SubmenuItem(shared_cds_string, shared_cds_menu, current_menu) # Remote Docker Daemon Option def remote_url_unix(): remote_url_string = "Configure a remote Docker connection" - remote_url_menu = SelectionMenu(strings=[], - title=remote_url_string, - subtitle=setting_utils.current_string("remote_url"), - prologue_text="""You can specify a remote Docker Daemon URL. + remote_url_menu = SelectionMenu( + strings=[], + title=remote_url_string, + subtitle=setting_utils.current_string("remote_url"), + prologue_text="""You can specify a remote Docker Daemon URL. - Default is %s.""" % DEFAULTS['remote_url'], - formatter=menu_formatter - ) - - remote_url_menu.append_item(FunctionItem(text="Insert a remote Docker Daemon URL", - function=setting_utils.read_value, - args=['remote_url', - RegexValidator(setting_utils.URL_REGEX), - 'Write a Docker Daemon URL ' - '(format http[s]://:):', - 'Docker Daemon URL is not a valid URL (remove ' - 'the trailing slash, if present)' - ], - should_exit=False - ) - ) + Default is %s.""" % DEFAULTS['remote_url'], + formatter=menu_formatter + ) + + remote_url_menu.append_item( + FunctionItem( + text="Insert a remote Docker Daemon URL", + function=setting_utils.read_value, + args=['remote_url', + RegexValidator(setting_utils.URL_REGEX), + 'Write a Docker Daemon URL ' + '(format http[s]://:):', + 'Docker Daemon URL is not a valid URL (remove ' + 'the trailing slash, if present)' + ], + should_exit=False + ) + ) remote_url_item = SubmenuItem(remote_url_string, remote_url_menu, current_menu) # Docker Daemon TLS Path Option cert_path_string = "Configure a Docker Daemon TLS Cert Path" - cert_path_menu = SelectionMenu(strings=[], - title=cert_path_string, - subtitle=setting_utils.current_string("cert_path"), - prologue_text="""When using a remote Docker Daemon, a TLS Cert could be """ - """required. - - Default is %s.""" % DEFAULTS['cert_path'], - formatter=menu_formatter - ) - - cert_path_menu.append_item(FunctionItem(text="Insert a Docker Daemon TLS Cert Path", - function=setting_utils.read_value, - args=['cert_path', - RegexValidator(r'^.+$'), - 'Write a TSL Cert Path:', - 'TLS Cert Path not valid!' - ], - should_exit=False - ) - ) - cert_path_menu.append_item(FunctionItem(text="Reset value to Empty String", - function=setting_utils.update_setting_value, - args=["cert_path", None], - should_exit=False - ) - ) + cert_path_menu = SelectionMenu( + strings=[], + title=cert_path_string, + subtitle=setting_utils.current_string("cert_path"), + prologue_text="""When using a remote Docker Daemon, a TLS Cert could be required. + + Default is %s.""" % DEFAULTS['cert_path'], + formatter=menu_formatter + ) + + cert_path_menu.append_item( + FunctionItem( + text="Insert a Docker Daemon TLS Cert Path", + function=setting_utils.read_value, + args=['cert_path', + RegexValidator(r'^.+$'), + 'Write a TSL Cert Path:', + 'TLS Cert Path not valid!' + ], + should_exit=False + ) + ) + cert_path_menu.append_item( + FunctionItem( + text="Reset value to Empty String", + function=setting_utils.update_setting_value, + args=["cert_path", None], + should_exit=False + ) + ) cert_path_item = SubmenuItem(cert_path_string, cert_path_menu, remote_url_menu) remote_url_menu.append_item(cert_path_item) - remote_url_menu.append_item(FunctionItem(text="Reset remote Docker connection to default", - function=setting_utils.update_setting_values, - args=[[("remote_url", None), ("cert_path", None)]], - should_exit=False - ) - ) + remote_url_menu.append_item( + FunctionItem( + text="Reset remote Docker connection to default", + function=setting_utils.update_setting_values, + args=[[("remote_url", None), ("cert_path", None)]], + should_exit=False + ) + ) return remote_url_item @@ -241,6 +274,6 @@ def remote_url_unix(): current_menu.append_item(hosthome_item) current_menu.append_item(shared_item) current_menu.append_item(image_update_policy_item) - current_menu.append_item(shared_cd_item) + current_menu.append_item(shared_cds_item) if platform_remote_url_item: current_menu.append_item(platform_remote_url_item) diff --git a/src/Kathara/cli/ui/setting/KubernetesOptionsHandler.py b/src/Kathara/cli/ui/setting/KubernetesOptionsHandler.py index d8a93a18..0ffedc25 100644 --- a/src/Kathara/cli/ui/setting/KubernetesOptionsHandler.py +++ b/src/Kathara/cli/ui/setting/KubernetesOptionsHandler.py @@ -10,122 +10,146 @@ class KubernetesOptionsHandler(OptionsHandler): def add_items(self, current_menu: ConsoleMenu, menu_formatter: MenuFormatBuilder) -> None: # API URL Option api_server_url_string = "Insert a Kubernetes API Server URL" - api_server_url_menu = SelectionMenu(strings=[], - title=api_server_url_string, - subtitle=setting_utils.current_string("api_server_url"), - prologue_text="""You can specify a remote Kubernetes API Server URL to """ - """connect to when Megalos is not used on a Kubernetes """ - """master. - Default is %s.""" % DEFAULTS['api_server_url'], - formatter=menu_formatter - ) + api_server_url_menu = SelectionMenu( + strings=[], + title=api_server_url_string, + subtitle=setting_utils.current_string("api_server_url"), + prologue_text="""You can specify a remote Kubernetes API Server URL to """ + """connect to when Megalos is not used on a Kubernetes master. + + Default is %s.""" % DEFAULTS['api_server_url'], + formatter=menu_formatter + ) - api_server_url_menu.append_item(FunctionItem(text=api_server_url_string, - function=setting_utils.read_value, - args=['api_server_url', - RegexValidator(setting_utils.URL_REGEX), - 'Write a Kubernetes API Server URL:', - 'Kubernetes API Server URL is not a valid URL (remove ' - 'the trailing slash, if present)' - ], - should_exit=True - ) - ) - api_server_url_menu.append_item(FunctionItem(text="Reset value to Empty String", - function=setting_utils.update_setting_value, - args=["api_server_url", None], - should_exit=True - ) - ) + api_server_url_menu.append_item( + FunctionItem( + text=api_server_url_string, + function=setting_utils.read_value, + args=['api_server_url', + RegexValidator(setting_utils.URL_REGEX), + 'Write a Kubernetes API Server URL:', + 'Kubernetes API Server URL is not a valid URL (remove ' + 'the trailing slash, if present)' + ], + should_exit=True + ) + ) + api_server_url_menu.append_item( + FunctionItem( + text="Reset value to Empty String", + function=setting_utils.update_setting_value, + args=["api_server_url", None], + should_exit=True + ) + ) api_url_item = SubmenuItem(api_server_url_string, api_server_url_menu, current_menu) # API Token Option api_token_string = "Insert a Kubernetes API Token" - api_token_menu = SelectionMenu(strings=[], - title=api_token_string, - prologue_text="""When using a remote Kubernetes API Server, you must also """ - """specify the authentication token to use. - Default is %s.""" % DEFAULTS['api_token'], - formatter=menu_formatter - ) + api_token_menu = SelectionMenu( + strings=[], + title=api_token_string, + prologue_text="""When using a remote Kubernetes API Server, you must also """ + """specify the authentication token to use. + + Default is %s.""" % DEFAULTS['api_token'], + formatter=menu_formatter + ) - api_token_menu.append_item(FunctionItem(text=api_token_string, - function=setting_utils.read_value, - args=['api_token', - RegexValidator(r'^.+$'), - 'Write a Kubernetes API Token:', - 'Kubernetes API Token not valid!' - ], - should_exit=True - ) - ) - api_token_menu.append_item(FunctionItem(text="Reset value to Empty String", - function=setting_utils.update_setting_value, - args=["api_token", None], - should_exit=True - ) - ) + api_token_menu.append_item( + FunctionItem( + text=api_token_string, + function=setting_utils.read_value, + args=['api_token', + RegexValidator(r'^.+$'), + 'Write a Kubernetes API Token:', + 'Kubernetes API Token not valid!' + ], + should_exit=True + ) + ) + api_token_menu.append_item( + FunctionItem( + text="Reset value to Empty String", + function=setting_utils.update_setting_value, + args=["api_token", None], + should_exit=True + ) + ) api_token_item = SubmenuItem(api_token_string, api_token_menu, current_menu) # Shared Mount Option host_shared_string = "Automatically mount /shared on startup" - host_shared_menu = SelectionMenu(strings=[], - title=host_shared_string, - subtitle=setting_utils.current_bool("host_shared"), - prologue_text="""Each Kubernetes worker node creates a /home/shared """ - """directory and it is made available for reading/writing """ - """inside the device under the special directory `/shared`. - Default is %s.""" % - setting_utils.format_bool(DEFAULTS['host_shared']), - formatter=menu_formatter - ) + host_shared_menu = SelectionMenu( + strings=[], + title=host_shared_string, + subtitle=setting_utils.current_bool("host_shared"), + prologue_text="""Each Kubernetes worker node creates a /home/shared """ + """directory and it is made available for reading/writing """ + """inside the device under the special directory `/shared`. + + Default is %s.""" % + setting_utils.format_bool(DEFAULTS['host_shared']), + formatter=menu_formatter + ) - host_shared_menu.append_item(FunctionItem(text="Yes", - function=setting_utils.update_setting_value, - args=["host_shared", True], - should_exit=True - ) - ) - host_shared_menu.append_item(FunctionItem(text="No", - function=setting_utils.update_setting_value, - args=["host_shared", False], - should_exit=True - ) - ) + host_shared_menu.append_item( + FunctionItem( + text="Yes", + function=setting_utils.update_setting_value, + args=["host_shared", True], + should_exit=True + ) + ) + host_shared_menu.append_item( + FunctionItem( + text="No", + function=setting_utils.update_setting_value, + args=["host_shared", False], + should_exit=True + ) + ) host_shared_item = SubmenuItem(host_shared_string, host_shared_menu, current_menu) # Image Pull Policy Option image_pull_policy_string = "Image Pull Policy" - image_pull_policy_menu = SelectionMenu(strings=[], - title=image_pull_policy_string, - subtitle=setting_utils.current_string("image_pull_policy"), - prologue_text="""Specify the image pull policy for Docker images """ - """used by devices. - Default is %s.""" % DEFAULTS['image_pull_policy'], - formatter=menu_formatter - ) + image_pull_policy_menu = SelectionMenu( + strings=[], + title=image_pull_policy_string, + subtitle=setting_utils.current_string("image_pull_policy"), + prologue_text="""Specify the image pull policy for Docker images used by devices. + + Default is `%s`.""" % DEFAULTS['image_pull_policy'], + formatter=menu_formatter + ) - image_pull_policy_menu.append_item(FunctionItem(text="Always", - function=setting_utils.update_setting_value, - args=["image_pull_policy", "Always"], - should_exit=True - ) - ) - image_pull_policy_menu.append_item(FunctionItem(text="If Not Present", - function=setting_utils.update_setting_value, - args=["image_pull_policy", "IfNotPresent"], - should_exit=True - ) - ) - image_pull_policy_menu.append_item(FunctionItem(text="Never", - function=setting_utils.update_setting_value, - args=["image_pull_policy", "Never"], - should_exit=True - ) - ) + image_pull_policy_menu.append_item( + FunctionItem( + text="Always", + function=setting_utils.update_setting_value, + args=["image_pull_policy", "Always"], + should_exit=True + ) + ) + image_pull_policy_menu.append_item( + FunctionItem( + text="If Not Present", + function=setting_utils.update_setting_value, + args=["image_pull_policy", "IfNotPresent"], + should_exit=True + ) + ) + image_pull_policy_menu.append_item( + FunctionItem( + text="Never", + function=setting_utils.update_setting_value, + args=["image_pull_policy", "Never"], + should_exit=True + ) + ) image_pull_policy_item = SubmenuItem(image_pull_policy_string, image_pull_policy_menu, current_menu) diff --git a/src/Kathara/cli/ui/setting/SettingsMenuFactory.py b/src/Kathara/cli/ui/setting/SettingsMenuFactory.py index 6fa0f24e..1aff7df7 100644 --- a/src/Kathara/cli/ui/setting/SettingsMenuFactory.py +++ b/src/Kathara/cli/ui/setting/SettingsMenuFactory.py @@ -9,7 +9,8 @@ class SettingsMenuFactory(object): __slots__ = ['menu_formatter'] def __init__(self) -> None: - self.menu_formatter: MenuFormatBuilder = MenuFormatBuilder().set_title_align('center') \ + self.menu_formatter: MenuFormatBuilder = MenuFormatBuilder() \ + .set_title_align('center') \ .set_subtitle_align('center') \ .set_prologue_text_align('center') \ .set_border_style_type(MenuBorderStyleType.DOUBLE_LINE_BORDER) \ diff --git a/src/Kathara/cli/ui/setting/utils.py b/src/Kathara/cli/ui/setting/utils.py index cacf7f2e..efaa82d7 100644 --- a/src/Kathara/cli/ui/setting/utils.py +++ b/src/Kathara/cli/ui/setting/utils.py @@ -35,6 +35,7 @@ def current_string(attribute_name: str, text: Optional[str] = None) -> Callable[ ")" if text else "" ) + def current_enum(attribute_name: str, to_string: Callable[[int], str], text: Optional[str] = None) -> Callable[[], str]: return lambda: "%sCurrent: %s%s" % (text + " (" if text else "", to_string(getattr(Setting.get_instance(), attribute_name)), diff --git a/src/Kathara/cli/ui/utils.py b/src/Kathara/cli/ui/utils.py index dbea7521..a31a8855 100644 --- a/src/Kathara/cli/ui/utils.py +++ b/src/Kathara/cli/ui/utils.py @@ -154,7 +154,7 @@ def osx_connect() -> None: # Types for argparse def alphanumeric(value, pat=re.compile(r"^\w+$")): if not pat.match(value): - raise argparse.ArgumentTypeError("Invalid alphanumeric value") + raise argparse.ArgumentTypeError("invalid alphanumeric value") return value @@ -165,12 +165,19 @@ def interface_cd_mac(value): parts = value.split('/') (n, cd) = parts[0].split(':') if len(parts) == 2: - mac = parts[1] + if parts[1]: + mac = parts[1] + else: + raise ValueError except ValueError: - raise argparse.ArgumentTypeError("Invalid interface definition: %s" % value) + raise argparse.ArgumentTypeError("invalid interface definition: %s" % value) + + if not re.search(r"^\w+$", cd): + raise argparse.ArgumentTypeError(f"invalid interface definition, " + f"collision domain `{cd}` contains non-alphanumeric characters") return n, cd, mac -def cd_mac_address(value): +def cd_mac(value): return parse_cd_mac_address(value) diff --git a/src/Kathara/manager/docker/DockerLink.py b/src/Kathara/manager/docker/DockerLink.py index a20a3d59..ee1413d9 100644 --- a/src/Kathara/manager/docker/DockerLink.py +++ b/src/Kathara/manager/docker/DockerLink.py @@ -98,12 +98,13 @@ def create(self, link: Link) -> None: # If a network with the same name exists, return it instead of creating a new one. link_name = self.get_network_name(link) networks = self.get_links_api_objects_by_filters(link_name=link_name) + if networks: link.api_object = networks.pop() else: network_ipam_config = docker.types.IPAMConfig(driver='null') - user_label = "shared_cd" if Setting.get_instance().shared_cd == SharedCollisionDomainsOption.USERS \ + user_label = "shared_cd" if Setting.get_instance().shared_cds == SharedCollisionDomainsOption.USERS \ else utils.get_current_user_name() link.api_object = self.client.networks.create( name=link_name, @@ -112,7 +113,7 @@ def create(self, link: Link) -> None: ipam=network_ipam_config, labels={ "lab_hash": link.lab.hash if - Setting.get_instance().shared_cd == SharedCollisionDomainsOption.NOT_SHARED else None, + Setting.get_instance().shared_cds == SharedCollisionDomainsOption.NOT_SHARED else None, "name": link.name, "user": user_label, "app": "kathara", @@ -164,7 +165,7 @@ def wipe(self, user: str = None) -> None: Returns: None """ - user_label = "shared_cd" if Setting.get_instance().shared_cd == SharedCollisionDomainsOption.USERS else user + user_label = "shared_cd" if Setting.get_instance().shared_cds == SharedCollisionDomainsOption.USERS else user networks = self.get_links_api_objects_by_filters(user=user_label) for item in networks: item.reload() @@ -366,9 +367,9 @@ def get_network_name(link: Link) -> str: str: The name of the Docker network in the format "|net_prefix|_|username_prefix|_|name|". If shared collision domains, the format is: "|net_prefix|_|lab_hash|". """ - if Setting.get_instance().shared_cd == SharedCollisionDomainsOption.LABS: + if Setting.get_instance().shared_cds == SharedCollisionDomainsOption.LABS: return f"{Setting.get_instance().net_prefix}_{utils.get_current_user_name()}_{link.name}" - elif Setting.get_instance().shared_cd == SharedCollisionDomainsOption.USERS: + elif Setting.get_instance().shared_cds == SharedCollisionDomainsOption.USERS: return f"{Setting.get_instance().net_prefix}_{link.name}" - elif Setting.get_instance().shared_cd == SharedCollisionDomainsOption.NOT_SHARED: + elif Setting.get_instance().shared_cds == SharedCollisionDomainsOption.NOT_SHARED: return f"{Setting.get_instance().net_prefix}_{utils.get_current_user_name()}_{link.lab.hash}_{link.name}" diff --git a/src/Kathara/manager/docker/DockerManager.py b/src/Kathara/manager/docker/DockerManager.py index fcd894ce..aa2c91f8 100644 --- a/src/Kathara/manager/docker/DockerManager.py +++ b/src/Kathara/manager/docker/DockerManager.py @@ -519,8 +519,8 @@ def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab: lab_networks = dict( map(lambda x: (x.name, x), self.get_links_api_objects( lab_hash=reconstructed_lab.hash \ - if Setting.get_instance().shared_cd == SharedCollisionDomainsOption.NOT_SHARED else None, - all_users=Setting.get_instance().shared_cd == SharedCollisionDomainsOption.USERS + if Setting.get_instance().shared_cds == SharedCollisionDomainsOption.NOT_SHARED else None, + all_users=Setting.get_instance().shared_cds == SharedCollisionDomainsOption.USERS )) ) @@ -587,8 +587,8 @@ def update_lab_from_api(self, lab: Lab) -> None: deployed_networks = dict( map(lambda x: (x.name, x), self.get_links_api_objects( lab_hash=lab.hash \ - if Setting.get_instance().shared_cd == SharedCollisionDomainsOption.NOT_SHARED else None, - all_users=Setting.get_instance().shared_cd == SharedCollisionDomainsOption.USERS + if Setting.get_instance().shared_cds == SharedCollisionDomainsOption.NOT_SHARED else None, + all_users=Setting.get_instance().shared_cds == SharedCollisionDomainsOption.USERS )) ) for network in deployed_networks.values(): diff --git a/src/Kathara/setting/addon/DockerSettingsAddon.py b/src/Kathara/setting/addon/DockerSettingsAddon.py index f91fd5c4..e988ce5b 100644 --- a/src/Kathara/setting/addon/DockerSettingsAddon.py +++ b/src/Kathara/setting/addon/DockerSettingsAddon.py @@ -7,7 +7,7 @@ "hosthome_mount": False, "shared_mount": True, "image_update_policy": "Prompt", - "shared_cd": SharedCollisionDomainsOption.NOT_SHARED, + "shared_cds": SharedCollisionDomainsOption.NOT_SHARED, "remote_url": None, "cert_path": None, "network_plugin": "kathara/katharanp_vde" @@ -15,14 +15,14 @@ class DockerSettingsAddon(SettingsAddon): - __slots__ = ['hosthome_mount', 'shared_mount', 'image_update_policy', 'shared_cd', + __slots__ = ['hosthome_mount', 'shared_mount', 'image_update_policy', 'shared_cds', 'remote_url', 'cert_path', 'network_plugin'] def __init__(self) -> None: self.hosthome_mount: bool = False self.shared_mount: bool = True self.image_update_policy: str = 'Prompt' - self.shared_cd: int = SharedCollisionDomainsOption.NOT_SHARED + self.shared_cds: int = SharedCollisionDomainsOption.NOT_SHARED self.remote_url: Optional[str] = None self.cert_path: Optional[str] = None self.network_plugin: Optional[str] = "kathara/katharanp_vde" @@ -32,7 +32,7 @@ def _to_dict(self) -> Dict[str, Any]: 'hosthome_mount': self.hosthome_mount, 'shared_mount': self.shared_mount, 'image_update_policy': self.image_update_policy, - 'shared_cd': self.shared_cd, + 'shared_cds': self.shared_cds, 'remote_url': self.remote_url, 'cert_path': self.cert_path, 'network_plugin': self.network_plugin diff --git a/src/Kathara/types.py b/src/Kathara/types.py index f831b721..00dea9f5 100644 --- a/src/Kathara/types.py +++ b/src/Kathara/types.py @@ -4,13 +4,13 @@ class SharedCollisionDomainsOption(IntEnum): """Enum representing options for shared collision domains option. - Attributes: - NOT_SHARED (int): Represents the option for not sharing collision domains (value: 1). - LABS (int): Represents the option for sharing collision domains among network scenarios of the same user - (value: 2). - USERS (int): Represents the option for sharing collision domains among network scenarios of different0 users - (value: 3). - """ + Attributes: + NOT_SHARED (int): Represents the option for not sharing collision domains (value: 1). + LABS (int): Represents the option for sharing collision domains among network scenarios of the same user + (value: 2). + USERS (int): Represents the option for sharing collision domains among network scenarios of different users + (value: 3). + """ NOT_SHARED = 1 LABS = 2 USERS = 3 @@ -18,8 +18,8 @@ class SharedCollisionDomainsOption(IntEnum): @staticmethod def to_string(value): if value == 1: - return "Not shared" + return "Not Shared" elif value == 2: - return "Share collision domain between network scenarios" + return "Share collision domains between network scenarios" elif value == 3: - return "Share collision domain between users" + return "Share collision domains between users" diff --git a/tests/manager/docker/docker_link_test.py b/tests/manager/docker/docker_link_test.py index c2d7426e..a92a7d18 100644 --- a/tests/manager/docker/docker_link_test.py +++ b/tests/manager/docker/docker_link_test.py @@ -63,7 +63,7 @@ def test_get_network_name(mock_get_current_user_name, mock_setting_get_instance, mock_get_current_user_name.return_value = 'user' setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': SharedCollisionDomainsOption.NOT_SHARED, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'net_prefix': 'kathara', 'remote_url': None, }) @@ -74,11 +74,11 @@ def test_get_network_name(mock_get_current_user_name, mock_setting_get_instance, @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @mock.patch("src.Kathara.utils.get_current_user_name") -def test_get_network_name_shared_cd_between_labs(mock_get_current_user_name, mock_setting_get_instance, default_link): +def test_get_network_name_shared_cds_between_labs(mock_get_current_user_name, mock_setting_get_instance, default_link): mock_get_current_user_name.return_value = 'user' setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': SharedCollisionDomainsOption.LABS, + 'shared_cds': SharedCollisionDomainsOption.LABS, 'net_prefix': 'kathara', 'remote_url': None }) @@ -89,11 +89,11 @@ def test_get_network_name_shared_cd_between_labs(mock_get_current_user_name, moc @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @mock.patch("src.Kathara.utils.get_current_user_name") -def test_get_network_name_shared_cd_between_users(mock_get_current_user_name, mock_setting_get_instance, default_link): +def test_get_network_name_shared_cds_between_users(mock_get_current_user_name, mock_setting_get_instance, default_link): mock_get_current_user_name.return_value = 'user' setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': SharedCollisionDomainsOption.USERS, + 'shared_cds': SharedCollisionDomainsOption.USERS, 'net_prefix': 'kathara', 'remote_url': None }) @@ -113,7 +113,7 @@ def test_create(mock_get_current_user_name, mock_setting_get_instance, docker_li mock_get_current_user_name.return_value = 'user' setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': SharedCollisionDomainsOption.NOT_SHARED, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'net_prefix': 'kathara', 'remote_url': None, 'network_plugin': 'kathara/katharanp' @@ -141,14 +141,14 @@ def test_create_bridge_link(docker_link, bridged_link): @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @mock.patch("src.Kathara.utils.get_current_user_name") -def test_create_shared_cd_between_users(mock_get_current_user_name, mock_setting_get_instance, docker_link, +def test_create_shared_cds_between_users(mock_get_current_user_name, mock_setting_get_instance, docker_link, default_link): docker_link.client.networks.list.return_value = [] mock_get_current_user_name.return_value = 'user' setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': SharedCollisionDomainsOption.USERS, + 'shared_cds': SharedCollisionDomainsOption.USERS, 'net_prefix': 'kathara', 'remote_url': None, 'network_plugin': 'kathara/katharanp' @@ -172,14 +172,14 @@ def test_create_shared_cd_between_users(mock_get_current_user_name, mock_setting @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @mock.patch("src.Kathara.utils.get_current_user_name") -def test_create_shared_cd_between_labs(mock_get_current_user_name, mock_setting_get_instance, docker_link, +def test_create_shared_cds_between_labs(mock_get_current_user_name, mock_setting_get_instance, docker_link, default_link): docker_link.client.networks.list.return_value = [] mock_get_current_user_name.return_value = 'user' setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': SharedCollisionDomainsOption.LABS, + 'shared_cds': SharedCollisionDomainsOption.LABS, 'net_prefix': 'kathara', 'remote_url': None, 'network_plugin': 'kathara/katharanp' diff --git a/tests/manager/docker/docker_machine_test.py b/tests/manager/docker/docker_machine_test.py index f4336508..56456beb 100644 --- a/tests/manager/docker/docker_machine_test.py +++ b/tests/manager/docker/docker_machine_test.py @@ -11,6 +11,7 @@ from src.Kathara.model.Machine import Machine from src.Kathara.manager.docker.DockerMachine import DockerMachine from src.Kathara.exceptions import MachineNotFoundError, DockerPluginError, MachineBinaryError +from src.Kathara.types import SharedCollisionDomainsOption # @@ -77,7 +78,7 @@ def test_create(mock_get_current_user_name, mock_setting_get_instance, mock_copy setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'device_prefix': 'dev_prefix', "device_shell": '/bin/bash', 'enable_ipv6': False, @@ -128,7 +129,7 @@ def test_create_ipv6(mock_get_current_user_name, mock_setting_get_instance, mock setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'device_prefix': 'dev_prefix', "device_shell": '/bin/bash', 'enable_ipv6': True, @@ -185,7 +186,7 @@ def test_create_privileged(mock_get_current_user_name, mock_setting_get_instance mock_get_current_user_name.return_value = "test-user" setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'device_prefix': 'dev_prefix', "device_shell": '/bin/bash', 'enable_ipv6': True, @@ -249,7 +250,7 @@ def __init__(self, name): setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'device_prefix': 'dev_prefix', "device_shell": '/bin/bash', 'enable_ipv6': False, @@ -314,7 +315,7 @@ def __init__(self, name): setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'device_prefix': 'dev_prefix', "device_shell": '/bin/bash', 'enable_ipv6': False, @@ -483,7 +484,7 @@ def test_deploy_and_start_machine(mock_create, mock_start, docker_machine, defau def test_deploy_machines(mock_deploy_and_start, mock_setting_get_instance, docker_machine): setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'device_prefix': 'dev_prefix', "device_shell": '/bin/bash', 'enable_ipv6': False, @@ -661,7 +662,7 @@ def test_exec(mock_get_machines_api_objects_by_filters, mock_setting_get_instanc setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'device_prefix': 'dev_prefix', "device_shell": '/bin/bash', 'enable_ipv6': False, @@ -934,7 +935,7 @@ def test_get_container_name_lab_hash(mock_get_current_user_name, mock_setting_ge setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'device_prefix': 'dev_prefix' }) mock_setting_get_instance.return_value = setting_mock @@ -944,12 +945,25 @@ def test_get_container_name_lab_hash(mock_get_current_user_name, mock_setting_ge @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @mock.patch("src.Kathara.utils.get_current_user_name") -def test_get_container_name_lab_hash_shared_cd(mock_get_current_user_name, mock_setting_get_instance): +def test_get_container_name_lab_hash_shared_cd_lab(mock_get_current_user_name, mock_setting_get_instance): mock_get_current_user_name.return_value = "kathara-user" setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': True, + 'shared_cd': SharedCollisionDomainsOption.LABS, + 'device_prefix': 'dev_prefix' + }) + mock_setting_get_instance.return_value = setting_mock + + +@mock.patch("src.Kathara.setting.Setting.Setting.get_instance") +@mock.patch("src.Kathara.utils.get_current_user_name") +def test_get_container_name_lab_hash_shared_cd_user(mock_get_current_user_name, mock_setting_get_instance): + mock_get_current_user_name.return_value = "kathara-user" + + setting_mock = Mock() + setting_mock.configure_mock(**{ + 'shared_cd': SharedCollisionDomainsOption.USERS, 'device_prefix': 'dev_prefix' }) mock_setting_get_instance.return_value = setting_mock diff --git a/tests/manager/docker/docker_manager_test.py b/tests/manager/docker/docker_manager_test.py index 473e9800..20a99a8d 100644 --- a/tests/manager/docker/docker_manager_test.py +++ b/tests/manager/docker/docker_manager_test.py @@ -15,6 +15,7 @@ from src.Kathara.manager.docker.stats.DockerMachineStats import DockerMachineStats from src.Kathara.exceptions import MachineNotFoundError, LabNotFoundError, InvocationError, LinkNotFoundError, \ MachineNotRunningError +from src.Kathara.types import SharedCollisionDomainsOption # @@ -519,7 +520,7 @@ def test_wipe_all_users(mock_setting_get_instance, mock_get_current_user_name, m docker_manager): setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': False, + 'shared_cds': SharedCollisionDomainsOption.NOT_SHARED, 'remote_url': None }) mock_setting_get_instance.return_value = setting_mock @@ -534,11 +535,31 @@ def test_wipe_all_users(mock_setting_get_instance, mock_get_current_user_name, m @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.wipe") @mock.patch("src.Kathara.utils.get_current_user_name") @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") -def test_wipe_all_users_and_shared_cd(mock_setting_get_instance, mock_get_current_user_name, mock_wipe_machines, - mock_wipe_links, docker_manager): +def test_wipe_all_users_and_shared_cd_lab(mock_setting_get_instance, mock_get_current_user_name, mock_wipe_machines, + mock_wipe_links, docker_manager): setting_mock = Mock() setting_mock.configure_mock(**{ - 'shared_cd': True, + 'shared_cd': SharedCollisionDomainsOption.LABS, + 'remote_url': None + }) + mock_setting_get_instance.return_value = setting_mock + mock_get_current_user_name.return_value = "kathara_user" + + docker_manager.wipe(all_users=True) + assert not mock_get_current_user_name.called + mock_wipe_machines.assert_called_once_with(user=None) + mock_wipe_links.assert_called_once_with(user=None) + + +@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.wipe") +@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.wipe") +@mock.patch("src.Kathara.utils.get_current_user_name") +@mock.patch("src.Kathara.setting.Setting.Setting.get_instance") +def test_wipe_all_users_and_shared_cd_user(mock_setting_get_instance, mock_get_current_user_name, mock_wipe_machines, + mock_wipe_links, docker_manager): + setting_mock = Mock() + setting_mock.configure_mock(**{ + 'shared_cd': SharedCollisionDomainsOption.USERS, 'remote_url': None }) mock_setting_get_instance.return_value = setting_mock diff --git a/tests/types_test.py b/tests/types_test.py index 342ef404..ad71324f 100644 --- a/tests/types_test.py +++ b/tests/types_test.py @@ -4,7 +4,8 @@ from src.Kathara.types import SharedCollisionDomainsOption + def test_shared_collision_domains_option_to_string(): - assert SharedCollisionDomainsOption.to_string(1) == 'Not shared' - assert SharedCollisionDomainsOption.to_string(2) == 'Share collision domain between network scenarios' - assert SharedCollisionDomainsOption.to_string(3) == 'Share collision domain between users' + assert SharedCollisionDomainsOption.to_string(1) == 'Not Shared' + assert SharedCollisionDomainsOption.to_string(2) == 'Share collision domains between network scenarios' + assert SharedCollisionDomainsOption.to_string(3) == 'Share collision domains between users' From 51e62aea942275ae5bb3686e6093a64d4b5e3b23 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sat, 23 Dec 2023 15:33:45 +0100 Subject: [PATCH 26/44] Minor fixes to labels (#256) --- .../cli/ui/setting/DockerOptionsHandler.py | 2 +- src/Kathara/manager/docker/DockerLink.py | 18 ++++++++++-------- tests/manager/docker/docker_link_test.py | 17 +++++++---------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Kathara/cli/ui/setting/DockerOptionsHandler.py b/src/Kathara/cli/ui/setting/DockerOptionsHandler.py index 1e9f9f91..82954726 100644 --- a/src/Kathara/cli/ui/setting/DockerOptionsHandler.py +++ b/src/Kathara/cli/ui/setting/DockerOptionsHandler.py @@ -184,7 +184,7 @@ def add_items(self, current_menu: ConsoleMenu, menu_formatter: MenuFormatBuilder FunctionItem( text="Do not share collision domains", function=setting_utils.update_setting_value, - args=["shareds_cd", SharedCollisionDomainsOption.NOT_SHARED], + args=["shared_cds", SharedCollisionDomainsOption.NOT_SHARED], should_exit=True ) ) diff --git a/src/Kathara/manager/docker/DockerLink.py b/src/Kathara/manager/docker/DockerLink.py index ee1413d9..86e3ad4e 100644 --- a/src/Kathara/manager/docker/DockerLink.py +++ b/src/Kathara/manager/docker/DockerLink.py @@ -104,20 +104,22 @@ def create(self, link: Link) -> None: else: network_ipam_config = docker.types.IPAMConfig(driver='null') - user_label = "shared_cd" if Setting.get_instance().shared_cds == SharedCollisionDomainsOption.USERS \ - else utils.get_current_user_name() + additional_labels = {} + if Setting.get_instance().shared_cds != SharedCollisionDomainsOption.USERS: + additional_labels["user"] = utils.get_current_user_name() + if Setting.get_instance().shared_cds == SharedCollisionDomainsOption.NOT_SHARED: + additional_labels["lab_hash"] = link.lab.hash + link.api_object = self.client.networks.create( name=link_name, driver=f"{Setting.get_instance().network_plugin}:{utils.get_architecture()}", check_duplicate=True, ipam=network_ipam_config, labels={ - "lab_hash": link.lab.hash if - Setting.get_instance().shared_cds == SharedCollisionDomainsOption.NOT_SHARED else None, "name": link.name, - "user": user_label, "app": "kathara", - "external": ";".join([x.get_full_name() for x in link.external]) + "external": ";".join([x.get_full_name() for x in link.external]), + **additional_labels } ) @@ -165,7 +167,7 @@ def wipe(self, user: str = None) -> None: Returns: None """ - user_label = "shared_cd" if Setting.get_instance().shared_cds == SharedCollisionDomainsOption.USERS else user + user_label = user if Setting.get_instance().shared_cds != SharedCollisionDomainsOption.USERS else None networks = self.get_links_api_objects_by_filters(user=user_label) for item in networks: item.reload() @@ -372,4 +374,4 @@ def get_network_name(link: Link) -> str: elif Setting.get_instance().shared_cds == SharedCollisionDomainsOption.USERS: return f"{Setting.get_instance().net_prefix}_{link.name}" elif Setting.get_instance().shared_cds == SharedCollisionDomainsOption.NOT_SHARED: - return f"{Setting.get_instance().net_prefix}_{utils.get_current_user_name()}_{link.lab.hash}_{link.name}" + return f"{Setting.get_instance().net_prefix}_{utils.get_current_user_name()}_{link.name}_{link.lab.hash}" diff --git a/tests/manager/docker/docker_link_test.py b/tests/manager/docker/docker_link_test.py index a92a7d18..102d55c5 100644 --- a/tests/manager/docker/docker_link_test.py +++ b/tests/manager/docker/docker_link_test.py @@ -69,7 +69,7 @@ def test_get_network_name(mock_get_current_user_name, mock_setting_get_instance, }) mock_setting_get_instance.return_value = setting_mock link_name = DockerLink.get_network_name(default_link) - assert link_name == "kathara_user_lab-hash_A" + assert link_name == "kathara_user_A_lab-hash" @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @@ -121,16 +121,16 @@ def test_create(mock_get_current_user_name, mock_setting_get_instance, docker_li mock_setting_get_instance.return_value = setting_mock docker_link.create(default_link) docker_link.client.networks.create.assert_called_once_with( - name="kathara_user_lab-hash_A", + name="kathara_user_A_lab-hash", driver=f"{setting_mock.network_plugin}:{utils.get_architecture()}", check_duplicate=True, ipam=docker.types.IPAMConfig(driver='null'), labels={ - "lab_hash": default_link.lab.hash, "name": "A", - "user": "user", "app": "kathara", - "external": "" + "external": "", + "user": "user", + "lab_hash": default_link.lab.hash, } ) @@ -142,7 +142,7 @@ def test_create_bridge_link(docker_link, bridged_link): @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @mock.patch("src.Kathara.utils.get_current_user_name") def test_create_shared_cds_between_users(mock_get_current_user_name, mock_setting_get_instance, docker_link, - default_link): + default_link): docker_link.client.networks.list.return_value = [] mock_get_current_user_name.return_value = 'user' @@ -161,9 +161,7 @@ def test_create_shared_cds_between_users(mock_get_current_user_name, mock_settin check_duplicate=True, ipam=docker.types.IPAMConfig(driver='null'), labels={ - "lab_hash": None, "name": "A", - "user": "shared_cd", "app": "kathara", "external": "" } @@ -173,7 +171,7 @@ def test_create_shared_cds_between_users(mock_get_current_user_name, mock_settin @mock.patch("src.Kathara.setting.Setting.Setting.get_instance") @mock.patch("src.Kathara.utils.get_current_user_name") def test_create_shared_cds_between_labs(mock_get_current_user_name, mock_setting_get_instance, docker_link, - default_link): + default_link): docker_link.client.networks.list.return_value = [] mock_get_current_user_name.return_value = 'user' @@ -192,7 +190,6 @@ def test_create_shared_cds_between_labs(mock_get_current_user_name, mock_setting check_duplicate=True, ipam=docker.types.IPAMConfig(driver='null'), labels={ - "lab_hash": None, "name": "A", "user": "user", "app": "kathara", From 9682fe7ac3b9e0b32c13139546c817bc6fae9b67 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sat, 23 Dec 2023 23:29:00 +0100 Subject: [PATCH 27/44] Add MAC Address support in Megalos (#137) --- .../manager/kubernetes/KubernetesMachine.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Kathara/manager/kubernetes/KubernetesMachine.py b/src/Kathara/manager/kubernetes/KubernetesMachine.py index 2070b11e..b97f9239 100644 --- a/src/Kathara/manager/kubernetes/KubernetesMachine.py +++ b/src/Kathara/manager/kubernetes/KubernetesMachine.py @@ -46,10 +46,12 @@ "umount /etc/resolv.conf", "umount /etc/hosts", - # Parse hostlab.b64 + # Parse hostlab.b64 (if present) + "if [ -f \"/tmp/kathara/hostlab.b64\" ]; then " "base64 -d /tmp/kathara/hostlab.b64 > /hostlab.tar.gz", # Extract hostlab.tar.gz data into / "tar xmfz /hostlab.tar.gz -C /; rm -f hostlab.tar.gz", + "fi", # Copy the machine folder (if present) from the hostlab directory into the root folder of the container # In this way, files are all replaced in the container root folder @@ -95,7 +97,7 @@ "/hostlab/{machine_name}.startup &> /var/log/startup.log; fi", # Remove the Kubernetes' default gateway which points to the eth0 interface and causes problems sometimes. - "route del default dev eth0 || true", + "ip route del default dev eth0 || true", # Placeholder for user commands "{machine_commands}", @@ -342,11 +344,10 @@ def _build_definition(self, machine: Machine, config_map: client.V1ConfigMap) -> # to execute custom commands coming from .startup file and "exec" option # Build the final startup commands string sysctl_commands = "; ".join(["sysctl -w -q %s=%d" % item for item in machine.meta["sysctls"].items()]) + machine_commands = "; ".join(machine.meta['exec_commands']) if machine.meta['exec_commands'] else ":" + startup_commands_string = "; ".join(STARTUP_COMMANDS) \ - .format(machine_name=machine.name, - sysctl_commands=sysctl_commands, - machine_commands="; ".join(machine.meta['exec_commands']) - ) + .format(machine_name=machine.name, sysctl_commands=sysctl_commands, machine_commands=machine_commands) post_start = client.V1LifecycleHandler( _exec=client.V1ExecAction( @@ -378,10 +379,15 @@ def _build_definition(self, machine: Machine, config_map: client.V1ConfigMap) -> pod_annotations = {} network_interfaces = [] for (idx, interface) in machine.interfaces.items(): + additional_data = {} + if interface.mac_address: + additional_data["mac"] = interface.mac_address + network_interfaces.append({ "name": interface.link.api_object["metadata"]["name"], "namespace": machine.lab.hash, - "interface": "net%d" % idx + "interface": "net%d" % idx, + **additional_data }) pod_annotations["k8s.v1.cni.cncf.io/networks"] = json.dumps(network_interfaces) From 280fa30256dba75597adeecabc09fc218654d8f2 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sat, 23 Dec 2023 23:35:20 +0100 Subject: [PATCH 28/44] Add MAC Addr in K8s reconstructed lab (#137) --- src/Kathara/manager/kubernetes/KubernetesManager.py | 7 ++++++- src/Kathara/model/Machine.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Kathara/manager/kubernetes/KubernetesManager.py b/src/Kathara/manager/kubernetes/KubernetesManager.py index 85884124..82b596f9 100644 --- a/src/Kathara/manager/kubernetes/KubernetesManager.py +++ b/src/Kathara/manager/kubernetes/KubernetesManager.py @@ -583,7 +583,12 @@ def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab: network = lab_networks[network_conf['name']] link = reconstructed_lab.get_or_new_link(network['metadata']['labels']['name']) link.api_object = network - device.add_interface(link) + + iface_mac_addr = None + if "mac" in network_conf: + iface_mac_addr = network_conf['mac'] + + device.add_interface(link, mac_address=iface_mac_addr) return reconstructed_lab diff --git a/src/Kathara/model/Machine.py b/src/Kathara/model/Machine.py index 78827aa4..e30dcd43 100644 --- a/src/Kathara/model/Machine.py +++ b/src/Kathara/model/Machine.py @@ -682,7 +682,8 @@ def __str__(self) -> str: if interface.mac_address: formatted_machine += f" (MAC Address: {interface.mac_address})" - formatted_machine += f"\nBridged Connection: {self.meta['bridged']}" + if 'bridged' in self.meta: + formatted_machine += f"\nBridged Connection: {self.meta['bridged']}" if self.meta["sysctls"]: formatted_machine += "\nSysctls:" From f89a469664f2b27650876f111d53f3da4ea419da Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sat, 23 Dec 2023 23:43:20 +0100 Subject: [PATCH 29/44] Fix filters when retrieving the Docker link object --- src/Kathara/manager/docker/DockerLink.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Kathara/manager/docker/DockerLink.py b/src/Kathara/manager/docker/DockerLink.py index 2f11375f..d8f6bd04 100644 --- a/src/Kathara/manager/docker/DockerLink.py +++ b/src/Kathara/manager/docker/DockerLink.py @@ -96,7 +96,16 @@ def create(self, link: Link) -> None: return # If a network with the same name exists, return it instead of creating a new one. - networks = self.get_links_api_objects_by_filters(link_name=link.name) + filter_lab_hash = None + filter_user = None + if Setting.get_instance().shared_cds == SharedCollisionDomainsOption.NOT_SHARED: + filter_lab_hash = link.lab.hash + if Setting.get_instance().shared_cds != SharedCollisionDomainsOption.USERS: + filter_user = utils.get_current_user_name() + + networks = self.get_links_api_objects_by_filters( + link_name=link.name, lab_hash=filter_lab_hash, user=filter_user + ) if networks: link.api_object = networks.pop() else: From f9358244b84ce8b7e9ae0ded8f413b751e1d654a Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Thu, 28 Dec 2023 18:01:13 +0100 Subject: [PATCH 30/44] Add `copy_directory_from_path` method (#257) --- .../foundation/model/FilesystemMixin.py | 37 +++++++++++++++---- src/Kathara/model/Machine.py | 15 ++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/Kathara/foundation/model/FilesystemMixin.py b/src/Kathara/foundation/model/FilesystemMixin.py index 9926ca41..e986dfe9 100644 --- a/src/Kathara/foundation/model/FilesystemMixin.py +++ b/src/Kathara/foundation/model/FilesystemMixin.py @@ -2,7 +2,9 @@ import os.path from typing import Optional, List, BinaryIO, TextIO, Union +from fs import open_fs from fs.base import FS +from fs.copy import copy_dir from ...exceptions import InvocationError @@ -73,7 +75,7 @@ def update_file_from_string(self, content: str, dst_path: str) -> None: fs.errors.ResourceNotFound: If the path is not found in the fs. """ if not self.fs: - raise InvocationError("There is no filesystem associated to this network scenario.") + raise InvocationError("There is no filesystem associated to this object.") with self.fs.open(dst_path, "a") as dst_file: dst_file.write(content) @@ -93,7 +95,7 @@ def create_file_from_list(self, lines: List[str], dst_path: str) -> None: fs.errors.ResourceNotFound: If the path is not found in the fs. """ if not self.fs: - raise InvocationError("There is no filesystem associated to this network scenario.") + raise InvocationError("There is no filesystem associated to this object.") directory = os.path.dirname(dst_path) self.fs.makedirs(directory, recreate=True) @@ -116,7 +118,7 @@ def update_file_from_list(self, lines: List[str], dst_path: str) -> None: fs.errors.ResourceNotFound: If the path is not found in the fs. """ if not self.fs: - raise InvocationError("There is no filesystem associated to this network scenario.") + raise InvocationError("There is no filesystem associated to this object.") with self.fs.open(dst_path, "a") as dst_file: dst_file.writelines(line + '\n' for line in lines) @@ -136,7 +138,7 @@ def create_file_from_path(self, src_path: str, dst_path: str) -> None: fs.errors.ResourceNotFound: If the path is not found in the fs. """ if not self.fs: - raise InvocationError("There is no filesystem associated to this network scenario.") + raise InvocationError("There is no filesystem associated to this object.") directory = os.path.dirname(dst_path) self.fs.makedirs(directory, recreate=True) @@ -160,7 +162,7 @@ def create_file_from_stream(self, stream: Union[BinaryIO, TextIO], dst_path: str fs.errors.ResourceNotFound: If the path is not found in the fs. """ if not self.fs: - raise InvocationError("There is no filesystem associated to this network scenario.") + raise InvocationError("There is no filesystem associated to this object.") directory = os.path.dirname(dst_path) self.fs.makedirs(directory, recreate=True) @@ -174,6 +176,25 @@ def create_file_from_stream(self, stream: Union[BinaryIO, TextIO], dst_path: str except io.UnsupportedOperation: raise io.UnsupportedOperation("To create a file from stream, you must open it with read permissions.") + def copy_directory_from_path(self, src_path: str, dst_path: str) -> None: + """Copy a directory from a src_path in the host filesystem into a dst_path in this fs. + + Args: + src_path (str): The source path of the directory to copy. + dst_path (str): The destination path where to copy the directory. + + Returns: + None + + Raises: + InvocationError: If the fs is None. + """ + if not self.fs: + raise InvocationError("There is no filesystem associated to this object.") + + directory_fs = open_fs(f"osfs://{os.path.abspath(src_path)}") + copy_dir(directory_fs, ".", self.fs, dst_path) + def write_line_before(self, file_path: str, line_to_add: str, searched_line: str, first_occurrence: bool = False) \ -> int: """Write a new line before a specific line in a file. @@ -194,7 +215,7 @@ def write_line_before(self, file_path: str, line_to_add: str, searched_line: str LineNotFoundError: If the searched line is not found in the file. """ if not self.fs: - raise InvocationError("There is no filesystem associated to this network scenario.") + raise InvocationError("There is no filesystem associated to this object.") n_added = 0 with self.fs.open(file_path, "r+") as file: @@ -232,7 +253,7 @@ def write_line_after(self, file_path: str, line_to_add: str, searched_line: str, LineNotFoundError: If the searched line is not found in the file. """ if not self.fs: - raise InvocationError("There is no filesystem associated to this network scenario.") + raise InvocationError("There is no filesystem associated to this object.") n_added = 0 with self.fs.open(file_path, "r+") as file: @@ -269,7 +290,7 @@ def delete_line(self, file_path: str, line_to_delete: str, first_occurrence: boo LineNotFoundError: If the searched line is not found in the file. """ if not self.fs: - raise InvocationError("There is no filesystem associated to this network scenario.") + raise InvocationError("There is no filesystem associated to this object.") n_deleted = 0 with self.fs.open(file_path, "r+") as file: diff --git a/src/Kathara/model/Machine.py b/src/Kathara/model/Machine.py index e30dcd43..f9feef6d 100644 --- a/src/Kathara/model/Machine.py +++ b/src/Kathara/model/Machine.py @@ -600,6 +600,21 @@ def create_file_from_stream(self, stream: Union[BinaryIO, TextIO], dst_path: str super().create_file_from_stream(stream, dst_path) + def copy_directory_from_path(self, src_path: str, dst_path: str) -> None: + """Copy a directory from a src_path in the host filesystem into a dst_path in the fs of the device. + + Args: + src_path (str): The source path of the directory to copy. + dst_path (str): The destination path on the device where to copy the directory. + + Returns: + None + """ + if not self.fs: + self.fs = self.lab.fs.makedir(self.name, recreate=True) + + super().copy_directory_from_path(src_path, dst_path) + def write_line_before(self, file_path: str, line_to_add: str, searched_line: str, first_occurrence: bool = False) \ -> int: """Write a new line before a specific line in a file. From 1d7faf1071580c5c3e24b2846158ead202c06167 Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Fri, 29 Dec 2023 12:10:21 +0100 Subject: [PATCH 31/44] Add `copy_directory_from_path` unit tests (#257) --- tests/model/filesystem_mixin_test.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/model/filesystem_mixin_test.py b/tests/model/filesystem_mixin_test.py index da3053de..810e6ac1 100644 --- a/tests/model/filesystem_mixin_test.py +++ b/tests/model/filesystem_mixin_test.py @@ -186,6 +186,26 @@ def test_create_file_from_stream_byte(): mock_fs.upload.assert_called_once_with("/", stream) +# +# TEST: copy_directory_from_path +# +def test_copy_directory_from_path(): + filesystem = FilesystemMixin() + filesystem.fs = fs.open_fs(f"mem://") + with mock.patch("src.Kathara.foundation.model.FilesystemMixin.open_fs") as mock_open_fs: + with mock.patch("src.Kathara.foundation.model.FilesystemMixin.copy_dir") as mock_copy_dir: + mock_open_fs.return_value = "directory" + filesystem.copy_directory_from_path("src_path", "dst_path") + mock_open_fs.assert_called_once() + mock_copy_dir.assert_called_once_with("directory", ".", filesystem.fs, "dst_path") + + +def test_copy_directory_from_path_invocation_error(): + filesystem = FilesystemMixin() + with pytest.raises(InvocationError): + filesystem.copy_directory_from_path("src_path", "dst_path") + + # # TEST: write_line_before # From a5eae5d62e1ee0b6815e5b9a1cf036e2ba67a651 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Thu, 4 Jan 2024 13:42:49 +0100 Subject: [PATCH 32/44] Pass only one "lab" identifier in Managers (#254) --- src/Kathara/manager/Kathara.py | 3 +- src/Kathara/manager/docker/DockerManager.py | 35 ++++----- .../manager/kubernetes/KubernetesManager.py | 74 ++++++++----------- src/Kathara/utils.py | 58 +++++++++------ tests/manager/docker/docker_manager_test.py | 68 ++++++++++++++++- .../kubernetes/kubernetes_manager_test.py | 36 +++------ 6 files changed, 163 insertions(+), 111 deletions(-) diff --git a/src/Kathara/manager/Kathara.py b/src/Kathara/manager/Kathara.py index 3d1193e3..bf5f5656 100644 --- a/src/Kathara/manager/Kathara.py +++ b/src/Kathara/manager/Kathara.py @@ -453,7 +453,8 @@ def check_image(self, image_name: str) -> None: None Raises: - ConnectionError: If the image is not locally available and there is no connection to a remote image repository. + ConnectionError: If the image is not locally available and there is no connection to a + remote image repository. ImageNotFoundError: If the image is not found. """ self.manager.check_image(image_name) diff --git a/src/Kathara/manager/docker/DockerManager.py b/src/Kathara/manager/docker/DockerManager.py index 2700a7e8..efe5456a 100644 --- a/src/Kathara/manager/docker/DockerManager.py +++ b/src/Kathara/manager/docker/DockerManager.py @@ -23,7 +23,8 @@ from ...model.Link import Link from ...model.Machine import Machine from ...setting.Setting import Setting -from ...utils import pack_files_for_tar, import_pywintypes +from ...utils import pack_files_for_tar, import_pywintypes, \ + check_required_single_not_none_var, check_single_not_none_var pywintypes = import_pywintypes() @@ -259,9 +260,7 @@ def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -320,9 +319,7 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -364,9 +361,7 @@ def exec(self, machine_name: str, command: Union[List[str], str], lab_hash: Opti Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -417,9 +412,7 @@ def get_machine_api_object(self, machine_name: str, lab_hash: Optional[str] = No InvocationError: If a running network scenario hash or name is not specified. MachineNotFoundError: If the specified device is not found. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -452,6 +445,7 @@ def get_machines_api_objects(self, lab_hash: Optional[str] = None, lab_name: Opt Returns: List[docker.models.containers.Container]: Docker API objects of devices. """ + check_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -482,9 +476,7 @@ def get_link_api_object(self, link_name: str, lab_hash: Optional[str] = None, la InvocationError: If a running network scenario hash or name is not specified. LinkNotFoundError: If the collision domain is not found. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -517,6 +509,7 @@ def get_links_api_objects(self, lab_hash: Optional[str] = None, lab_name: Option Returns: List[docker.models.networks.Network]: Docker API objects of networks. """ + check_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -667,6 +660,7 @@ def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[ Generator[Dict[str, DockerMachineStats], None, None]: A generator containing dicts that has API Object identifier as keys and DockerMachineStats objects as values. """ + check_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -699,9 +693,7 @@ def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, l Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -732,6 +724,7 @@ def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str Generator[Dict[str, DockerLinkStats], None, None]: A generator containing dicts that has API Object identifier as keys and DockerLinksStats objects as values. """ + check_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -762,9 +755,7 @@ def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_nam Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: diff --git a/src/Kathara/manager/kubernetes/KubernetesManager.py b/src/Kathara/manager/kubernetes/KubernetesManager.py index 36123378..b739543c 100644 --- a/src/Kathara/manager/kubernetes/KubernetesManager.py +++ b/src/Kathara/manager/kubernetes/KubernetesManager.py @@ -19,7 +19,7 @@ from ...model.Lab import Lab from ...model.Link import Link from ...model.Machine import Machine -from ...utils import pack_files_for_tar +from ...utils import pack_files_for_tar, check_required_single_not_none_var, check_single_not_none_var class KubernetesManager(IManager): @@ -236,9 +236,7 @@ def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -326,9 +324,7 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -367,19 +363,17 @@ def exec(self, machine_name: str, command: Union[List[str], str], lab_hash: Opti Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: lab_hash = utils.generate_urlsafe_hash(lab_name) + lab_hash = lab_hash.lower() + if wait: logging.warning("Wait option has no effect on Megalos.") - lab_hash = lab_hash.lower() - return self.k8s_machine.exec(lab_hash, machine_name, command, stderr=True, tty=False, is_stream=True) def copy_files(self, machine: Machine, guest_to_host: Dict[str, io.IOBase]) -> None: @@ -418,12 +412,7 @@ def get_machine_api_object(self, machine_name: str, lab_hash: Optional[str] = No InvocationError: If a running network scenario hash or name is not specified. MachineNotFoundError: If the device is not found. """ - if all_users: - logging.warning("User-specific options have no effect on Megalos.") - - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -431,6 +420,9 @@ def get_machine_api_object(self, machine_name: str, lab_hash: Optional[str] = No lab_hash = lab_hash.lower() + if all_users: + logging.warning("User-specific options have no effect on Megalos.") + pods = self.k8s_machine.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name) if pods: return pods.pop() @@ -453,9 +445,7 @@ def get_machines_api_objects(self, lab_hash: Optional[str] = None, lab_name: Opt Returns: List[client.V1Pod]: Kubernetes Pod objects of devices. """ - if all_users: - logging.warning("User-specific options have no effect on Megalos.") - + check_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -463,6 +453,9 @@ def get_machines_api_objects(self, lab_hash: Optional[str] = None, lab_name: Opt lab_hash = lab_hash.lower() if lab_hash else None + if all_users: + logging.warning("User-specific options have no effect on Megalos.") + return self.k8s_machine.get_machines_api_objects_by_filters(lab_hash=lab_hash) def get_link_api_object(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, @@ -486,12 +479,7 @@ def get_link_api_object(self, link_name: str, lab_hash: Optional[str] = None, la InvocationError: If a running network scenario hash or name is not specified. LinkNotFoundError: If the collision domain is not found. """ - if all_users: - logging.warning("User-specific options have no effect on Megalos.") - - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -499,6 +487,9 @@ def get_link_api_object(self, link_name: str, lab_hash: Optional[str] = None, la lab_hash = lab_hash.lower() + if all_users: + logging.warning("User-specific options have no effect on Megalos.") + networks = self.k8s_link.get_links_api_objects_by_filters(lab_hash=lab_hash, link_name=link_name) if networks: return networks.pop() @@ -521,9 +512,7 @@ def get_links_api_objects(self, lab_hash: Optional[str] = None, lab_name: Option Returns: List[Any]: Kubernetes API objects of networks. """ - if all_users: - logging.warning("User-specific options have no effect on Megalos.") - + check_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -531,6 +520,9 @@ def get_links_api_objects(self, lab_hash: Optional[str] = None, lab_name: Option lab_hash = lab_hash.lower() if lab_hash else None + if all_users: + logging.warning("User-specific options have no effect on Megalos.") + return self.k8s_link.get_links_api_objects_by_filters(lab_hash=lab_hash) def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab: @@ -625,9 +617,7 @@ def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[ Generator[Dict[str, KubernetesMachineStats], None, None]: A generator containing dicts that has API Object identifier as keys and KubernetesMachineStats objects as values. """ - if all_users: - logging.warning("User-specific options have no effect on Megalos.") - + check_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -635,6 +625,9 @@ def get_machines_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[ lab_hash = lab_hash.lower() if lab_hash else None + if all_users: + logging.warning("User-specific options have no effect on Megalos.") + return self.k8s_machine.get_machines_stats(lab_hash=lab_hash, machine_name=machine_name) def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, @@ -658,9 +651,7 @@ def get_machine_stats(self, machine_name: str, lab_hash: Optional[str] = None, l Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -692,9 +683,7 @@ def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str Generator[Dict[str, KubernetesLinkStats], None, None]: A generator containing dicts that has API Object identifier as keys and KubernetesLinkStats objects as values. """ - if all_users: - logging.warning("User-specific options have no effect on Megalos.") - + check_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: @@ -702,6 +691,9 @@ def get_links_stats(self, lab_hash: Optional[str] = None, lab_name: Optional[str lab_hash = lab_hash.lower() if lab_hash else None + if all_users: + logging.warning("User-specific options have no effect on Megalos.") + return self.k8s_link.get_links_stats(lab_hash=lab_hash, link_name=link_name) def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_name: Optional[str] = None, @@ -726,9 +718,7 @@ def get_link_stats(self, link_name: str, lab_hash: Optional[str] = None, lab_nam Raises: InvocationError: If a running network scenario hash or name is not specified. """ - if not lab_hash and not lab_name and not lab: - raise InvocationError("You must specify a running network scenario hash, name or object.") - + check_required_single_not_none_var(lab_hash=lab_hash, lab_name=lab_name, lab=lab) if lab: lab_hash = lab.hash elif lab_name: diff --git a/src/Kathara/utils.py b/src/Kathara/utils.py index b8904bb2..d75651c3 100644 --- a/src/Kathara/utils.py +++ b/src/Kathara/utils.py @@ -21,7 +21,7 @@ from binaryornot.check import is_binary from slug import slug -from .exceptions import HostArchitectureError +from .exceptions import HostArchitectureError, InvocationError # Platforms constants definition. MAC_OS: str = "darwin" @@ -109,6 +109,22 @@ def get_pool_size() -> int: return cpu_count() +def check_single_not_none_var(**kwargs) -> None: + not_none = [x for x in kwargs.values() if x is not None] + + if len(not_none) > 1: + raise InvocationError(f"You must specify only a parameter among {', '.join(kwargs.keys())}") + + +def check_required_single_not_none_var(**kwargs) -> None: + not_none = [x for x in kwargs.values() if x is not None] + + if len(not_none) == 0: + raise InvocationError(f"You must specify a parameter among {', '.join(kwargs.keys())}") + elif len(not_none) > 1: + raise InvocationError(f"You must specify only a parameter among {', '.join(kwargs.keys())}") + + # Platform Specific Functions def is_platform(desired_platform: str) -> bool: return _platform == desired_platform @@ -154,26 +170,6 @@ def wait_user_input_windows() -> bool: return b'\r' in msvcrt.getch() if msvcrt.kbhit() else False -# Architecture Test -def get_architecture() -> str: - architecture = machine().lower() - - logging.debug("Machine architecture is `%s`." % architecture) - - if architecture in ["x86_64", "amd64"]: - return "amd64" - elif architecture == "i686": - return "386" - elif architecture in ["arm64", "aarch64"]: - return "arm64" - elif architecture == "armv7l": - return "armv7" - elif architecture == "armv6l": - return "armv6" - else: - raise HostArchitectureError(architecture) - - def convert_win_2_linux(filename: str, write: bool = False) -> Optional[bytes]: if not is_binary(filename): file_obj = None @@ -260,6 +256,26 @@ def passwd_info(): return exec_by_platform(passwd_info, lambda: None, passwd_info) +# Architecture Test +def get_architecture() -> str: + architecture = machine().lower() + + logging.debug("Machine architecture is `%s`." % architecture) + + if architecture in ["x86_64", "amd64"]: + return "amd64" + elif architecture == "i686": + return "386" + elif architecture in ["arm64", "aarch64"]: + return "arm64" + elif architecture == "armv7l": + return "armv7" + elif architecture == "armv6l": + return "armv6" + else: + raise HostArchitectureError(architecture) + + # Formatting Functions def human_readable_bytes(size_bytes: int) -> str: if size_bytes == 0: diff --git a/tests/manager/docker/docker_manager_test.py b/tests/manager/docker/docker_manager_test.py index 1d9388a5..f01d34f4 100644 --- a/tests/manager/docker/docker_manager_test.py +++ b/tests/manager/docker/docker_manager_test.py @@ -460,6 +460,11 @@ def test_undeploy_lab_lab_obj_selected_machines(mock_undeploy_machine, mock_unde mock_undeploy_link.assert_called_once_with(expected_hash) +def test_undeploy_lab_lab_hash_lab_obj(docker_manager, two_device_scenario): + with pytest.raises(InvocationError): + docker_manager.undeploy_lab(lab_hash=two_device_scenario.hash, lab=two_device_scenario) + + # # TEST: wipe # @@ -565,6 +570,13 @@ def test_connect_tty_lab_obj(mock_connect, mock_get_current_user_name, docker_ma wait=True) +def test_connect_tty_lab_hash_lab_obj(docker_manager, default_device, two_device_scenario): + with pytest.raises(InvocationError): + docker_manager.connect_tty(default_device.name, + lab_hash=two_device_scenario.hash, + lab=two_device_scenario) + + @mock.patch("src.Kathara.utils.get_current_user_name") @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.connect") def test_connect_tty_with_custom_shell(mock_connect, mock_get_current_user_name, docker_manager, default_device): @@ -664,6 +676,12 @@ def test_exec_lab_obj(mock_exec, mock_get_current_user_name, docker_manager, def ) +def test_exec_lab_hash_lab_obj(docker_manager, default_device, two_device_scenario): + with pytest.raises(InvocationError): + docker_manager.exec(default_device.name, ["test", "command"], + lab_hash=two_device_scenario.hash, lab=two_device_scenario) + + @mock.patch("src.Kathara.utils.get_current_user_name") @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.exec") def test_exec_wait(mock_exec, mock_get_current_user_name, docker_manager, default_device): @@ -753,6 +771,13 @@ def test_get_machine_api_object_lab_obj_no_user(mock_get_machines_api_objects, d machine_name="test_device", user=None) +def test_get_machine_api_object_lab_hash_lab_obj(docker_manager, default_device, two_device_scenario): + with pytest.raises(InvocationError): + docker_manager.get_machine_api_object("test_device", + lab_hash=two_device_scenario.hash, + lab=two_device_scenario, all_users=False) + + @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters") def test_get_machine_api_object_invocation_error(mock_get_machines_api_objects, docker_manager, default_device): with pytest.raises(InvocationError): @@ -826,6 +851,12 @@ def test_get_machines_api_objects_lab_obj_no_user(mock_get_machines_api_objects, mock_get_machines_api_objects.assert_called_once_with(lab_hash=two_device_scenario.hash, user=None) +def test_get_machines_api_objects_lab_hash_lab_obj(docker_manager, default_device, two_device_scenario): + with pytest.raises(InvocationError): + docker_manager.get_machines_api_objects(lab_hash=two_device_scenario.hash, + lab=two_device_scenario, all_users=False) + + @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters") def test_get_machines_api_objects_no_labs(mock_get_machines_api_objects, docker_manager): docker_manager.get_machines_api_objects(all_users=True) @@ -903,6 +934,13 @@ def test_get_link_api_object_lab_obj_no_user(mock_get_links_api_objects, docker_ link_name="test_link", user=None) +def test_get_link_api_object_lab_hash_lab_obj(docker_manager, docker_network, two_device_scenario): + with pytest.raises(InvocationError): + docker_manager.get_link_api_object("test_link", + lab_hash=two_device_scenario.hash, + lab=two_device_scenario, all_users=True) + + @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters") def test_get_link_api_object_invocation_error(mock_get_links_api_objects, docker_manager): with pytest.raises(InvocationError): @@ -981,6 +1019,12 @@ def test_get_links_api_objects_lab_obj_no_user(mock_get_links_api_objects, docke user=None) +def test_get_links_api_objects_lab_hash_lab_obj(docker_manager, docker_network, two_device_scenario): + with pytest.raises(InvocationError): + docker_manager.get_links_api_objects(lab_hash=two_device_scenario.hash, + lab=two_device_scenario, all_users=True) + + @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters") def test_get_links_api_objects_no_labs(mock_get_links_api_objects, docker_manager): docker_manager.get_links_api_objects(all_users=True) @@ -1184,6 +1228,11 @@ def test_get_machines_stats_lab_obj_user(mock_get_machines_stats, mock_get_curre machine_name=None, user="kathara-user") +def test_get_machines_stats_lab_hash_lab_obj(docker_manager, two_device_scenario): + with pytest.raises(InvocationError): + docker_manager.get_machines_stats(lab_hash=two_device_scenario.hash, lab=two_device_scenario) + + @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_stats") def test_get_machines_stats_no_labs(mock_get_machines_stats, docker_manager): docker_manager.get_machines_stats(all_users=True) @@ -1247,8 +1296,14 @@ def test_get_machine_stats_lab_obj_user(mock_get_machines_stats, default_device, all_users=False) +def test_get_machine_stats_lab_hash_lab_obj(default_device, docker_manager, two_device_scenario): + with pytest.raises(InvocationError): + next(docker_manager.get_machine_stats(machine_name="test_device", lab_hash=two_device_scenario.hash, + lab=two_device_scenario)) + + @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_stats") -def test_get_machine_stats_no_name_no_hash(mock_get_machines_stats, docker_manager): +def test_get_machine_stats_no_labs(mock_get_machines_stats, docker_manager): with pytest.raises(InvocationError): next(docker_manager.get_machine_stats(machine_name="test_device", all_users=True)) assert not mock_get_machines_stats.called @@ -1302,6 +1357,11 @@ def test_get_links_stats_lab_obj_user(mock_get_links_stats, mock_get_current_use link_name=None, user="kathara-user") +def test_get_links_stats_lab_hash_lab_obj(docker_manager, two_device_scenario): + with pytest.raises(InvocationError): + docker_manager.get_links_stats(lab_hash=two_device_scenario.hash, lab=two_device_scenario) + + @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_stats") def test_get_links_stats_no_labs(mock_get_links_stats, docker_manager): docker_manager.get_links_stats(all_users=True) @@ -1357,6 +1417,12 @@ def test_get_link_stats_lab_obj_user(mock_get_links_stats, docker_network, docke link_name="test_network", all_users=False) +def test_get_link_stats_lab_hash_lab_obj(docker_network, docker_manager, two_device_scenario): + with pytest.raises(InvocationError): + next(docker_manager.get_link_stats(link_name="test_network", lab_hash=two_device_scenario.hash, + lab=two_device_scenario)) + + @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_stats") def test_get_link_stats_invocation_error(mock_get_links_stats, docker_network, docker_manager): mock_get_links_stats.return_value = iter([{"test_network": DockerLinkStats(docker_network)}]) diff --git a/tests/manager/kubernetes/kubernetes_manager_test.py b/tests/manager/kubernetes/kubernetes_manager_test.py index b84c95af..f29e8b4f 100644 --- a/tests/manager/kubernetes/kubernetes_manager_test.py +++ b/tests/manager/kubernetes/kubernetes_manager_test.py @@ -618,24 +618,16 @@ def test_get_machine_api_object_lab_obj(mock_get_machines_api_objects, kubernete lab_hash=two_device_scenario.hash.lower()) -@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") -def test_get_machine_api_object_lab_hash_and_name(mock_get_machines_api_objects, kubernetes_manager, default_device): - default_device.api_object.name = "default_device" - mock_get_machines_api_objects.return_value = [default_device.api_object] - kubernetes_manager.get_machine_api_object(machine_name="default_device", lab_name="lab_name", lab_hash="lab_hash") - mock_get_machines_api_objects.assert_called_once_with(machine_name="default_device", - lab_hash=generate_urlsafe_hash("lab_name").lower()) +def test_get_machine_api_object_lab_hash_and_name(kubernetes_manager, default_device): + with pytest.raises(InvocationError): + kubernetes_manager.get_machine_api_object(machine_name="default_device", lab_name="lab_name", + lab_hash="lab_hash") -@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") -def test_get_machine_api_object_lab_hash_and_name_and_obj(mock_get_machines_api_objects, kubernetes_manager, - default_device, two_device_scenario): - default_device.api_object.name = "default_device" - mock_get_machines_api_objects.return_value = [default_device.api_object] - kubernetes_manager.get_machine_api_object(machine_name="default_device", lab_name="lab_name", - lab_hash="lab_hash", lab=two_device_scenario) - mock_get_machines_api_objects.assert_called_once_with(machine_name="default_device", - lab_hash=two_device_scenario.hash.lower()) +def test_get_machine_api_object_lab_hash_and_name_and_obj(kubernetes_manager, default_device, two_device_scenario): + with pytest.raises(InvocationError): + kubernetes_manager.get_machine_api_object(machine_name="default_device", lab_name="lab_name", + lab_hash="lab_hash", lab=two_device_scenario) @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") @@ -732,14 +724,10 @@ def test_get_link_api_object_lab_obj(mock_get_links_api_objects, kubernetes_mana lab_hash=two_device_scenario.hash.lower()) -@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters") -def test_get_link_api_object_lab_hash_and_name_and_obj(mock_get_links_api_objects, kubernetes_manager, - kubernetes_network, two_device_scenario): - mock_get_links_api_objects.return_value = [kubernetes_network] - kubernetes_manager.get_link_api_object(link_name="test_network", lab_name="lab_name", lab_hash="lab_hash", - lab=two_device_scenario) - mock_get_links_api_objects.assert_called_once_with(link_name="test_network", - lab_hash=two_device_scenario.hash.lower()) +def test_get_link_api_object_lab_hash_and_name_and_obj(kubernetes_manager, kubernetes_network, two_device_scenario): + with pytest.raises(InvocationError): + kubernetes_manager.get_link_api_object(link_name="test_network", lab_name="lab_name", lab_hash="lab_hash", + lab=two_device_scenario) @mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters") From df8e5491545b9b282af78be21f52721be35ce2e1 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Thu, 4 Jan 2024 16:14:38 +0100 Subject: [PATCH 33/44] Minor fixes to check command and connect/exec exceptions --- src/Kathara/cli/command/CheckCommand.py | 22 ++++++++----------- src/Kathara/manager/docker/DockerMachine.py | 10 ++++----- .../manager/kubernetes/KubernetesMachine.py | 12 ++++++---- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Kathara/cli/command/CheckCommand.py b/src/Kathara/cli/command/CheckCommand.py index 7400ecc0..12724bee 100644 --- a/src/Kathara/cli/command/CheckCommand.py +++ b/src/Kathara/cli/command/CheckCommand.py @@ -36,13 +36,10 @@ def run(self, current_path: str, argv: List[str]) -> None: self.parse_args(argv) args = self.get_args() - print("*\tCurrent Manager is: %s" % Kathara.get_instance().get_formatted_manager_name()) - - print("*\tManager version is: %s" % Kathara.get_instance().get_release_version()) - + print(f"*\tCurrent Manager is: {Kathara.get_instance().get_formatted_manager_name()}") + print(f"*\tManager version is: {Kathara.get_instance().get_release_version()}") print("*\tPython version is: %s" % sys.version.replace("\n", "- ")) - - print("*\tKathara version is: %s" % version.CURRENT_VERSION) + print(f"*\tKathara version is: {version.CURRENT_VERSION}") def linux_platform_info(): info = os.uname() @@ -51,20 +48,19 @@ def linux_platform_info(): platform_info = utils.exec_by_platform( linux_platform_info, lambda: platform.platform(), lambda: platform.platform() ) - print("*\tOperating System version is: %s" % str(platform_info)) - - print("*\tTrying to run `Hello World` container...") + print(f"*\tOperating System version is: {str(platform_info)}") + print(f"*\tTrying to run container with `{Setting.get_instance().image}` image...") Setting.get_instance().open_terminals = False args['no_shared'] = False args['no_hosthome'] = False lab = Lab("kathara_test") - lab.get_or_new_machine("hello_world") + machine = lab.get_or_new_machine("hello_world") try: - Kathara.get_instance().deploy_lab(lab) + Kathara.get_instance().deploy_machine(machine) print("*\tContainer run successfully.") - Kathara.get_instance().undeploy_lab(lab_hash=lab.hash) + Kathara.get_instance().undeploy_machine(machine) except Exception as e: - logging.exception("\t! Running `Hello World` failed: %s" % str(e)) + logging.exception(f"\t! Running `Hello World` failed: {str(e)}") diff --git a/src/Kathara/manager/docker/DockerMachine.py b/src/Kathara/manager/docker/DockerMachine.py index 168c4420..eb583c36 100644 --- a/src/Kathara/manager/docker/DockerMachine.py +++ b/src/Kathara/manager/docker/DockerMachine.py @@ -17,7 +17,7 @@ from ... import utils from ...event.EventDispatcher import EventDispatcher from ...exceptions import MountDeniedError, MachineAlreadyExistsError, MachineNotFoundError, DockerPluginError, \ - MachineBinaryError + MachineBinaryError, MachineNotRunningError from ...model.Interface import Interface from ...model.Lab import Lab from ...model.Link import Link, BRIDGE_LINK_NAME @@ -540,12 +540,12 @@ def connect(self, lab_hash: str, machine_name: str, user: str = None, shell: str None Raises: - MachineNotFoundError: If the specified device is not running. + MachineNotRunningError: If the specified device is not running. ValueError: If the wait values is neither a boolean nor a tuple, or an invalid tuple. """ containers = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name, user=user) if not containers: - raise MachineNotFoundError("The specified device `%s` is not running." % machine_name) + raise MachineNotRunningError(machine_name) container = containers.pop() if not shell: @@ -643,14 +643,14 @@ def exec(self, lab_hash: str, machine_name: str, command: Union[str, List], user Generator[Tuple[bytes, bytes]]: A generator of tuples containing the stdout and stderr in bytes. Raises: - MachineNotFoundError: If the specified device is not running. + MachineNotRunningError: If the specified device is not running. ValueError: If the wait values is neither a boolean nor a tuple, or an invalid tuple. """ logging.debug("Executing command `%s` to device with name: %s" % (command, machine_name)) containers = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name, user=user) if not containers: - raise MachineNotFoundError("The specified device `%s` is not running." % machine_name) + raise MachineNotRunningError(machine_name) container = containers.pop() if isinstance(wait, tuple): diff --git a/src/Kathara/manager/kubernetes/KubernetesMachine.py b/src/Kathara/manager/kubernetes/KubernetesMachine.py index ec91958a..7992ba25 100644 --- a/src/Kathara/manager/kubernetes/KubernetesMachine.py +++ b/src/Kathara/manager/kubernetes/KubernetesMachine.py @@ -21,7 +21,7 @@ from .stats.KubernetesMachineStats import KubernetesMachineStats from ... import utils from ...event.EventDispatcher import EventDispatcher -from ...exceptions import MachineAlreadyExistsError, MachineNotFoundError, MachineNotReadyError +from ...exceptions import MachineAlreadyExistsError, MachineNotFoundError, MachineNotReadyError, MachineNotRunningError from ...model.Lab import Lab from ...model.Machine import Machine from ...setting.Setting import Setting @@ -584,11 +584,12 @@ def connect(self, lab_hash: str, machine_name: str, shell: Union[str, List[str]] None Raises: + MachineNotRunningError: If the specified device is not running. MachineNotReadyError: If the device is not ready. """ pods = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name) if not pods: - raise MachineNotFoundError("The specified device `%s` is not running." % machine_name) + raise MachineNotRunningError(machine_name) deployment = pods.pop() if 'Running' not in deployment.status.phase: @@ -682,8 +683,11 @@ def exec(self, lab_hash: str, machine_name: str, command: Union[str, List], tty: Returns: Generator[Tuple[bytes, bytes]]: A generator of tuples containing the stdout and stderr in bytes. + + Raises: + MachineNotRunningError: If the specified device is not running. """ - command = shlex.split(command) if type(command) == str else command + command = shlex.split(command) if command is str else command logging.debug("Executing command `%s` to device with name: %s" % (command, machine_name)) @@ -691,7 +695,7 @@ def exec(self, lab_hash: str, machine_name: str, command: Union[str, List], tty: # Retrieve the pod of current Deployment pods = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name) if not pods: - raise MachineNotFoundError("The specified device `%s` is not running." % machine_name) + raise MachineNotRunningError(machine_name) pod = pods.pop() response = stream(self.core_client.connect_get_namespaced_pod_exec, From 1c93323886b84e9fd0d7f548282c6e5070ec976c Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Thu, 4 Jan 2024 16:15:29 +0100 Subject: [PATCH 34/44] check command exception fix --- src/Kathara/cli/command/CheckCommand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kathara/cli/command/CheckCommand.py b/src/Kathara/cli/command/CheckCommand.py index 12724bee..fd4d024f 100644 --- a/src/Kathara/cli/command/CheckCommand.py +++ b/src/Kathara/cli/command/CheckCommand.py @@ -63,4 +63,4 @@ def linux_platform_info(): print("*\tContainer run successfully.") Kathara.get_instance().undeploy_machine(machine) except Exception as e: - logging.exception(f"\t! Running `Hello World` failed: {str(e)}") + logging.exception(f"\t! Running container failed: {str(e)}") From 698b8431f93e040f315c3e73a63edd56ea592307 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Thu, 4 Jan 2024 16:19:26 +0100 Subject: [PATCH 35/44] Fix check command lab params --- src/Kathara/cli/command/CheckCommand.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Kathara/cli/command/CheckCommand.py b/src/Kathara/cli/command/CheckCommand.py index fd4d024f..b6d83516 100644 --- a/src/Kathara/cli/command/CheckCommand.py +++ b/src/Kathara/cli/command/CheckCommand.py @@ -52,12 +52,11 @@ def linux_platform_info(): print(f"*\tTrying to run container with `{Setting.get_instance().image}` image...") Setting.get_instance().open_terminals = False - args['no_shared'] = False - args['no_hosthome'] = False lab = Lab("kathara_test") - machine = lab.get_or_new_machine("hello_world") + lab.add_option('hosthome_mount', False) + machine = lab.get_or_new_machine("hello_world") try: Kathara.get_instance().deploy_machine(machine) print("*\tContainer run successfully.") From ef75fa1d4c6a7e5223bb592f30e71861852ac45c Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Thu, 4 Jan 2024 18:15:29 +0100 Subject: [PATCH 36/44] Add timer to `_wait_machines_startup` (fix #258) --- src/Kathara/cli/command/CheckCommand.py | 1 - .../manager/kubernetes/KubernetesMachine.py | 28 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Kathara/cli/command/CheckCommand.py b/src/Kathara/cli/command/CheckCommand.py index b6d83516..3ac1544e 100644 --- a/src/Kathara/cli/command/CheckCommand.py +++ b/src/Kathara/cli/command/CheckCommand.py @@ -34,7 +34,6 @@ def __init__(self) -> None: def run(self, current_path: str, argv: List[str]) -> None: self.parse_args(argv) - args = self.get_args() print(f"*\tCurrent Manager is: {Kathara.get_instance().get_formatted_manager_name()}") print(f"*\tManager version is: {Kathara.get_instance().get_release_version()}") diff --git a/src/Kathara/manager/kubernetes/KubernetesMachine.py b/src/Kathara/manager/kubernetes/KubernetesMachine.py index 7992ba25..1f69f661 100644 --- a/src/Kathara/manager/kubernetes/KubernetesMachine.py +++ b/src/Kathara/manager/kubernetes/KubernetesMachine.py @@ -1,9 +1,12 @@ import hashlib import json import logging +import os import re import shlex +import signal import sys +import threading import uuid from multiprocessing.dummy import Pool from typing import Optional, Set, List, Union, Generator, Tuple, Dict @@ -28,6 +31,7 @@ RP_FILTER_NAMESPACE = "net.ipv4.conf.%s.rp_filter" MAX_RESTART_COUNT = 3 +MAX_TIME_ERROR = 180 # Known commands that each container should execute # Run order: shared.startup, machine.startup and machine.meta['exec_commands'] @@ -41,7 +45,7 @@ # Removes /etc/bind already existing configuration from k8s internal DNS "rm -Rf /etc/bind/*", - # Unmount the /etc/resolv.conf and /etc/hosts files, automatically mounted by Docker inside the container. + # Unmount the /etc/resolv.conf and /etc/hosts files, automatically mounted by Kubernetes inside the container. # In this way, they can be overwritten by custom user files. "umount /etc/resolv.conf", "umount /etc/hosts", @@ -186,8 +190,27 @@ def _wait_machines_startup(self, lab: Lab, selected_machines: Set[str]) -> None: machines_ready = 0 machines_failed = 0 + # Create a timer to raise an exception if the execution gets stuck + def raise_timeout_error(): + logging.error( + f"Network scenario startup is not responding for over {MAX_TIME_ERROR} seconds, exiting. " + f"To check devices status, use the following command:\n\t" + f"kubectl -n {lab.hash} get pods" + ) + + # We should send a SIGINT to the main thread to exit the w.stream + os.kill(os.getpid(), signal.SIGINT) + + timer = threading.Timer(MAX_TIME_ERROR, raise_timeout_error) + timer.start() + w = watch.Watch() for event in w.stream(self.kubernetes_namespace.client.list_namespaced_pod, namespace=lab.hash): + # Every new event, cancel and create the timer + timer.cancel() + timer = threading.Timer(MAX_TIME_ERROR, raise_timeout_error) + timer.start() + machine_name = event['object'].metadata.labels['name'] if not selected_machines or machine_name in selected_machines: @@ -216,6 +239,9 @@ def _wait_machines_startup(self, lab: Lab, selected_machines: Set[str]) -> None: logging.warning(f"Device `{machine_name}` has been restarted {restart_count} times.") if machines_ready + machines_failed == len(machines): + # Finished watching, cancel the last timer + timer.cancel() + w.stop() if machines_ready == len(machines): From 194f59b048f3d248d922e9374770f48976b6b9ce Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Thu, 4 Jan 2024 18:47:30 +0100 Subject: [PATCH 37/44] Fix tests (#258) --- .../kubernetes/kubernetes_machine_test.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/manager/kubernetes/kubernetes_machine_test.py b/tests/manager/kubernetes/kubernetes_machine_test.py index 7953b655..6111f78e 100644 --- a/tests/manager/kubernetes/kubernetes_machine_test.py +++ b/tests/manager/kubernetes/kubernetes_machine_test.py @@ -427,8 +427,9 @@ def test_deploy_machine(mock_create, kubernetes_machine, default_device): # # TEST: deploy_machines # +@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine._wait_machines_startup") @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine._deploy_machine") -def test_deploy_machines(mock_deploy, kubernetes_machine): +def test_deploy_machines(mock_deploy, mock_wait_machines_startup, kubernetes_machine): lab = Lab("Default scenario") lab.get_or_new_machine("pc1", **{'image': 'kathara/test1'}) @@ -439,15 +440,17 @@ def test_deploy_machines(mock_deploy, kubernetes_machine): kubernetes_machine.deploy_machines(lab) assert mock_deploy.call_count == 2 + mock_wait_machines_startup.assert_called_once_with(lab, None) # # TEST: undeploy # +@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine._wait_machines_shutdown") @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine._undeploy_machine") @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") -def test_undeploy_one_device(mock_get_machines_api_objects_by_filters, mock_undeploy_machine, kubernetes_machine, - default_device): +def test_undeploy_one_device(mock_get_machines_api_objects_by_filters, mock_undeploy_machine, + mock_wait_machines_shutdown, kubernetes_machine, default_device): default_device.api_object.metadata.labels = {'name': "test_device"} mock_get_machines_api_objects_by_filters.return_value = [default_device.api_object] mock_undeploy_machine.return_value = None @@ -456,12 +459,14 @@ def test_undeploy_one_device(mock_get_machines_api_objects_by_filters, mock_unde mock_get_machines_api_objects_by_filters.assert_called_once() mock_undeploy_machine.assert_called_once_with(default_device.api_object) + mock_wait_machines_shutdown.assert_called_once_with("lab_hash", {default_device.name}) +@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine._wait_machines_shutdown") @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine._undeploy_machine") @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") -def test_undeploy_three_devices(mock_get_machines_api_objects_by_filters, mock_undeploy_machine, kubernetes_machine, - default_device): +def test_undeploy_three_devices(mock_get_machines_api_objects_by_filters, mock_undeploy_machine, + mock_wait_machines_shutdown, kubernetes_machine, default_device): default_device.api_object.metadata.labels = {'name': "test_device"} # fill the list with more devices mock_get_machines_api_objects_by_filters.return_value = [default_device.api_object, @@ -472,11 +477,14 @@ def test_undeploy_three_devices(mock_get_machines_api_objects_by_filters, mock_u mock_get_machines_api_objects_by_filters.assert_called_once() assert mock_undeploy_machine.call_count == 3 + mock_wait_machines_shutdown.assert_called_once_with("lab_hash", {"test_device"}) +@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine._wait_machines_shutdown") @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine._undeploy_machine") @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters") -def test_undeploy_no_devices(mock_get_machines_api_objects_by_filters, mock_undeploy_machine, kubernetes_machine): +def test_undeploy_no_devices(mock_get_machines_api_objects_by_filters, mock_undeploy_machine, + mock_wait_machines_shutdown, kubernetes_machine): mock_get_machines_api_objects_by_filters.return_value = [] mock_undeploy_machine.return_value = None @@ -484,6 +492,7 @@ def test_undeploy_no_devices(mock_get_machines_api_objects_by_filters, mock_unde mock_get_machines_api_objects_by_filters.assert_called_once() assert not mock_undeploy_machine.called + assert not mock_wait_machines_shutdown.called # From 3705f25595e09c3d4e8ddbf87de466bf9e7f49ee Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Thu, 4 Jan 2024 20:02:32 +0100 Subject: [PATCH 38/44] Relax regex for sysctls and envs (#259) --- src/Kathara/model/Machine.py | 6 +++--- tests/model/machine_test.py | 22 +++++++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Kathara/model/Machine.py b/src/Kathara/model/Machine.py index f9feef6d..00b37c74 100644 --- a/src/Kathara/model/Machine.py +++ b/src/Kathara/model/Machine.py @@ -159,7 +159,7 @@ def add_meta(self, name: str, value: Any) -> None: return old_value if name == "sysctl": - matches = re.search(r"^(?Pnet\.([\w-]+\.)+[\w-]+)=(?P-?\w+)$", value) + matches = re.search(r"^(?Pnet\.([\w-]+\.)+[\w-]+)=(?P[^=]*)$", value) # Check for valid kv-pair if matches: @@ -169,7 +169,7 @@ def add_meta(self, name: str, value: Any) -> None: old_value = self.meta['sysctls'][key] if key in self.meta['sysctls'] else None # Convert to int if possible - self.meta['sysctls'][key] = int(val) if val.strip('-').isnumeric() else val + self.meta['sysctls'][key] = int(val) if val.lstrip('-').isnumeric() else val else: raise MachineOptionError( "Invalid sysctl value (`%s`) on `%s`, missing `=` or value not in `net.` namespace." @@ -178,7 +178,7 @@ def add_meta(self, name: str, value: Any) -> None: return old_value if name == "env": - matches = re.search(r"^(?P\w+)=(?P\S+)$", value) + matches = re.search(r"^(?P\w+)=(?P[^=]*)$", value) # Check for valid kv-pair if matches: diff --git a/tests/model/machine_test.py b/tests/model/machine_test.py index 691fa7c8..e646740a 100644 --- a/tests/model/machine_test.py +++ b/tests/model/machine_test.py @@ -176,8 +176,18 @@ def test_add_meta_sysctl_negative_number(default_device: Machine): def test_add_meta_sysctl_negative_number_not_format(default_device: Machine): + default_device.add_meta("sysctl", "net.test_sysctl.negative=-1-") + assert default_device.meta['sysctls']['net.test_sysctl.negative'] == "-1-" + + +def test_add_meta_sysctl_with_spaces(default_device: Machine): + default_device.add_meta("sysctl", "net.ipv4.tcp_rmem=4096 87380 33554432") + assert default_device.meta['sysctls']['net.ipv4.tcp_rmem'] == "4096 87380 33554432" + + +def test_add_meta_sysctl_double_equal(default_device: Machine): with pytest.raises(MachineOptionError): - default_device.add_meta("sysctl", "net.test_sysctl.negative=-1-") + default_device.add_meta("sysctl", "net.test_sysctl.text=test=again") def test_add_meta_env(default_device: Machine): @@ -190,6 +200,16 @@ def test_add_meta_env_number(default_device: Machine): assert default_device.meta['envs']['MY_ENV_VAR'] == "1" +def test_add_meta_env_with_spaces(default_device: Machine): + default_device.add_meta("env", "MY_ENV_VAR=spaced value") + assert default_device.meta['envs']['MY_ENV_VAR'] == "spaced value" + + +def test_add_meta_env_double_equal(default_device: Machine): + with pytest.raises(MachineOptionError): + default_device.add_meta("env", "MY_ENV_VAR=test=not valid") + + def test_add_meta_env_not_format_exception(default_device: Machine): with pytest.raises(MachineOptionError): default_device.add_meta("env", "MY_ENV_VAR") From d8800708b5834412b94255c3d03e6a2729d039ae Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Mon, 8 Jan 2024 12:36:57 +0100 Subject: [PATCH 39/44] Bump version + Changelog + Compilation with Python3.11 --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- SECURITY.md | 2 +- pyproject.toml | 14 +++++++------- scripts/Linux-Deb/Makefile | 2 +- scripts/Linux-Deb/debian/changelog | 8 +++++--- scripts/Linux-Deb/debian/control | 2 +- scripts/Linux-Pkg/Docker-Linux-Build/Dockerfile | 8 ++------ scripts/Linux-Pkg/Makefile | 4 +--- scripts/Linux-Pkg/pkginfo/PKGBUILD | 6 +++--- scripts/Linux-Pkg/pkginfo/kathara.changelog | 8 +++++--- scripts/Linux-Rpm/Docker-Linux-Build/Dockerfile | 4 ++-- scripts/Linux-Rpm/Makefile | 2 +- scripts/Linux-Rpm/rpm/kathara.spec | 16 +++++++++------- scripts/OSX/Makefile | 4 ++-- scripts/OSX/README.md | 4 ++-- scripts/Windows/README.md | 12 +++++------- scripts/Windows/WindowsBuild.bat | 2 +- scripts/Windows/installer.iss | 4 ++-- scripts/pip-package/README.md | 1 + setup.cfg | 4 ++-- setup.py | 8 ++++---- src/Kathara/version.py | 2 +- 22 files changed, 59 insertions(+), 60 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f06a2dff..f9e90b55 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -22,7 +22,7 @@ body: attributes: label: Kathará Version description: "Please provide the Kathará version you are using (`kathara -v`)." - placeholder: "3.7.0" + placeholder: "3.7.1" validations: required: true - type: textarea diff --git a/SECURITY.md b/SECURITY.md index 6d7d1db3..a7440881 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ We release patches for security vulnerabilities only for the last version: | Version | Supported Versions | |---------|--------------------| -| 3.7.0 | :white_check_mark: | +| 3.7.1 | :white_check_mark: | ## Reporting a Vulnerability diff --git a/pyproject.toml b/pyproject.toml index d488af12..6d722cb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,19 @@ [project] name = "kathara" -version = "3.7.0" -description = "A lightweight container-based emulation system." +version = "3.7.1" +description = "A lightweight container-based network emulation tool." readme = "README.md" requires-python = ">=3.9" license = { file = "LICENSE" } keywords = ["NETWORK-EMULATION", "CONTAINERS", "NFV"] authors = [ - { name = "Kathara Framework", email = "contact@kathara.org" } # Optional + { name = "Kathara Framework", email = "contact@kathara.org" } ] maintainers = [ - { name = "Tommaso Caiazzi", email = "contact@kathara.org" }, # Optional - { name = "Mariano Scazzariello", email = "contact@kathara.org" }, # Optional - { name = "Lorenzo Ariemma", email = "contact@kathara.org" }, # Optional + { name = "Tommaso Caiazzi", email = "contact@kathara.org" }, + { name = "Mariano Scazzariello", email = "contact@kathara.org" }, + { name = "Lorenzo Ariemma", email = "contact@kathara.org" }, ] classifiers = [ @@ -30,7 +30,7 @@ classifiers = [ dependencies = [ "binaryornot>=0.4.4", - "docker>=6.0.1", + "docker>=7.0.0", "kubernetes>=23.3.0", "requests>=2.22.0", "coloredlogs>=10.0", diff --git a/scripts/Linux-Deb/Makefile b/scripts/Linux-Deb/Makefile index 37a24f3a..4bd87fed 100644 --- a/scripts/Linux-Deb/Makefile +++ b/scripts/Linux-Deb/Makefile @@ -1,6 +1,6 @@ #!/usr/bin/make -f -VERSION=3.7.0 +VERSION=3.7.1 DEBIAN_PACKAGE_VERSION=1 LAUNCHPAD_NAME=user NO_BINARY_PACKAGES=pyroute2|pyuv|deepdiff diff --git a/scripts/Linux-Deb/debian/changelog b/scripts/Linux-Deb/debian/changelog index 5be19092..0d1a5cf6 100644 --- a/scripts/Linux-Deb/debian/changelog +++ b/scripts/Linux-Deb/debian/changelog @@ -1,7 +1,9 @@ kathara (__VERSION__-__DEBIAN_PACKAGE_VERSION____UBUNTU_VERSION__) __UBUNTU_VERSION__; urgency=low - * Add support for the new Kathara Network Plugin based on VDE software switches. It is possible to select the old Network Plugin (based on Linux Bridges) from "kathara settings" - * Switch the default image to "kathara/base" for new installations - * Fix Docker images fetching in "kathara settings" + * It is now possible to specify a MAC Address for a network interface + * (Docker) Collision domains are now created per-network-scenario by default + * (Docker) If a ".shutdown" file is present in the network scenario, Kathara now correctly waits for the script termination before removing the container + * Several fixes of "lconfig" and "vconfig" commands + * Minor fixes and improvements -- Kathara Team __DATE__ diff --git a/scripts/Linux-Deb/debian/control b/scripts/Linux-Deb/debian/control index 9428d8c7..eced2333 100644 --- a/scripts/Linux-Deb/debian/control +++ b/scripts/Linux-Deb/debian/control @@ -20,7 +20,7 @@ Package: kathara Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} Suggests: docker.io (>= 17.03.2) | docker-ce (>= 17.03.2), xterm -Description: Lightweight network emulation tool. +Description: A lightweight container-based network emulation tool. It can be really helpful in showing interactive demos/lessons, testing production networks in a sandbox environment, or developing new network protocols. diff --git a/scripts/Linux-Pkg/Docker-Linux-Build/Dockerfile b/scripts/Linux-Pkg/Docker-Linux-Build/Dockerfile index 66ef8778..d1669049 100644 --- a/scripts/Linux-Pkg/Docker-Linux-Build/Dockerfile +++ b/scripts/Linux-Pkg/Docker-Linux-Build/Dockerfile @@ -10,12 +10,8 @@ RUN pacman -S --noconfirm git \ base-devel \ chrpath \ patchelf \ - ruby-ronn-ng - -RUN pacman -S --noconfirm wget && \ - wget https://archive.archlinux.org/packages/p/python/python-3.10.10-1-x86_64.pkg.tar.zst && \ - pacman -U --noconfirm python-3.10.10-1-x86_64.pkg.tar.zst && \ - rm -f python-3.10.10-1-x86_64.pkg.tar.zst + ruby-ronn-ng \ + python RUN useradd -m -s /bin/bash builduser diff --git a/scripts/Linux-Pkg/Makefile b/scripts/Linux-Pkg/Makefile index 65ce93b2..3e5a2a01 100644 --- a/scripts/Linux-Pkg/Makefile +++ b/scripts/Linux-Pkg/Makefile @@ -1,6 +1,6 @@ #!/usr/bin/make -f -VERSION=3.7.0 +VERSION=3.7.1 PACKAGE_VERSION=1 AUR_NAME=user AUR_MAIL=contact@kathara.org @@ -34,13 +34,11 @@ build-local: prepare-files mkdir -p /home/builduser/build/src/ cp -r /home/builduser/kathara-source/Kathara-$(VERSION) /home/builduser/build/src tar -zcf /home/builduser/build/kathara-$(VERSION).tar.gz /home/builduser/build/src/Kathara-$(VERSION) - sed -i -e 's/__PYTHON_DEP__//g' /home/builduser/build/PKGBUILD sed -i -e 's/__SOURCE__/"kathara-$(VERSION).tar.gz"/g' /home/builduser/build/PKGBUILD makepkg mv kathara*.pkg.tar.zst /home/builduser/output/ build-remote: prepare-files - sed -i -e "s/__PYTHON_DEP__/'python310'/g" /home/builduser/build/PKGBUILD sed -i -e 's/__SOURCE__/"https:\/\/github.com\/KatharaFramework\/Kathara\/archive\/refs\/tags\/\$$\pkgver.tar.gz"/g' /home/builduser/build/PKGBUILD cp -r /home/builduser/ssh/ /home/builduser/.ssh/ && chmod 600 /home/builduser/.ssh/id_rsa ssh-keyscan aur.archlinux.org > /home/builduser/.ssh/known_hosts diff --git a/scripts/Linux-Pkg/pkginfo/PKGBUILD b/scripts/Linux-Pkg/pkginfo/PKGBUILD index 926029f3..e4b47111 100644 --- a/scripts/Linux-Pkg/pkginfo/PKGBUILD +++ b/scripts/Linux-Pkg/pkginfo/PKGBUILD @@ -1,14 +1,14 @@ pkgname=kathara pkgver=__VERSION__ pkgrel=__PACKAGE_VERSION__ -pkgdesc="Lightweight network emulation system based on Docker containers." +pkgdesc="A lightweight container-based network emulation tool." arch=('any') url="https://www.kathara.org/" license=('GPL3') install="kathara.install" changelog="kathara.changelog" makedepends=( - __PYTHON_DEP__ + 'python' 'chrpath' 'patchelf' 'make' @@ -17,7 +17,7 @@ makedepends=( optdepends=( 'docker: for running network scenarios in a local environment' 'xterm: for opening devices terminals' - 'tmux: for opening many devices terminals' + 'tmux: for devices terminals multiplexing' ) source=(__SOURCE__) md5sums=('SKIP') diff --git a/scripts/Linux-Pkg/pkginfo/kathara.changelog b/scripts/Linux-Pkg/pkginfo/kathara.changelog index 4bde257a..587afeb0 100644 --- a/scripts/Linux-Pkg/pkginfo/kathara.changelog +++ b/scripts/Linux-Pkg/pkginfo/kathara.changelog @@ -1,6 +1,8 @@ __DATE__ Kathara Team <******@kathara.org> * Release v__VERSION__ - * Add support for the new Kathara Network Plugin based on VDE software switches. It is possible to select the old Network Plugin (based on Linux Bridges) from "kathara settings" - * Switch the default image to "kathara/base" for new installations - * Fix Docker images fetching in "kathara settings" \ No newline at end of file + * It is now possible to specify a MAC Address for a network interface + * (Docker) Collision domains are now created per-network-scenario by default + * (Docker) If a ".shutdown" file is present in the network scenario, Kathara now correctly waits for the script termination before removing the container + * Several fixes of "lconfig" and "vconfig" commands + * Minor fixes and improvements \ No newline at end of file diff --git a/scripts/Linux-Rpm/Docker-Linux-Build/Dockerfile b/scripts/Linux-Rpm/Docker-Linux-Build/Dockerfile index 20534255..3fae18c5 100644 --- a/scripts/Linux-Rpm/Docker-Linux-Build/Dockerfile +++ b/scripts/Linux-Rpm/Docker-Linux-Build/Dockerfile @@ -14,8 +14,8 @@ RUN dnf install -y rubygem-rails \ rpmdevtools \ patchelf \ pip \ - python3.10 \ - python3.10-devel + python3.11 \ + python3.11-devel RUN gem install ronn-ng diff --git a/scripts/Linux-Rpm/Makefile b/scripts/Linux-Rpm/Makefile index aeac70da..00690889 100644 --- a/scripts/Linux-Rpm/Makefile +++ b/scripts/Linux-Rpm/Makefile @@ -1,6 +1,6 @@ #!/usr/bin/make -f -VERSION=3.7.0 +VERSION=3.7.1 PACKAGE_VERSION=1 .PHONY: all clean docker-build-image prepare-source prepare-man-pages prepare-bash-completion pack-source build diff --git a/scripts/Linux-Rpm/rpm/kathara.spec b/scripts/Linux-Rpm/rpm/kathara.spec index 7262fd7a..31a7678c 100644 --- a/scripts/Linux-Rpm/rpm/kathara.spec +++ b/scripts/Linux-Rpm/rpm/kathara.spec @@ -8,7 +8,7 @@ URL: https://www.kathara.org/ Source: %{name}-%{version}.tar.gz %description -Lightweight network emulation system based on Docker containers. +A lightweight container-based network emulation tool. It can be really helpful in showing interactive demos/lessons, testing production networks in a sandbox environment, or developing @@ -18,15 +18,15 @@ new network protocols. %prep %autosetup -python3.10 -m venv %{_builddir}/venv +python3.11 -m venv %{_builddir}/venv %{_builddir}/venv/bin/pip install --upgrade pip %{_builddir}/venv/bin/pip install -r src/requirements.txt -%{_builddir}/venv/bin/pip install nuitka==1.7.10 +%{_builddir}/venv/bin/pip install nuitka %{_builddir}/venv/bin/pip install pytest %build %{_builddir}/venv/bin/python -m pytest -cd src && %{_builddir}/venv/bin/python -m nuitka --lto=no --plugin-enable=pylint-warnings --plugin-enable=multiprocessing --follow-imports --standalone --include-plugin-directory=Kathara --output-filename=kathara kathara.py +cd src && %{_builddir}/venv/bin/python -m nuitka --lto=yes --plugin-enable=pylint-warnings --plugin-enable=multiprocessing --follow-imports --standalone --include-plugin-directory=Kathara --output-filename=kathara kathara.py %install mv %{_builddir}/%{buildsubdir}/src/kathara.dist %{_builddir}/%{buildsubdir}/kathara.dist @@ -68,6 +68,8 @@ chmod g+s %{_libdir}/kathara/kathara %changelog * __DATE__ Kathara Team <******@kathara.org> - __VERSION__-__PACKAGE_VERSION__ -- Add support for the new Kathara Network Plugin based on VDE software switches. It is possible to select the old Network Plugin (based on Linux Bridges) from "kathara settings" -- Switch the default image to "kathara/base" for new installations -- Fix Docker images fetching in "kathara settings" \ No newline at end of file +- It is now possible to specify a MAC Address for a network interface +- (Docker) Collision domains are now created per-network-scenario by default +- (Docker) If a ".shutdown" file is present in the network scenario, Kathara now correctly waits for the script termination before removing the container +- Several fixes of "lconfig" and "vconfig" commands +- Minor fixes and improvements \ No newline at end of file diff --git a/scripts/OSX/Makefile b/scripts/OSX/Makefile index 2f363ffe..e0f26c9e 100644 --- a/scripts/OSX/Makefile +++ b/scripts/OSX/Makefile @@ -1,7 +1,7 @@ #!/usr/bin/make -s PRODUCT=Kathara -VERSION=3.7.0 +VERSION=3.7.1 TARGET_DIRECTORY=Output APPLE_DEVELOPER_CERTIFICATE_ID=FakeID ROFF_DIR=../../docs/Roff @@ -18,7 +18,7 @@ allSigned_arm64: deps binary_arm64 manpages createInstaller_arm64 signProduct_ar default: all py_env: - python3.10 -m venv $(VENV_DIR) + python3.11 -m venv $(VENV_DIR) deps: py_env $(VENV_DIR)/bin/pip install pyinstaller diff --git a/scripts/OSX/README.md b/scripts/OSX/README.md index 4979d3d4..3d5492b9 100644 --- a/scripts/OSX/README.md +++ b/scripts/OSX/README.md @@ -3,7 +3,7 @@ 1. Install `pkgbuild`, `productbuild` and `make` from Apple developer repository - If you have `XCode` installed you probably already have them installed - Otherwise, download them from [here](https://developer.apple.com/devcenter/mac/index.action) - - **NOTE:** You need Python 3.10 in order to compile the binary ([Quick link](https://www.python.org/downloads/release/python-3107/)) + - **NOTE:** You need Python 3.11 in order to compile the binary ([Quick link](https://www.python.org/downloads/release/python-3117/)) 2. Change the Kathara version number in both `src/Kathara/version.py` and `Makefile` files. 3. Run `make all_x86` or `make all_arm64` to automatically compile and create the package for the desired architecture. You can compile the `x86` package on a Mac with Intel CPU, and the `arm64` on a Mac with Apple CPU. @@ -19,7 +19,7 @@ You can compile the `x86` package on a Mac with Intel CPU, and the `arm64` on a 2. Install `pkgbuild`, `productbuild` and `make` from Apple developer repository - If you have `XCode` installed you probably already have them installed - Otherwise, download them from [here](https://developer.apple.com/devcenter/mac/index.action) - - **NOTE:** You need Python 3.10 in order to compile the binary ([Quick link](https://www.python.org/downloads/release/python-3107/)) + - **NOTE:** You need Python 3.11 in order to compile the binary ([Quick link](https://www.python.org/downloads/release/python-3117/)) 3. Change the Kathara version number in both `src/Kathara/version.py` and `Makefile` files. 4. Run `make allSigned_x86` or `make allSigned_arm64` to automatically compile and create the package for the desired architecture. You can compile the `x86` package on a Mac with Intel CPU, and the `arm64` on a Mac with Apple CPU. diff --git a/scripts/Windows/README.md b/scripts/Windows/README.md index e5520fbe..44e1d533 100644 --- a/scripts/Windows/README.md +++ b/scripts/Windows/README.md @@ -1,10 +1,8 @@ # Compiling Kathara for Windows 1. Download and Install Inno Setup ([Quick link](http://www.jrsoftware.org/download.php/is.exe)) -2. Download and Install Python 3.10 ([Quick link](https://www.python.org/downloads/release/python-3107/)) -3. Install pyinstaller `python -m pip install pyinstaller` (from an Administrator cmd or Powershell) -3. Install Kathara python dependencies `python -m pip install -r ..\..\src\requirements.txt` (from an Administrator cmd or Powershell) -4. Change the Kathara version number in both `src/Kathara/version.py` and `installer.iss` files. -5. Create binary package running `WindowsBuild.bat` -6. Compile Inno Script Setup file -7. Share the `Kathara-setup.exe` in output folder :) \ No newline at end of file +2. Download and Install Python 3.11 ([Quick link](https://www.python.org/downloads/release/python-3117/)) +3. Change the Kathara version number in both `src/Kathara/version.py` and `installer.iss` files. +4. Create binary package running `WindowsBuild.bat` +5. Compile Inno Script Setup file +6. Share the `Kathara-setup.exe` in output folder :) \ No newline at end of file diff --git a/scripts/Windows/WindowsBuild.bat b/scripts/Windows/WindowsBuild.bat index c249b1cb..5aaea2cd 100644 --- a/scripts/Windows/WindowsBuild.bat +++ b/scripts/Windows/WindowsBuild.bat @@ -2,7 +2,7 @@ set VENV_DIR=%cd%\venv rmdir /S /Q %VENV_DIR% -python3.10 -m venv %VENV_DIR% +python -m venv %VENV_DIR% if %errorlevel% neq 0 exit /b %errorlevel% CALL %VENV_DIR%\Scripts\activate if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/scripts/Windows/installer.iss b/scripts/Windows/installer.iss index 1134f70a..f74973e8 100644 --- a/scripts/Windows/installer.iss +++ b/scripts/Windows/installer.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Kathara" -#define MyAppVersion "3.7.0" +#define MyAppVersion "3.7.1" #define MyAppPublisher "Kathara Team" #define MyAppURL "https://www.kathara.org" #define MyAppExeName "kathara.exe" @@ -37,7 +37,7 @@ ArchitecturesInstallIn64BitMode=x64 UninstallDisplayIcon={app}\Kathara.exe [Messages] -WelcomeLabel2=Kathara is a lightweight network emulation system based on Docker containers.%n%nThis will install [name/ver] on your computer.%n%nYou will be guided through the steps necessary to install this software. +WelcomeLabel2=Kathara is a lightweight container-based network emulation tool.%n%nThis will install [name/ver] on your computer.%n%nYou will be guided through the steps necessary to install this software. [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" diff --git a/scripts/pip-package/README.md b/scripts/pip-package/README.md index d49292ca..10bd31c5 100644 --- a/scripts/pip-package/README.md +++ b/scripts/pip-package/README.md @@ -4,6 +4,7 @@ 1. `src/Kathara/version.py`. 2. `setup.py` (change `version` and `download_url`). 3. `setup.cfg` (change `version`). + 4. `pyproject.toml` (change `version`). 2. Run `make all`. This will: 1. Create a Kathara Python package. 2. Upload the packet on PyPI. diff --git a/setup.cfg b/setup.cfg index d54c032d..9bb2f565 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,9 @@ [metadata] name = kathara -version = 3.7.0 +version = 3.7.1 author = Kathara Framework author_email = contact@kathara.org -description = A lightweight container based emulation system +description = A lightweight container-based network emulation tool. long_description = file: README.md long_description_content_type = text/markdown url = www.kathara.org diff --git a/setup.py b/setup.py index bbd25549..c635d861 100644 --- a/setup.py +++ b/setup.py @@ -7,17 +7,17 @@ package_dir={'': 'src'}, packages=find_packages('src'), py_modules=['kathara'], - version='3.7.0', + version='3.7.1', license='gpl-3.0', - description='A lightweight container based emulation system.', + description='A lightweight container-based network emulation tool.', author='Kathara Framework', author_email='contact@kathara.org', url='https://www.kathara.org', - download_url='https://github.com/KatharaFramework/Kathara/archive/refs/tags/3.7.0.tar.gz', + download_url='https://github.com/KatharaFramework/Kathara/archive/refs/tags/3.7.1.tar.gz', keywords=['NETWORK-EMULATION', 'CONTAINERS', 'NFV'], install_requires=[ "binaryornot>=0.4.4", - "docker>=6.0.1", + "docker>=7.0.0", "kubernetes>=23.3.0", "requests>=2.22.0", "coloredlogs>=10.0", diff --git a/src/Kathara/version.py b/src/Kathara/version.py index 47dd420e..55c7761f 100644 --- a/src/Kathara/version.py +++ b/src/Kathara/version.py @@ -1,6 +1,6 @@ from typing import Tuple -CURRENT_VERSION = "3.7.0" +CURRENT_VERSION = "3.7.1" def parse(version: str) -> Tuple: From dc13454e93ae05b7c88e414de919278e6ae0e625 Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Mon, 8 Jan 2024 13:04:17 +0100 Subject: [PATCH 40/44] Adding `conf_name` to `LabParser.parse` method (#261) --- src/Kathara/parser/netkit/LabParser.py | 27 +++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Kathara/parser/netkit/LabParser.py b/src/Kathara/parser/netkit/LabParser.py index 48ca83e2..884e13b3 100644 --- a/src/Kathara/parser/netkit/LabParser.py +++ b/src/Kathara/parser/netkit/LabParser.py @@ -11,29 +11,30 @@ class LabParser(object): """Class responsible for parsing the lab.conf file.""" @staticmethod - def parse(path: str) -> Lab: - """Parse the lab.conf and return the corresponding Kathara network scenario. + def parse(path: str, conf_name: str = "lab.conf") -> Lab: + """Parse the lab configuration identified by conf_name and return the corresponding Kathara network scenario. Args: - path (str): The path to lab.conf file. + path (str): The path to the directory containing the configuration file. + conf_name (str): The name of the network scenario configuration file (default is 'lab.conf'). Returns: Kathara.model.Lab.Lab: A Kathara network scenario. """ - lab_conf_path = os.path.join(path, 'lab.conf') + lab_conf_path = os.path.join(path, conf_name) if not os.path.exists(lab_conf_path): - raise IOError("No lab.conf in given directory.") + raise IOError(f"No {conf_name} in given directory.") if os.stat(lab_conf_path).st_size == 0: - raise IOError("lab.conf file is empty.") + raise IOError(f"{conf_name} file is empty.") - # Reads lab.conf in memory so it is faster. + # Reads lab.conf in memory, so it is faster. try: with open(lab_conf_path, 'r') as lab_file: lab_mem_file = mmap.mmap(lab_file.fileno(), 0, access=mmap.ACCESS_READ) except Exception: - raise IOError("Cannot open lab.conf file.") + raise IOError(f"Cannot open {conf_name} file.") lab = Lab(None, path=path) @@ -51,7 +52,7 @@ def parse(path: str) -> Lab: value = matches.group("value").replace('"', '').replace("'", '') if key in RESERVED_MACHINE_NAMES: - raise ValueError(f"In lab.conf - Line {line_number}: " + raise ValueError(f"In {conf_name} - Line {line_number}: " f"`{key}` is a reserved name, you can not use it for a device.") try: @@ -61,25 +62,25 @@ def parse(path: str) -> Lab: try: cd_name, mac_address = parse_cd_mac_address(value) except SyntaxError as e: - raise SyntaxError(f"In lab.conf - Line {line_number}: {str(e)}") + raise SyntaxError(f"In {conf_name} - Line {line_number}: {str(e)}") if re.search(r"^\w+$", cd_name): lab.connect_machine_to_link(key, cd_name, machine_iface_number=interface_number, mac_address=mac_address) else: - raise SyntaxError(f"In lab.conf - Line {line_number}: " + raise SyntaxError(f"In {conf_name} - Line {line_number}: " f"Collision domain `{value}` contains non-alphanumeric characters.") except ValueError: # Not an interface, add it to the machine metas. if lab.assign_meta_to_machine(key, arg, value) is not None: - logging.warning(f"In lab.conf - Line {line_number}: " + logging.warning(f"In {conf_name} - Line {line_number}: " f"Device `{key}` already has a value assigned to meta `{arg}`. " f"Previous value has been overwritten with `{value}`.") else: if not line.startswith('#') and \ line.strip(): if not any([line.startswith(f"{x}=") for x in LAB_METADATA]): - raise SyntaxError(f"In lab.conf - Line {line_number}: `{line}`.") + raise SyntaxError(f"In {conf_name} - Line {line_number}: `{line}`.") else: (key, value) = line.split("=") key = key.replace("LAB_", "").lower() From d1fc800bd1f0fe624ed3c75cee2f262ac0436206 Mon Sep 17 00:00:00 2001 From: Tommaso Caiazzi Date: Mon, 8 Jan 2024 18:01:25 +0100 Subject: [PATCH 41/44] Update pip-package build --- scripts/pip-package/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/pip-package/Makefile b/scripts/pip-package/Makefile index 7df99560..c3b8d919 100644 --- a/scripts/pip-package/Makefile +++ b/scripts/pip-package/Makefile @@ -4,7 +4,7 @@ all: clean package upload package: clean - cd ../../ && python3 setup.py sdist --dist-dir='scripts/pip-package/dist' + cd ../../ && python3 -m build --outdir='scripts/pip-package/dist' upload: package python3 -m pip install twine From e75484eb6181d4d1dd0eabaf746196e9d3ee7693 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Mon, 8 Jan 2024 18:04:16 +0100 Subject: [PATCH 42/44] Update man + Fix OSX compilation --- docs/footer.txt | 2 +- docs/kathara-check.1.ronn | 5 +++-- docs/kathara-lab.conf.5.ronn | 2 +- docs/kathara-lconfig.1.ronn | 8 +++++--- docs/kathara-vconfig.1.ronn | 8 +++++--- docs/kathara-vstart.1.ronn | 10 +++++++--- scripts/OSX/Makefile | 2 +- scripts/OSX/README.md | 14 ++++++++------ 8 files changed, 31 insertions(+), 20 deletions(-) diff --git a/docs/footer.txt b/docs/footer.txt index d8c5ddb4..4b6aa8a7 100644 --- a/docs/footer.txt +++ b/docs/footer.txt @@ -18,5 +18,5 @@ People involved also include: ## COPYRIGHT -Copyright © 2017-2023 License GPLv3+: GNU GPL version 3 or later . +Copyright © 2017-2024 License GPLv3+: GNU GPL version 3 or later . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. diff --git a/docs/kathara-check.1.ronn b/docs/kathara-check.1.ronn index 4ee627b2..5601e4f1 100644 --- a/docs/kathara-check.1.ronn +++ b/docs/kathara-check.1.ronn @@ -23,10 +23,11 @@ Should give an output like this: * Current Manager is: `` * Manager version is: `` - * Trying to run `Hello World` container... - * Container run successfully. * Python version is: `` * Kathara version is: `` + * Operating System version is: `` + * Trying to run container with `` image... + * Container run successfully. **NOTE:** If you are using the released version, the Python version could be different from the one installed in your system because it is packed into the Kathara binary. diff --git a/docs/kathara-lab.conf.5.ronn b/docs/kathara-lab.conf.5.ronn index 5c3112af..015cdf72 100644 --- a/docs/kathara-lab.conf.5.ronn +++ b/docs/kathara-lab.conf.5.ronn @@ -16,7 +16,7 @@ If `arg` is an integer value, then `value` is the name of the collision domain t - `arg`: An integer value representing the interface number (e.g., 0). - `CD1`: The name of the collision domain to which the specified interface must be connected. Note that the name of the collision domain must not contain spaces (" "), commas (",") and dots ("."). -- `/MAC_ADDR`: An optional parameter to specify the MAC address of the interface of `device` (MAC address must be in the format `XX:XX:XX:XX:XX:XX`). If `MAC_ADDR` is not provided, Kathará will assign a random one. +- `/MAC_ADDR`: An optional parameter to specify the MAC address of the interface of `device` (MAC address must be in the format `XX:XX:XX:XX:XX:XX`). If `MAC_ADDR` is not provided, Kathara will assign a random one. ### EXAMPLES diff --git a/docs/kathara-lconfig.1.ronn b/docs/kathara-lconfig.1.ronn index bbe31744..d0d102ec 100644 --- a/docs/kathara-lconfig.1.ronn +++ b/docs/kathara-lconfig.1.ronn @@ -4,7 +4,7 @@ kathara-lconfig(1) -- Attach network interfaces to a running Kathara device of a ## SYNOPSIS -`kathara lconfig` [`-h`] [`-d` ] `-n` (`--add` [ ...] \| `--rm` [ ...]) +`kathara lconfig` [`-h`] [`-d` ] `-n` (`--add` [ ...] \| `--rm` [ ...]) ## DESCRIPTION @@ -26,8 +26,10 @@ Manage the network interfaces of a running Kathara device in a network scenario. * `--add` [ ...]: Specify the collision domain to be connected to the device: - + `CD`: The name of the collision domain to which the specified interface must be connected. Note that the name of the collision domain must not contain spaces (" "), commas (",") and dots ("."). - + `/MAC_ADDR`: An optional parameter to specify the MAC address of the interface of `pc1` (MAC address must be in the format `XX:XX:XX:XX:XX:XX`). If `MAC_ADDR` is not provided, Kathará will assign a random one. + + `CD`: The name of the collision domain to which the specified interface must be connected. Note that the name of the collision domain must not contain spaces (" "), commas (",") and dots ("."). + + `/MAC_ADDR`: An optional parameter to specify the MAC address of the interface (MAC address must be in the format `XX:XX:XX:XX:XX:XX`). If `MAC_ADDR` is not provided, Kathara will assign a random one. Equip the device with an additional network interface attached to a (virtual) collision domain whose name is . The number of the resulting network interface is generated incrementing the number of the last network interface used by the device. diff --git a/docs/kathara-vconfig.1.ronn b/docs/kathara-vconfig.1.ronn index 8c7d2612..142bf2f9 100644 --- a/docs/kathara-vconfig.1.ronn +++ b/docs/kathara-vconfig.1.ronn @@ -4,7 +4,7 @@ kathara-vconfig(1) -- Attach network interfaces to a running Kathara device ## SYNOPSIS -`kathara vconfig` [`-h`] `-n` (`--add` [ ...] \| `--rm` [ ...]) +`kathara vconfig` [`-h`] `-n` (`--add` [ ...] \| `--rm` [ ...]) ## DESCRIPTION @@ -20,8 +20,10 @@ Manage the network interfaces of a running Kathara device. The affected device i * `--add` [ ...]: Specify the collision domain to be connected to the device: - + `CD`: The name of the collision domain to which the specified interface must be connected. Note that the name of the collision domain must not contain spaces (" "), commas (",") and dots ("."). - + `/MAC_ADDR`: An optional parameter to specify the MAC address of the interface of `pc1` (MAC address must be in the format `XX:XX:XX:XX:XX:XX`). If `MAC_ADDR` is not provided, Kathará will assign a random one. + + `CD`: The name of the collision domain to which the specified interface must be connected. Note that the name of the collision domain must not contain spaces (" "), commas (",") and dots ("."). + + `/MAC_ADDR`: An optional parameter to specify the MAC address of the interface (MAC address must be in the format `XX:XX:XX:XX:XX:XX`). If `MAC_ADDR` is not provided, Kathara will assign a random one. Equip the device with an additional network interface attached to a (virtual) collision domain whose name is . The number of the resulting network interface is generated incrementing the number of the last network interface used by the device. diff --git a/docs/kathara-vstart.1.ronn b/docs/kathara-vstart.1.ronn index ed5566c1..7ac4a0e9 100644 --- a/docs/kathara-vstart.1.ronn +++ b/docs/kathara-vstart.1.ronn @@ -6,7 +6,7 @@ kathara-vstart(1) -- Start a new Kathara device `kathara vstart` `-n` [`-h`] [`--noterminals` | `--terminals` | `--privileged` | `--num_terms` ] -[`--eth` [ ...]] +[`--eth` [ ...]] [`-e` [ [ ...]]] [`--mem` ] [`--cpus` ] [`-i` ] [`--no-hosthome` \| `--hosthome`] [`--xterm` ] [`--print`] [`--bridged`] @@ -42,9 +42,13 @@ Notice: unless differently stated, command line arguments (DEVICE_NAME) and opti * `-n` , `--name` : Name of the device to be started. -* `--eth` [ ...]: +* `--eth` [ ...]: Set a specific interface on a collision domain. - + + `CD`: The name of the collision domain to which the specified interface must be connected. Note that the name of the collision domain must not contain spaces (" "), commas (",") and dots ("."). + + `/MAC_ADDR`: An optional parameter to specify the MAC address of the interface (MAC address must be in the format `XX:XX:XX:XX:XX:XX`). If `MAC_ADDR` is not provided, Kathara will assign a random one. + Equip the device with a network interface. `N` is a positive integer starting from 0. The network interface will be attached to a (virtual) collision domain whose name is `CD`. Attaching interfaces of different devices to the same collision domain allows them to exchange network traffic. should be declared as a sequential number, starting from 0; if any intermediate number is missing an exception is raised. diff --git a/scripts/OSX/Makefile b/scripts/OSX/Makefile index e0f26c9e..9e4618e8 100644 --- a/scripts/OSX/Makefile +++ b/scripts/OSX/Makefile @@ -24,7 +24,7 @@ deps: py_env $(VENV_DIR)/bin/pip install pyinstaller $(VENV_DIR)/bin/pip install -r ../../src/requirements.txt $(VENV_DIR)/bin/pip install pytest - gem install ronn-ng --user-install + /opt/homebrew/opt/ruby/bin/gem install ronn-ng --user-install manpages: cd ../../docs && make roff-build; diff --git a/scripts/OSX/README.md b/scripts/OSX/README.md index 3d5492b9..3c2d300f 100644 --- a/scripts/OSX/README.md +++ b/scripts/OSX/README.md @@ -4,14 +4,15 @@ - If you have `XCode` installed you probably already have them installed - Otherwise, download them from [here](https://developer.apple.com/devcenter/mac/index.action) - **NOTE:** You need Python 3.11 in order to compile the binary ([Quick link](https://www.python.org/downloads/release/python-3117/)) -2. Change the Kathara version number in both `src/Kathara/version.py` and `Makefile` files. -3. Run `make all_x86` or `make all_arm64` to automatically compile and create the package for the desired architecture. +2. Install Ruby from Homebrew: `brew install ruby` +3. Change the Kathara version number in both `src/Kathara/version.py` and `Makefile` files. +4. Run `make all_x86` or `make all_arm64` to automatically compile and create the package for the desired architecture. You can compile the `x86` package on a Mac with Intel CPU, and the `arm64` on a Mac with Apple CPU. - Run `make deps` to automatically download and install dependencies for your architecture - Run `make binary_x86_64` or `make binary_arm64` to automatically compile the package - Run `make createInstaller_x86_64` or `make createInstaller_arm64` to create package - Run `make clean` to clean all intermediate files -4. Share the Kathara pkg in `Output` folder :) +5. Share the Kathara pkg in `Output` folder :) # Compiling Kathara for Mac OSX (Signed) @@ -20,12 +21,13 @@ You can compile the `x86` package on a Mac with Intel CPU, and the `arm64` on a - If you have `XCode` installed you probably already have them installed - Otherwise, download them from [here](https://developer.apple.com/devcenter/mac/index.action) - **NOTE:** You need Python 3.11 in order to compile the binary ([Quick link](https://www.python.org/downloads/release/python-3117/)) -3. Change the Kathara version number in both `src/Kathara/version.py` and `Makefile` files. -4. Run `make allSigned_x86` or `make allSigned_arm64` to automatically compile and create the package for the desired architecture. +3. Install Ruby from Homebrew: `brew install ruby` +4. Change the Kathara version number in both `src/Kathara/version.py` and `Makefile` files. +5. Run `make allSigned_x86` or `make allSigned_arm64` to automatically compile and create the package for the desired architecture. You can compile the `x86` package on a Mac with Intel CPU, and the `arm64` on a Mac with Apple CPU. - Run `make deps` to automatically download and install dependencies for your architecture - Run `make binary_x86_64` or `make binary_arm64` to automatically compile the package - Run `make createInstaller_x86_64` or `make createInstaller_arm64` to create package - Run `make signProduct_x86_64` or `make signProduct_arm64` to sign the package - Run `make clean` to clean all intermediate files -5. Share the Kathara pkg signed in `Output` folder :) \ No newline at end of file +6. Share the Kathara pkg signed in `Output` folder :) \ No newline at end of file From 99615ce322858d6302b6aa7fc0da00c7afd85d0b Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Tue, 9 Jan 2024 12:09:18 +0100 Subject: [PATCH 43/44] Update Ruby to v3 in Debian build --- .../Docker-Linux-Build/Dockerfile_template | 18 ++++++++++++++++-- scripts/Linux-Deb/Makefile | 5 ----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template b/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template index 1515853d..31d0e3ce 100644 --- a/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template +++ b/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template @@ -21,10 +21,25 @@ RUN apt update && \ libffi-dev \ lintian \ patchelf \ - ruby-full \ software-properties-common \ zlib1g-dev +RUN apt install -y \ + curl \ + autoconf \ + bison \ + libssl-dev \ + libyaml-dev \ + libreadline6-dev \ + libncurses5-dev \ + libgdbm6 \ + libgdbm-dev \ + libdb-dev && \ + curl -fsSL https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-installer | bash && \ + /root/.rbenv/bin/rbenv install 3.3.0 && \ + /root/.rbenv/bin/rbenv global 3.3.0 + +ENV PATH="/root/.rbenv/shims:/root/.rbenv/bin:$PATH" RUN add-apt-repository ppa:deadsnakes/ppa \ && DEBIAN_FRONTEND=noninteractive apt install -y \ @@ -34,7 +49,6 @@ RUN add-apt-repository ppa:deadsnakes/ppa \ python3-pip \ python3.11-distutils -__NOKOGIRI__ RUN gem install ronn-ng RUN mkdir /root/.gnupg && chmod 700 /root/.gnupg diff --git a/scripts/Linux-Deb/Makefile b/scripts/Linux-Deb/Makefile index 4bd87fed..2ef93ca9 100644 --- a/scripts/Linux-Deb/Makefile +++ b/scripts/Linux-Deb/Makefile @@ -19,11 +19,6 @@ docker-signed_%: docker-build-image_% docker-build-image_%: cd Docker-Linux-Build && cp Dockerfile_template Dockerfile - if [ "$*" = "bionic" ]; then \ - cd Docker-Linux-Build && sed -i -e "s|__NOKOGIRI__|RUN gem install nokogiri -v 1.12.5|g" Dockerfile; \ - else \ - cd Docker-Linux-Build && sed -i -e "s|__NOKOGIRI__||g" Dockerfile; \ - fi; cd Docker-Linux-Build && sed -i -e "s|__DISTRO__|$*|g" Dockerfile cd Docker-Linux-Build && docker build -t kathara/linux-build-deb:$* . cd Docker-Linux-Build && rm Dockerfile From 2d82e4559e0ee48a1ada7fd0e53703fcdcd63abc Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Tue, 9 Jan 2024 15:49:32 +0100 Subject: [PATCH 44/44] Fix Ruby Homebrew path on OSX --- scripts/OSX/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/OSX/Makefile b/scripts/OSX/Makefile index 9e4618e8..ce5c0f38 100644 --- a/scripts/OSX/Makefile +++ b/scripts/OSX/Makefile @@ -24,7 +24,7 @@ deps: py_env $(VENV_DIR)/bin/pip install pyinstaller $(VENV_DIR)/bin/pip install -r ../../src/requirements.txt $(VENV_DIR)/bin/pip install pytest - /opt/homebrew/opt/ruby/bin/gem install ronn-ng --user-install + $$(brew --prefix ruby)/bin/gem install ronn-ng --user-install manpages: cd ../../docs && make roff-build;