Fix airspace and remaining port problems from refactor
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Any, ClassVar, Dict, List, Type
|
||||
from pydantic._internal._generics import PydanticGenericMetadata
|
||||
from typing_extensions import Unpack
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from prettytable import MARKDOWN, PrettyTable
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, Field, validate_call
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.simulator.network.hardware.base import Layer3Interface, NetworkInterface, WiredNetworkInterface
|
||||
@@ -42,29 +40,31 @@ def format_hertz(hertz: float, format_terahertz: bool = False, decimals: int = 3
|
||||
else: # Hertz
|
||||
return format_str.format(hertz) + " Hz"
|
||||
|
||||
AirSpaceFrequencyRegistry: Dict[str,Dict] = {
|
||||
"WIFI_2_4" : {'frequency': 2.4e9, 'data_rate_bps':100_000_000.0},
|
||||
"WIFI_5" : {'frequency': 5e9, 'data_rate_bps':500_000_000.0},
|
||||
|
||||
_default_frequency_set: Dict[str, Dict] = {
|
||||
"WIFI_2_4": {"frequency": 2.4e9, "data_rate_bps": 100_000_000.0},
|
||||
"WIFI_5": {"frequency": 5e9, "data_rate_bps": 500_000_000.0},
|
||||
}
|
||||
"""Frequency configuration that is automatically used for any new airspace."""
|
||||
|
||||
def register_frequency(freq_name: str, freq_hz: int, data_rate_bps: int) -> None:
|
||||
if freq_name in AirSpaceFrequencyRegistry:
|
||||
raise RuntimeError(f"Cannot register new frequency {freq_name} because it's already registered.")
|
||||
AirSpaceFrequencyRegistry.update({freq_name:{'frequency': freq_hz, 'data_rate_bps':data_rate_bps}})
|
||||
|
||||
def maximum_data_rate_mbps(frequency_name:str) -> float:
|
||||
def register_default_frequency(freq_name: str, freq_hz: float, data_rate_bps: float):
|
||||
"""Add to the default frequency configuration. This is intended as a plugin hook.
|
||||
|
||||
If your plugin makes use of bespoke frequencies for wireless communication, you should make a call to this method
|
||||
whereever you define components that rely on the bespoke frequencies. That way, as soon as your components are
|
||||
imported, this function automatically updates the default frequency set.
|
||||
|
||||
This should also be run before instances of AirSpace are created.
|
||||
|
||||
:param freq_name: The frequency name. If this clashes with an existing frequency name, it will be overwritten.
|
||||
:type freq_name: str
|
||||
:param freq_hz: The frequency itself, measured in Hertz.
|
||||
:type freq_hz: float
|
||||
:param data_rate_bps: The transmission capacity over this frequency, in bits per second.
|
||||
:type data_rate_bps: float
|
||||
"""
|
||||
Retrieves the maximum data transmission rate in megabits per second (Mbps).
|
||||
|
||||
This is derived by converting the maximum data rate from bits per second, as defined
|
||||
in `maximum_data_rate_bps`, to megabits per second.
|
||||
|
||||
:return: The maximum data rate in megabits per second.
|
||||
"""
|
||||
return AirSpaceFrequencyRegistry[frequency_name]['data_rate_bps']
|
||||
return data_rate / 1_000_000.0
|
||||
|
||||
|
||||
_default_frequency_set.update({freq_name: {"frequency": freq_hz, "data_rate_bps": data_rate_bps}})
|
||||
|
||||
|
||||
class AirSpace(BaseModel):
|
||||
@@ -77,27 +77,21 @@ class AirSpace(BaseModel):
|
||||
"""
|
||||
|
||||
wireless_interfaces: Dict[str, WirelessNetworkInterface] = Field(default_factory=lambda: {})
|
||||
wireless_interfaces_by_frequency: Dict[int, List[WirelessNetworkInterface]] = Field(
|
||||
default_factory=lambda: {}
|
||||
)
|
||||
wireless_interfaces_by_frequency: Dict[int, List[WirelessNetworkInterface]] = Field(default_factory=lambda: {})
|
||||
bandwidth_load: Dict[int, float] = Field(default_factory=lambda: {})
|
||||
frequency_max_capacity_mbps_: Dict[int, float] = Field(default_factory=lambda: {})
|
||||
frequencies: Dict[str, Dict] = Field(default_factory=lambda: copy.deepcopy(_default_frequency_set))
|
||||
|
||||
def get_frequency_max_capacity_mbps(self, frequency: str) -> float:
|
||||
@validate_call
|
||||
def get_frequency_max_capacity_mbps(self, freq_name: str) -> float:
|
||||
"""
|
||||
Retrieves the maximum data transmission capacity for a specified frequency.
|
||||
|
||||
This method checks a dictionary holding custom maximum capacities. If the frequency is found, it returns the
|
||||
custom set maximum capacity. If the frequency is not found in the dictionary, it defaults to the standard
|
||||
maximum data rate associated with that frequency.
|
||||
|
||||
:param frequency: The frequency for which the maximum capacity is queried.
|
||||
|
||||
:param freq_name: The frequency for which the maximum capacity is queried.
|
||||
:return: The maximum capacity in Mbps for the specified frequency.
|
||||
"""
|
||||
if frequency in self.frequency_max_capacity_mbps_:
|
||||
return self.frequency_max_capacity_mbps_[frequency]
|
||||
return maximum_data_rate_mbps(frequency)
|
||||
if freq_name in self.frequencies:
|
||||
return self.frequencies[freq_name]["data_rate_bps"] / (1024.0 * 1024.0)
|
||||
return 0.0
|
||||
|
||||
def set_frequency_max_capacity_mbps(self, cfg: Dict[int, float]):
|
||||
"""
|
||||
@@ -105,10 +99,29 @@ class AirSpace(BaseModel):
|
||||
|
||||
:param cfg: A dictionary mapping frequencies to their new maximum capacities in Mbps.
|
||||
"""
|
||||
self.frequency_max_capacity_mbps_ = cfg
|
||||
for freq, mbps in cfg.items():
|
||||
self.frequencies[freq]["data_rate_bps"] = mbps * 1024 * 1024
|
||||
print(f"Overriding {freq} max capacity as {mbps:.3f} mbps")
|
||||
|
||||
def register_frequency(self, freq_name: str, freq_hz: float, data_rate_bps: float) -> None:
|
||||
"""
|
||||
Define a new frequency for this airspace.
|
||||
|
||||
:param freq_name: The frequency name. If this clashes with an existing frequency name, it will be overwritten.
|
||||
:type freq_name: str
|
||||
:param freq_hz: The frequency itself, measured in Hertz.
|
||||
:type freq_hz: float
|
||||
:param data_rate_bps: The transmission capacity over this frequency, in bits per second.
|
||||
:type data_rate_bps: float
|
||||
"""
|
||||
if freq_name in self.frequencies:
|
||||
_LOGGER.info(
|
||||
f"Overwriting Air space frequency {freq_name}. "
|
||||
f"Previous data rate: {self.frequencies[freq_name]['data_rate_bps']}. "
|
||||
f"Current data rate: {data_rate_bps}."
|
||||
)
|
||||
self.frequencies.update({freq_name: {"frequency": freq_hz, "data_rate_bps": data_rate_bps}})
|
||||
|
||||
def show_bandwidth_load(self, markdown: bool = False):
|
||||
"""
|
||||
Prints a table of the current bandwidth load for each frequency on the airspace.
|
||||
@@ -130,7 +143,13 @@ class AirSpace(BaseModel):
|
||||
load_percent = load / maximum_capacity if maximum_capacity > 0 else 0.0
|
||||
if load_percent > 1.0:
|
||||
load_percent = 1.0
|
||||
table.add_row([format_hertz(frequency), f"{load_percent:.0%}", f"{maximum_capacity:.3f}"])
|
||||
table.add_row(
|
||||
[
|
||||
format_hertz(self.frequencies[frequency]["frequency"]),
|
||||
f"{load_percent:.0%}",
|
||||
f"{maximum_capacity:.3f}",
|
||||
]
|
||||
)
|
||||
print(table)
|
||||
|
||||
def show_wireless_interfaces(self, markdown: bool = False):
|
||||
@@ -162,7 +181,7 @@ class AirSpace(BaseModel):
|
||||
interface.mac_address,
|
||||
interface.ip_address if hasattr(interface, "ip_address") else None,
|
||||
interface.subnet_mask if hasattr(interface, "subnet_mask") else None,
|
||||
format_hertz(interface.frequency),
|
||||
format_hertz(self.frequencies[interface.frequency]["frequency"]),
|
||||
f"{interface.speed:.3f}",
|
||||
status,
|
||||
]
|
||||
|
||||
@@ -130,15 +130,15 @@ class Network(SimComponent):
|
||||
def firewall_nodes(self) -> List[Node]:
|
||||
"""The Firewalls in the Network."""
|
||||
return [node for node in self.nodes.values() if node.__class__.__name__ == "Firewall"]
|
||||
|
||||
|
||||
@property
|
||||
def extended_hostnodes(self) -> List[Node]:
|
||||
"""Extended nodes that inherited HostNode in the network"""
|
||||
"""Extended nodes that inherited HostNode in the network."""
|
||||
return [node for node in self.nodes.values() if node.__class__.__name__.lower() in HostNode._registry]
|
||||
|
||||
|
||||
@property
|
||||
def extended_networknodes(self) -> List[Node]:
|
||||
"""Extended nodes that inherited NetworkNode in the network"""
|
||||
"""Extended nodes that inherited NetworkNode in the network."""
|
||||
return [node for node in self.nodes.values() if node.__class__.__name__.lower() in NetworkNode._registry]
|
||||
|
||||
@property
|
||||
|
||||
@@ -332,7 +332,7 @@ class HostNode(Node):
|
||||
super().__init__(**kwargs)
|
||||
self.connect_nic(NIC(ip_address=ip_address, subnet_mask=subnet_mask))
|
||||
|
||||
def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None:
|
||||
def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None:
|
||||
"""
|
||||
Register a hostnode type.
|
||||
|
||||
@@ -340,7 +340,7 @@ class HostNode(Node):
|
||||
:type identifier: str
|
||||
:raises ValueError: When attempting to register an hostnode with a name that is already allocated.
|
||||
"""
|
||||
if identifier == 'default':
|
||||
if identifier == "default":
|
||||
return
|
||||
# Enforce lowercase registry entries because it makes comparisons everywhere else much easier.
|
||||
identifier = identifier.lower()
|
||||
|
||||
@@ -19,7 +19,7 @@ class NetworkNode(Node):
|
||||
_registry: ClassVar[Dict[str, Type["NetworkNode"]]] = {}
|
||||
"""Registry of application types. Automatically populated when subclasses are defined."""
|
||||
|
||||
def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None:
|
||||
def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None:
|
||||
"""
|
||||
Register a networknode type.
|
||||
|
||||
@@ -27,7 +27,7 @@ class NetworkNode(Node):
|
||||
:type identifier: str
|
||||
:raises ValueError: When attempting to register an networknode with a name that is already allocated.
|
||||
"""
|
||||
if identifier == 'default':
|
||||
if identifier == "default":
|
||||
return
|
||||
identifier = identifier.lower()
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
@@ -130,19 +130,20 @@ class ACLRule(SimComponent):
|
||||
dst_port: Optional[int] = None
|
||||
match_count: int = 0
|
||||
|
||||
@field_validator('protocol', mode='before')
|
||||
def protocol_valid(cls, val:Optional[str]) -> Optional[str]:
|
||||
@field_validator("protocol", mode="before")
|
||||
def protocol_valid(cls, val: Optional[str]) -> Optional[str]:
|
||||
"""Assert that the protocol for the rule is predefined in the IPProtocol lookup."""
|
||||
if val is not None:
|
||||
assert val in IPProtocol.values(), f"Cannot create ACL rule with invalid protocol {val}"
|
||||
return val
|
||||
|
||||
@field_validator('src_port', 'dst_port', mode='before')
|
||||
def ports_valid(cls, val:Optional[int]) -> Optional[int]:
|
||||
@field_validator("src_port", "dst_port", mode="before")
|
||||
def ports_valid(cls, val: Optional[int]) -> Optional[int]:
|
||||
"""Assert that the port for the rule is predefined in the Port lookup."""
|
||||
if val is not None:
|
||||
assert val in Port.values(), f"Cannot create ACL rule with invalid port {val}"
|
||||
return val
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
rule_strings = []
|
||||
for key, value in self.model_dump(exclude={"uuid", "request_manager"}).items():
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any, Dict, Optional, Union
|
||||
|
||||
from pydantic import validate_call
|
||||
|
||||
from primaite.simulator.network.airspace import AirSpace, AirSpaceFrequency, IPWirelessNetworkInterface
|
||||
from primaite.simulator.network.airspace import AirSpace, IPWirelessNetworkInterface
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router, RouterInterface
|
||||
from primaite.simulator.network.transmission.data_link_layer import Frame
|
||||
@@ -116,7 +116,7 @@ class WirelessRouter(Router):
|
||||
>>> wireless_router.configure_wireless_access_point(
|
||||
... ip_address="10.10.10.1",
|
||||
... subnet_mask="255.255.255.0"
|
||||
... frequency=AirSpaceFrequency["WIFI_2_4"]
|
||||
... frequency="WIFI_2_4"
|
||||
... )
|
||||
"""
|
||||
|
||||
@@ -153,7 +153,7 @@ class WirelessRouter(Router):
|
||||
self,
|
||||
ip_address: IPV4Address,
|
||||
subnet_mask: IPV4Address,
|
||||
frequency: Optional[int] = AirSpaceFrequency["WIFI_2_4"],
|
||||
frequency: Optional[str] = "WIFI_2_4",
|
||||
):
|
||||
"""
|
||||
Configures a wireless access point (WAP).
|
||||
@@ -166,12 +166,12 @@ class WirelessRouter(Router):
|
||||
|
||||
:param ip_address: The IP address to be assigned to the wireless access point.
|
||||
:param subnet_mask: The subnet mask associated with the IP address
|
||||
:param frequency: The operating frequency of the wireless access point, defined by the AirSpaceFrequency
|
||||
:param frequency: The operating frequency of the wireless access point, defined by the air space frequency
|
||||
enum. This determines the frequency band (e.g., 2.4 GHz or 5 GHz) the access point will use for wireless
|
||||
communication. Default is AirSpaceFrequency["WIFI_2_4"].
|
||||
communication. Default is "WIFI_2_4".
|
||||
"""
|
||||
if not frequency:
|
||||
frequency = AirSpaceFrequency["WIFI_2_4"]
|
||||
frequency = "WIFI_2_4"
|
||||
self.sys_log.info("Configuring wireless access point")
|
||||
|
||||
self.wireless_access_point.disable() # Temporarily disable the WAP for reconfiguration
|
||||
@@ -264,7 +264,7 @@ class WirelessRouter(Router):
|
||||
if "wireless_access_point" in cfg:
|
||||
ip_address = cfg["wireless_access_point"]["ip_address"]
|
||||
subnet_mask = cfg["wireless_access_point"]["subnet_mask"]
|
||||
frequency = AirSpaceFrequency[cfg["wireless_access_point"]["frequency"]]
|
||||
frequency = cfg["wireless_access_point"]["frequency"]
|
||||
router.configure_wireless_access_point(ip_address=ip_address, subnet_mask=subnet_mask, frequency=frequency)
|
||||
|
||||
if "acl" in cfg:
|
||||
|
||||
@@ -9,11 +9,11 @@ from primaite.utils.validators import IPV4Address
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
IPProtocol : dict[str, str] = dict(
|
||||
NONE = "none",
|
||||
TCP = "tcp",
|
||||
UDP = "udp",
|
||||
ICMP = "icmp",
|
||||
IPProtocol: dict[str, str] = dict(
|
||||
NONE="none",
|
||||
TCP="tcp",
|
||||
UDP="udp",
|
||||
ICMP="icmp",
|
||||
)
|
||||
|
||||
# class IPProtocol(Enum):
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
from enum import Enum
|
||||
from typing import List, Union
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
Port: dict[str, int] = dict(
|
||||
UNUSED = -1,
|
||||
NONE = 0,
|
||||
WOL = 9,
|
||||
FTP_DATA = 20,
|
||||
FTP = 21,
|
||||
SSH = 22,
|
||||
SMTP = 25,
|
||||
DNS = 53,
|
||||
HTTP = 80,
|
||||
POP3 = 110,
|
||||
SFTP = 115,
|
||||
NTP = 123,
|
||||
IMAP = 143,
|
||||
SNMP = 161,
|
||||
SNMP_TRAP = 162,
|
||||
ARP = 219,
|
||||
LDAP = 389,
|
||||
HTTPS = 443,
|
||||
SMB = 445,
|
||||
IPP = 631,
|
||||
SQL_SERVER = 1433,
|
||||
MYSQL = 3306,
|
||||
RDP = 3389,
|
||||
RTP = 5004,
|
||||
RTP_ALT = 5005,
|
||||
DNS_ALT = 5353,
|
||||
HTTP_ALT = 8080,
|
||||
HTTPS_ALT = 8443,
|
||||
POSTGRES_SERVER = 5432,
|
||||
UNUSED=-1,
|
||||
NONE=0,
|
||||
WOL=9,
|
||||
FTP_DATA=20,
|
||||
FTP=21,
|
||||
SSH=22,
|
||||
SMTP=25,
|
||||
DNS=53,
|
||||
HTTP=80,
|
||||
POP3=110,
|
||||
SFTP=115,
|
||||
NTP=123,
|
||||
IMAP=143,
|
||||
SNMP=161,
|
||||
SNMP_TRAP=162,
|
||||
ARP=219,
|
||||
LDAP=389,
|
||||
HTTPS=443,
|
||||
SMB=445,
|
||||
IPP=631,
|
||||
SQL_SERVER=1433,
|
||||
MYSQL=3306,
|
||||
RDP=3389,
|
||||
RTP=5004,
|
||||
RTP_ALT=5005,
|
||||
DNS_ALT=5353,
|
||||
HTTP_ALT=8080,
|
||||
HTTPS_ALT=8443,
|
||||
POSTGRES_SERVER=5432,
|
||||
)
|
||||
|
||||
# class Port():
|
||||
|
||||
@@ -44,7 +44,7 @@ class Application(IOSoftware):
|
||||
_registry: ClassVar[Dict[str, Type["Application"]]] = {}
|
||||
"""Registry of application types. Automatically populated when subclasses are defined."""
|
||||
|
||||
def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None:
|
||||
def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None:
|
||||
"""
|
||||
Register an application type.
|
||||
|
||||
@@ -52,7 +52,7 @@ class Application(IOSoftware):
|
||||
:type identifier: str
|
||||
:raises ValueError: When attempting to register an application with a name that is already allocated.
|
||||
"""
|
||||
if identifier == 'default':
|
||||
if identifier == "default":
|
||||
return
|
||||
super().__init_subclass__(**kwargs)
|
||||
if identifier in cls._registry:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
from enum import Enum
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
@@ -76,9 +76,7 @@ class SessionManager:
|
||||
"""
|
||||
|
||||
def __init__(self, sys_log: SysLog):
|
||||
self.sessions_by_key: Dict[
|
||||
Tuple[str, IPv4Address, IPv4Address, Optional[int], Optional[int]], Session
|
||||
] = {}
|
||||
self.sessions_by_key: Dict[Tuple[str, IPv4Address, IPv4Address, Optional[int], Optional[int]], Session] = {}
|
||||
self.sessions_by_uuid: Dict[str, Session] = {}
|
||||
self.sys_log: SysLog = sys_log
|
||||
self.software_manager: SoftwareManager = None # Noqa
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Dict, Optional
|
||||
|
||||
from primaite.simulator.file_system.file_system import File
|
||||
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.services.service import Service
|
||||
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class Service(IOSoftware):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None:
|
||||
def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None:
|
||||
"""
|
||||
Register a hostnode type.
|
||||
|
||||
@@ -60,7 +60,7 @@ class Service(IOSoftware):
|
||||
:type identifier: str
|
||||
:raises ValueError: When attempting to register an hostnode with a name that is already allocated.
|
||||
"""
|
||||
if identifier == 'default':
|
||||
if identifier == "default":
|
||||
return
|
||||
# Enforce lowercase registry entries because it makes comparisons everywhere else much easier.
|
||||
identifier = identifier.lower()
|
||||
|
||||
@@ -14,7 +14,6 @@ from primaite.simulator.core import RequestManager, RequestType, SimComponent
|
||||
from primaite.simulator.file_system.file_system import FileSystem, Folder
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.core.session_manager import Session
|
||||
from primaite.simulator.system.core.sys_log import SysLog
|
||||
|
||||
|
||||
Reference in New Issue
Block a user