Merge remote-tracking branch 'origin/dev' into feature/2137-refactor-request-api
This commit is contained in:
@@ -37,6 +37,7 @@ SessionManager.
|
||||
- FTP Services: `FTPClient` and `FTPServer`
|
||||
- HTTP Services: `WebBrowser` to simulate a web client and `WebServer`
|
||||
- Fixed an issue where the services were still able to run even though the node the service is installed on is turned off
|
||||
- NTP Services: `NTPClient` and `NTPServer`
|
||||
|
||||
### Removed
|
||||
- Removed legacy simulation modules: `acl`, `common`, `environment`, `links`, `nodes`, `pol`
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.. only:: comment
|
||||
|
||||
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||
|
||||
NTP Client Server
|
||||
=================
|
||||
|
||||
NTP Server
|
||||
----------
|
||||
The ``NTPServer`` provides a NTP Server simulation by extending the base Service class.
|
||||
|
||||
NTP Client
|
||||
----------
|
||||
The ``NTPClient`` provides a NTP Client simulation by extending the base Service class.
|
||||
|
||||
Key capabilities
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
- Simulates NTP requests and NTPPacket transfer across a network
|
||||
- Leverages the Service base class for install/uninstall, status tracking, etc.
|
||||
|
||||
Usage
|
||||
^^^^^
|
||||
- Install on a Node via the ``SoftwareManager`` to start the database service.
|
||||
- Service runs on TCP port 123 by default.
|
||||
|
||||
Implementation
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
- NTP request and responses use a ``NTPPacket`` object
|
||||
- Extends Service class for integration with ``SoftwareManager``.
|
||||
|
||||
NTP Client
|
||||
----------
|
||||
|
||||
The NTPClient provides a client interface for connecting to the ``NTPServer``.
|
||||
|
||||
Key features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Connects to the ``NTPServer`` via the ``SoftwareManager``.
|
||||
|
||||
Usage
|
||||
^^^^^
|
||||
|
||||
- Install on a Node via the ``SoftwareManager`` to start the database service.
|
||||
- Service runs on TCP port 123 by default.
|
||||
|
||||
Implementation
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
- Leverages ``SoftwareManager`` for sending payloads over the network.
|
||||
- Provides easy interface for Nodes to find IP addresses via domain names.
|
||||
- Extends base Service class.
|
||||
@@ -593,7 +593,7 @@ class ActionManager:
|
||||
max_nics_per_node: int = 8, # allows calculating shape
|
||||
max_acl_rules: int = 10, # allows calculating shape
|
||||
protocols: List[str] = ["TCP", "UDP", "ICMP"], # allow mapping index to protocol
|
||||
ports: List[str] = ["HTTP", "DNS", "ARP", "FTP"], # allow mapping index to port
|
||||
ports: List[str] = ["HTTP", "DNS", "ARP", "FTP", "NTP"], # allow mapping index to port
|
||||
ip_address_list: Optional[List[str]] = None, # to allow us to map an index to an ip address.
|
||||
act_map: Optional[Dict[int, Dict]] = None, # allows restricting set of possible actions
|
||||
) -> None:
|
||||
|
||||
@@ -139,7 +139,10 @@ class ServiceObservation(AbstractObservation):
|
||||
service_state = access_from_nested_dict(state, self.where)
|
||||
if service_state is NOT_PRESENT_IN_STATE:
|
||||
return self.default_observation
|
||||
return {"operating_status": service_state["operating_state"], "health_status": service_state["health_state"]}
|
||||
return {
|
||||
"operating_status": service_state["operating_state"],
|
||||
"health_status": service_state["health_state_visible"],
|
||||
}
|
||||
|
||||
@property
|
||||
def space(self) -> spaces.Space:
|
||||
@@ -346,7 +349,7 @@ class NicObservation(AbstractObservation):
|
||||
|
||||
:param where: Where in the simulation state dictionary to find the relevant information for this NIC. A typical
|
||||
example may look like this:
|
||||
['network','nodes',<node_hostname>,'NICs',<nic_index>]
|
||||
['network','nodes',<node_hostname>,'NICs',<nic_number>]
|
||||
If None, this denotes that the NIC does not exist and the observation will be populated with zeroes.
|
||||
:type where: Optional[Tuple[str]], optional
|
||||
"""
|
||||
|
||||
@@ -132,7 +132,7 @@ class DatabaseFileIntegrity(AbstractReward):
|
||||
node_hostname = config.get("node_hostname")
|
||||
folder_name = config.get("folder_name")
|
||||
file_name = config.get("file_name")
|
||||
if not node_hostname and folder_name and file_name:
|
||||
if not (node_hostname and folder_name and file_name):
|
||||
msg = f"{cls.__name__} could not be initialised with parameters {config}"
|
||||
_LOGGER.error(msg)
|
||||
raise ValueError(msg)
|
||||
@@ -148,8 +148,8 @@ class WebServer404Penalty(AbstractReward):
|
||||
|
||||
:param node_hostname: Hostname of the node which contains the web server service.
|
||||
:type node_hostname: str
|
||||
:param service_node: Name of the web server service.
|
||||
:type service_node: str
|
||||
:param service_name: Name of the web server service.
|
||||
:type service_name: str
|
||||
"""
|
||||
self.location_in_state = ["network", "nodes", node_hostname, "services", service_name]
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||
from primaite.simulator.system.services.dns.dns_server import DNSServer
|
||||
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
||||
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
|
||||
from primaite.simulator.system.services.ntp.ntp_client import NTPClient
|
||||
from primaite.simulator.system.services.ntp.ntp_server import NTPServer
|
||||
from primaite.simulator.system.services.web_server.web_server import WebServer
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
@@ -266,6 +268,8 @@ class PrimaiteGame:
|
||||
"WebServer": WebServer,
|
||||
"FTPClient": FTPClient,
|
||||
"FTPServer": FTPServer,
|
||||
"NTPClient": NTPClient,
|
||||
"NTPServer": NTPServer,
|
||||
}
|
||||
if service_type in service_types_mapping:
|
||||
_LOGGER.debug(f"installing {service_type} on node {new_node.hostname}")
|
||||
|
||||
35
src/primaite/simulator/network/protocols/ntp.py
Normal file
35
src/primaite/simulator/network/protocols/ntp.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from primaite.simulator.network.protocols.packet import DataPacket
|
||||
|
||||
|
||||
class NTPReply(BaseModel):
|
||||
"""Represents a NTP Reply packet."""
|
||||
|
||||
ntp_datetime: datetime
|
||||
"NTP datetime object set by NTP Server."
|
||||
|
||||
|
||||
class NTPPacket(DataPacket):
|
||||
"""
|
||||
Represents the NTP layer of a network frame.
|
||||
|
||||
:param ntp_request: NTPRequest packet from NTP client.
|
||||
:param ntp_reply: NTPReply packet from NTP Server.
|
||||
"""
|
||||
|
||||
ntp_reply: Optional[NTPReply] = None
|
||||
|
||||
def generate_reply(self, ntp_server_time: datetime) -> NTPPacket:
|
||||
"""Generate a NTPPacket containing the time in a NTPReply object.
|
||||
|
||||
:param time: datetime object representing the time from the NTP server.
|
||||
:return: A new NTPPacket object.
|
||||
"""
|
||||
self.ntp_reply = NTPReply(ntp_datetime=ntp_server_time)
|
||||
return self
|
||||
132
src/primaite/simulator/system/services/ntp/ntp_client.py
Normal file
132
src/primaite/simulator/system/services/ntp/ntp_client.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from datetime import datetime
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Dict, Optional
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.simulator.network.protocols.ntp import NTPPacket
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.services.service import Service, ServiceOperatingState
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class NTPClient(Service):
|
||||
"""Represents a NTP client as a service."""
|
||||
|
||||
ntp_server: Optional[IPv4Address] = None
|
||||
"The NTP server the client sends requests to."
|
||||
time: Optional[datetime] = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["name"] = "NTPClient"
|
||||
kwargs["port"] = Port.NTP
|
||||
kwargs["protocol"] = IPProtocol.TCP
|
||||
super().__init__(**kwargs)
|
||||
self.start()
|
||||
|
||||
def configure(self, ntp_server_ip_address: IPv4Address) -> None:
|
||||
"""
|
||||
Set the IP address for the NTP server.
|
||||
|
||||
:param ntp_server_ip_address: IPv4 address of NTP server.
|
||||
:param ntp_client_ip_Address: IPv4 address of NTP client.
|
||||
"""
|
||||
self.ntp_server = ntp_server_ip_address
|
||||
self.sys_log.info(f"{self.name}: ntp_server: {self.ntp_server}")
|
||||
|
||||
def describe_state(self) -> Dict:
|
||||
"""
|
||||
Describes the current state of the software.
|
||||
|
||||
The specifics of the software's state, including its health, criticality,
|
||||
and any other pertinent information, should be implemented in subclasses.
|
||||
|
||||
:return: A dictionary containing key-value pairs representing the current state
|
||||
of the software.
|
||||
:rtype: Dict
|
||||
"""
|
||||
state = super().describe_state()
|
||||
return state
|
||||
|
||||
def reset_component_for_episode(self, episode: int):
|
||||
"""
|
||||
Resets the Service component for a new episode.
|
||||
|
||||
This method ensures the Service is ready for a new episode, including resetting any
|
||||
stateful properties or statistics, and clearing any message queues.
|
||||
"""
|
||||
pass
|
||||
|
||||
def send(
|
||||
self,
|
||||
payload: NTPPacket,
|
||||
session_id: Optional[str] = None,
|
||||
dest_ip_address: IPv4Address = None,
|
||||
dest_port: [Port] = Port.NTP,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""Requests NTP data from NTP server.
|
||||
|
||||
:param payload: The payload to be sent.
|
||||
:param session_id: The Session ID the payload is to originate from. Optional.
|
||||
:param dest_ip_address: The ip address of the payload destination.
|
||||
:param dest_port: The port of the payload destination.
|
||||
|
||||
:return: True if successful, False otherwise.
|
||||
"""
|
||||
return super().send(
|
||||
payload=payload,
|
||||
dest_ip_address=dest_ip_address,
|
||||
dest_port=dest_port,
|
||||
session_id=session_id,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def receive(
|
||||
self,
|
||||
payload: NTPPacket,
|
||||
session_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""Receives time data from server.
|
||||
|
||||
:param payload: The payload to be sent.
|
||||
:param session_id: The Session ID the payload is to originate from. Optional.
|
||||
:return: True if successful, False otherwise.
|
||||
"""
|
||||
if not isinstance(payload, NTPPacket):
|
||||
_LOGGER.debug(f"{payload} is not a NTPPacket")
|
||||
return False
|
||||
if payload.ntp_reply.ntp_datetime:
|
||||
self.sys_log.info(
|
||||
f"{self.name}: \
|
||||
Received time update from NTP server{payload.ntp_reply.ntp_datetime}"
|
||||
)
|
||||
self.time = payload.ntp_reply.ntp_datetime
|
||||
return True
|
||||
|
||||
def request_time(self) -> None:
|
||||
"""Send request to ntp_server."""
|
||||
ntp_server_packet = NTPPacket()
|
||||
|
||||
self.send(payload=ntp_server_packet, dest_ip_address=self.ntp_server)
|
||||
|
||||
def apply_timestep(self, timestep: int) -> None:
|
||||
"""
|
||||
For each timestep request the time from the NTP server.
|
||||
|
||||
In this instance, if any multi-timestep processes are currently
|
||||
occurring (such as restarting or installation), 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
|
||||
"""
|
||||
self.sys_log.info(f"{self.name} apply_timestep")
|
||||
super().apply_timestep(timestep)
|
||||
if self.operating_state == ServiceOperatingState.RUNNING:
|
||||
# request time from server
|
||||
self.request_time()
|
||||
else:
|
||||
self.sys_log.debug(f"{self.name} ntp client not running")
|
||||
73
src/primaite/simulator/system/services/ntp/ntp_server.py
Normal file
73
src/primaite/simulator/system/services/ntp/ntp_server.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.simulator.network.protocols.ntp import NTPPacket
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.services.service import Service
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class NTPServer(Service):
|
||||
"""Represents a NTP server as a service."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["name"] = "NTPServer"
|
||||
kwargs["port"] = Port.NTP
|
||||
kwargs["protocol"] = IPProtocol.TCP
|
||||
super().__init__(**kwargs)
|
||||
self.start()
|
||||
|
||||
def describe_state(self) -> Dict:
|
||||
"""
|
||||
Describes the current state of the software.
|
||||
|
||||
The specifics of the software's state, including its health, criticality,
|
||||
and any other pertinent information, should be implemented in subclasses.
|
||||
|
||||
:return: A dictionary containing key-value pairs representing the current
|
||||
state of the software.
|
||||
:rtype: Dict
|
||||
"""
|
||||
state = super().describe_state()
|
||||
return state
|
||||
|
||||
def reset_component_for_episode(self, episode: int):
|
||||
"""
|
||||
Resets the Service component for a new episode.
|
||||
|
||||
This method ensures the Service is ready for a new episode, including
|
||||
resetting any stateful properties or statistics, and clearing any message
|
||||
queues.
|
||||
"""
|
||||
pass
|
||||
|
||||
def receive(
|
||||
self,
|
||||
payload: NTPPacket,
|
||||
session_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""
|
||||
Receives a request from NTPClient.
|
||||
|
||||
Check that request has a valid IP address.
|
||||
|
||||
:param payload: The payload to send.
|
||||
:param session_id: Id of the session (Optional).
|
||||
|
||||
:return: True if valid NTP request else False.
|
||||
"""
|
||||
if not (isinstance(payload, NTPPacket)):
|
||||
_LOGGER.debug(f"{payload} is not a NTPPacket")
|
||||
return False
|
||||
payload: NTPPacket = payload
|
||||
|
||||
# generate a reply with the current time
|
||||
time = datetime.now()
|
||||
payload = payload.generate_reply(time)
|
||||
# send reply
|
||||
self.send(payload, session_id)
|
||||
return True
|
||||
@@ -137,8 +137,8 @@ class Software(SimComponent):
|
||||
state = super().describe_state()
|
||||
state.update(
|
||||
{
|
||||
"health_state": self.health_state_actual.value,
|
||||
"health_state_red_view": self.health_state_visible.value,
|
||||
"health_state_actual": self.health_state_actual.value,
|
||||
"health_state_visible": self.health_state_visible.value,
|
||||
"criticality": self.criticality.value,
|
||||
"patching_count": self.patching_count,
|
||||
"scanning_count": self.scanning_count,
|
||||
|
||||
86
tests/integration_tests/system/test_ntp_client_server.py
Normal file
86
tests/integration_tests/system/test_ntp_client_server.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from ipaddress import IPv4Address
|
||||
from time import sleep
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.protocols.ntp import NTPPacket
|
||||
from primaite.simulator.system.services.ntp.ntp_client import NTPClient
|
||||
from primaite.simulator.system.services.ntp.ntp_server import NTPServer
|
||||
from primaite.simulator.system.services.service import ServiceOperatingState
|
||||
|
||||
# Create simple network for testing
|
||||
# Define one node to be an NTP server and another node to be a NTP Client.
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def create_ntp_network(client_server) -> Tuple[NTPClient, Computer, NTPServer, Server]:
|
||||
"""
|
||||
+------------+ +------------+
|
||||
| ntp | | ntp |
|
||||
| client_1 +------------+ server_1 |
|
||||
| | | |
|
||||
+------------+ +------------+
|
||||
|
||||
"""
|
||||
client, server = client_server
|
||||
|
||||
server.power_on()
|
||||
server.software_manager.install(NTPServer)
|
||||
ntp_server: NTPServer = server.software_manager.software.get("NTPServer")
|
||||
ntp_server.start()
|
||||
|
||||
client.power_on()
|
||||
client.software_manager.install(NTPClient)
|
||||
ntp_client: NTPClient = client.software_manager.software.get("NTPClient")
|
||||
ntp_client.start()
|
||||
|
||||
return ntp_client, client, ntp_server, server
|
||||
|
||||
|
||||
def test_ntp_client_server(create_ntp_network):
|
||||
ntp_client, client, ntp_server, server = create_ntp_network
|
||||
|
||||
ntp_server: NTPServer = server.software_manager.software["NTPServer"]
|
||||
ntp_client: NTPClient = client.software_manager.software["NTPClient"]
|
||||
|
||||
assert ntp_server.operating_state == ServiceOperatingState.RUNNING
|
||||
assert ntp_client.operating_state == ServiceOperatingState.RUNNING
|
||||
ntp_client.configure(ntp_server_ip_address=IPv4Address("192.168.0.2"))
|
||||
|
||||
assert ntp_client.time is None
|
||||
ntp_client.request_time()
|
||||
assert ntp_client.time is not None
|
||||
first_time = ntp_client.time
|
||||
sleep(0.1)
|
||||
ntp_client.apply_timestep(1) # Check time advances
|
||||
second_time = ntp_client.time
|
||||
assert first_time < second_time
|
||||
|
||||
|
||||
# Test ntp client behaviour when ntp server is unavailable.
|
||||
def test_ntp_server_failure(create_ntp_network):
|
||||
ntp_client, client, ntp_server, server = create_ntp_network
|
||||
|
||||
ntp_server: NTPServer = server.software_manager.software["NTPServer"]
|
||||
ntp_client: NTPClient = client.software_manager.software["NTPClient"]
|
||||
|
||||
assert ntp_client.operating_state == ServiceOperatingState.RUNNING
|
||||
assert ntp_client.operating_state == ServiceOperatingState.RUNNING
|
||||
ntp_client.configure(ntp_server_ip_address=IPv4Address("192.168.0.2"))
|
||||
|
||||
# Turn off ntp server.
|
||||
ntp_server.stop()
|
||||
assert ntp_server.operating_state == ServiceOperatingState.STOPPED
|
||||
# And request a time update.
|
||||
ntp_client.request_time()
|
||||
assert ntp_client.time is None
|
||||
|
||||
# Restart ntp server.
|
||||
ntp_server.start()
|
||||
assert ntp_server.operating_state == ServiceOperatingState.RUNNING
|
||||
ntp_client.request_time()
|
||||
assert ntp_client.time is not None
|
||||
Reference in New Issue
Block a user