#1816 - Added full documentation on the database client/server, and the internal frame processing process
This commit is contained in:
@@ -21,3 +21,4 @@ Contents
|
|||||||
simulation_components/network/router
|
simulation_components/network/router
|
||||||
simulation_components/network/switch
|
simulation_components/network/switch
|
||||||
simulation_components/network/network
|
simulation_components/network/network
|
||||||
|
simulation_components/internal_frame_processing
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
.. only:: comment
|
||||||
|
|
||||||
|
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||||
|
|
||||||
|
|
||||||
|
Database Client Server
|
||||||
|
======================
|
||||||
|
|
||||||
|
Database Service
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The ``DatabaseService`` provides a SQL database server simulation by extending the base Service class.
|
||||||
|
|
||||||
|
Key capabilities
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
- Initialises a SQLite database file in the ``Node``'s ``FileSystem`` upon creation.
|
||||||
|
- Handles connecting clients by maintaining a dictionary of connections mapped to session IDs.
|
||||||
|
- Authenticates connections using a configurable password.
|
||||||
|
- Executes SQL queries against the SQLite database.
|
||||||
|
- Returns query results and status codes back to clients.
|
||||||
|
- Leverages the Service base class for install/uninstall, status tracking, etc.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
^^^^^
|
||||||
|
- Install on a Node via the ``SoftwareManager`` to start the database service.
|
||||||
|
- Clients connect, execute queries, and disconnect.
|
||||||
|
- Service runs on TCP port 5432 by default.
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
- Uses SQLite for persistent storage.
|
||||||
|
- Creates the database file within the node's file system.
|
||||||
|
- Manages client connections in a dictionary by session ID.
|
||||||
|
- Processes SQL queries via the SQLite cursor and connection.
|
||||||
|
- Returns results and status codes in a standard dictionary format.
|
||||||
|
- Extends Service class for integration with ``SoftwareManager``.
|
||||||
|
|
||||||
|
Database Client
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The DatabaseClient provides a client interface for connecting to the ``DatabaseService``.
|
||||||
|
|
||||||
|
Key features
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
- Connects to the ``DatabaseService`` via the ``SoftwareManager``.
|
||||||
|
- Executes SQL queries and retrieves result sets.
|
||||||
|
- Handles connecting, querying, and disconnecting.
|
||||||
|
- Provides a simple ``query`` method for running SQL.
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
- Initialise with server IP address and optional password.
|
||||||
|
- Connect to the ``DatabaseService`` with ``connect``.
|
||||||
|
- Execute SQL queries via ``query``.
|
||||||
|
- Retrieve results in a dictionary.
|
||||||
|
- Disconnect when finished.
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
- Leverages ``SoftwareManager`` for sending payloads over the network.
|
||||||
|
- Connect and disconnect methods manage sessions.
|
||||||
|
- Provides easy interface for applications to query database.
|
||||||
|
- Payloads serialised as dictionaries for transmission.
|
||||||
|
- Extends base Application class.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
.. only:: comment
|
||||||
|
|
||||||
|
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||||
|
|
||||||
|
.. _about:
|
||||||
|
|
||||||
|
Internal Frame Processing
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Inbound
|
||||||
|
-------
|
||||||
|
|
||||||
|
At the NIC
|
||||||
|
^^^^^^^^^^
|
||||||
|
When a Frame is received on the Node's NIC:
|
||||||
|
|
||||||
|
- The NIC checks if it is enabled. If so, it will process the Frame.
|
||||||
|
- The Frame's received timestamp is set.
|
||||||
|
- The Frame is captured by the NIC's PacketCapture if configured.
|
||||||
|
- The NIC decrements the IP Packet's TTL by 1.
|
||||||
|
- The NIC calls the Node's ``receive_frame`` method, passing itself as the receiving NIC and the Frame.
|
||||||
|
|
||||||
|
|
||||||
|
At the Node
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
When ``receive_frame`` is called on the Node:
|
||||||
|
|
||||||
|
- The source IP address is added to the ARP cache if not already present.
|
||||||
|
- The Frame's protocol is checked:
|
||||||
|
- If ARP or ICMP, the Frame is passed to that protocol's handler method.
|
||||||
|
- Otherwise it is passed to the SessionManager's ``receive_frame`` method.
|
||||||
|
|
||||||
|
At the SessionManager
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
When ``receive_frame`` is called on the SessionManager:
|
||||||
|
|
||||||
|
- It extracts the key session details from the Frame:
|
||||||
|
- Protocol (TCP, UDP, etc)
|
||||||
|
- Source IP
|
||||||
|
- Destination IP
|
||||||
|
- Source Port
|
||||||
|
- Destination Port
|
||||||
|
- It checks if an existing Session matches these details.
|
||||||
|
- If no match, a new Session is created to represent this exchange.
|
||||||
|
- The payload and new/existing Session ID are passed to the SoftwareManager's ``receive_payload_from_session_manager`` method.
|
||||||
|
|
||||||
|
At the SoftwareManager
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Inside ``receive_payload_from_session_manager``:
|
||||||
|
|
||||||
|
- The SoftwareManager checks its port/protocol mapping to find which Service or Application is listening on the destination port and protocol.
|
||||||
|
- The payload and Session ID are forwarded to that receiver Service/Application instance via their ``receive`` method.
|
||||||
|
- The Service/Application can then process the payload as needed.
|
||||||
|
|
||||||
|
Outbound
|
||||||
|
--------
|
||||||
|
|
||||||
|
At the Service/Application
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
When a Service or Application needs to send a payload:
|
||||||
|
|
||||||
|
- It calls the SoftwareManager's ``send_payload_to_session_manager`` method.
|
||||||
|
- Passes the payload, and either destination IP and destination port for new payloads, or session id for existing sessions.
|
||||||
|
|
||||||
|
At the SoftwareManager
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Inside ``send_payload_to_session_manager``:
|
||||||
|
|
||||||
|
- The SoftwareManager forwards the payload and details through to to the SessionManager's ``receive_payload_from_software_manager`` method.
|
||||||
|
|
||||||
|
At the SessionManager
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
When ``receive_payload_from_software_manager`` is called:
|
||||||
|
|
||||||
|
- If a Session ID was provided, it looks up the Session.
|
||||||
|
- Gets the destination MAC address by checking the ARP cache.
|
||||||
|
- If no Session ID was provided, the destination Port, IP address and Mac Address are used along with the outbound IP Address and Mac Address to create a new Session.
|
||||||
|
- Calls `send_payload_to_nic`` to construct and send the Frame.
|
||||||
|
|
||||||
|
When ``send_payload_to_nic`` is called:
|
||||||
|
|
||||||
|
- It constructs a new Frame with the payload, using the source NIC's MAC, source IP, destination MAC, etc.
|
||||||
|
- The outbound NIC is looked up via the ARP cache based on destination IP.
|
||||||
|
- The constructed Frame is passed to the outbound NIC's ``send_frame`` method.
|
||||||
|
|
||||||
|
At the NIC
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
When ``send_frame`` is called:
|
||||||
|
|
||||||
|
- The NIC checks if it is enabled before sending.
|
||||||
|
- If enabled, it sends the Frame out to the connected Link.
|
||||||
|
|||||||
18
docs/source/simulation_components/network/software.rst
Normal file
18
docs/source/simulation_components/network/software.rst
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.. only:: comment
|
||||||
|
|
||||||
|
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||||
|
|
||||||
|
|
||||||
|
Software
|
||||||
|
========
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Contents
|
||||||
|
########
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 8
|
||||||
|
|
||||||
|
database_client_server
|
||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from random import choice
|
from random import choice
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class FileType(Enum):
|
class FileType(Enum):
|
||||||
@@ -95,7 +96,7 @@ class FileType(Enum):
|
|||||||
"Generic DB file. Used by sqlite3."
|
"Generic DB file. Used by sqlite3."
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _missing_(cls, value):
|
def _missing_(cls, value: Any) -> FileType:
|
||||||
return cls.UNKNOWN
|
return cls.UNKNOWN
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -118,7 +119,7 @@ class FileType(Enum):
|
|||||||
return size if size else 0
|
return size if size else 0
|
||||||
|
|
||||||
|
|
||||||
def get_file_type_from_extension(file_type_extension: str):
|
def get_file_type_from_extension(file_type_extension: str) -> FileType:
|
||||||
"""
|
"""
|
||||||
Get a FileType from a file type extension.
|
Get a FileType from a file type extension.
|
||||||
|
|
||||||
|
|||||||
@@ -960,6 +960,7 @@ class Node(SimComponent):
|
|||||||
return state
|
return state
|
||||||
|
|
||||||
def show(self, markdown: bool = False, component: Literal["NIC", "OPEN_PORTS"] = "NIC"):
|
def show(self, markdown: bool = False, component: Literal["NIC", "OPEN_PORTS"] = "NIC"):
|
||||||
|
"""A multi-use .show function that accepts either NIC or OPEN_PORTS."""
|
||||||
if component == "NIC":
|
if component == "NIC":
|
||||||
self._show_nic(markdown)
|
self._show_nic(markdown)
|
||||||
elif component == "OPEN_PORTS":
|
elif component == "OPEN_PORTS":
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from primaite.simulator.network.hardware.nodes.switch import Switch
|
|||||||
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.database_client import DatabaseClient
|
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||||
from primaite.simulator.system.services.database import DatabaseService
|
from primaite.simulator.system.services.database_service import DatabaseService
|
||||||
|
|
||||||
|
|
||||||
def client_server_routed() -> Network:
|
def client_server_routed() -> Network:
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ class Application(IOSoftware):
|
|||||||
return state
|
return state
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
"""Open the Application"""
|
"""Open the Application."""
|
||||||
if self.operating_state == ApplicationOperatingState.CLOSED:
|
if self.operating_state == ApplicationOperatingState.CLOSED:
|
||||||
self.sys_log.info(f"Running Application {self.name}")
|
self.sys_log.info(f"Running Application {self.name}")
|
||||||
self.operating_state = ApplicationOperatingState.RUNNING
|
self.operating_state = ApplicationOperatingState.RUNNING
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Close the Application"""
|
"""Close the Application."""
|
||||||
if self.operating_state == ApplicationOperatingState.RUNNING:
|
if self.operating_state == ApplicationOperatingState.RUNNING:
|
||||||
self.sys_log.info(f"Closed Application{self.name}")
|
self.sys_log.info(f"Closed Application{self.name}")
|
||||||
self.operating_state = ApplicationOperatingState.CLOSED
|
self.operating_state = ApplicationOperatingState.CLOSED
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ from primaite.simulator.system.core.software_manager import SoftwareManager
|
|||||||
|
|
||||||
|
|
||||||
class DatabaseClient(Application):
|
class DatabaseClient(Application):
|
||||||
|
"""
|
||||||
|
A DatabaseClient application.
|
||||||
|
|
||||||
|
Extends the Application class to provide functionality for connecting, querying, and disconnecting from a
|
||||||
|
Database Service. It mainly operates over TCP protocol.
|
||||||
|
|
||||||
|
:ivar server_ip_address: The IPv4 address of the Database Service server, defaults to None.
|
||||||
|
"""
|
||||||
|
|
||||||
server_ip_address: Optional[IPv4Address] = None
|
server_ip_address: Optional[IPv4Address] = None
|
||||||
connected: bool = False
|
connected: bool = False
|
||||||
|
|
||||||
@@ -20,9 +29,21 @@ class DatabaseClient(Application):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def describe_state(self) -> Dict:
|
def describe_state(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Describes the current state of the ACLRule.
|
||||||
|
|
||||||
|
:return: A dictionary representing the current state.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
return super().describe_state()
|
return super().describe_state()
|
||||||
|
|
||||||
def connect(self, server_ip_address: IPv4Address, password: Optional[str] = None) -> bool:
|
def connect(self, server_ip_address: IPv4Address, password: Optional[str] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Connect to a Database Service.
|
||||||
|
|
||||||
|
:param server_ip_address: The IPv4 Address of the Node the Database Service is running on.
|
||||||
|
:param password: The Database Service password. Is optional and has a default value of None.
|
||||||
|
"""
|
||||||
if not self.connected and self.operating_state.RUNNING:
|
if not self.connected and self.operating_state.RUNNING:
|
||||||
return self._connect(server_ip_address, password)
|
return self._connect(server_ip_address, password)
|
||||||
|
|
||||||
@@ -44,6 +65,7 @@ class DatabaseClient(Application):
|
|||||||
return self._connect(server_ip_address, password, True)
|
return self._connect(server_ip_address, password, True)
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
|
"""Disconnect from the Database Service."""
|
||||||
if self.connected and self.operating_state.RUNNING:
|
if self.connected and self.operating_state.RUNNING:
|
||||||
software_manager: SoftwareManager = self.software_manager
|
software_manager: SoftwareManager = self.software_manager
|
||||||
software_manager.send_payload_to_session_manager(
|
software_manager.send_payload_to_session_manager(
|
||||||
@@ -54,6 +76,11 @@ class DatabaseClient(Application):
|
|||||||
self.server_ip_address = None
|
self.server_ip_address = None
|
||||||
|
|
||||||
def query(self, sql: str):
|
def query(self, sql: str):
|
||||||
|
"""
|
||||||
|
Send a query to the Database Service.
|
||||||
|
|
||||||
|
:param sql: The SQL query.
|
||||||
|
"""
|
||||||
if self.connected and self.operating_state.RUNNING:
|
if self.connected and self.operating_state.RUNNING:
|
||||||
software_manager: SoftwareManager = self.software_manager
|
software_manager: SoftwareManager = self.software_manager
|
||||||
software_manager.send_payload_to_session_manager(
|
software_manager.send_payload_to_session_manager(
|
||||||
@@ -75,6 +102,13 @@ class DatabaseClient(Application):
|
|||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
||||||
|
"""
|
||||||
|
Receive a payload from the Software Manager.
|
||||||
|
|
||||||
|
:param payload: A payload to receive.
|
||||||
|
:param session_id: The session id the payload relates to.
|
||||||
|
:return: True.
|
||||||
|
"""
|
||||||
if isinstance(payload, dict) and payload.get("type"):
|
if isinstance(payload, dict) and payload.get("type"):
|
||||||
if payload["type"] == "connect_response":
|
if payload["type"] == "connect_response":
|
||||||
self.connected = payload["response"] == True
|
self.connected = payload["response"] == True
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from primaite.simulator.network.transmission.transport_layer import Port
|
|||||||
from primaite.simulator.system.applications.application import Application, ApplicationOperatingState
|
from primaite.simulator.system.applications.application import Application, ApplicationOperatingState
|
||||||
from primaite.simulator.system.core.sys_log import SysLog
|
from primaite.simulator.system.core.sys_log import SysLog
|
||||||
from primaite.simulator.system.services.service import Service, ServiceOperatingState
|
from primaite.simulator.system.services.service import Service, ServiceOperatingState
|
||||||
from primaite.simulator.system.software import IOSoftware, SoftwareType
|
from primaite.simulator.system.software import IOSoftware
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from primaite.simulator.system.core.session_manager import SessionManager
|
from primaite.simulator.system.core.session_manager import SessionManager
|
||||||
@@ -37,6 +37,11 @@ class SoftwareManager:
|
|||||||
self.file_system: FileSystem = file_system
|
self.file_system: FileSystem = file_system
|
||||||
|
|
||||||
def get_open_ports(self) -> List[Port]:
|
def get_open_ports(self) -> List[Port]:
|
||||||
|
"""
|
||||||
|
Get a list of open ports.
|
||||||
|
|
||||||
|
:return: A list of all open ports on the Node.
|
||||||
|
"""
|
||||||
open_ports = [Port.ARP]
|
open_ports = [Port.ARP]
|
||||||
for software in self.port_protocol_mapping.values():
|
for software in self.port_protocol_mapping.values():
|
||||||
if software.operating_state in {ApplicationOperatingState.RUNNING, ServiceOperatingState.RUNNING}:
|
if software.operating_state in {ApplicationOperatingState.RUNNING, ServiceOperatingState.RUNNING}:
|
||||||
@@ -45,6 +50,11 @@ class SoftwareManager:
|
|||||||
return open_ports
|
return open_ports
|
||||||
|
|
||||||
def install(self, software_class: Type[IOSoftwareClass]):
|
def install(self, software_class: Type[IOSoftwareClass]):
|
||||||
|
"""
|
||||||
|
Install an Application or Service.
|
||||||
|
|
||||||
|
:param software_class: The software class.
|
||||||
|
"""
|
||||||
if software_class in self._software_class_to_name_map:
|
if software_class in self._software_class_to_name_map:
|
||||||
self.sys_log.info(f"Cannot install {software_class} as it is already installed")
|
self.sys_log.info(f"Cannot install {software_class} as it is already installed")
|
||||||
return
|
return
|
||||||
@@ -59,6 +69,11 @@ class SoftwareManager:
|
|||||||
software.operating_state = ApplicationOperatingState.CLOSED
|
software.operating_state = ApplicationOperatingState.CLOSED
|
||||||
|
|
||||||
def uninstall(self, software_name: str):
|
def uninstall(self, software_name: str):
|
||||||
|
"""
|
||||||
|
Uninstall an Application or Service.
|
||||||
|
|
||||||
|
:param software_name: The software name.
|
||||||
|
"""
|
||||||
if software_name in self.software:
|
if software_name in self.software:
|
||||||
software = self.software.pop(software_name) # noqa
|
software = self.software.pop(software_name) # noqa
|
||||||
del software
|
del software
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ipaddress import IPv4Address
|
|
||||||
from sqlite3 import OperationalError
|
from sqlite3 import OperationalError
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
@@ -9,7 +8,6 @@ from prettytable import MARKDOWN, PrettyTable
|
|||||||
from primaite.simulator.file_system.file_system import File
|
from primaite.simulator.file_system.file_system import File
|
||||||
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.core.session_manager import Session
|
|
||||||
from primaite.simulator.system.core.software_manager import SoftwareManager
|
from primaite.simulator.system.core.software_manager import SoftwareManager
|
||||||
from primaite.simulator.system.services.service import Service, ServiceOperatingState
|
from primaite.simulator.system.services.service import Service, ServiceOperatingState
|
||||||
from primaite.simulator.system.software import SoftwareHealthState
|
from primaite.simulator.system.software import SoftwareHealthState
|
||||||
@@ -2,7 +2,7 @@ from ipaddress import IPv4Address
|
|||||||
|
|
||||||
from primaite.simulator.network.hardware.nodes.server import Server
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||||
from primaite.simulator.system.services.database import DatabaseService
|
from primaite.simulator.system.services.database_service import DatabaseService
|
||||||
|
|
||||||
|
|
||||||
def test_database_client_server_connection(uc2_network):
|
def test_database_client_server_connection(uc2_network):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import json
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from primaite.simulator.network.hardware.base import Node
|
from primaite.simulator.network.hardware.base import Node
|
||||||
from primaite.simulator.system.services.database import DatabaseService
|
from primaite.simulator.system.services.database_service import DatabaseService
|
||||||
|
|
||||||
DDL = """
|
DDL = """
|
||||||
CREATE TABLE IF NOT EXISTS user (
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
|
|||||||
Reference in New Issue
Block a user