diff --git a/tests/unit_tests/primaite/__init__.py b/src/primaite/simulator/network/__init__.py similarity index 100% rename from tests/unit_tests/primaite/__init__.py rename to src/primaite/simulator/network/__init__.py diff --git a/src/primaite/simulator/network/physical_layer.py b/src/primaite/simulator/network/physical_layer.py new file mode 100644 index 00000000..bb4b120e --- /dev/null +++ b/src/primaite/simulator/network/physical_layer.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +import re +import secrets +from ipaddress import IPv4Address +from typing import Dict, List, Optional + +from primaite.simulator.core import SimComponent + + +def generate_mac_address(oui: Optional[str] = None) -> str: + """ + Generate a random MAC Address.. + + :Example: + + >>> generate_mac_address() + 'ef:7e:97:c8:a8:ce' + + >>> generate_mac_address(oui='aa:bb:cc') + 'aa:bb:cc:42:ba:41' + + :param oui: The Organizationally Unique Identifier (OUI) portion of the MAC address. It should be a string with + the first 3 bytes (24 bits) in the format "XX:XX:XX". + :raises ValueError: If the 'oui' is not in the correct format (hexadecimal and 6 characters). + """ + random_bytes = [secrets.randbits(8) for _ in range(6)] + + if oui: + oui_pattern = re.compile(r"^([0-9A-Fa-f]{2}[:-]){2}[0-9A-Fa-f]{2}$") + if not oui_pattern.match(oui): + raise ValueError( + f"Invalid oui. The oui should be in the format 'xx:xx:xx', where x is a hexadecimal digit, got '{oui}'." + ) + oui_bytes = [int(chunk, 16) for chunk in oui.split(":")] + mac = oui_bytes + random_bytes[len(oui_bytes) :] + else: + mac = random_bytes + + return ":".join(f"{b:02x}" for b in mac) + + +class Link(SimComponent): + """ + Represents a network link between two network interface cards (NICs). + + :param endpoint_a: The first NIC connected to the Link. + :type endpoint_a: NIC + :param endpoint_b: The second NIC connected to the Link. + :type endpoint_b: NIC + :param bandwidth: The bandwidth of the Link in Mbps (default is 100 Mbps). + :type bandwidth: int + """ + + endpoint_a: NIC + endpoint_b: NIC + bandwidth: int + current_load: int = 0 + + def __init__(self, endpoint_a: NIC, endpoint_b: NIC, bandwidth: int = 100): + """ + Initialize the Link instance. + + When a Link is created, it automatically connects the endpoints to itself. + + :param endpoint_a: The first NIC connected to the link. + :type endpoint_a: NIC + :param endpoint_b: The second NIC connected to the link. + :type endpoint_b: NIC + :param bandwidth: The bandwidth of the link in Mbps (default is 100 Mbps). + :type bandwidth: int + :raise ValueError: If endpoint_a equals endpoint_b. + """ + super().__init__(endpoint_a=endpoint_a, endpoint_b=endpoint_b, bandwidth=bandwidth) + if self.endpoint_a == self.endpoint_b: + raise ValueError("endpoint_a and endpoint_b cannot be the same NIC") + + def send_frame(self, sender_nic: NIC, frame): + """ + Send a network frame from one NIC to another connected NIC. + + :param sender_nic: The NIC sending the frame. + :type sender_nic: NIC + :param frame: The network frame to be sent. + :type frame: Frame + """ + pass + + def receive_frame(self, sender_nic: NIC, frame): + """ + Receive a network frame from a connected NIC. + + :param sender_nic: The NIC sending the frame. + :type sender_nic: NIC + :param frame: The network frame being received. + :type frame: Frame + """ + pass + + def describe_state(self) -> Dict: + """ + Get the current state of the Libk as a dict. + + :return: A dict containing the current state of the Link. + """ + pass + + def apply_action(self, action: str): + """ + Apply an action to the Link. + + :param action: The action to be applied. + :type action: str + """ + pass + + +class NIC(SimComponent): + """ + Models a Network Interface Card (NIC) in a computer or network device. + + :param ip_address: The IPv4 address assigned to the NIC. + :param subnet_mask: The subnet mask assigned to the NIC. + :param gateway: The default gateway IP address for forwarding network traffic to other networks. + :param mac_address: The MAC address of the NIC. Defaults to a randomly set MAC address. + :param speed: The speed of the NIC in Mbps. + :param mtu: The Maximum Transmission Unit (MTU) of the NIC in Bytes, representing the largest data packet size it + can handle without fragmentation. + :param wake_on_lan: Indicates if the NIC supports Wake-on-LAN functionality. + :param dns_servers: List of IP addresses of DNS servers used for name resolution. + :param connected_link: The link to which the NIC is connected (default is None). + :param enabled: Indicates whether the NIC is enabled. + """ + + ip_address: IPv4Address + "The IP address assigned to the NIC for communication on an IP-based network." + subnet_mask: str + "The subnet mask assigned to the NIC." + gateway: IPv4Address + "The default gateway IP address for forwarding network traffic to other networks. Randomly generated upon creation." + mac_address: str = generate_mac_address() + "The MAC address of the NIC. Defaults to a randomly set MAC address." + speed: Optional[int] = 100 + "The speed of the NIC in Mbps. Default is 100 Mbps." + mtu: Optional[int] = 1500 + "The Maximum Transmission Unit (MTU) of the NIC in Bytes. Default is 1500 B" + wake_on_lan: Optional[bool] = False + "Indicates if the NIC supports Wake-on-LAN functionality." + dns_servers: List[IPv4Address] = [] + "List of IP addresses of DNS servers used for name resolution." + connected_link: Optional[Link] = None + "The Link to which the NIC is connected." + enabled: bool = False + "Indicates whether the NIC is enabled." + + def connect_link(self, link: Link): + """ + Connect the NIC to a link. + + :param link: The link to which the NIC is connected. + :type link: :class:`~primaite.simulator.network.physical_layer.Link` + """ + pass + + def disconnect_link(self): + """Disconnect the NIC from the connected :class:`~primaite.simulator.network.physical_layer.Link`.""" + pass + + def add_dns_server(self, ip_address: IPv4Address): + """ + Add a DNS server IP address. + + :param ip_address: The IP address of the DNS server to be added. + :type ip_address: ipaddress.IPv4Address + """ + pass + + def remove_dns_server(self, ip_address: IPv4Address): + """ + Remove a DNS server IP Address. + + :param ip_address: The IP address of the DNS server to be removed. + :type ip_address: ipaddress.IPv4Address + """ + pass + + def send_frame(self, frame): + """ + Send a network frame from the NIC to the connected link. + + :param frame: The network frame to be sent. + :type frame: :class:`~primaite.simulator.network.osi_layers.Frame` + """ + pass + + def receive_frame(self, frame): + """ + Receive a network frame from the connected link. + + The Frame is passed to the Node. + + :param frame: The network frame being received. + :type frame: :class:`~primaite.simulator.network.osi_layers.Frame` + """ + pass + + def describe_state(self) -> Dict: + """ + Get the current state of the NIC as a dict. + + :return: A dict containing the current state of the NIC. + """ + pass + + def apply_action(self, action: str): + """ + Apply an action to the NIC. + + :param action: The action to be applied. + :type action: str + """ + pass diff --git a/tests/unit_tests/primaite/simulator/__init__.py b/tests/unit_tests/_primaite/__init__.py similarity index 100% rename from tests/unit_tests/primaite/simulator/__init__.py rename to tests/unit_tests/_primaite/__init__.py diff --git a/tests/unit_tests/_primaite/_simulator/__init__.py b/tests/unit_tests/_primaite/_simulator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/_primaite/_simulator/_network/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_physical_layer.py b/tests/unit_tests/_primaite/_simulator/_network/test_physical_layer.py new file mode 100644 index 00000000..137e2cd6 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_network/test_physical_layer.py @@ -0,0 +1,26 @@ +import re + +import pytest + +from primaite.simulator.network.physical_layer import generate_mac_address + + +def test_mac_address_generation(): + """Tests random mac address generation.""" + mac_address = generate_mac_address() + assert re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", mac_address) + + +def test_mac_address_with_oui(): + """Tests random mac address generation with oui.""" + oui = "aa:bb:cc" + mac_address = generate_mac_address(oui=oui) + assert re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", mac_address) + assert mac_address[:8] == oui + + +def test_invalid_oui_mac_address(): + """Tests random mac address generation fails with invalid oui.""" + invalid_oui = "aa-bb-cc" + with pytest.raises(ValueError): + generate_mac_address(oui=invalid_oui) diff --git a/tests/unit_tests/primaite/simulator/test_core.py b/tests/unit_tests/_primaite/_simulator/test_core.py similarity index 100% rename from tests/unit_tests/primaite/simulator/test_core.py rename to tests/unit_tests/_primaite/_simulator/test_core.py