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`
|
- FTP Services: `FTPClient` and `FTPServer`
|
||||||
- HTTP Services: `WebBrowser` to simulate a web client and `WebServer`
|
- 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
|
- 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
|
||||||
- Removed legacy simulation modules: `acl`, `common`, `environment`, `links`, `nodes`, `pol`
|
- 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_nics_per_node: int = 8, # allows calculating shape
|
||||||
max_acl_rules: int = 10, # allows calculating shape
|
max_acl_rules: int = 10, # allows calculating shape
|
||||||
protocols: List[str] = ["TCP", "UDP", "ICMP"], # allow mapping index to protocol
|
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.
|
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
|
act_map: Optional[Dict[int, Dict]] = None, # allows restricting set of possible actions
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -139,7 +139,10 @@ class ServiceObservation(AbstractObservation):
|
|||||||
service_state = access_from_nested_dict(state, self.where)
|
service_state = access_from_nested_dict(state, self.where)
|
||||||
if service_state is NOT_PRESENT_IN_STATE:
|
if service_state is NOT_PRESENT_IN_STATE:
|
||||||
return self.default_observation
|
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
|
@property
|
||||||
def space(self) -> spaces.Space:
|
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
|
:param where: Where in the simulation state dictionary to find the relevant information for this NIC. A typical
|
||||||
example may look like this:
|
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.
|
If None, this denotes that the NIC does not exist and the observation will be populated with zeroes.
|
||||||
:type where: Optional[Tuple[str]], optional
|
:type where: Optional[Tuple[str]], optional
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class DatabaseFileIntegrity(AbstractReward):
|
|||||||
node_hostname = config.get("node_hostname")
|
node_hostname = config.get("node_hostname")
|
||||||
folder_name = config.get("folder_name")
|
folder_name = config.get("folder_name")
|
||||||
file_name = config.get("file_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}"
|
msg = f"{cls.__name__} could not be initialised with parameters {config}"
|
||||||
_LOGGER.error(msg)
|
_LOGGER.error(msg)
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
@@ -148,8 +148,8 @@ class WebServer404Penalty(AbstractReward):
|
|||||||
|
|
||||||
:param node_hostname: Hostname of the node which contains the web server service.
|
:param node_hostname: Hostname of the node which contains the web server service.
|
||||||
:type node_hostname: str
|
:type node_hostname: str
|
||||||
:param service_node: Name of the web server service.
|
:param service_name: Name of the web server service.
|
||||||
:type service_node: str
|
:type service_name: str
|
||||||
"""
|
"""
|
||||||
self.location_in_state = ["network", "nodes", node_hostname, "services", service_name]
|
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.dns.dns_server import DNSServer
|
||||||
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
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.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
|
from primaite.simulator.system.services.web_server.web_server import WebServer
|
||||||
|
|
||||||
_LOGGER = getLogger(__name__)
|
_LOGGER = getLogger(__name__)
|
||||||
@@ -266,6 +268,8 @@ class PrimaiteGame:
|
|||||||
"WebServer": WebServer,
|
"WebServer": WebServer,
|
||||||
"FTPClient": FTPClient,
|
"FTPClient": FTPClient,
|
||||||
"FTPServer": FTPServer,
|
"FTPServer": FTPServer,
|
||||||
|
"NTPClient": NTPClient,
|
||||||
|
"NTPServer": NTPServer,
|
||||||
}
|
}
|
||||||
if service_type in service_types_mapping:
|
if service_type in service_types_mapping:
|
||||||
_LOGGER.debug(f"installing {service_type} on node {new_node.hostname}")
|
_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 = super().describe_state()
|
||||||
state.update(
|
state.update(
|
||||||
{
|
{
|
||||||
"health_state": self.health_state_actual.value,
|
"health_state_actual": self.health_state_actual.value,
|
||||||
"health_state_red_view": self.health_state_visible.value,
|
"health_state_visible": self.health_state_visible.value,
|
||||||
"criticality": self.criticality.value,
|
"criticality": self.criticality.value,
|
||||||
"patching_count": self.patching_count,
|
"patching_count": self.patching_count,
|
||||||
"scanning_count": self.scanning_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