Skip to content

Commit

Permalink
Add parameter to specify MAC Address (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
Skazza94 committed Dec 18, 2023
1 parent 4dbaa9f commit c6d4432
Show file tree
Hide file tree
Showing 22 changed files with 296 additions and 179 deletions.
24 changes: 14 additions & 10 deletions src/Kathara/cli/command/VconfigCommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -37,7 +38,7 @@ def __init__(self) -> None:

group.add_argument(
'--add',
type=alphanumeric,
type=str,
dest='to_add',
metavar='CD',
nargs='+',
Expand All @@ -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)
8 changes: 8 additions & 0 deletions src/Kathara/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/Kathara/foundation/manager/IManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/Kathara/manager/Kathara.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
57 changes: 35 additions & 22 deletions src/Kathara/manager/docker/DockerMachine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down
15 changes: 8 additions & 7 deletions src/Kathara/manager/docker/DockerManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"]),
Expand Down
4 changes: 2 additions & 2 deletions src/Kathara/manager/kubernetes/KubernetesMachine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
7 changes: 4 additions & 3 deletions src/Kathara/manager/kubernetes/KubernetesManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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})
Expand Down
34 changes: 34 additions & 0 deletions src/Kathara/model/Interface.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit c6d4432

Please sign in to comment.