diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index bddc1f28..72ca1b51 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -499,9 +499,13 @@ class Folder(FileSystemItemABC): def apply_timestep(self, timestep: int): """ - Used to run the actions that last over multiple timesteps. + Apply a single timestep of simulation dynamics to this service. - :param: timestep: the current timestep. + In this instance, if any multi-timestep processes are currently occurring (such as scanning), + then they are brought one step closer to being finished. + + :param timestep: The current timestep number. (Amount of time since simulation episode began) + :type timestep: int """ super().apply_timestep(timestep=timestep) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 7b8e44ff..ed7719d7 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -914,6 +914,18 @@ class Node(SimComponent): revealed_to_red: bool = False "Informs whether the node has been revealed to a red agent." + start_up_duration: int = 3 + "Time steps needed for the node to start up." + + start_up_countdown: int = -1 + "Time steps needed until node is booted up." + + shut_down_duration: int = 3 + "Time steps needed for the node to shut down." + + shut_down_countdown: int = -1 + "Time steps needed until node is shut down." + def __init__(self, **kwargs): """ Initialize the Node with various components and managers. @@ -1042,22 +1054,62 @@ class Node(SimComponent): ) print(table) + def apply_timestep(self, timestep: int): + """ + Apply a single timestep of simulation dynamics to this service. + + In this instance, if any multi-timestep processes are currently occurring + (such as starting up or shutting down), then they are brought one step closer to + being finished. + + :param timestep: The current timestep number. (Amount of time since simulation episode began) + :type timestep: int + """ + super().apply_timestep(timestep=timestep) + + # count down to boot up + if self.start_up_countdown > 0: + self.start_up_countdown -= 1 + else: + if self.operating_state == NodeOperatingState.BOOTING: + self.operating_state = NodeOperatingState.ON + self.sys_log.info("Turned on") + for nic in self.nics.values(): + if nic._connected_link: + nic.enable() + + # count down to shut down + if self.shut_down_countdown > 0: + self.shut_down_countdown -= 1 + else: + if self.operating_state == NodeOperatingState.SHUTTING_DOWN: + self.operating_state = NodeOperatingState.OFF + self.sys_log.info("Turned off") + def power_on(self): """Power on the Node, enabling its NICs if it is in the OFF state.""" if self.operating_state == NodeOperatingState.OFF: - self.operating_state = NodeOperatingState.ON - self.sys_log.info("Turned on") - for nic in self.nics.values(): - if nic._connected_link: - nic.enable() + self.operating_state = NodeOperatingState.BOOTING + self.start_up_countdown = self.start_up_duration + + if self.start_up_duration <= 0: + self.operating_state = NodeOperatingState.ON + self.sys_log.info("Turned on") + for nic in self.nics.values(): + if nic._connected_link: + nic.enable() def power_off(self): """Power off the Node, disabling its NICs if it is in the ON state.""" if self.operating_state == NodeOperatingState.ON: for nic in self.nics.values(): nic.disable() - self.operating_state = NodeOperatingState.OFF - self.sys_log.info("Turned off") + self.operating_state = NodeOperatingState.SHUTTING_DOWN + self.shut_down_countdown = self.shut_down_duration + + if self.shut_down_duration >= 0: + self.operating_state = NodeOperatingState.OFF + self.sys_log.info("Turned off") def connect_nic(self, nic: NIC): """ @@ -1135,7 +1187,7 @@ class Node(SimComponent): f"Ping statistics for {target_ip_address}: " f"Packets: Sent = {pings}, " f"Received = {request_replies}, " - f"Lost = {pings-request_replies} ({(pings-request_replies)/pings*100}% loss)" + f"Lost = {pings - request_replies} ({(pings - request_replies) / pings * 100}% loss)" ) return passed return False diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index be20f89f..25d1bd21 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -1,7 +1,7 @@ from ipaddress import IPv4Address from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.base import NIC +from primaite.simulator.network.hardware.base import NIC, NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.server import Server @@ -110,19 +110,19 @@ def arcd_uc2_network() -> Network: network = Network() # Router 1 - router_1 = Router(hostname="router_1", num_ports=5) + router_1 = Router(hostname="router_1", num_ports=5, operating_state=NodeOperatingState.ON) router_1.power_on() router_1.configure_port(port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0") router_1.configure_port(port=2, ip_address="192.168.10.1", subnet_mask="255.255.255.0") # Switch 1 - switch_1 = Switch(hostname="switch_1", num_ports=8) + switch_1 = Switch(hostname="switch_1", num_ports=8, operating_state=NodeOperatingState.ON) switch_1.power_on() network.connect(endpoint_a=router_1.ethernet_ports[1], endpoint_b=switch_1.switch_ports[8]) router_1.enable_port(1) # Switch 2 - switch_2 = Switch(hostname="switch_2", num_ports=8) + switch_2 = Switch(hostname="switch_2", num_ports=8, operating_state=NodeOperatingState.ON) switch_2.power_on() network.connect(endpoint_a=router_1.ethernet_ports[2], endpoint_b=switch_2.switch_ports[8]) router_1.enable_port(2) @@ -134,6 +134,7 @@ def arcd_uc2_network() -> Network: subnet_mask="255.255.255.0", default_gateway="192.168.10.1", dns_server=IPv4Address("192.168.1.10"), + operating_state=NodeOperatingState.ON, ) client_1.power_on() network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) @@ -148,6 +149,7 @@ def arcd_uc2_network() -> Network: subnet_mask="255.255.255.0", default_gateway="192.168.10.1", dns_server=IPv4Address("192.168.1.10"), + operating_state=NodeOperatingState.ON, ) client_2.power_on() network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) @@ -158,6 +160,7 @@ def arcd_uc2_network() -> Network: ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, ) domain_controller.power_on() domain_controller.software_manager.install(DNSServer) @@ -171,6 +174,7 @@ def arcd_uc2_network() -> Network: subnet_mask="255.255.255.0", default_gateway="192.168.1.1", dns_server=IPv4Address("192.168.1.10"), + operating_state=NodeOperatingState.ON, ) database_server.power_on() network.connect(endpoint_b=database_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[3]) @@ -244,6 +248,7 @@ def arcd_uc2_network() -> Network: subnet_mask="255.255.255.0", default_gateway="192.168.1.1", dns_server=IPv4Address("192.168.1.10"), + operating_state=NodeOperatingState.ON, ) web_server.power_on() web_server.software_manager.install(DatabaseClient) @@ -267,6 +272,7 @@ def arcd_uc2_network() -> Network: subnet_mask="255.255.255.0", default_gateway="192.168.1.1", dns_server=IPv4Address("192.168.1.10"), + operating_state=NodeOperatingState.ON, ) backup_server.power_on() backup_server.software_manager.install(FTPServer) @@ -279,6 +285,7 @@ def arcd_uc2_network() -> Network: subnet_mask="255.255.255.0", default_gateway="192.168.1.1", dns_server=IPv4Address("192.168.1.10"), + operating_state=NodeOperatingState.ON, ) security_suite.power_on() network.connect(endpoint_b=security_suite.ethernet_port[1], endpoint_a=switch_1.switch_ports[7]) diff --git a/tests/integration_tests/network/test_frame_transmission.py b/tests/integration_tests/network/test_frame_transmission.py index 85717b25..7da9fe76 100644 --- a/tests/integration_tests/network/test_frame_transmission.py +++ b/tests/integration_tests/network/test_frame_transmission.py @@ -1,17 +1,15 @@ -from primaite.simulator.network.hardware.base import Link, NIC, Node +from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState def test_node_to_node_ping(): """Tests two Nodes are able to ping each other.""" - node_a = Node(hostname="node_a") - nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0") + node_a = Node(hostname="node_a", operating_state=NodeOperatingState.ON) + nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON) node_a.connect_nic(nic_a) - node_a.power_on() - node_b = Node(hostname="node_b") + node_b = Node(hostname="node_b", operating_state=NodeOperatingState.ON) nic_b = NIC(ip_address="192.168.0.11", subnet_mask="255.255.255.0") node_b.connect_nic(nic_b) - node_b.power_on() Link(endpoint_a=nic_a, endpoint_b=nic_b) @@ -20,22 +18,19 @@ def test_node_to_node_ping(): def test_multi_nic(): """Tests that Nodes with multiple NICs can ping each other and the data go across the correct links.""" - node_a = Node(hostname="node_a") + node_a = Node(hostname="node_a", operating_state=NodeOperatingState.ON) nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0") node_a.connect_nic(nic_a) - node_a.power_on() - node_b = Node(hostname="node_b") + node_b = Node(hostname="node_b", operating_state=NodeOperatingState.ON) nic_b1 = NIC(ip_address="192.168.0.11", subnet_mask="255.255.255.0") nic_b2 = NIC(ip_address="10.0.0.12", subnet_mask="255.0.0.0") node_b.connect_nic(nic_b1) node_b.connect_nic(nic_b2) - node_b.power_on() - node_c = Node(hostname="node_c") + node_c = Node(hostname="node_c", operating_state=NodeOperatingState.ON) nic_c = NIC(ip_address="10.0.0.13", subnet_mask="255.0.0.0") node_c.connect_nic(nic_c) - node_c.power_on() Link(endpoint_a=nic_a, endpoint_b=nic_b1) diff --git a/tests/integration_tests/network/test_link_connection.py b/tests/integration_tests/network/test_link_connection.py index ef65f078..0ddf54df 100644 --- a/tests/integration_tests/network/test_link_connection.py +++ b/tests/integration_tests/network/test_link_connection.py @@ -1,17 +1,15 @@ -from primaite.simulator.network.hardware.base import Link, NIC, Node +from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState def test_link_up(): """Tests Nodes, NICs, and Links can all be connected and be in an enabled/up state.""" - node_a = Node(hostname="node_a") + node_a = Node(hostname="node_a", operating_state=NodeOperatingState.ON) nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0") node_a.connect_nic(nic_a) - node_a.power_on() - node_b = Node(hostname="node_b") + node_b = Node(hostname="node_b", operating_state=NodeOperatingState.ON) nic_b = NIC(ip_address="192.168.0.11", subnet_mask="255.255.255.0") node_b.connect_nic(nic_b) - node_b.power_on() link = Link(endpoint_a=nic_a, endpoint_b=nic_b) diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index cb420e22..6053c457 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -2,7 +2,7 @@ from typing import Tuple import pytest -from primaite.simulator.network.hardware.base import Link, NIC, Node +from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState from primaite.simulator.network.hardware.nodes.router import ACLAction, Router from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -10,18 +10,15 @@ from primaite.simulator.network.transmission.transport_layer import Port @pytest.fixture(scope="function") def pc_a_pc_b_router_1() -> Tuple[Node, Node, Router]: - pc_a = Node(hostname="pc_a", default_gateway="192.168.0.1") + pc_a = Node(hostname="pc_a", default_gateway="192.168.0.1", operating_state=NodeOperatingState.ON) nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0") pc_a.connect_nic(nic_a) - pc_a.power_on() - pc_b = Node(hostname="pc_b", default_gateway="192.168.1.1") + pc_b = Node(hostname="pc_b", default_gateway="192.168.1.1", operating_state=NodeOperatingState.ON) nic_b = NIC(ip_address="192.168.1.10", subnet_mask="255.255.255.0") pc_b.connect_nic(nic_b) - pc_b.power_on() - router_1 = Router(hostname="router_1") - router_1.power_on() + router_1 = Router(hostname="router_1", operating_state=NodeOperatingState.ON) router_1.configure_port(1, "192.168.0.1", "255.255.255.0") router_1.configure_port(2, "192.168.1.1", "255.255.255.0") diff --git a/tests/integration_tests/network/test_switched_network.py b/tests/integration_tests/network/test_switched_network.py index dc7742f4..5b305702 100644 --- a/tests/integration_tests/network/test_switched_network.py +++ b/tests/integration_tests/network/test_switched_network.py @@ -1,4 +1,4 @@ -from primaite.simulator.network.hardware.base import Link +from primaite.simulator.network.hardware.base import Link, NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.hardware.nodes.switch import Switch @@ -7,17 +7,22 @@ from primaite.simulator.network.hardware.nodes.switch import Switch def test_switched_network(): """Tests a node can ping another node via the switch.""" client_1 = Computer( - hostname="client_1", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.0" + hostname="client_1", + ip_address="192.168.1.10", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.0", + operating_state=NodeOperatingState.ON, ) - client_1.power_on() server_1 = Server( - hostname=" server_1", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.11" + hostname=" server_1", + ip_address="192.168.1.11", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.11", + operating_state=NodeOperatingState.ON, ) - server_1.power_on() - switch_1 = Switch(hostname="switch_1", num_ports=6) - switch_1.power_on() + switch_1 = Switch(hostname="switch_1", num_ports=6, operating_state=NodeOperatingState.ON) Link(endpoint_a=client_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[1]) Link(endpoint_a=server_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[2]) diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index c956682f..e03e1d28 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -11,13 +11,31 @@ def node() -> Node: def test_node_startup(node): assert node.operating_state == NodeOperatingState.OFF node.apply_request(["startup"]) + assert node.operating_state == NodeOperatingState.BOOTING + + idx = 0 + while node.operating_state == NodeOperatingState.BOOTING: + node.apply_timestep(timestep=idx) + idx += 1 + assert node.operating_state == NodeOperatingState.ON def test_node_shutdown(node): assert node.operating_state == NodeOperatingState.OFF node.apply_request(["startup"]) + idx = 0 + while node.operating_state == NodeOperatingState.BOOTING: + node.apply_timestep(timestep=idx) + idx += 1 + assert node.operating_state == NodeOperatingState.ON node.apply_request(["shutdown"]) + + idx = 0 + while node.operating_state == NodeOperatingState.SHUTTING_DOWN: + node.apply_timestep(timestep=idx) + idx += 1 + assert node.operating_state == NodeOperatingState.OFF