#2064: Edited services and applications to handle when they are shut down

This commit is contained in:
Czar Echavez
2023-11-23 19:49:03 +00:00
parent 4ee29efd1f
commit bd6c27244c
15 changed files with 232 additions and 30 deletions

View File

@@ -52,6 +52,17 @@ class Network(SimComponent):
) )
return rm 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 @property
def routers(self) -> List[Router]: def routers(self) -> List[Router]:
"""The Routers in the Network.""" """The Routers in the Network."""

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import re import re
import secrets import secrets
from enum import Enum
from ipaddress import IPv4Address, IPv4Network from ipaddress import IPv4Address, IPv4Network
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Literal, Optional, Tuple, Union 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.core import RequestManager, RequestType, SimComponent
from primaite.simulator.domain.account import Account from primaite.simulator.domain.account import Account
from primaite.simulator.file_system.file_system import FileSystem 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.protocols.arp import ARPEntry, ARPPacket
from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame
from primaite.simulator.network.transmission.network_layer import ICMPPacket, ICMPType, IPPacket, IPProtocol from primaite.simulator.network.transmission.network_layer import ICMPPacket, ICMPType, IPPacket, IPProtocol
@@ -856,19 +856,6 @@ class ICMP:
return sequence, icmp_packet.identifier 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): class Node(SimComponent):
""" """
A basic Node class that represents a node on the network. A basic Node class that represents a node on the network.
@@ -1090,18 +1077,21 @@ class Node(SimComponent):
else: else:
if self.operating_state == NodeOperatingState.BOOTING: if self.operating_state == NodeOperatingState.BOOTING:
self.operating_state = NodeOperatingState.ON 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(): for nic in self.nics.values():
if nic._connected_link: if nic._connected_link:
nic.enable() nic.enable()
self._start_up_actions()
# count down to shut down # count down to shut down
if self.shut_down_countdown > 0: if self.shut_down_countdown > 0:
self.shut_down_countdown -= 1 self.shut_down_countdown -= 1
else: else:
if self.operating_state == NodeOperatingState.SHUTTING_DOWN: if self.operating_state == NodeOperatingState.SHUTTING_DOWN:
self.operating_state = NodeOperatingState.OFF 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 resetting turn back on
if self.is_resetting: if self.is_resetting:
@@ -1418,6 +1408,24 @@ class Node(SimComponent):
_LOGGER.info(f"Removed application {application.uuid} from node {self.uuid}") _LOGGER.info(f"Removed application {application.uuid} from node {self.uuid}")
self._application_request_manager.remove_request(application.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: def __contains__(self, item: Any) -> bool:
if isinstance(item, Service): if isinstance(item, Service):
return item.uuid in self.services return item.uuid in self.services

View File

@@ -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."

View File

@@ -35,6 +35,9 @@ class FTPCommand(Enum):
class FTPStatusCode(Enum): class FTPStatusCode(Enum):
"""Status code of the current FTP request.""" """Status code of the current FTP request."""
NOT_FOUND = 14
"""Destination not found."""
OK = 200 OK = 200
"""Command successful.""" """Command successful."""

View File

@@ -2,7 +2,12 @@ from ipaddress import IPv4Address
from typing import Dict, Optional from typing import Dict, Optional
from urllib.parse import urlparse 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.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.application import Application
@@ -61,7 +66,7 @@ class WebBrowser(Application):
:type: url: str :type: url: str
""" """
# reset latest response # reset latest response
self.latest_response = None self.latest_response = HttpResponsePacket(status_code=HttpStatusCode.NOT_FOUND)
try: try:
parsed_url = urlparse(url) parsed_url = urlparse(url)
@@ -91,11 +96,19 @@ class WebBrowser(Application):
payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url=url) payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url=url)
# send request # send request
return self.send( if self.send(
payload=payload, payload=payload,
dest_ip_address=self.domain_name_ip_address, dest_ip_address=self.domain_name_ip_address,
dest_port=parsed_url.port if parsed_url.port else Port.HTTP, 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( def send(
self, self,

View File

@@ -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 # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now
# create FTP packet # create FTP packet
payload: FTPPacket = FTPPacket( payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.PORT, ftp_command_args=Port.FTP)
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 self.send(payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id):
if payload.status_code == FTPStatusCode.OK: if payload.status_code == FTPStatusCode.OK:
@@ -271,7 +268,10 @@ class FTPClient(FTPServiceABC):
the same node. the same node.
""" """
if payload.status_code is None: 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 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) self._process_ftp_command(payload=payload, session_id=session_id)
return True return True

View File

@@ -89,5 +89,8 @@ class FTPServer(FTPServiceABC):
if payload.status_code is not None: if payload.status_code is not None:
return False 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) self.send(self._process_ftp_command(payload=payload, session_id=session_id), session_id)
return True return True

View File

@@ -1,8 +1,9 @@
from enum import Enum from enum import Enum
from typing import Dict, Optional from typing import Any, Dict, Optional
from primaite import getLogger from primaite import getLogger
from primaite.simulator.core import RequestManager, RequestType 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 from primaite.simulator.system.software import IOSoftware, SoftwareHealthState
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
@@ -40,6 +41,21 @@ class Service(IOSoftware):
restart_countdown: Optional[int] = None restart_countdown: Optional[int] = None
"If currently restarting, how many timesteps remain until the restart is finished." "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): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@@ -91,6 +107,11 @@ class Service(IOSoftware):
def start(self, **kwargs) -> None: def start(self, **kwargs) -> None:
"""Start the service.""" """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: if self.operating_state == ServiceOperatingState.STOPPED:
self.sys_log.info(f"Starting service {self.name}") self.sys_log.info(f"Starting service {self.name}")
self.operating_state = ServiceOperatingState.RUNNING self.operating_state = ServiceOperatingState.RUNNING

View File

@@ -160,4 +160,7 @@ class WebServer(Service):
self.sys_log.error("Payload is not an HTTPPacket") self.sys_log.error("Payload is not an HTTPPacket")
return False 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) return self._process_http_request(payload=payload, session_id=session_id)

View File

@@ -5,6 +5,7 @@ from typing import Any, Dict, Optional
from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.core import RequestManager, RequestType, SimComponent
from primaite.simulator.file_system.file_system import FileSystem, Folder 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.network.transmission.transport_layer import Port
from primaite.simulator.system.core.session_manager import Session from primaite.simulator.system.core.session_manager import Session
from primaite.simulator.system.core.sys_log import SysLog 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. :param kwargs: Additional keyword arguments specific to the implementation.
:return: True if the payload was successfully received and processed, False otherwise. :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

View File

@@ -17,6 +17,7 @@ from primaite.game.session import PrimaiteSession
# from primaite.primaite_session import PrimaiteSession # from primaite.primaite_session import PrimaiteSession
from primaite.simulator.network.container import Network from primaite.simulator.network.container import Network
from primaite.simulator.network.networks import arcd_uc2_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.network.transmission.transport_layer import Port
from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.application import Application
from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.core.sys_log import SysLog
@@ -38,6 +39,12 @@ from primaite.simulator.network.hardware.base import Node
class TestService(Service): class TestService(Service):
"""Test Service class""" """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: def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
pass pass

View File

@@ -60,3 +60,40 @@ def test_ftp_client_retrieve_file_from_server(uc2_network):
# client should have retrieved the file # client should have retrieved the file
assert ftp_client.file_system.get_file(folder_name="downloads", file_name="test_file.txt") 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

View File

@@ -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

View File

@@ -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.computer import Computer
from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.hardware.nodes.server import Server
from primaite.simulator.network.protocols.http import HttpStatusCode 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 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 is not None
assert web_client.latest_response.status_code == HttpStatusCode.OK 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

View File

@@ -3,6 +3,7 @@ from ipaddress import IPv4Address
import pytest import pytest
from primaite.simulator.network.hardware.base import Node 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.computer import Computer
from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.hardware.nodes.server import Server
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode 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") @pytest.fixture(scope="function")
def ftp_server() -> Node: def ftp_server() -> Node:
node = Server( 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.install(software_class=FTPServer)
node.software_manager.software["FTPServer"].start()
return node return node
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def ftp_client() -> Node: def ftp_client() -> Node:
node = Computer( 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 return node