From bd6c27244c349940a0ec8fa21ca7845786071301 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 23 Nov 2023 19:49:03 +0000 Subject: [PATCH] #2064: Edited services and applications to handle when they are shut down --- src/primaite/simulator/network/container.py | 11 +++++ .../simulator/network/hardware/base.py | 40 +++++++++++------- .../network/hardware/node_operating_state.py | 14 +++++++ .../simulator/network/protocols/ftp.py | 3 ++ .../system/applications/web_browser.py | 21 ++++++++-- .../system/services/ftp/ftp_client.py | 8 ++-- .../system/services/ftp/ftp_server.py | 3 ++ .../simulator/system/services/service.py | 23 +++++++++- .../system/services/web_server/web_server.py | 3 ++ src/primaite/simulator/system/software.py | 6 ++- tests/conftest.py | 7 ++++ .../system/test_ftp_client_server.py | 37 ++++++++++++++++ .../system/test_service_on_node.py | 42 +++++++++++++++++++ .../system/test_web_client_server.py | 30 ++++++++++++- .../_simulator/_system/_services/test_ftp.py | 14 +++++-- 15 files changed, 232 insertions(+), 30 deletions(-) create mode 100644 src/primaite/simulator/network/hardware/node_operating_state.py create mode 100644 tests/integration_tests/system/test_service_on_node.py diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 9fbafc29..a356549a 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -52,6 +52,17 @@ class Network(SimComponent): ) return rm + def apply_timestep(self, timestep: int) -> None: + """Apply a timestep evolution to this the network and its nodes and links.""" + super().apply_timestep(timestep=timestep) + # apply timestep to nodes + for node_id in self.nodes: + self.nodes[node_id].apply_timestep(timestep=timestep) + + # apply timestep to links + for link_id in self.links: + self.links[link_id].apply_timestep(timestep=timestep) + @property def routers(self) -> List[Router]: """The Routers in the Network.""" diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 29d3a05c..ebf669eb 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -2,7 +2,6 @@ from __future__ import annotations import re import secrets -from enum import Enum from ipaddress import IPv4Address, IPv4Network from pathlib import Path from typing import Any, Dict, Literal, Optional, Tuple, Union @@ -15,6 +14,7 @@ from primaite.simulator import SIM_OUTPUT from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.domain.account import Account from primaite.simulator.file_system.file_system import FileSystem +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.protocols.arp import ARPEntry, ARPPacket from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame from primaite.simulator.network.transmission.network_layer import ICMPPacket, ICMPType, IPPacket, IPProtocol @@ -856,19 +856,6 @@ class ICMP: return sequence, icmp_packet.identifier -class NodeOperatingState(Enum): - """Enumeration of Node Operating States.""" - - ON = 1 - "The node is powered on." - OFF = 2 - "The node is powered off." - BOOTING = 3 - "The node is in the process of booting up." - SHUTTING_DOWN = 4 - "The node is in the process of shutting down." - - class Node(SimComponent): """ A basic Node class that represents a node on the network. @@ -1090,18 +1077,21 @@ class Node(SimComponent): else: if self.operating_state == NodeOperatingState.BOOTING: self.operating_state = NodeOperatingState.ON - self.sys_log.info("Turned on") + self.sys_log.info(f"{self.hostname}: Turned on") for nic in self.nics.values(): if nic._connected_link: nic.enable() + self._start_up_actions() + # 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") + self.sys_log.info(f"{self.hostname}: Turned off") + self._shut_down_actions() # if resetting turn back on if self.is_resetting: @@ -1418,6 +1408,24 @@ class Node(SimComponent): _LOGGER.info(f"Removed application {application.uuid} from node {self.uuid}") self._application_request_manager.remove_request(application.uuid) + def _shut_down_actions(self): + """Actions to perform when the node is shut down.""" + # Turn off all the services in the node + for service_id in self.services: + self.services[service_id].stop() + + # Turn off all the applications in the node + for app_id in self.applications: + self.applications[app_id].close() + + # Turn off all processes in the node + # for process_id in self.processes: + # self.processes[process_id] + + def _start_up_actions(self): + """Actions to perform when the node is starting up.""" + pass + def __contains__(self, item: Any) -> bool: if isinstance(item, Service): return item.uuid in self.services diff --git a/src/primaite/simulator/network/hardware/node_operating_state.py b/src/primaite/simulator/network/hardware/node_operating_state.py new file mode 100644 index 00000000..1fd1225f --- /dev/null +++ b/src/primaite/simulator/network/hardware/node_operating_state.py @@ -0,0 +1,14 @@ +from enum import Enum + + +class NodeOperatingState(Enum): + """Enumeration of Node Operating States.""" + + ON = 1 + "The node is powered on." + OFF = 2 + "The node is powered off." + BOOTING = 3 + "The node is in the process of booting up." + SHUTTING_DOWN = 4 + "The node is in the process of shutting down." diff --git a/src/primaite/simulator/network/protocols/ftp.py b/src/primaite/simulator/network/protocols/ftp.py index 9ecc7df8..0fd3fe43 100644 --- a/src/primaite/simulator/network/protocols/ftp.py +++ b/src/primaite/simulator/network/protocols/ftp.py @@ -35,6 +35,9 @@ class FTPCommand(Enum): class FTPStatusCode(Enum): """Status code of the current FTP request.""" + NOT_FOUND = 14 + """Destination not found.""" + OK = 200 """Command successful.""" diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index ea9c3ac3..bb9552d8 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -2,7 +2,12 @@ from ipaddress import IPv4Address from typing import Dict, Optional from urllib.parse import urlparse -from primaite.simulator.network.protocols.http import HttpRequestMethod, HttpRequestPacket, HttpResponsePacket +from primaite.simulator.network.protocols.http import ( + HttpRequestMethod, + HttpRequestPacket, + HttpResponsePacket, + HttpStatusCode, +) from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application @@ -61,7 +66,7 @@ class WebBrowser(Application): :type: url: str """ # reset latest response - self.latest_response = None + self.latest_response = HttpResponsePacket(status_code=HttpStatusCode.NOT_FOUND) try: parsed_url = urlparse(url) @@ -91,11 +96,19 @@ class WebBrowser(Application): payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url=url) # send request - return self.send( + if self.send( payload=payload, dest_ip_address=self.domain_name_ip_address, dest_port=parsed_url.port if parsed_url.port else Port.HTTP, - ) + ): + self.sys_log.info( + f"{self.name}: Received HTTP {payload.request_method.name} " + f"Response {payload.request_url} - {self.latest_response.status_code.value}" + ) + return self.latest_response.status_code is HttpStatusCode.OK + else: + self.sys_log.error(f"Error sending Http Packet {str(payload)}") + return False def send( self, diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 3e286da1..649b9b50 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -72,10 +72,7 @@ class FTPClient(FTPServiceABC): # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now # create FTP packet - payload: FTPPacket = FTPPacket( - ftp_command=FTPCommand.PORT, - ftp_command_args=Port.FTP, - ) + payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.PORT, ftp_command_args=Port.FTP) if self.send(payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id): if payload.status_code == FTPStatusCode.OK: @@ -271,7 +268,10 @@ class FTPClient(FTPServiceABC): the same node. """ if payload.status_code is None: + self.sys_log.error(f"FTP Server could not be found - Error Code: {payload.status_code.value}") return False + self.sys_log.info(f"{self.name}: Received FTP Response {payload.ftp_command.name} {payload.status_code.value}") + self._process_ftp_command(payload=payload, session_id=session_id) return True diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 23414601..bc21dec3 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -89,5 +89,8 @@ class FTPServer(FTPServiceABC): if payload.status_code is not None: return False + if not super().receive(payload=payload, session_id=session_id, **kwargs): + return False + self.send(self._process_ftp_command(payload=payload, session_id=session_id), session_id) return True diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index e2b04c15..3a1a4c9d 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -1,8 +1,9 @@ from enum import Enum -from typing import Dict, Optional +from typing import Any, Dict, Optional from primaite import getLogger from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.system.software import IOSoftware, SoftwareHealthState _LOGGER = getLogger(__name__) @@ -40,6 +41,21 @@ class Service(IOSoftware): restart_countdown: Optional[int] = None "If currently restarting, how many timesteps remain until the restart is finished." + def receive(self, payload: Any, session_id: str, **kwargs) -> bool: + """ + Receives a payload from the SessionManager. + + The specifics of how the payload is processed and whether a response payload + is generated should be implemented in subclasses. + + + :param payload: The payload to receive. + :param session_id: The identifier of the session that the payload is associated with. + :param kwargs: Additional keyword arguments specific to the implementation. + :return: True if the payload was successfully received and processed, False otherwise. + """ + return super().receive(payload=payload, session_id=session_id, **kwargs) + def __init__(self, **kwargs): super().__init__(**kwargs) @@ -91,6 +107,11 @@ class Service(IOSoftware): def start(self, **kwargs) -> None: """Start the service.""" + # cant start the service if the node it is on is off + if self.software_manager and self.software_manager.node.operating_state is not NodeOperatingState.ON: + self.sys_log.error(f"Unable to start service. {self.software_manager.node.hostname} is not turned on.") + return + if self.operating_state == ServiceOperatingState.STOPPED: self.sys_log.info(f"Starting service {self.name}") self.operating_state = ServiceOperatingState.RUNNING diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index cb1a4738..76176cd8 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -160,4 +160,7 @@ class WebServer(Service): self.sys_log.error("Payload is not an HTTPPacket") return False + if not super().receive(payload=payload, session_id=session_id, **kwargs): + return False + return self._process_http_request(payload=payload, session_id=session_id) diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index f2627557..c29bec20 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -5,6 +5,7 @@ from typing import Any, Dict, Optional 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.transport_layer import Port from primaite.simulator.system.core.session_manager import Session from primaite.simulator.system.core.sys_log import SysLog @@ -261,4 +262,7 @@ class IOSoftware(Software): :param kwargs: Additional keyword arguments specific to the implementation. :return: True if the payload was successfully received and processed, False otherwise. """ - pass + # return false if node that software is on is off + if self.software_manager and self.software_manager.node.operating_state is NodeOperatingState.OFF: + return False + return True diff --git a/tests/conftest.py b/tests/conftest.py index 6a65b12f..4cc36e6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,7 @@ from primaite.game.session import PrimaiteSession # from primaite.primaite_session import PrimaiteSession from primaite.simulator.network.container import Network from primaite.simulator.network.networks import arcd_uc2_network +from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.sys_log import SysLog @@ -38,6 +39,12 @@ from primaite.simulator.network.hardware.base import Node class TestService(Service): """Test Service class""" + def __init__(self, **kwargs): + kwargs["name"] = "TestService" + kwargs["port"] = Port.HTTP + kwargs["protocol"] = IPProtocol.TCP + super().__init__(**kwargs) + def receive(self, payload: Any, session_id: str, **kwargs) -> bool: pass diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index 48dc2960..d8968b2d 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -60,3 +60,40 @@ def test_ftp_client_retrieve_file_from_server(uc2_network): # client should have retrieved the file assert ftp_client.file_system.get_file(folder_name="downloads", file_name="test_file.txt") + + +def test_ftp_client_tries_to_connect_to_offline_server(uc2_network): + """Test checks to make sure that the client can't do anything when the server is offline.""" + client_1: Computer = uc2_network.get_node_by_hostname("client_1") + backup_server: Server = uc2_network.get_node_by_hostname("backup_server") + + ftp_client: FTPClient = client_1.software_manager.software["FTPClient"] + ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] + + assert ftp_client.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING + + # create file on ftp server + ftp_server.file_system.create_file(file_name="test_file.txt", folder_name="file_share") + + backup_server.power_off() + + for i in range(backup_server.shut_down_duration + 1): + uc2_network.apply_timestep(timestep=i) + + assert ftp_client.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.STOPPED + + assert ( + ftp_client.request_file( + src_folder_name="file_share", + src_file_name="test_file.txt", + dest_folder_name="downloads", + dest_file_name="test_file.txt", + dest_ip_address=backup_server.nics.get(next(iter(backup_server.nics))).ip_address, + ) + is False + ) + + # client should have retrieved the file + assert ftp_client.file_system.get_file(folder_name="downloads", file_name="test_file.txt") is None diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py new file mode 100644 index 00000000..e596dcd8 --- /dev/null +++ b/tests/integration_tests/system/test_service_on_node.py @@ -0,0 +1,42 @@ +from typing import Tuple + +import pytest +from conftest import TestService + +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.system.services.service import Service, ServiceOperatingState + + +@pytest.fixture(scope="function") +def service_on_node() -> Tuple[Server, Service]: + server = Server( + hostname="server", ip_address="192.168.0.1", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON + ) + server.software_manager.install(TestService) + + service = server.software_manager.software["TestService"] + service.start() + + return server, service + + +def test_server_turns_off_service(service_on_node): + """Check that the service is turned off when the server is turned off""" + server, service = service_on_node + + assert server.operating_state is NodeOperatingState.ON + assert service.operating_state is ServiceOperatingState.RUNNING + + server.power_off() + + for i in range(server.shut_down_duration + 1): + server.apply_timestep(timestep=i) + + assert server.operating_state is NodeOperatingState.OFF + assert service.operating_state is ServiceOperatingState.STOPPED + + +def test_server_turns_on_service(service_on_node): + """Check that turning on the server turns on service.""" + pass diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index f4546cbf..f3995c84 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -1,3 +1,4 @@ +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.http import HttpStatusCode @@ -47,6 +48,33 @@ def test_web_page_get_users_page_request_with_ip_address(uc2_network): assert web_client.get_webpage(f"http://{web_server_ip}/users/") is True - # latest reponse should have status code 200 + # latest response should have status code 200 assert web_client.latest_response is not None assert web_client.latest_response.status_code == HttpStatusCode.OK + + +def test_web_page_request_from_shut_down_server(uc2_network): + """Test to see that the web server does not respond when the server is off.""" + client_1: Computer = uc2_network.get_node_by_hostname("client_1") + web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] + web_client.run() + + web_server: Server = uc2_network.get_node_by_hostname("web_server") + + assert web_client.operating_state == ApplicationOperatingState.RUNNING + + assert web_client.get_webpage("http://arcd.com/users/") is True + + # latest response should have status code 200 + assert web_client.latest_response.status_code == HttpStatusCode.OK + + web_server.power_off() + + for i in range(web_server.shut_down_duration + 1): + uc2_network.apply_timestep(timestep=i) + + # node should be off + assert web_server.operating_state is NodeOperatingState.OFF + + assert web_client.get_webpage("http://arcd.com/users/") is False + assert web_client.latest_response.status_code == HttpStatusCode.NOT_FOUND diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py index d382b8dd..9957b6f6 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address import pytest from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode @@ -15,17 +16,24 @@ from primaite.simulator.system.services.ftp.ftp_server import FTPServer @pytest.fixture(scope="function") def ftp_server() -> Node: node = Server( - hostname="ftp_server", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" + hostname="ftp_server", + ip_address="192.168.1.10", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, ) node.software_manager.install(software_class=FTPServer) - node.software_manager.software["FTPServer"].start() return node @pytest.fixture(scope="function") def ftp_client() -> Node: node = Computer( - hostname="ftp_client", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" + hostname="ftp_client", + ip_address="192.168.1.11", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, ) return node