#2257: added common node attributes page + ability to set node operating state via config + tests

This commit is contained in:
Czar Echavez
2024-02-29 15:20:54 +00:00
parent eefc2739c8
commit 49a4e1fb56
18 changed files with 227 additions and 70 deletions

View File

@@ -0,0 +1,35 @@
.. only:: comment
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
.. _Node Attributes:
Common Attributes
#################
Node Attributes
===============
Attributes that are shared by all nodes.
.. include:: common_node_attributes.rst
.. _Network Node Attributes:
Network Node Attributes
=======================
Attributes that are shared by nodes that inherit from :py:mod:`primaite.simulator.network.hardware.nodes.network.network_node.NetworkNode`
.. include:: common_host_node_attributes.rst
.. _Host Node Attributes:
Host Node Attributes
====================
Attributes that are shared by nodes that inherit from :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.HostNode`
.. include:: common_host_node_attributes.rst
.. |NODE| replace:: node

View File

@@ -2,6 +2,8 @@
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
.. _common_host_node_attributes:
``ip_address``
--------------
@@ -19,13 +21,6 @@ The subnet mask for the |NODE| to use.
The IP address that the |NODE| will use as the default gateway. Typically, this is the IP address of the closest router that the |NODE| is connected to.
``dns_server``
--------------
Optional. Default value is ``None``
The IP address of the node which holds an instance of the :ref:`DNSServer`. Some applications may use a domain name e.g. the :ref:`WebBrowser`
.. include:: ../software/applications.rst
.. include:: ../software/services.rst

View File

@@ -2,6 +2,8 @@
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
.. _common_network_node_attributes:
``routes``
----------

View File

@@ -2,6 +2,8 @@
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
.. _common_node_attributes:
``ref``
-------
@@ -11,3 +13,43 @@ Human readable name used as reference for the |NODE|. Not used in code.
------------
The hostname of the |NODE|. This will be used to reference the |NODE|.
``operating_state``
-------------------
The initial operating state of the node.
Optional. Default value is ``ON``.
Options available are:
- ``ON``
- ``OFF``
- ``BOOTING``
- ``SHUTTING_DOWN``
Note that YAML may assume non quoted ``ON`` and ``OFF`` as ``True`` and ``False`` respectively. To prevent this, use ``"ON"`` or ``"OFF"``
See :py:mod:`primaite.simulator.network.hardware.node_operating_state.NodeOperatingState`
``dns_server``
--------------
Optional. Default value is ``None``.
The IP address of the node which holds an instance of the :ref:`DNSServer`. Some applications may use a domain name e.g. the :ref:`WebBrowser`
``start_up_duration``
---------------------
Optional. Default value is ``3``.
The number of time steps required to occur in order for the node to cycle from ``OFF`` to ``BOOTING_UP`` and then finally ``ON``.
``shut_down_duration``
----------------------
Optional. Default value is ``3``.
The number of time steps required to occur in order for the node to cycle from ``ON`` to ``SHUTTING_DOWN`` and then finally ``OFF``.

View File

@@ -12,34 +12,22 @@ complex, specialized hardware components inherit from and build upon.
The key elements defined in ``base.py`` are:
NetworkInterface
================
``NetworkInterface``
====================
- Abstract base class for network interfaces like NICs. Defines common attributes like MAC address, speed, MTU.
- Requires subclasses to implement ``enable()``, ``disable()``, ``send_frame()`` and ``receive_frame()``.
- Provides basic state description and request handling capabilities.
Node
====
``Node``
========
The Node class stands as a central component in ``base.py``, acting as the superclass for all network nodes within a
PrimAITE simulation.
Node Attributes
---------------
- **hostname**: The network hostname of the node.
- **operating_state**: Indicates the current hardware state of the node.
- **network_interfaces**: Maps interface names to NetworkInterface objects on the node.
- **network_interface**: Maps port IDs to ``NetworkInterface`` objects on the node.
- **dns_server**: Specifies DNS servers for domain name resolution.
- **start_up_duration**: The time it takes for the node to become fully operational after being powered on.
- **shut_down_duration**: The time required for the node to properly shut down.
- **sys_log**: A system log for recording events related to the node.
- **session_manager**: Manages user sessions within the node.
- **software_manager**: Controls the installation and management of software and services on the node.
See :ref:`Node Attributes`
.. _Node Start up and Shut down:

View File

@@ -79,7 +79,7 @@ Python
data_manipulation_bot.configure(server_ip_address=IPv4Address("192.168.1.14"), payload="DELETE")
data_manipulation_bot.run()
This would connect to the database service at 192.168.1.14, authenticate, and execute the SQL statement to drop the 'users' table.
This would connect to the database service at 192.168.1.14, authenticate, and execute the SQL statement to delete database contents.
Example with ``DataManipulationAgent``
""""""""""""""""""""""""""""""""""""""

View File

@@ -24,12 +24,6 @@ Usage
- Retrieve results in a dictionary.
- Disconnect when finished.
To create database backups:
- Configure the backup server on the :ref:`DatabaseService` by providing the Backup server ``IPv4Address`` with ``configure_backup``
- Create a backup using ``backup_database``. This fails if the backup server is not configured.
- Restore a backup using ``restore_backup``. By default, this uses the database created via ``backup_database``.
Implementation
==============

View File

@@ -13,4 +13,4 @@ The list of applications that are considered system software are:
- ``WebBrowser``
More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE`
More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.HostNode.SYSTEM_SOFTWARE`

View File

@@ -15,4 +15,4 @@ The list of services that are considered system software are:
- ``FTPClient``
- ``NTPClient``
More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE`
More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.HostNode.SYSTEM_SOFTWARE`

View File

@@ -10,7 +10,7 @@ Software
Base Software
-------------
All software which inherits ``IOSoftware`` installed on a node will not work unless the node has been turned on.
Software which inherits ``IOSoftware`` installed on a node will not work unless the node has been turned on.
See :ref:`Node Start up and Shut down`

View File

@@ -234,7 +234,9 @@ class PrimaiteGame:
subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")),
default_gateway=node_cfg["default_gateway"],
dns_server=node_cfg.get("dns_server", None),
operating_state=NodeOperatingState.ON,
operating_state=NodeOperatingState.ON
if not (p := node_cfg.get("operating_state"))
else NodeOperatingState[p.upper()],
)
elif n_type == "server":
new_node = Server(
@@ -243,13 +245,17 @@ class PrimaiteGame:
subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")),
default_gateway=node_cfg["default_gateway"],
dns_server=node_cfg.get("dns_server", None),
operating_state=NodeOperatingState.ON,
operating_state=NodeOperatingState.ON
if not (p := node_cfg.get("operating_state"))
else NodeOperatingState[p.upper()],
)
elif n_type == "switch":
new_node = Switch(
hostname=node_cfg["hostname"],
num_ports=int(node_cfg.get("num_ports", "8")),
operating_state=NodeOperatingState.ON,
operating_state=NodeOperatingState.ON
if not (p := node_cfg.get("operating_state"))
else NodeOperatingState[p.upper()],
)
elif n_type == "router":
new_node = Router.from_config(node_cfg)
@@ -359,7 +365,9 @@ class PrimaiteGame:
new_node.shut_down_duration = 0
net.add_node(new_node)
new_node.power_on()
# run through the power on step if the node is to be turned on at the start
if new_node.operating_state == NodeOperatingState.ON:
new_node.power_on()
game.ref_map_nodes[node_ref] = new_node.uuid
# set start up and shut down duration

View File

@@ -1,7 +1,7 @@
# flake8: noqa
"""Core of the PrimAITE Simulator."""
from abc import ABC, abstractmethod
from typing import Callable, ClassVar, Dict, List, Optional, Union
from abc import abstractmethod
from typing import Callable, Dict, List, Optional, Union
from uuid import uuid4
from pydantic import BaseModel, ConfigDict, Field

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from ipaddress import IPv4Address
from typing import Any, Dict, Optional
from typing import Any, ClassVar, Dict, Optional
from primaite import getLogger
from primaite.simulator.network.hardware.base import IPWiredNetworkInterface, Link, Node
@@ -253,17 +253,6 @@ class NIC(IPWiredNetworkInterface):
return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}"
SYSTEM_SOFTWARE = {
"HostARP": HostARP,
"ICMP": ICMP,
"DNSClient": DNSClient,
"FTPClient": FTPClient,
"NTPClient": NTPClient,
"WebBrowser": WebBrowser,
}
"""List of system software that is automatically installed on nodes."""
class HostNode(Node):
"""
Represents a host node in the network.
@@ -308,6 +297,16 @@ class HostNode(Node):
* Web Browser: Provides web browsing capabilities.
"""
SYSTEM_SOFTWARE: ClassVar[Dict] = {
"HostARP": HostARP,
"ICMP": ICMP,
"DNSClient": DNSClient,
"FTPClient": FTPClient,
"NTPClient": NTPClient,
"WebBrowser": WebBrowser,
}
"""List of system software that is automatically installed on nodes."""
network_interfaces: Dict[str, NIC] = {}
"The Network Interfaces on the node."
network_interface: Dict[int, NIC] = {}
@@ -324,7 +323,7 @@ class HostNode(Node):
This method equips the host with essential network services and applications, preparing it for various
network-related tasks and operations.
"""
for _, software_class in SYSTEM_SOFTWARE.items():
for _, software_class in self.SYSTEM_SOFTWARE.items():
self.software_manager.install(software_class)
super()._install_system_software()

View File

@@ -12,6 +12,8 @@ from primaite.simulator.network.hardware.nodes.network.router import (
RouterInterface,
)
from primaite.simulator.network.transmission.data_link_layer import Frame
from primaite.simulator.network.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.core.sys_log import SysLog
from primaite.utils.validators import IPV4Address
@@ -479,7 +481,12 @@ class Firewall(Router):
@classmethod
def from_config(cls, cfg: dict) -> "Firewall":
"""Create a firewall based on a config dict."""
firewall = Firewall(hostname=cfg["hostname"], operating_state=NodeOperatingState.ON)
firewall = Firewall(
hostname=cfg["hostname"],
operating_state=NodeOperatingState.ON
if not (p := cfg.get("operating_state"))
else NodeOperatingState[p.upper()],
)
if "ports" in cfg:
internal_port = cfg["ports"]["internal_port"]
external_port = cfg["ports"]["external_port"]
@@ -505,34 +512,82 @@ class Firewall(Router):
if "acl" in cfg:
# acl rules for internal_inbound_acl
if cfg["acl"]["internal_inbound_acl"]:
firewall.internal_inbound_acl.max_acl_rules
firewall.internal_inbound_acl._default_config = cfg["acl"]["internal_inbound_acl"]
firewall.internal_inbound_acl._reset_rules_to_default()
for r_num, r_cfg in cfg["acl"]["internal_inbound_acl"].items():
firewall.internal_inbound_acl.add_rule(
action=ACLAction[r_cfg["action"]],
src_port=None if not (p := r_cfg.get("src_port")) else Port[p],
dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p],
protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p],
src_ip_address=r_cfg.get("src_ip"),
dst_ip_address=r_cfg.get("dst_ip"),
position=r_num,
)
# acl rules for internal_outbound_acl
if cfg["acl"]["internal_outbound_acl"]:
firewall.internal_outbound_acl._default_config = cfg["acl"]["internal_outbound_acl"]
firewall.internal_outbound_acl._reset_rules_to_default()
for r_num, r_cfg in cfg["acl"]["internal_outbound_acl"].items():
firewall.internal_outbound_acl.add_rule(
action=ACLAction[r_cfg["action"]],
src_port=None if not (p := r_cfg.get("src_port")) else Port[p],
dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p],
protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p],
src_ip_address=r_cfg.get("src_ip"),
dst_ip_address=r_cfg.get("dst_ip"),
position=r_num,
)
# acl rules for dmz_inbound_acl
if cfg["acl"]["dmz_inbound_acl"]:
firewall.dmz_inbound_acl._default_config = cfg["acl"]["dmz_inbound_acl"]
firewall.dmz_inbound_acl._reset_rules_to_default()
for r_num, r_cfg in cfg["acl"]["dmz_inbound_acl"].items():
firewall.dmz_inbound_acl.add_rule(
action=ACLAction[r_cfg["action"]],
src_port=None if not (p := r_cfg.get("src_port")) else Port[p],
dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p],
protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p],
src_ip_address=r_cfg.get("src_ip"),
dst_ip_address=r_cfg.get("dst_ip"),
position=r_num,
)
# acl rules for dmz_outbound_acl
if cfg["acl"]["dmz_outbound_acl"]:
firewall.dmz_outbound_acl._default_config = cfg["acl"]["dmz_outbound_acl"]
firewall.dmz_outbound_acl._reset_rules_to_default()
for r_num, r_cfg in cfg["acl"]["dmz_outbound_acl"].items():
firewall.dmz_outbound_acl.add_rule(
action=ACLAction[r_cfg["action"]],
src_port=None if not (p := r_cfg.get("src_port")) else Port[p],
dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p],
protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p],
src_ip_address=r_cfg.get("src_ip"),
dst_ip_address=r_cfg.get("dst_ip"),
position=r_num,
)
# acl rules for external_inbound_acl
if cfg["acl"]["external_inbound_acl"]:
firewall.external_inbound_acl._default_config = cfg["acl"]["external_inbound_acl"]
firewall.external_inbound_acl._reset_rules_to_default()
for r_num, r_cfg in cfg["acl"]["external_inbound_acl"].items():
firewall.external_inbound_acl.add_rule(
action=ACLAction[r_cfg["action"]],
src_port=None if not (p := r_cfg.get("src_port")) else Port[p],
dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p],
protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p],
src_ip_address=r_cfg.get("src_ip"),
dst_ip_address=r_cfg.get("dst_ip"),
position=r_num,
)
# acl rules for external_outbound_acl
if cfg["acl"]["external_outbound_acl"]:
firewall.external_outbound_acl._default_config = cfg["acl"]["external_outbound_acl"]
firewall.external_outbound_acl._reset_rules_to_default()
for r_num, r_cfg in cfg["acl"]["external_outbound_acl"].items():
firewall.external_outbound_acl.add_rule(
action=ACLAction[r_cfg["action"]],
src_port=None if not (p := r_cfg.get("src_port")) else Port[p],
dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p],
protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p],
src_ip_address=r_cfg.get("src_ip"),
dst_ip_address=r_cfg.get("dst_ip"),
position=r_num,
)
if "routes" in cfg:
for route in cfg.get("routes"):
firewall.route_table.add_route(

View File

@@ -1401,7 +1401,9 @@ class Router(NetworkNode):
router = Router(
hostname=cfg["hostname"],
num_ports=int(cfg.get("num_ports", "5")),
operating_state=NodeOperatingState.ON,
operating_state=NodeOperatingState.ON
if not (p := cfg.get("operating_state"))
else NodeOperatingState[p.upper()],
)
if "ports" in cfg:
for port_num, port_cfg in cfg["ports"].items():

View File

@@ -141,6 +141,17 @@ simulation:
default_gateway: 192.168.10.1
dns_server: 192.168.1.10
# pre installed services and applications
- ref: client_3
type: computer
hostname: client_3
ip_address: 192.168.10.23
subnet_mask: 255.255.255.0
default_gateway: 192.168.10.1
dns_server: 192.168.1.10
start_up_duration: 0
shut_down_duration: 0
operating_state: "OFF"
# pre installed services and applications
links:
- ref: switch_1___client_1

View File

@@ -1,6 +1,8 @@
from primaite.config.load import example_config_path
from primaite.simulator.network.container import Network
from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
from primaite.simulator.network.hardware.nodes.host.computer import Computer
from tests.integration_tests.configuration_file_parsing import BASIC_CONFIG, DMZ_NETWORK, load_config
def test_example_config():
@@ -24,3 +26,19 @@ def test_dmz_config():
assert len(network.routers) == 2 # 2 routers in network
assert len(network.switches) == 3 # 3 switches in network
assert len(network.servers) == 2 # 2 servers in network
def test_basic_config():
"""Test that the basic_switched_network config can be parsed properly."""
game = load_config(BASIC_CONFIG)
network: Network = game.simulation.network
assert len(network.nodes) == 4 # 4 nodes in network
client_1: Computer = network.get_node_by_hostname("client_1")
assert client_1.operating_state == NodeOperatingState.ON
client_2: Computer = network.get_node_by_hostname("client_2")
assert client_2.operating_state == NodeOperatingState.ON
# client 3 should not be online
client_3: Computer = network.get_node_by_hostname("client_3")
assert client_3.operating_state == NodeOperatingState.OFF

View File

@@ -1,6 +1,14 @@
from ipaddress import IPv4Address
from pathlib import Path
from typing import Union
from primaite.game.game import APPLICATION_TYPES_MAPPING, SERVICE_TYPES_MAPPING
import yaml
from primaite.config.load import example_config_path
from primaite.game.agent.data_manipulation_bot import DataManipulationAgent
from primaite.game.agent.interface import ProxyAgent, RandomAgent
from primaite.game.game import APPLICATION_TYPES_MAPPING, PrimaiteGame, SERVICE_TYPES_MAPPING
from primaite.simulator.network.container import Network
from primaite.simulator.network.hardware.nodes.host.computer import Computer
from primaite.simulator.system.applications.database_client import DatabaseClient
from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot