From 275115f4ddc8e4d5db9561969f2b8aada1c43ae3 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 12 Sep 2023 12:12:38 +0000 Subject: [PATCH 01/15] Updated pull_request_template.md --- .azuredevops/pull_request_template.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.azuredevops/pull_request_template.md b/.azuredevops/pull_request_template.md index f7533b37..bd25cdc1 100644 --- a/.azuredevops/pull_request_template.md +++ b/.azuredevops/pull_request_template.md @@ -5,10 +5,12 @@ *How have you tested this (if applicable)?* ## Checklist -- [ ] This PR is linked to a **work item** -- [ ] I have performed **self-review** of the code -- [ ] I have written **tests** for any new functionality added with this PR -- [ ] I have updated the **documentation** if this PR changes or adds functionality -- [ ] I have written/updated **design docs** if this PR implements new functionality -- [ ] I have update the **change log** -- [ ] I have run **pre-commit** checks for code style +- [ ] PR is linked to a **work item** +- [ ] **acceptance criteria** of linked ticket are met +- [ ] performed **self-review** of the code +- [ ] written **tests** for any new functionality added with this PR +- [ ] updated the **documentation** if this PR changes or adds functionality +- [ ] written/updated **design docs** if this PR implements new functionality +- [ ] updated the **change log** +- [ ] ran **pre-commit** checks for code style +- [ ] attended to any **TO-DOs** left in the code From f91329405812a90c0bbff9deb21d2e3ad61fe9ba Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Mon, 18 Sep 2023 14:20:19 +0100 Subject: [PATCH 02/15] #1916: moved services into their own subdirectories --- src/primaite/simulator/network/networks.py | 6 +++--- src/primaite/simulator/system/services/database/__init__.py | 0 .../system/services/{ => database}/database_service.py | 0 src/primaite/simulator/system/services/dns/__init__.py | 0 .../simulator/system/services/{ => dns}/dns_client.py | 0 .../simulator/system/services/{ => dns}/dns_server.py | 0 .../test_uc2_data_manipulation_scenario.py | 2 +- tests/integration_tests/system/test_database_on_node.py | 2 +- tests/integration_tests/system/test_dns_client_server.py | 6 ++---- .../_primaite/_simulator/_system/_services/test_database.py | 4 +--- .../_primaite/_simulator/_system/_services/test_dns.py | 4 ++-- 11 files changed, 10 insertions(+), 14 deletions(-) create mode 100644 src/primaite/simulator/system/services/database/__init__.py rename src/primaite/simulator/system/services/{ => database}/database_service.py (100%) create mode 100644 src/primaite/simulator/system/services/dns/__init__.py rename src/primaite/simulator/system/services/{ => dns}/dns_client.py (100%) rename src/primaite/simulator/system/services/{ => dns}/dns_server.py (100%) diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 78d2e68f..f594c29a 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -9,9 +9,9 @@ from primaite.simulator.network.hardware.nodes.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.database_client import DatabaseClient -from primaite.simulator.system.services.database_service import DatabaseService -from primaite.simulator.system.services.dns_client import DNSClient -from primaite.simulator.system.services.dns_server import DNSServer +from primaite.simulator.system.services.database.database_service import DatabaseService +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.red_services.data_manipulation_bot import DataManipulationBot diff --git a/src/primaite/simulator/system/services/database/__init__.py b/src/primaite/simulator/system/services/database/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/system/services/database_service.py b/src/primaite/simulator/system/services/database/database_service.py similarity index 100% rename from src/primaite/simulator/system/services/database_service.py rename to src/primaite/simulator/system/services/database/database_service.py diff --git a/src/primaite/simulator/system/services/dns/__init__.py b/src/primaite/simulator/system/services/dns/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/system/services/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py similarity index 100% rename from src/primaite/simulator/system/services/dns_client.py rename to src/primaite/simulator/system/services/dns/dns_client.py diff --git a/src/primaite/simulator/system/services/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py similarity index 100% rename from src/primaite/simulator/system/services/dns_server.py rename to src/primaite/simulator/system/services/dns/dns_server.py diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index a859e5ff..50998f09 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -1,7 +1,7 @@ from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.system.applications.database_client import DatabaseClient -from primaite.simulator.system.services.database_service import DatabaseService +from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 2a77a31b..c69f131c 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -2,7 +2,7 @@ from ipaddress import IPv4Address from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.system.applications.database_client import DatabaseClient -from primaite.simulator.system.services.database_service import DatabaseService +from primaite.simulator.system.services.database.database_service import DatabaseService def test_database_client_server_connection(uc2_network): diff --git a/tests/integration_tests/system/test_dns_client_server.py b/tests/integration_tests/system/test_dns_client_server.py index 640c268a..e82d97a4 100644 --- a/tests/integration_tests/system/test_dns_client_server.py +++ b/tests/integration_tests/system/test_dns_client_server.py @@ -1,9 +1,7 @@ -from ipaddress import IPv4Address - from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server -from primaite.simulator.system.services.dns_client import DNSClient -from primaite.simulator.system.services.dns_server import DNSServer +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.service import ServiceOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py index d41c63c7..7662fbff 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py @@ -1,9 +1,7 @@ -import json - import pytest from primaite.simulator.network.hardware.base import Node -from primaite.simulator.system.services.database_service import DatabaseService +from primaite.simulator.system.services.database.database_service import DatabaseService @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py index b4f20539..f501d14a 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py @@ -6,8 +6,8 @@ from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.protocols.dns import DNSPacket, DNSReply, DNSRequest from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port -from primaite.simulator.system.services.dns_client import DNSClient -from primaite.simulator.system.services.dns_server import DNSServer +from primaite.simulator.system.services.dns.dns_client import DNSClient +from primaite.simulator.system.services.dns.dns_server import DNSServer @pytest.fixture(scope="function") From 2e76b3f1621d92e971184a46ec0f777673937c52 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Wed, 20 Sep 2023 16:23:35 +0100 Subject: [PATCH 03/15] #1916: FTP client STOR request to FTP server --- src/primaite/simulator/network/networks.py | 72 ++++++--- .../simulator/network/protocols/arp.py | 4 +- .../simulator/network/protocols/dns.py | 4 +- .../simulator/network/protocols/ftp.py | 55 +++++++ .../simulator/network/protocols/packet.py | 12 ++ .../network/transmission/data_link_layer.py | 5 + .../system/services/dns/dns_client.py | 1 + .../system/services/dns/dns_server.py | 1 + .../simulator/system/services/ftp/__init__.py | 0 .../system/services/ftp/ftp_client.py | 150 ++++++++++++++++++ .../system/services/ftp/ftp_server.py | 71 +++++++++ .../system/test_ftp_client_server.py | 59 +++++++ .../_simulator/_system/_services/test_ftp.py | 41 +++++ 13 files changed, 454 insertions(+), 21 deletions(-) create mode 100644 src/primaite/simulator/network/protocols/ftp.py create mode 100644 src/primaite/simulator/network/protocols/packet.py create mode 100644 src/primaite/simulator/system/services/ftp/__init__.py create mode 100644 src/primaite/simulator/system/services/ftp/ftp_client.py create mode 100644 src/primaite/simulator/system/services/ftp/ftp_server.py create mode 100644 tests/integration_tests/system/test_ftp_client_server.py create mode 100644 tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index f594c29a..63cb05e0 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -12,6 +12,8 @@ from primaite.simulator.system.applications.database_client import DatabaseClien from primaite.simulator.system.services.database.database_service import DatabaseService 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.red_services.data_manipulation_bot import DataManipulationBot @@ -136,13 +138,13 @@ def arcd_uc2_network() -> Network: ) client_1.power_on() client_1.software_manager.install(DNSClient) - client_1_dns_client_service: DNSServer = client_1.software_manager.software["DNSClient"] # noqa - client_1_dns_client_service.start() network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) client_1.software_manager.install(DataManipulationBot) db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"] db_manipulation_bot.configure(server_ip_address=IPv4Address("192.168.1.14"), payload="DROP TABLE IF EXISTS user;") + client_1.software_manager.install(FTPClient) + # Client 2 client_2 = Computer( hostname="client_2", @@ -153,10 +155,10 @@ def arcd_uc2_network() -> Network: ) client_2.power_on() client_2.software_manager.install(DNSClient) - client_2_dns_client_service: DNSServer = client_2.software_manager.software["DNSClient"] # noqa - client_2_dns_client_service.start() network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) + client_2.software_manager.install(FTPClient) + # Domain Controller domain_controller = Server( hostname="domain_controller", @@ -191,20 +193,48 @@ def arcd_uc2_network() -> Network: );""" user_insert_statements = [ - "INSERT INTO user (name, email, age, city, occupation) VALUES ('John Doe', 'johndoe@example.com', 32, 'New York', 'Engineer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Jane Smith', 'janesmith@example.com', 27, 'Los Angeles', 'Designer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Bob Johnson', 'bobjohnson@example.com', 45, 'Chicago', 'Manager');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Alice Lee', 'alicelee@example.com', 22, 'San Francisco', 'Student');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('David Kim', 'davidkim@example.com', 38, 'Houston', 'Consultant');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Emily Chen', 'emilychen@example.com', 29, 'Seattle', 'Software Developer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Frank Wang', 'frankwang@example.com', 55, 'New York', 'Entrepreneur');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Grace Park', 'gracepark@example.com', 31, 'Los Angeles', 'Marketing Specialist');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Henry Wu', 'henrywu@example.com', 40, 'Chicago', 'Accountant');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Isabella Kim', 'isabellakim@example.com', 26, 'San Francisco', 'Graphic Designer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Jake Lee', 'jakelee@example.com', 33, 'Houston', 'Sales Manager');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Kelly Chen', 'kellychen@example.com', 28, 'Seattle', 'Web Developer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Lucas Liu', 'lucasliu@example.com', 42, 'New York', 'Lawyer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Maggie Wang', 'maggiewang@example.com', 30, 'Los Angeles', 'Data Analyst');", # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('John Doe', 'johndoe@example.com', 32, 'New York', 'Engineer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Jane Smith', 'janesmith@example.com', 27, 'Los Angeles', 'Designer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Bob Johnson', 'bobjohnson@example.com', 45, 'Chicago', 'Manager');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Alice Lee', 'alicelee@example.com', 22, 'San Francisco', 'Student');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('David Kim', 'davidkim@example.com', 38, 'Houston', 'Consultant');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Emily Chen', 'emilychen@example.com', 29, 'Seattle', 'Software Developer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Frank Wang', 'frankwang@example.com', 55, 'New York', 'Entrepreneur');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Grace Park', 'gracepark@example.com', 31, 'Los Angeles', 'Marketing Specialist');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Henry Wu', 'henrywu@example.com', 40, 'Chicago', 'Accountant');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Isabella Kim', 'isabellakim@example.com', 26, 'San Francisco', 'Graphic Designer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Jake Lee', 'jakelee@example.com', 33, 'Houston', 'Sales Manager');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Kelly Chen', 'kellychen@example.com', 28, 'Seattle', 'Web Developer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Lucas Liu', 'lucasliu@example.com', 42, 'New York', 'Lawyer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Maggie Wang', 'maggiewang@example.com', 30, 'Los Angeles', 'Data Analyst');", + # noqa ] database_server.software_manager.install(DatabaseService) database_service: DatabaseService = database_server.software_manager.software["DatabaseService"] # noqa @@ -232,7 +262,6 @@ def arcd_uc2_network() -> Network: # register the web_server to a domain dns_server_service: DNSServer = domain_controller.software_manager.software["DNSServer"] # noqa - dns_server_service.start() dns_server_service.dns_register("arcd.com", web_server.ip_address) # Backup Server @@ -246,6 +275,8 @@ def arcd_uc2_network() -> Network: backup_server.power_on() network.connect(endpoint_b=backup_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[4]) + backup_server.software_manager.install(FTPServer) + # Security Suite security_suite = Server( hostname="security_suite", @@ -271,4 +302,7 @@ def arcd_uc2_network() -> Network: # Allow DNS requests router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.DNS, dst_port=Port.DNS, position=1) + # Allow FTP requests + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=2) + return network diff --git a/src/primaite/simulator/network/protocols/arp.py b/src/primaite/simulator/network/protocols/arp.py index 5e38cc66..7b3e4509 100644 --- a/src/primaite/simulator/network/protocols/arp.py +++ b/src/primaite/simulator/network/protocols/arp.py @@ -5,6 +5,8 @@ from typing import Optional from pydantic import BaseModel +from primaite.simulator.network.protocols.packet import DataPacket + class ARPEntry(BaseModel): """ @@ -18,7 +20,7 @@ class ARPEntry(BaseModel): nic_uuid: str -class ARPPacket(BaseModel): +class ARPPacket(DataPacket): """ Represents the ARP layer of a network frame. diff --git a/src/primaite/simulator/network/protocols/dns.py b/src/primaite/simulator/network/protocols/dns.py index 41bf5e0c..4f9be51b 100644 --- a/src/primaite/simulator/network/protocols/dns.py +++ b/src/primaite/simulator/network/protocols/dns.py @@ -5,6 +5,8 @@ from typing import Optional from pydantic import BaseModel +from primaite.simulator.network.protocols.packet import DataPacket + class DNSRequest(BaseModel): """Represents a DNS Request packet of a network frame. @@ -26,7 +28,7 @@ class DNSReply(BaseModel): "IP Address of the Domain Name requested." -class DNSPacket(BaseModel): +class DNSPacket(DataPacket): """ Represents the DNS layer of a network frame. diff --git a/src/primaite/simulator/network/protocols/ftp.py b/src/primaite/simulator/network/protocols/ftp.py new file mode 100644 index 00000000..ab277045 --- /dev/null +++ b/src/primaite/simulator/network/protocols/ftp.py @@ -0,0 +1,55 @@ +from enum import Enum +from typing import Any + +from primaite.simulator.network.protocols.packet import DataPacket + + +class FTPCommand(Enum): + """FTP Commands that are allowed.""" + + PORT = "PORT" + """Set a port to be used for the FTP transfer.""" + + STOR = "STOR" + """Copy or put data to the FTP server.""" + + RETR = "RETR" + """Retrieve data from the FTP server.""" + + DELE = "DELE" + """Delete the file in the specified path.""" + + RMD = "RMD" + """Remove the directory in the specified path.""" + + MKD = "MKD" + """Make a directory in the specified path.""" + + LIST = "LIST" + """Return a list of files in the specified path.""" + + QUIT = "QUIT" + """Ends connection between client and server.""" + + +class FTPStatusCode(Enum): + """Status code of the current FTP request.""" + + OK = 200 + """Command successful.""" + + ERROR = 500 + """General error code.""" + + +class FTPPacket(DataPacket): + """Represents an FTP Packet.""" + + ftp_command: FTPCommand + """Command type of the packet.""" + + ftp_command_args: Any + """Arguments for command.""" + + status_code: FTPStatusCode = None + """Status of the response.""" diff --git a/src/primaite/simulator/network/protocols/packet.py b/src/primaite/simulator/network/protocols/packet.py new file mode 100644 index 00000000..1adcc800 --- /dev/null +++ b/src/primaite/simulator/network/protocols/packet.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel + + +class DataPacket(BaseModel): + """Data packet abstract class.""" + + packet_payload_size: float = 0 + """Size of the packet.""" + + def get_packet_size(self) -> float: + """Returns the size of the packet header and payload.""" + return self.packet_payload_size + float(len(self.model_dump_json().encode("utf-8"))) diff --git a/src/primaite/simulator/network/transmission/data_link_layer.py b/src/primaite/simulator/network/transmission/data_link_layer.py index b7986622..fa823a60 100644 --- a/src/primaite/simulator/network/transmission/data_link_layer.py +++ b/src/primaite/simulator/network/transmission/data_link_layer.py @@ -5,6 +5,7 @@ from pydantic import BaseModel from primaite import getLogger from primaite.simulator.network.protocols.arp import ARPPacket +from primaite.simulator.network.protocols.packet import DataPacket from primaite.simulator.network.transmission.network_layer import ICMPPacket, IPPacket, IPProtocol from primaite.simulator.network.transmission.primaite_layer import PrimaiteHeader from primaite.simulator.network.transmission.transport_layer import TCPHeader, UDPHeader @@ -132,6 +133,10 @@ class Frame(BaseModel): @property def size(self) -> float: # noqa - Keep it as MBits as this is how they're expressed """The size of the Frame in Bytes.""" + # get the payload size if it is a data packet + if isinstance(self.payload, DataPacket): + return self.payload.get_packet_size() + return float(len(self.model_dump_json().encode("utf-8"))) @property diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index cf5278af..56d5d8b4 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -27,6 +27,7 @@ class DNSClient(Service): # TCP for now kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) + self.start() def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index c6a9afd3..c3c39595 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -26,6 +26,7 @@ class DNSServer(Service): # TCP for now kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) + self.start() def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/services/ftp/__init__.py b/src/primaite/simulator/system/services/ftp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py new file mode 100644 index 00000000..687e9a12 --- /dev/null +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -0,0 +1,150 @@ +from ipaddress import IPv4Address +from typing import Optional + +from primaite.simulator.file_system.file_system import File +from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.core.software_manager import SoftwareManager +from primaite.simulator.system.services.service import Service + + +class FTPClient(Service): + """ + A class for simulating an FTP client service. + + This class inherits from the `Service` class and provides methods to emulate FTP + RFC 959: https://datatracker.ietf.org/doc/html/rfc959 + """ + + connected: bool = False + """Keeps track of whether or not the FTP client is connected to an FTP server""" + + def __init__(self, **kwargs): + kwargs["name"] = "FTPClient" + kwargs["port"] = Port.FTP + kwargs["protocol"] = IPProtocol.TCP + super().__init__(**kwargs) + self.start() + + def _connect_to_server( + self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP + ) -> bool: + """ + Connects the client to a given FTP server. + + :param: dest_ip_address: IP address of the FTP server the client needs to connect to. Optional. + :type: Optional[IPv4Address] + :param: dest_port: Port of the FTP server the client needs to connect to. Optional. + :type: Optional[Port] + :param: server_password: The password to use when connecting to the FTP server. Optional. + :type: Optional[str] + """ + # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now + + # create FTP packet + payload: FTPPacket = FTPPacket( + ftp_command=FTPCommand.PORT, + ftp_command_args=Port.FTP, + ) + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port + ) + return payload.status_code == FTPStatusCode.OK + + def _disconnect_from_server( + self, + ftp_server_ip_address: Optional[IPv4Address] = None, + ) -> bool: + # send a disconnect request payload to FTP server + # return true if connected successfully else false + self.connected = False + + def _process_response(self, payload: FTPPacket): + """ + Process any FTPPacket responses. + + :param: payload: The FTPPacket payload + :type: FTPPacket + """ + if payload.ftp_command == FTPCommand.PORT: + if payload.status_code == FTPStatusCode.OK: + self.connected = True + + def send_file( + self, + dest_ip_address: IPv4Address, + src_folder_name: str, + src_file_name: str, + dest_folder_name: str, + dest_file_name: str, + dest_port: Optional[Port] = Port.FTP, + is_reattempt: Optional[bool] = False, + ) -> bool: + """Send a file to a target IP address.""" + file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name) + if not file_to_transfer: + self.sys_log.error(f"Unable to send file that does not exist: {src_folder_name}/{src_file_name}") + return False + + # check if FTP is currently connected to IP + self.connected = self._connect_to_server( + dest_ip_address=dest_ip_address, + dest_port=dest_port, + ) + + if not self.connected: + if is_reattempt: + return False + + return self.send_file( + src_folder_name=file_to_transfer.folder.name, + src_file_name=file_to_transfer.name, + dest_folder_name=dest_folder_name, + dest_file_name=dest_file_name, + dest_ip_address=dest_ip_address, + dest_port=dest_port, + is_reattempt=True, + ) + else: + # send STOR request + payload: FTPPacket = FTPPacket( + ftp_command=FTPCommand.STOR, + ftp_command_args={ + "dest_folder_name": dest_folder_name, + "dest_file_name": dest_file_name, + "file_size": file_to_transfer.sim_size, + }, + packet_payload_size=file_to_transfer.sim_size, + ) + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port + ) + + if payload.status_code == Port.FTP: + self._disconnect_from_server() + return True + + def request_file( + self, + dest_ip_address: IPv4Address, + src_folder_name: str, + src_file_name: str, + dest_folder_name: str, + dest_file_name: str, + dest_port: Optional[Port] = Port.FTP, + is_reattempt: Optional[bool] = False, + ) -> bool: + """Request a file from a target IP address.""" + pass + + def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool: + """Receives a payload from the SessionManager.""" + if not isinstance(payload, FTPPacket): + self.sys_log.error(f"{payload} is not an FTP packet") + return False + + self._process_response(payload=payload) + return True diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py new file mode 100644 index 00000000..ead6d503 --- /dev/null +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -0,0 +1,71 @@ +from typing import Any, Optional + +from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode +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 + + +class FTPServer(Service): + """ + A class for simulating an FTP server service. + + This class inherits from the `Service` class and provides methods to emulate FTP + RFC 959: https://datatracker.ietf.org/doc/html/rfc959 + """ + + server_password: Optional[str] = None + """Password needed to connect to FTP server. Default is None.""" + + def __init__(self, **kwargs): + kwargs["name"] = "FTPServer" + kwargs["port"] = Port.FTP + kwargs["protocol"] = IPProtocol.TCP + super().__init__(**kwargs) + self.start() + + def _process_ftp_command(self, payload: FTPPacket) -> FTPPacket: + # handle PORT request + if payload.ftp_command == FTPCommand.PORT: + # check that the port is valid + if isinstance(payload.ftp_command_args, Port) and payload.ftp_command_args.value in range(0, 65535): + # return successful connection + payload.status_code = FTPStatusCode.OK + + # handle STOR request + if payload.ftp_command == FTPCommand.STOR: + # check that the file is created in the computed hosting the FTP server + if self._process_store_data(payload=payload): + payload.status_code = FTPStatusCode.OK + + return payload + + def _process_store_data(self, payload: FTPPacket) -> bool: + """Handle the transfer of data from Client to this Server.""" + try: + file_name = payload.ftp_command_args["dest_file_name"] + folder_name = payload.ftp_command_args["dest_folder_name"] + file_size = payload.ftp_command_args["file_size"] + self.file_system.create_file( + file_name=file_name, + folder_name=folder_name, + size=file_size, + ) + self.sys_log.info( + f"Created item in {self.name}: {payload.ftp_command_args['dest_folder_name']}/" + f"{payload.ftp_command_args['dest_file_name']}" + ) + # file should exist + return self.file_system.get_file(file_name=file_name, folder_name=folder_name) is not None + except Exception as e: + self.sys_log.error(f"Unable to store file in {self.name}: {e}") + return False + + def receive(self, payload: Any, session_id: Optional[str] = None, **kwargs) -> bool: + """Receives a payload from the SessionManager.""" + if not isinstance(payload, FTPPacket): + self.sys_log.error(f"{payload} is not an FTP packet") + return False + + self.send(self._process_ftp_command(payload=payload), session_id) + return True diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py new file mode 100644 index 00000000..e062e0b7 --- /dev/null +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -0,0 +1,59 @@ +from ipaddress import IPv4Address + +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.server import Server +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.service import ServiceOperatingState + + +def test_ftp_client_store_file_in_server(uc2_network): + """ + Test checks to see if the client is able to store files in the backup server. + """ + 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 client + ftp_client.file_system.create_file(file_name="test_file.txt") + + ftp_client.send_file( + src_folder_name="root", + src_file_name="test_file.txt", + dest_folder_name="client_1_backup", + dest_file_name="test_file.txt", + dest_ip_address=backup_server.nics.get(next(iter(backup_server.nics))).ip_address, + ) + + assert ftp_server.file_system.get_file(folder_name="client_1_backup", file_name="test_file.txt") + + +def test_ftp_client_retrieve_file_from_server(uc2_network): + """ + Test checks to see if the client is able to retrieve files from the backup server. + """ + 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") + + 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, + ) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py new file mode 100644 index 00000000..64013207 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py @@ -0,0 +1,41 @@ +from ipaddress import IPv4Address + +import pytest + +from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer + + +@pytest.fixture(scope="function") +def ftp_server() -> Node: + node = Node(hostname="ftp_server") + node.software_manager.install(software_class=FTPServer) + node.software_manager.software["FTPServer"].start() + return node + + +@pytest.fixture(scope="function") +def ftp_client() -> Node: + node = Node(hostname="ftp_client") + node.software_manager.install(software_class=FTPClient) + node.software_manager.software["FTPClient"].start() + return node + + +def test_create_ftp_server(ftp_server): + assert ftp_server is not None + ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + assert ftp_server_service.name is "FTPServer" + assert ftp_server_service.port is Port.FTP + assert ftp_server_service.protocol is IPProtocol.TCP + + +def test_create_ftp_client(ftp_client): + assert ftp_client is not None + ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + assert ftp_client_service.name is "FTPClient" + assert ftp_client_service.port is Port.FTP + assert ftp_client_service.protocol is IPProtocol.TCP From 58edb6d3e493600bf1bca47dae76db42164f167a Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Thu, 21 Sep 2023 15:13:30 +0100 Subject: [PATCH 04/15] #1916: Created FTP superclass + working retrieve file method for FTP --- .../simulator/file_system/file_system.py | 2 +- .../system/services/ftp/ftp_client.py | 82 +++++++---- .../system/services/ftp/ftp_server.py | 45 +----- .../system/services/ftp/ftp_service.py | 132 ++++++++++++++++++ .../system/test_ftp_client_server.py | 3 + 5 files changed, 191 insertions(+), 73 deletions(-) create mode 100644 src/primaite/simulator/system/services/ftp/ftp_service.py diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index b2037729..9581a32b 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -226,7 +226,7 @@ class FileSystem(SimComponent): folder = self.get_folder(folder_name) if folder: return folder.get_file(file_name) - self.fs.sys_log.info(f"file not found /{folder_name}/{file_name}") + self.sys_log.info(f"file not found /{folder_name}/{file_name}") def delete_file(self, folder_name: str, file_name: str): """ diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 687e9a12..8e93df1b 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -6,10 +6,10 @@ from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPS from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.core.software_manager import SoftwareManager -from primaite.simulator.system.services.service import Service +from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC -class FTPClient(Service): +class FTPClient(FTPServiceABC): """ A class for simulating an FTP client service. @@ -61,17 +61,6 @@ class FTPClient(Service): # return true if connected successfully else false self.connected = False - def _process_response(self, payload: FTPPacket): - """ - Process any FTPPacket responses. - - :param: payload: The FTPPacket payload - :type: FTPPacket - """ - if payload.ftp_command == FTPCommand.PORT: - if payload.status_code == FTPStatusCode.OK: - self.connected = True - def send_file( self, dest_ip_address: IPv4Address, @@ -109,23 +98,13 @@ class FTPClient(Service): ) else: # send STOR request - payload: FTPPacket = FTPPacket( - ftp_command=FTPCommand.STOR, - ftp_command_args={ - "dest_folder_name": dest_folder_name, - "dest_file_name": dest_file_name, - "file_size": file_to_transfer.sim_size, - }, - packet_payload_size=file_to_transfer.sim_size, + return self._send_data( + file=file_to_transfer, + dest_folder_name=dest_folder_name, + dest_file_name=dest_file_name, + dest_ip_address=dest_ip_address, + dest_port=dest_port, ) - software_manager: SoftwareManager = self.software_manager - software_manager.send_payload_to_session_manager( - payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port - ) - - if payload.status_code == Port.FTP: - self._disconnect_from_server() - return True def request_file( self, @@ -138,7 +117,48 @@ class FTPClient(Service): is_reattempt: Optional[bool] = False, ) -> bool: """Request a file from a target IP address.""" - pass + # check if FTP is currently connected to IP + self.connected = self._connect_to_server( + dest_ip_address=dest_ip_address, + dest_port=dest_port, + ) + + if not self.connected: + if is_reattempt: + return False + + return self.request_file( + src_folder_name=src_folder_name, + src_file_name=src_file_name, + dest_folder_name=dest_folder_name, + dest_file_name=dest_file_name, + dest_ip_address=dest_ip_address, + dest_port=dest_port, + is_reattempt=True, + ) + else: + # send retrieve request + payload: FTPPacket = FTPPacket( + ftp_command=FTPCommand.RETR, + ftp_command_args={ + "src_folder_name": src_folder_name, + "src_file_name": src_file_name, + "dest_file_name": dest_file_name, + "dest_folder_name": dest_folder_name, + }, + ) + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port + ) + + # the payload should have ok status code + if payload.status_code == FTPStatusCode.OK: + self.sys_log.info(f"File {src_folder_name}/{src_file_name} found in FTP server.") + return True + else: + self.sys_log.error(f"File {src_folder_name}/{src_file_name} does not exist in FTP server") + return False def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool: """Receives a payload from the SessionManager.""" @@ -146,5 +166,5 @@ class FTPClient(Service): self.sys_log.error(f"{payload} is not an FTP packet") return False - self._process_response(payload=payload) + self._process_ftp_command(payload=payload, session_id=session_id) return True diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index ead6d503..c575479a 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -1,12 +1,12 @@ from typing import Any, Optional -from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode +from primaite.simulator.network.protocols.ftp import FTPPacket 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 +from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC -class FTPServer(Service): +class FTPServer(FTPServiceABC): """ A class for simulating an FTP server service. @@ -24,48 +24,11 @@ class FTPServer(Service): super().__init__(**kwargs) self.start() - def _process_ftp_command(self, payload: FTPPacket) -> FTPPacket: - # handle PORT request - if payload.ftp_command == FTPCommand.PORT: - # check that the port is valid - if isinstance(payload.ftp_command_args, Port) and payload.ftp_command_args.value in range(0, 65535): - # return successful connection - payload.status_code = FTPStatusCode.OK - - # handle STOR request - if payload.ftp_command == FTPCommand.STOR: - # check that the file is created in the computed hosting the FTP server - if self._process_store_data(payload=payload): - payload.status_code = FTPStatusCode.OK - - return payload - - def _process_store_data(self, payload: FTPPacket) -> bool: - """Handle the transfer of data from Client to this Server.""" - try: - file_name = payload.ftp_command_args["dest_file_name"] - folder_name = payload.ftp_command_args["dest_folder_name"] - file_size = payload.ftp_command_args["file_size"] - self.file_system.create_file( - file_name=file_name, - folder_name=folder_name, - size=file_size, - ) - self.sys_log.info( - f"Created item in {self.name}: {payload.ftp_command_args['dest_folder_name']}/" - f"{payload.ftp_command_args['dest_file_name']}" - ) - # file should exist - return self.file_system.get_file(file_name=file_name, folder_name=folder_name) is not None - except Exception as e: - self.sys_log.error(f"Unable to store file in {self.name}: {e}") - return False - def receive(self, payload: Any, session_id: Optional[str] = None, **kwargs) -> bool: """Receives a payload from the SessionManager.""" if not isinstance(payload, FTPPacket): self.sys_log.error(f"{payload} is not an FTP packet") return False - self.send(self._process_ftp_command(payload=payload), session_id) + self.send(self._process_ftp_command(payload=payload, session_id=session_id), session_id) return True diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py new file mode 100644 index 00000000..6214c510 --- /dev/null +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -0,0 +1,132 @@ +from abc import ABC +from ipaddress import IPv4Address +from typing import Optional + +from primaite.simulator.file_system.file_system import File +from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.core.software_manager import SoftwareManager +from primaite.simulator.system.services.service import Service + + +class FTPServiceABC(Service, ABC): + """ + Abstract Base Class for FTP Client and Service. + + Contains shared methods between both classes. + """ + + def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: + # handle PORT request + if payload.ftp_command == FTPCommand.PORT: + # check that the port is valid + if isinstance(payload.ftp_command_args, Port) and payload.ftp_command_args.value in range(0, 65535): + # return successful connection + payload.status_code = FTPStatusCode.OK + + # handle STOR request + if payload.ftp_command == FTPCommand.STOR: + # check that the file is created in the computed hosting the FTP server + if self._store_data(payload=payload): + payload.status_code = FTPStatusCode.OK + + if payload.ftp_command == FTPCommand.RETR: + # check that the file exists in the FTP Server + file: File = self.file_system.get_file( + folder_name=payload.ftp_command_args["src_folder_name"], + file_name=payload.ftp_command_args["src_file_name"], + ) + if file: + payload.status_code = FTPStatusCode.OK + self._send_data( + file=file, + dest_folder_name=payload.ftp_command_args["dest_folder_name"], + dest_file_name=payload.ftp_command_args["dest_file_name"], + session_id=session_id, + ) + + return payload + + def _store_data(self, payload: FTPPacket) -> bool: + """ + Handle the transfer of data. + + :param: payload: The FTP Packet that contains the file data + :type: FTPPacket + """ + try: + file_name = payload.ftp_command_args["dest_file_name"] + folder_name = payload.ftp_command_args["dest_folder_name"] + file_size = payload.ftp_command_args["file_size"] + self.file_system.create_file( + file_name=file_name, + folder_name=folder_name, + size=file_size, + ) + self.sys_log.info( + f"Created item in {self.sys_log.hostname}: {payload.ftp_command_args['dest_folder_name']}/" + f"{payload.ftp_command_args['dest_file_name']}" + ) + # file should exist + return self.file_system.get_file(file_name=file_name, folder_name=folder_name) is not None + except Exception as e: + self.sys_log.error(f"Unable to create file in {self.sys_log.hostname}: {e}") + return False + + def _send_data( + self, + file: File, + dest_folder_name: str, + dest_file_name: str, + dest_ip_address: Optional[IPv4Address] = None, + dest_port: Optional[Port] = None, + session_id: Optional[str] = None, + ) -> bool: + # send STOR request + payload: FTPPacket = FTPPacket( + ftp_command=FTPCommand.STOR, + ftp_command_args={ + "dest_folder_name": dest_folder_name, + "dest_file_name": dest_file_name, + "file_size": file.sim_size, + }, + packet_payload_size=file.sim_size, + ) + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id + ) + + if payload.status_code == FTPStatusCode.OK: + return True + + return False + + def _retrieve_data(self, payload: FTPPacket, session_id: Optional[str] = None) -> bool: + """ + Handle the transfer of data from Server to Client. + + :param: payload: The FTP Packet that contains the file data + :type: FTPPacket + """ + try: + # find the file + file_name = payload.ftp_command_args["src_file_name"] + folder_name = payload.ftp_command_args["src_folder_name"] + retrieved_file: File = self.file_system.get_file(folder_name=folder_name, file_name=file_name) + + # if file does not exist, return an error + if not retrieved_file: + self.sys_log.error( + f"File {payload.ftp_command_args['dest_folder_name']}/" + f"{payload.ftp_command_args['dest_file_name']} does not exist in {self.sys_log.hostname}" + ) + return False + else: + # send requested data + return self._send_data( + file=retrieved_file, dest_file_name=file_name, dest_folder_name=folder_name, session_id=session_id + ) + except Exception as e: + self.sys_log.error(f"Unable to retrieve file from {self.sys_log.hostname}: {e}") + return False diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index e062e0b7..fbbe6011 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -57,3 +57,6 @@ def test_ftp_client_retrieve_file_from_server(uc2_network): dest_file_name="test_file.txt", dest_ip_address=backup_server.nics.get(next(iter(backup_server.nics))).ip_address, ) + + # client should have retrieved the file + assert ftp_client.file_system.get_file(folder_name="downloads", file_name="test_file.txt") From 2c234ab67abbc18ca0dc22cb28773b2e2ae1c0e6 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Fri, 22 Sep 2023 15:38:01 +0100 Subject: [PATCH 05/15] #1916: Setting up a connected states + added tests + error states for if service is interacted with when not running --- .../simulator/network/protocols/ftp.py | 4 +- .../system/services/ftp/ftp_client.py | 40 +++++++++++++++--- .../system/services/ftp/ftp_server.py | 41 ++++++++++++++++++- .../system/services/ftp/ftp_service.py | 7 ---- .../_simulator/_system/_services/test_ftp.py | 41 +++++++++++++++++++ 5 files changed, 117 insertions(+), 16 deletions(-) diff --git a/src/primaite/simulator/network/protocols/ftp.py b/src/primaite/simulator/network/protocols/ftp.py index ab277045..91080219 100644 --- a/src/primaite/simulator/network/protocols/ftp.py +++ b/src/primaite/simulator/network/protocols/ftp.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any +from typing import Any, Optional from primaite.simulator.network.protocols.packet import DataPacket @@ -48,7 +48,7 @@ class FTPPacket(DataPacket): ftp_command: FTPCommand """Command type of the packet.""" - ftp_command_args: Any + ftp_command_args: Optional[Any] = None """Arguments for command.""" status_code: FTPStatusCode = None diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 8e93df1b..0e8f3dce 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -7,6 +7,7 @@ from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC +from primaite.simulator.system.services.service import ServiceOperatingState class FTPClient(FTPServiceABC): @@ -27,6 +28,15 @@ class FTPClient(FTPServiceABC): super().__init__(**kwargs) self.start() + def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: + # if server is down, return error + if self.operating_state != ServiceOperatingState.RUNNING: + payload.status_code = FTPStatusCode.ERROR + return payload + + # process client specific commands, otherwise call super + return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs) + def _connect_to_server( self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP ) -> bool: @@ -54,12 +64,18 @@ class FTPClient(FTPServiceABC): return payload.status_code == FTPStatusCode.OK def _disconnect_from_server( - self, - ftp_server_ip_address: Optional[IPv4Address] = None, + self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP ) -> bool: # send a disconnect request payload to FTP server - # return true if connected successfully else false - self.connected = False + payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.QUIT) + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port + ) + if payload.status_code == FTPStatusCode.OK: + self.connected = False + return True + return False def send_file( self, @@ -72,6 +88,10 @@ class FTPClient(FTPServiceABC): is_reattempt: Optional[bool] = False, ) -> bool: """Send a file to a target IP address.""" + # if service is not running, return error + if self.operating_state != ServiceOperatingState.RUNNING: + self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") + return False file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name) if not file_to_transfer: self.sys_log.error(f"Unable to send file that does not exist: {src_folder_name}/{src_file_name}") @@ -98,7 +118,7 @@ class FTPClient(FTPServiceABC): ) else: # send STOR request - return self._send_data( + self._send_data( file=file_to_transfer, dest_folder_name=dest_folder_name, dest_file_name=dest_file_name, @@ -106,6 +126,9 @@ class FTPClient(FTPServiceABC): dest_port=dest_port, ) + # send disconnect + return self._disconnect_from_server(dest_ip_address=dest_ip_address, dest_port=dest_port) + def request_file( self, dest_ip_address: IPv4Address, @@ -117,6 +140,10 @@ class FTPClient(FTPServiceABC): is_reattempt: Optional[bool] = False, ) -> bool: """Request a file from a target IP address.""" + # if service is not running, return error + if self.operating_state != ServiceOperatingState.RUNNING: + self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") + return False # check if FTP is currently connected to IP self.connected = self._connect_to_server( dest_ip_address=dest_ip_address, @@ -152,6 +179,9 @@ class FTPClient(FTPServiceABC): payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port ) + # send disconnect + self._disconnect_from_server(dest_ip_address=dest_ip_address, dest_port=dest_port) + # the payload should have ok status code if payload.status_code == FTPStatusCode.OK: self.sys_log.info(f"File {src_folder_name}/{src_file_name} found in FTP server.") diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index c575479a..6371d53a 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -1,9 +1,12 @@ -from typing import Any, Optional +from ipaddress import IPv4Address +from typing import Any, Dict, Optional -from primaite.simulator.network.protocols.ftp import FTPPacket +from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.core.session_manager import Session from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC +from primaite.simulator.system.services.service import ServiceOperatingState class FTPServer(FTPServiceABC): @@ -17,6 +20,9 @@ class FTPServer(FTPServiceABC): server_password: Optional[str] = None """Password needed to connect to FTP server. Default is None.""" + connections: Dict[str, IPv4Address] = {} + """Current active connections to the FTP server.""" + def __init__(self, **kwargs): kwargs["name"] = "FTPServer" kwargs["port"] = Port.FTP @@ -24,6 +30,37 @@ class FTPServer(FTPServiceABC): super().__init__(**kwargs) self.start() + def _get_session_details(self, session_id: str) -> Session: + """ + Returns the Session object from the given session id. + + :param: session_id: ID of the session that needs details retrieved + """ + return self.software_manager.session_manager.sessions_by_uuid[session_id] + + def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: + # if server is down, return error + if self.operating_state != ServiceOperatingState.RUNNING: + payload.status_code = FTPStatusCode.ERROR + return payload + + # process server specific commands, otherwise call super + if payload.ftp_command == FTPCommand.PORT: + # check that the port is valid + if isinstance(payload.ftp_command_args, Port) and payload.ftp_command_args.value in range(0, 65535): + # return successful connection + session_details = self._get_session_details(session_id) + self.connections[session_id] = session_details.with_ip_address + payload.status_code = FTPStatusCode.OK + return payload + + if payload.ftp_command == FTPCommand.QUIT: + session_details = self._get_session_details(session_id) + self.connections.pop(session_id) + payload.status_code = FTPStatusCode.OK + + return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs) + def receive(self, payload: Any, session_id: Optional[str] = None, **kwargs) -> bool: """Receives a payload from the SessionManager.""" if not isinstance(payload, FTPPacket): diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index 6214c510..a41d647c 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -17,13 +17,6 @@ class FTPServiceABC(Service, ABC): """ def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: - # handle PORT request - if payload.ftp_command == FTPCommand.PORT: - # check that the port is valid - if isinstance(payload.ftp_command_args, Port) and payload.ftp_command_args.value in range(0, 65535): - # return successful connection - payload.status_code = FTPStatusCode.OK - # handle STOR request if payload.ftp_command == FTPCommand.STOR: # check that the file is created in the computed hosting the FTP server diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py index 64013207..ea563a88 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address import pytest from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.services.ftp.ftp_client import FTPClient @@ -39,3 +40,43 @@ def test_create_ftp_client(ftp_client): assert ftp_client_service.name is "FTPClient" assert ftp_client_service.port is Port.FTP assert ftp_client_service.protocol is IPProtocol.TCP + + +def test_ftp_server_store_file(ftp_server): + """Test to make sure the FTP Server knows how to deal with request responses.""" + assert ftp_server.file_system.get_file(folder_name="downloads", file_name="file.txt") is None + + response: FTPPacket = FTPPacket( + ftp_command=FTPCommand.STOR, + ftp_command_args={ + "dest_folder_name": "downloads", + "dest_file_name": "file.txt", + "file_size": 24, + }, + packet_payload_size=24, + ) + + ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + ftp_server_service.receive(response) + + assert ftp_server.file_system.get_file(folder_name="downloads", file_name="file.txt") + + +def test_ftp_client_store_file(ftp_client): + """Test to make sure the FTP Client knows how to deal with request responses.""" + assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt") is None + + response: FTPPacket = FTPPacket( + ftp_command=FTPCommand.STOR, + ftp_command_args={ + "dest_folder_name": "downloads", + "dest_file_name": "file.txt", + "file_size": 24, + }, + packet_payload_size=24, + ) + + ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client_service.receive(response) + + assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt") From 2520b67889cdfcc2d01d590005203c492c5f1344 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Mon, 25 Sep 2023 14:31:57 +0100 Subject: [PATCH 06/15] #1916: - Added FTP to changelog - Added FTP to documentation - Added documentation in code - Clean up of methods - prevent repeats of the same code --- CHANGELOG.md | 3 +- .../system/ftp_client_server.rst | 62 ++++++++ .../simulation_components/system/software.rst | 1 + .../system/services/ftp/ftp_client.py | 148 ++++++++++++------ .../system/services/ftp/ftp_server.py | 14 +- .../system/services/ftp/ftp_service.py | 51 ++++-- 6 files changed, 215 insertions(+), 64 deletions(-) create mode 100644 docs/source/simulation_components/system/ftp_client_server.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index d9700f83..7147f82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,8 @@ SessionManager. 1. Creating a simulation - this notebook explains how to build up a simulation using the Python package. (WIP) - Red Agent Services: - Data Manipulator Bot - A red agent service which sends a payload to a target machine. (By default this payload is a SQL query that breaks a database) -- DNS Services: DNS Client and DNS Server +- DNS Services: `DNSClient` and `DNSServer` +- FTP Services: `FTPClient` and `FTPServer` ## [2.0.0] - 2023-07-26 diff --git a/docs/source/simulation_components/system/ftp_client_server.rst b/docs/source/simulation_components/system/ftp_client_server.rst new file mode 100644 index 00000000..084d4a85 --- /dev/null +++ b/docs/source/simulation_components/system/ftp_client_server.rst @@ -0,0 +1,62 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +FTP Client Server +================= + +FTP Server +---------- +Provides a FTP Client-Server simulation by extending the base Service class. + +Key capabilities +^^^^^^^^^^^^^^^^ + +- Simulates FTP requests and FTPPacket transfer across a network +- Allows the emulation of FTP commands between an FTP client and server: + - STOR: stores a file from client to server + - RETR: retrieves a file from the FTP server +- Leverages the Service base class for install/uninstall, status tracking, etc. + +Usage +^^^^^ +- Install on a Node via the ``SoftwareManager`` to start the FTP server service. +- Service runs on FTP (command) port 21 by default. (TODO: look at in depth implementation of FTP PORT command) + +Implementation +^^^^^^^^^^^^^^ + +- FTP request and responses use a ``FTPPacket`` object +- Extends Service class for integration with ``SoftwareManager``. + +FTP Client +---------- + +The ``FTPClient`` provides a client interface for connecting to the ``FTPServer``. + +Key features +^^^^^^^^^^^^ + +- Connects to the ``FTPServer`` via the ``SoftwareManager``. +- Simulates FTP requests and FTPPacket transfer across a network +- Allows the emulation of FTP commands between an FTP client and server: + - PORT: specifies the port that server should connect to on the client (currently only uses ``Port.FTP``) + - STOR: stores a file from client to server + - RETR: retrieves a file from the FTP server + - QUIT: disconnect from server +- Leverages the Service base class for install/uninstall, status tracking, etc. + +Usage +^^^^^ + +- Install on a Node via the ``SoftwareManager`` to start the FTP client service. +- Service runs on FTP (command) port 21 by default. (TODO: look at in depth implementation of FTP PORT command) +- Execute sending a file to the FTP server with ``send_file`` +- Execute retrieving a file from the FTP server with ``request_file`` + +Implementation +^^^^^^^^^^^^^^ + +- Leverages ``SoftwareManager`` for sending payloads over the network. +- Provides easy interface for Nodes to transfer files between each other. +- Extends base Service class. diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index 275fdaf9..921dfb9e 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -18,3 +18,4 @@ Contents database_client_server data_manipulation_bot dns_client_server + ftp_client_server diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 0e8f3dce..33fe32be 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -19,7 +19,7 @@ class FTPClient(FTPServiceABC): """ connected: bool = False - """Keeps track of whether or not the FTP client is connected to an FTP server""" + """Keeps track of whether or not the FTP client is connected to an FTP server.""" def __init__(self, **kwargs): kwargs["name"] = "FTPClient" @@ -29,7 +29,15 @@ class FTPClient(FTPServiceABC): self.start() def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: - # if server is down, return error + """ + Process the command in the FTP Packet. + + :param: payload: The FTP Packet to process + :type: payload: FTPPacket + :param: session_id: session ID linked to the FTP Packet. Optional. + :type: session_id: Optional[str] + """ + # if client service is down, return error if self.operating_state != ServiceOperatingState.RUNNING: payload.status_code = FTPStatusCode.ERROR return payload @@ -38,20 +46,27 @@ class FTPClient(FTPServiceABC): return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs) def _connect_to_server( - self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP + self, + dest_ip_address: Optional[IPv4Address] = None, + dest_port: Optional[Port] = Port.FTP, + is_reattempt: Optional[bool] = False, ) -> bool: """ Connects the client to a given FTP server. :param: dest_ip_address: IP address of the FTP server the client needs to connect to. Optional. - :type: Optional[IPv4Address] + :type: dest_ip_address: Optional[IPv4Address] :param: dest_port: Port of the FTP server the client needs to connect to. Optional. - :type: Optional[Port] - :param: server_password: The password to use when connecting to the FTP server. Optional. - :type: Optional[str] + :type: dest_port: Optional[Port] + :param: is_reattempt: Set to True if attempt to connect to FTP Server has been attempted. Default False. + :type: is_reattempt: Optional[bool] """ - # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now + # make sure the service is running before attempting + if self.operating_state != ServiceOperatingState.RUNNING: + self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") + return False + # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now # create FTP packet payload: FTPPacket = FTPPacket( ftp_command=FTPCommand.PORT, @@ -61,11 +76,30 @@ class FTPClient(FTPServiceABC): software_manager.send_payload_to_session_manager( payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port ) - return payload.status_code == FTPStatusCode.OK + + if payload.status_code == FTPStatusCode.OK: + return True + else: + if is_reattempt: + # reattempt failed + return False + else: + # try again + self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port, is_reattempt=True) def _disconnect_from_server( self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP ) -> bool: + """ + Connects the client from a given FTP server. + + :param: dest_ip_address: IP address of the FTP server the client needs to disconnect from. Optional. + :type: dest_ip_address: Optional[IPv4Address] + :param: dest_port: Port of the FTP server the client needs to disconnect from. Optional. + :type: dest_port: Optional[Port] + :param: is_reattempt: Set to True if attempt to disconnect from FTP Server has been attempted. Default False. + :type: is_reattempt: Optional[bool] + """ # send a disconnect request payload to FTP server payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.QUIT) software_manager: SoftwareManager = self.software_manager @@ -85,13 +119,32 @@ class FTPClient(FTPServiceABC): dest_folder_name: str, dest_file_name: str, dest_port: Optional[Port] = Port.FTP, - is_reattempt: Optional[bool] = False, ) -> bool: - """Send a file to a target IP address.""" - # if service is not running, return error - if self.operating_state != ServiceOperatingState.RUNNING: - self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") - return False + """ + Send a file to a target IP address. + + The function checks if the file exists in the FTP Client host. + The STOR command is then sent to the FTP Server. + + :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. + :type: dest_ip_address: IPv4Address + + :param: src_folder_name: The name of the folder that contains the file to send to the FTP Server. + :type: src_folder_name: str + + :param: src_file_name: The name of the file to send to the FTP Server. + :type: src_file_name: str + + :param: dest_folder_name: The name of the folder where the file will be stored in the FTP Server. + :type: dest_folder_name: str + + :param: dest_file_name: The name of the file to be saved on the FTP Server. + :type: dest_file_name: str + + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP. + :type: dest_port: Optional[Port] + """ + # check if the file to transfer exists on the client file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name) if not file_to_transfer: self.sys_log.error(f"Unable to send file that does not exist: {src_folder_name}/{src_file_name}") @@ -104,18 +157,7 @@ class FTPClient(FTPServiceABC): ) if not self.connected: - if is_reattempt: - return False - - return self.send_file( - src_folder_name=file_to_transfer.folder.name, - src_file_name=file_to_transfer.name, - dest_folder_name=dest_folder_name, - dest_file_name=dest_file_name, - dest_ip_address=dest_ip_address, - dest_port=dest_port, - is_reattempt=True, - ) + return False else: # send STOR request self._send_data( @@ -137,13 +179,30 @@ class FTPClient(FTPServiceABC): dest_folder_name: str, dest_file_name: str, dest_port: Optional[Port] = Port.FTP, - is_reattempt: Optional[bool] = False, ) -> bool: - """Request a file from a target IP address.""" - # if service is not running, return error - if self.operating_state != ServiceOperatingState.RUNNING: - self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") - return False + """ + Request a file from a target IP address. + + Sends a RETR command to the FTP Server. + + :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. + :type: dest_ip_address: IPv4Address + + :param: src_folder_name: The name of the folder that contains the file to send to the FTP Server. + :type: src_folder_name: str + + :param: src_file_name: The name of the file to send to the FTP Server. + :type: src_file_name: str + + :param: dest_folder_name: The name of the folder where the file will be stored in the FTP Server. + :type: dest_folder_name: str + + :param: dest_file_name: The name of the file to be saved on the FTP Server. + :type: dest_file_name: str + + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP. + :type: dest_port: Optional[Port] + """ # check if FTP is currently connected to IP self.connected = self._connect_to_server( dest_ip_address=dest_ip_address, @@ -151,18 +210,7 @@ class FTPClient(FTPServiceABC): ) if not self.connected: - if is_reattempt: - return False - - return self.request_file( - src_folder_name=src_folder_name, - src_file_name=src_file_name, - dest_folder_name=dest_folder_name, - dest_file_name=dest_file_name, - dest_ip_address=dest_ip_address, - dest_port=dest_port, - is_reattempt=True, - ) + return False else: # send retrieve request payload: FTPPacket = FTPPacket( @@ -191,7 +239,15 @@ class FTPClient(FTPServiceABC): return False def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool: - """Receives a payload from the SessionManager.""" + """ + Receives a payload from the SessionManager. + + :param: payload: FTPPacket payload. + :type: payload: FTPPacket + + :param: session_id: ID of the session. Optional. + :type: session_id: Optional[str] + """ if not isinstance(payload, FTPPacket): self.sys_log.error(f"{payload} is not an FTP packet") return False diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 6371d53a..1d028f0b 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -39,23 +39,31 @@ class FTPServer(FTPServiceABC): return self.software_manager.session_manager.sessions_by_uuid[session_id] def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: - # if server is down, return error + """ + Process the command in the FTP Packet. + + :param: payload: The FTP Packet to process + :type: payload: FTPPacket + :param: session_id: session ID linked to the FTP Packet. Optional. + :type: session_id: Optional[str] + """ + # if server service is down, return error if self.operating_state != ServiceOperatingState.RUNNING: payload.status_code = FTPStatusCode.ERROR return payload + session_details = self._get_session_details(session_id) + # process server specific commands, otherwise call super if payload.ftp_command == FTPCommand.PORT: # check that the port is valid if isinstance(payload.ftp_command_args, Port) and payload.ftp_command_args.value in range(0, 65535): # return successful connection - session_details = self._get_session_details(session_id) self.connections[session_id] = session_details.with_ip_address payload.status_code = FTPStatusCode.OK return payload if payload.ftp_command == FTPCommand.QUIT: - session_details = self._get_session_details(session_id) self.connections.pop(session_id) payload.status_code = FTPStatusCode.OK diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index a41d647c..f47b8f64 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -17,6 +17,14 @@ class FTPServiceABC(Service, ABC): """ def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: + """ + Process the command in the FTP Packet. + + :param: payload: The FTP Packet to process + :type: payload: FTPPacket + :param: session_id: session ID linked to the FTP Packet. Optional. + :type: session_id: Optional[str] + """ # handle STOR request if payload.ftp_command == FTPCommand.STOR: # check that the file is created in the computed hosting the FTP server @@ -24,25 +32,14 @@ class FTPServiceABC(Service, ABC): payload.status_code = FTPStatusCode.OK if payload.ftp_command == FTPCommand.RETR: - # check that the file exists in the FTP Server - file: File = self.file_system.get_file( - folder_name=payload.ftp_command_args["src_folder_name"], - file_name=payload.ftp_command_args["src_file_name"], - ) - if file: + if self._retrieve_data(payload=payload, session_id=session_id): payload.status_code = FTPStatusCode.OK - self._send_data( - file=file, - dest_folder_name=payload.ftp_command_args["dest_folder_name"], - dest_file_name=payload.ftp_command_args["dest_file_name"], - session_id=session_id, - ) return payload def _store_data(self, payload: FTPPacket) -> bool: """ - Handle the transfer of data. + Stores the data in the FTP Service's host machine. :param: payload: The FTP Packet that contains the file data :type: FTPPacket @@ -75,6 +72,27 @@ class FTPServiceABC(Service, ABC): dest_port: Optional[Port] = None, session_id: Optional[str] = None, ) -> bool: + """ + Sends data from the host FTP Service's machine to another FTP Service's host machine. + + :param: file: File to send to the target FTP Service. + :type: file: File + + :param: dest_folder_name: The name of the folder where the file will be stored in the FTP Server. + :type: dest_folder_name: str + + :param: dest_file_name: The name of the file to be saved on the FTP Server. + :type: dest_file_name: str + + :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. + :type: dest_ip_address: Optional[IPv4Address] + + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP. + :type: dest_port: Optional[Port] + + :param: session_id: session ID linked to the FTP Packet. Optional. + :type: session_id: Optional[str] + """ # send STOR request payload: FTPPacket = FTPPacket( ftp_command=FTPCommand.STOR, @@ -106,6 +124,8 @@ class FTPServiceABC(Service, ABC): # find the file file_name = payload.ftp_command_args["src_file_name"] folder_name = payload.ftp_command_args["src_folder_name"] + dest_folder_name = payload.ftp_command_args["dest_folder_name"] + dest_file_name = payload.ftp_command_args["dest_file_name"] retrieved_file: File = self.file_system.get_file(folder_name=folder_name, file_name=file_name) # if file does not exist, return an error @@ -118,7 +138,10 @@ class FTPServiceABC(Service, ABC): else: # send requested data return self._send_data( - file=retrieved_file, dest_file_name=file_name, dest_folder_name=folder_name, session_id=session_id + file=retrieved_file, + dest_file_name=dest_file_name, + dest_folder_name=dest_folder_name, + session_id=session_id, ) except Exception as e: self.sys_log.error(f"Unable to retrieve file from {self.sys_log.hostname}: {e}") From 0140fe7c48f9755565a941783f1d98efa4ad8749 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Mon, 25 Sep 2023 15:59:31 +0100 Subject: [PATCH 07/15] #1916: fix a problem with process_ftp_command method --- src/primaite/simulator/system/services/ftp/ftp_server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 1d028f0b..83c883f1 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -52,7 +52,8 @@ class FTPServer(FTPServiceABC): payload.status_code = FTPStatusCode.ERROR return payload - session_details = self._get_session_details(session_id) + if session_id: + session_details = self._get_session_details(session_id) # process server specific commands, otherwise call super if payload.ftp_command == FTPCommand.PORT: From 79615243e47ad0742d95f41d8f8ebf56de76fc86 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Tue, 26 Sep 2023 12:09:41 +0100 Subject: [PATCH 08/15] #1916: Added example usage for FTP --- .../system/ftp_client_server.rst | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/source/simulation_components/system/ftp_client_server.rst b/docs/source/simulation_components/system/ftp_client_server.rst index 084d4a85..0e4aeea3 100644 --- a/docs/source/simulation_components/system/ftp_client_server.rst +++ b/docs/source/simulation_components/system/ftp_client_server.rst @@ -60,3 +60,67 @@ Implementation - Leverages ``SoftwareManager`` for sending payloads over the network. - Provides easy interface for Nodes to transfer files between each other. - Extends base Service class. + + +Example Usage +---------- + +Dependencies +^^^^^^^^^^^^ + +.. code-block:: python + + from ipaddress import IPv4Address + + 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.system.services.ftp.ftp_server import FTPServer + from primaite.simulator.system.services.ftp.ftp_client import FTPClient + +Example peer to peer network +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + net = Network() + + pc1 = Computer(hostname="pc1", ip_address="120.10.10.10", subnet_mask="255.255.255.0") + srv = Server(hostname="srv", ip_address="120.10.10.20", subnet_mask="255.255.255.0") + pc1.power_on() + srv.power_on() + net.connect(pc1.ethernet_port[1], srv.ethernet_port[1]) + +Install the FTP Server +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + srv.software_manager.install(FTPServer) + pc1.software_manager.install(FTPClient) + client: FTPClient = pc1.software_manager.software['FTPClient'] + ftpserv: FTPServer = srv.software_manager.software['FTPServer'] + +Setting up the FTP Server +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Set up the FTP Server with a file that the client will need to retrieve + +.. code-block:: python + + srv.file_system.create_file('my_file.png') + +Check that file was retrieved +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + client.request_file( + src_folder_name='root', + src_file_name='my_file.png', + dest_folder_name='root', + dest_file_name='test.png', + dest_ip_address=IPv4Address("120.10.10.20") + ) + + print(client.get_file(folder_name="root", file_name="test.png")) From c096d06bcdf49f10769b320840ca1ee94ddda318 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Tue, 26 Sep 2023 15:14:24 +0100 Subject: [PATCH 09/15] #1796: pre installing system software --- .../system/ftp_client_server.rst | 4 ++-- src/primaite/simulator/network/hardware/base.py | 12 ++++++++++++ src/primaite/simulator/network/networks.py | 8 -------- .../_simulator/_system/_services/test_dns.py | 2 -- .../_simulator/_system/_services/test_ftp.py | 2 -- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/source/simulation_components/system/ftp_client_server.rst b/docs/source/simulation_components/system/ftp_client_server.rst index 0e4aeea3..94aef925 100644 --- a/docs/source/simulation_components/system/ftp_client_server.rst +++ b/docs/source/simulation_components/system/ftp_client_server.rst @@ -94,11 +94,11 @@ Example peer to peer network Install the FTP Server ^^^^^^^^^^^^^^^^^^^^^^ +FTP Client should be pre installed on nodes + .. code-block:: python srv.software_manager.install(FTPServer) - pc1.software_manager.install(FTPClient) - client: FTPClient = pc1.software_manager.software['FTPClient'] ftpserv: FTPServer = srv.software_manager.software['FTPServer'] Setting up the FTP Server diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index dd2130d2..00b1f097 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -25,6 +25,8 @@ from primaite.simulator.system.core.session_manager import SessionManager from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.processes.process import Process +from primaite.simulator.system.services.dns.dns_client import DNSClient +from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.service import Service _LOGGER = getLogger(__name__) @@ -937,6 +939,16 @@ class Node(SimComponent): self.arp.nics = self.nics self.session_manager.software_manager = self.software_manager + self._install_system_software() + + def _install_system_software(self): + """Install System Software - software that is usually provided with the OS.""" + # DNS Client + self.software_manager.install(DNSClient) + + # FTP + self.software_manager.install(FTPClient) + def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 63cb05e0..6122146b 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -10,9 +10,7 @@ from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.services.database.database_service import DatabaseService -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.red_services.data_manipulation_bot import DataManipulationBot @@ -137,14 +135,11 @@ def arcd_uc2_network() -> Network: dns_server=IPv4Address("192.168.1.10"), ) client_1.power_on() - client_1.software_manager.install(DNSClient) network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) client_1.software_manager.install(DataManipulationBot) db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"] db_manipulation_bot.configure(server_ip_address=IPv4Address("192.168.1.14"), payload="DROP TABLE IF EXISTS user;") - client_1.software_manager.install(FTPClient) - # Client 2 client_2 = Computer( hostname="client_2", @@ -154,11 +149,8 @@ def arcd_uc2_network() -> Network: dns_server=IPv4Address("192.168.1.10"), ) client_2.power_on() - client_2.software_manager.install(DNSClient) network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) - client_2.software_manager.install(FTPClient) - # Domain Controller domain_controller = Server( hostname="domain_controller", diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py index f501d14a..d86791cd 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py @@ -21,8 +21,6 @@ def dns_server() -> Node: @pytest.fixture(scope="function") def dns_client() -> Node: node = Node(hostname="dns_client") - node.software_manager.install(software_class=DNSClient) - node.software_manager.software["DNSClient"].start() return node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py index ea563a88..fce4a487 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py @@ -21,8 +21,6 @@ def ftp_server() -> Node: @pytest.fixture(scope="function") def ftp_client() -> Node: node = Node(hostname="ftp_client") - node.software_manager.install(software_class=FTPClient) - node.software_manager.software["FTPClient"].start() return node From 6202d320a6948b6831bd8801ca79c4b3f3447eaf Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Thu, 28 Sep 2023 12:23:49 +0100 Subject: [PATCH 10/15] #1796: Add ability to create and restore database backups + add more sys log messages + remove the link size checks temporarily --- .../simulator/network/hardware/base.py | 4 +- src/primaite/simulator/network/networks.py | 1 + .../services/database/database_service.py | 101 ++++++++++++++++++ .../system/services/ftp/ftp_client.py | 12 +-- .../system/services/ftp/ftp_server.py | 3 + .../system/services/ftp/ftp_service.py | 9 +- .../system/test_database_on_node.py | 28 +++++ .../system/test_ftp_client_server.py | 4 +- 8 files changed, 146 insertions(+), 16 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 00b1f097..7c08f9fc 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -498,7 +498,9 @@ class Link(SimComponent): def _can_transmit(self, frame: Frame) -> bool: if self.is_up: frame_size_Mbits = frame.size_Mbits # noqa - Leaving it as Mbits as this is how they're expressed - return self.current_load + frame_size_Mbits <= self.bandwidth + # return self.current_load + frame_size_Mbits <= self.bandwidth + # TODO: re add this check once packet size limiting and MTU checks are implemented + return True return False def transmit_frame(self, sender_nic: Union[NIC, SwitchPort], frame: Frame) -> bool: diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 6122146b..f54e1172 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -231,6 +231,7 @@ def arcd_uc2_network() -> Network: database_server.software_manager.install(DatabaseService) database_service: DatabaseService = database_server.software_manager.software["DatabaseService"] # noqa database_service.start() + database_service.configure_backup(backup_server=IPv4Address("192.168.1.16")) database_service._process_sql(ddl, None) # noqa for insert_statement in user_insert_statements: database_service._process_sql(insert_statement, None) # noqa diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 62120fc7..268bd54f 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -1,5 +1,6 @@ import sqlite3 from datetime import datetime +from ipaddress import IPv4Address from sqlite3 import OperationalError from typing import Any, Dict, List, Optional, Union @@ -9,6 +10,7 @@ from primaite.simulator.file_system.file_system import File from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.core.software_manager import SoftwareManager +from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState @@ -23,6 +25,15 @@ class DatabaseService(Service): password: Optional[str] = None connections: Dict[str, datetime] = {} + backup_server: IPv4Address = None + """IP address of the backup server.""" + + latest_backup_directory: str = None + """Directory of latest backup.""" + + latest_backup_file_name: str = None + """File name of latest backup.""" + def __init__(self, **kwargs): kwargs["name"] = "DatabaseService" kwargs["port"] = Port.POSTGRES_SERVER @@ -58,6 +69,96 @@ class DatabaseService(Service): table.add_row([row]) print(table) + def configure_backup(self, backup_server: IPv4Address): + """ + Set up the database backup. + + :param: backup_server_ip: The IP address of the backup server + """ + self.backup_server = backup_server + + def backup_database( + self, backup_directory: Optional[str] = "db_backup", backup_file_name: Optional[str] = None + ) -> bool: + """ + Create a backup of the database to the configured backup server. + + :param: backup_directory: Name of directory where backup will be stored. Optional. + :type: backup_directory: Optional[str] + + :param: backup_file_name: Name of file where backup will be stored. Optional. + :type: backup_file_name: Optional[str] + """ + # check if the backup server was configured + if self.backup_server is None: + self.sys_log.error(f"{self.name} - {self.sys_log.hostname}: not configured.") + return False + + if backup_file_name is None: + backup_file_name = f"{datetime.now().strftime('%d-%m-%Y_%H-%M')}.db" + + software_manager: SoftwareManager = self.software_manager + ftp_client_service: FTPClient = software_manager.software["FTPClient"] + + # send backup copy of database file to FTP server + response = ftp_client_service.send_file( + dest_ip_address=self.backup_server, + src_file_name=self._db_file.name, + src_folder_name=self._db_file.folder.name, + dest_folder_name=backup_directory, + dest_file_name=backup_file_name, + ) + + if response: + self.latest_backup_directory = backup_directory + self.latest_backup_file_name = backup_file_name + return True + + self.sys_log.error("Unable to create database backup.") + return False + + def restore_backup(self, backup_directory: Optional[str] = None, backup_file_name: Optional[str] = None) -> bool: + """ + Restore a backup from backup server. + + :param: backup_directory: Name of directory where backup will be stored. Optional. + :type: backup_directory: Optional[str] + + :param: backup_file_name: Name of file where backup will be stored. Optional. + :type: backup_file_name: Optional[str] + """ + if backup_directory is None: + backup_directory = self.latest_backup_directory + + if backup_file_name is None: + backup_file_name = self.latest_backup_file_name + + software_manager: SoftwareManager = self.software_manager + ftp_client_service: FTPClient = software_manager.software["FTPClient"] + + # retrieve backup file from backup server + response = ftp_client_service.request_file( + src_folder_name=backup_directory, + src_file_name=backup_file_name, + dest_folder_name="downloads", + dest_file_name="database.db", + dest_ip_address=self.backup_server, + ) + + if response: + # replace db file + self.file_system.delete_file(folder_name=self.folder.name, file_name="downloads.db") + + self.file_system.move_file( + src_folder_name="downloads", src_file_name="database.db", dst_folder_name=self.folder.name + ) + self._db_file = self.file_system.get_file(folder_name=self.folder.name, file_name="database.db") + + return self._db_file is not None + + self.sys_log.error("Unable to restore database backup.") + return False + def _create_db_file(self): """Creates the Simulation File and sqlite file in the file system.""" self._db_file: File = self.file_system.create_file(folder_name="database", file_name="database.db", real=True) diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 33fe32be..8359e8a0 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -107,7 +107,7 @@ class FTPClient(FTPServiceABC): payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port ) if payload.status_code == FTPStatusCode.OK: - self.connected = False + self.connected = None return True return False @@ -159,8 +159,9 @@ class FTPClient(FTPServiceABC): if not self.connected: return False else: + self.sys_log.info(f"Sending file {src_folder_name}/{src_file_name} to {str(dest_ip_address)}") # send STOR request - self._send_data( + return self._send_data( file=file_to_transfer, dest_folder_name=dest_folder_name, dest_file_name=dest_file_name, @@ -168,9 +169,6 @@ class FTPClient(FTPServiceABC): dest_port=dest_port, ) - # send disconnect - return self._disconnect_from_server(dest_ip_address=dest_ip_address, dest_port=dest_port) - def request_file( self, dest_ip_address: IPv4Address, @@ -222,14 +220,12 @@ class FTPClient(FTPServiceABC): "dest_folder_name": dest_folder_name, }, ) + self.sys_log.info(f"Requesting file {src_folder_name}/{src_file_name} from {str(dest_ip_address)}") software_manager: SoftwareManager = self.software_manager software_manager.send_payload_to_session_manager( payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port ) - # send disconnect - self._disconnect_from_server(dest_ip_address=dest_ip_address, dest_port=dest_port) - # the payload should have ok status code if payload.status_code == FTPStatusCode.OK: self.sys_log.info(f"File {src_folder_name}/{src_file_name} found in FTP server.") diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 83c883f1..93f8b45b 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -55,6 +55,9 @@ class FTPServer(FTPServiceABC): if session_id: session_details = self._get_session_details(session_id) + if payload.ftp_command is not None: + self.sys_log.info(f"Received FTP {payload.ftp_command.name} command.") + # process server specific commands, otherwise call super if payload.ftp_command == FTPCommand.PORT: # check that the port is valid diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index f47b8f64..c6d63751 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -25,6 +25,9 @@ class FTPServiceABC(Service, ABC): :param: session_id: session ID linked to the FTP Packet. Optional. :type: session_id: Optional[str] """ + if payload.ftp_command is not None: + self.sys_log.info(f"Received FTP {payload.ftp_command.name} command.") + # handle STOR request if payload.ftp_command == FTPCommand.STOR: # check that the file is created in the computed hosting the FTP server @@ -48,11 +51,7 @@ class FTPServiceABC(Service, ABC): file_name = payload.ftp_command_args["dest_file_name"] folder_name = payload.ftp_command_args["dest_folder_name"] file_size = payload.ftp_command_args["file_size"] - self.file_system.create_file( - file_name=file_name, - folder_name=folder_name, - size=file_size, - ) + self.file_system.create_file(file_name=file_name, folder_name=folder_name, size=file_size, real=True) self.sys_log.info( f"Created item in {self.sys_log.hostname}: {payload.ftp_command_args['dest_folder_name']}/" f"{payload.ftp_command_args['dest_file_name']}" diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index c69f131c..38196041 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.simulator.system.services.ftp.ftp_server import FTPServer def test_database_client_server_connection(uc2_network): @@ -57,3 +58,30 @@ def test_database_client_query(uc2_network): db_client.connect() assert db_client.query("SELECT * FROM user;") + + +def test_create_database_backup(uc2_network): + """Run the backup_database method and check if the FTP server has the relevant file.""" + db_server: Server = uc2_network.get_node_by_hostname("database_server") + db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + + # back up should be created + assert db_service.backup_database(backup_file_name="test_file.db") is True + + backup_server: Server = uc2_network.get_node_by_hostname("backup_server") + ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] + + # backup file should exist in the backup server + assert ftp_server.file_system.get_file(folder_name="db_backup", file_name="test_file.db") is not None + + +def test_restore_backup(uc2_network): + """Run the restore_backup method and check if the backup is properly restored.""" + db_server: Server = uc2_network.get_node_by_hostname("database_server") + db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + + # create a back up + assert db_service.backup_database(backup_file_name="test_file.db") is True + + # back up should be restored + assert db_service.restore_backup(backup_file_name="test_file.db") is True diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index fbbe6011..48dc2960 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -23,7 +23,7 @@ def test_ftp_client_store_file_in_server(uc2_network): # create file on ftp client ftp_client.file_system.create_file(file_name="test_file.txt") - ftp_client.send_file( + assert ftp_client.send_file( src_folder_name="root", src_file_name="test_file.txt", dest_folder_name="client_1_backup", @@ -50,7 +50,7 @@ def test_ftp_client_retrieve_file_from_server(uc2_network): # create file on ftp server ftp_server.file_system.create_file(file_name="test_file.txt", folder_name="file_share") - ftp_client.request_file( + assert ftp_client.request_file( src_folder_name="file_share", src_file_name="test_file.txt", dest_folder_name="downloads", From bca3e6344eab3d7fe310fcb316420afd47e981a7 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Thu, 28 Sep 2023 14:09:32 +0100 Subject: [PATCH 11/15] #1796: documentation --- CHANGELOG.md | 3 +++ .../simulation_components/system/database_client_server.rst | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7147f82b..cc9c26d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ SessionManager. - File System - ability to emulate a node's file system during a simulation - Example notebooks - There is currently 1 jupyter notebook which walks through using PrimAITE 1. Creating a simulation - this notebook explains how to build up a simulation using the Python package. (WIP) +- Database: + - `DatabaseClient` and `DatabaseService` created to allow emulation of database actions + - Ability to `backup_database` and `restore_backup` for a `DatabaseService` - Red Agent Services: - Data Manipulator Bot - A red agent service which sends a payload to a target machine. (By default this payload is a SQL query that breaks a database) - DNS Services: `DNSClient` and `DNSServer` diff --git a/docs/source/simulation_components/system/database_client_server.rst b/docs/source/simulation_components/system/database_client_server.rst index 99bbe25e..ef911e0e 100644 --- a/docs/source/simulation_components/system/database_client_server.rst +++ b/docs/source/simulation_components/system/database_client_server.rst @@ -60,6 +60,12 @@ Usage - Retrieve results in a dictionary. - Disconnect when finished. +To create database backups: + +- Configure the backup server the ``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 ^^^^^^^^^^^^^^ From 3dc8a0f222636b8f0ee6c9e2703e7077a9765643 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Fri, 29 Sep 2023 20:14:42 +0100 Subject: [PATCH 12/15] #1796 - Made the FTP copy real files. Hardcoded the DatabaseService backup folder and filename. Added db restore and final query check to the data manipulation e2e test. --- .../services/database/database_service.py | 39 +++++++++---------- .../system/services/ftp/ftp_client.py | 1 + .../system/services/ftp/ftp_service.py | 10 ++++- .../test_uc2_data_manipulation_scenario.py | 8 ++++ 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 268bd54f..f874b89b 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -41,6 +41,9 @@ class DatabaseService(Service): super().__init__(**kwargs) self._db_file: File self._create_db_file() + self._connect() + + def _connect(self): self._conn = sqlite3.connect(self._db_file.sim_path) self._cursor = self._conn.cursor() @@ -51,8 +54,10 @@ class DatabaseService(Service): :return: List of table names. """ sql = "SELECT name FROM sqlite_master WHERE type='table' AND name != 'sqlite_sequence';" - results = self._process_sql(sql) - return [row[0] for row in results["data"]] + results = self._process_sql(sql, None) + if isinstance(results["data"], dict): + return list(results["data"].keys()) + return [] def show(self, markdown: bool = False): """ @@ -77,9 +82,7 @@ class DatabaseService(Service): """ self.backup_server = backup_server - def backup_database( - self, backup_directory: Optional[str] = "db_backup", backup_file_name: Optional[str] = None - ) -> bool: + def backup_database(self) -> bool: """ Create a backup of the database to the configured backup server. @@ -94,8 +97,7 @@ class DatabaseService(Service): self.sys_log.error(f"{self.name} - {self.sys_log.hostname}: not configured.") return False - if backup_file_name is None: - backup_file_name = f"{datetime.now().strftime('%d-%m-%Y_%H-%M')}.db" + self._conn.close() software_manager: SoftwareManager = self.software_manager ftp_client_service: FTPClient = software_manager.software["FTPClient"] @@ -105,19 +107,19 @@ class DatabaseService(Service): dest_ip_address=self.backup_server, src_file_name=self._db_file.name, src_folder_name=self._db_file.folder.name, - dest_folder_name=backup_directory, - dest_file_name=backup_file_name, + dest_folder_name=str(self.uuid), + dest_file_name="database.db", + real_file_path=self._db_file.sim_path, ) + self._connect() if response: - self.latest_backup_directory = backup_directory - self.latest_backup_file_name = backup_file_name return True self.sys_log.error("Unable to create database backup.") return False - def restore_backup(self, backup_directory: Optional[str] = None, backup_file_name: Optional[str] = None) -> bool: + def restore_backup(self) -> bool: """ Restore a backup from backup server. @@ -127,32 +129,27 @@ class DatabaseService(Service): :param: backup_file_name: Name of file where backup will be stored. Optional. :type: backup_file_name: Optional[str] """ - if backup_directory is None: - backup_directory = self.latest_backup_directory - - if backup_file_name is None: - backup_file_name = self.latest_backup_file_name - software_manager: SoftwareManager = self.software_manager ftp_client_service: FTPClient = software_manager.software["FTPClient"] # retrieve backup file from backup server response = ftp_client_service.request_file( - src_folder_name=backup_directory, - src_file_name=backup_file_name, + src_folder_name=str(self.uuid), + src_file_name="database.db", dest_folder_name="downloads", dest_file_name="database.db", dest_ip_address=self.backup_server, ) if response: + self._conn.close() # replace db file self.file_system.delete_file(folder_name=self.folder.name, file_name="downloads.db") - self.file_system.move_file( src_folder_name="downloads", src_file_name="database.db", dst_folder_name=self.folder.name ) self._db_file = self.file_system.get_file(folder_name=self.folder.name, file_name="database.db") + self._connect() return self._db_file is not None diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 8359e8a0..c22f704b 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -119,6 +119,7 @@ class FTPClient(FTPServiceABC): dest_folder_name: str, dest_file_name: str, dest_port: Optional[Port] = Port.FTP, + real_file_path: Optional[str] = None, ) -> bool: """ Send a file to a target IP address. diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index c6d63751..5314b6a3 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -1,3 +1,4 @@ +import shutil from abc import ABC from ipaddress import IPv4Address from typing import Optional @@ -51,11 +52,17 @@ class FTPServiceABC(Service, ABC): file_name = payload.ftp_command_args["dest_file_name"] folder_name = payload.ftp_command_args["dest_folder_name"] file_size = payload.ftp_command_args["file_size"] - self.file_system.create_file(file_name=file_name, folder_name=folder_name, size=file_size, real=True) + real_file_path = payload.ftp_command_args.get("real_file_path") + is_real = real_file_path is not None + file = self.file_system.create_file( + file_name=file_name, folder_name=folder_name, size=file_size, real=is_real + ) self.sys_log.info( f"Created item in {self.sys_log.hostname}: {payload.ftp_command_args['dest_folder_name']}/" f"{payload.ftp_command_args['dest_file_name']}" ) + if is_real: + shutil.copy(real_file_path, file.sim_path) # file should exist return self.file_system.get_file(file_name=file_name, folder_name=folder_name) is not None except Exception as e: @@ -99,6 +106,7 @@ class FTPServiceABC(Service, ABC): "dest_folder_name": dest_folder_name, "dest_file_name": dest_file_name, "file_size": file.sim_size, + "real_file_path": file.sim_path if file.real else None, }, packet_payload_size=file.sim_size, ) diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index 50998f09..955fa20e 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -15,6 +15,8 @@ def test_data_manipulation(uc2_network): web_server: Server = uc2_network.get_node_by_hostname("web_server") db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_service.backup_database() + # First check that the DB client on the web_server can successfully query the users table on the database assert db_client.query("SELECT * FROM user;") @@ -23,3 +25,9 @@ def test_data_manipulation(uc2_network): # Now check that the DB client on the web_server cannot query the users table on the database assert not db_client.query("SELECT * FROM user;") + + # Now restore the database + db_service.restore_backup() + + # Now check that the DB client on the web_server can successfully query the users table on the database + assert db_client.query("SELECT * FROM user;") From 84405d7ed3578f4f99e924149bac52ebc441a55b Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Fri, 29 Sep 2023 20:19:26 +0100 Subject: [PATCH 13/15] #1796 - Added docstring to the test_uc2_data_manipulation_scenario.py --- .../e2e_integration_tests/test_uc2_data_manipulation_scenario.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index 955fa20e..13f4d1f3 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -6,6 +6,7 @@ from primaite.simulator.system.services.red_services.data_manipulation_bot impor def test_data_manipulation(uc2_network): + """Tests the UC2 data manipulation scenario end-to-end. Is a work in progress.""" client_1: Computer = uc2_network.get_node_by_hostname("client_1") db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"] From fdebfce406b8e28d1e3efb841d8e2d7ec6124187 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Mon, 2 Oct 2023 12:14:59 +0100 Subject: [PATCH 14/15] #1796: Fix test + making the restore test better --- .../system/test_database_on_node.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 38196041..92056981 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -66,13 +66,13 @@ def test_create_database_backup(uc2_network): db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] # back up should be created - assert db_service.backup_database(backup_file_name="test_file.db") is True + assert db_service.backup_database() is True backup_server: Server = uc2_network.get_node_by_hostname("backup_server") ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] # backup file should exist in the backup server - assert ftp_server.file_system.get_file(folder_name="db_backup", file_name="test_file.db") is not None + assert ftp_server.file_system.get_file(folder_name=db_service.uuid, file_name="database.db") is not None def test_restore_backup(uc2_network): @@ -81,7 +81,14 @@ def test_restore_backup(uc2_network): db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] # create a back up - assert db_service.backup_database(backup_file_name="test_file.db") is True + assert db_service.backup_database() is True + + # delete database locally + db_service.file_system.delete_file(folder_name="database", file_name="database.db") + + assert db_service.file_system.get_file(folder_name="database", file_name="database.db") is None # back up should be restored - assert db_service.restore_backup(backup_file_name="test_file.db") is True + assert db_service.restore_backup() is True + + assert db_service.file_system.get_file(folder_name="database", file_name="database.db") is not None From 97f0267539c67e847f416d5de46629edd23cac21 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Wed, 4 Oct 2023 11:33:18 +0100 Subject: [PATCH 15/15] #1796: apply PR suggestions + fix tests --- CHANGELOG.md | 2 +- .../system/database_client_server.rst | 2 +- .../simulator/network/hardware/base.py | 8 +------- .../network/hardware/nodes/computer.py | 13 ++++++++++++ .../services/database/database_service.py | 20 ++----------------- .../system/services/ftp/ftp_client.py | 9 ++++++--- .../_simulator/_system/_services/test_dns.py | 10 ++++++++-- .../_simulator/_system/_services/test_ftp.py | 10 ++++++++-- 8 files changed, 40 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc9c26d1..a5bc08f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ SessionManager. 1. Creating a simulation - this notebook explains how to build up a simulation using the Python package. (WIP) - Database: - `DatabaseClient` and `DatabaseService` created to allow emulation of database actions - - Ability to `backup_database` and `restore_backup` for a `DatabaseService` + - Ability for `DatabaseService` to backup its data to another server via FTP and restore data from backup - Red Agent Services: - Data Manipulator Bot - A red agent service which sends a payload to a target machine. (By default this payload is a SQL query that breaks a database) - DNS Services: `DNSClient` and `DNSServer` diff --git a/docs/source/simulation_components/system/database_client_server.rst b/docs/source/simulation_components/system/database_client_server.rst index ef911e0e..32568477 100644 --- a/docs/source/simulation_components/system/database_client_server.rst +++ b/docs/source/simulation_components/system/database_client_server.rst @@ -62,7 +62,7 @@ Usage To create database backups: -- Configure the backup server the ``DatabaseService`` by providing the Backup server ``IPv4Address`` with ``configure_backup`` +- Configure the backup server on the ``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``. diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 7c08f9fc..2725ab1a 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -25,8 +25,6 @@ from primaite.simulator.system.core.session_manager import SessionManager from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.processes.process import Process -from primaite.simulator.system.services.dns.dns_client import DNSClient -from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.service import Service _LOGGER = getLogger(__name__) @@ -945,11 +943,7 @@ class Node(SimComponent): def _install_system_software(self): """Install System Software - software that is usually provided with the OS.""" - # DNS Client - self.software_manager.install(DNSClient) - - # FTP - self.software_manager.install(FTPClient) + pass def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/network/hardware/nodes/computer.py b/src/primaite/simulator/network/hardware/nodes/computer.py index 5452666b..3ceb5291 100644 --- a/src/primaite/simulator/network/hardware/nodes/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/computer.py @@ -1,4 +1,6 @@ from primaite.simulator.network.hardware.base import NIC, Node +from primaite.simulator.system.services.dns.dns_client import DNSClient +from primaite.simulator.system.services.ftp.ftp_client import FTPClient class Computer(Node): @@ -36,3 +38,14 @@ class Computer(Node): def __init__(self, **kwargs): super().__init__(**kwargs) self.connect_nic(NIC(ip_address=kwargs["ip_address"], subnet_mask=kwargs["subnet_mask"])) + self._install_system_software() + + def _install_system_software(self): + """Install System Software - software that is usually provided with the OS.""" + # DNS Client + self.software_manager.install(DNSClient) + + # FTP + self.software_manager.install(FTPClient) + + super()._install_system_software() diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index f874b89b..0a6de8c3 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -83,15 +83,7 @@ class DatabaseService(Service): self.backup_server = backup_server def backup_database(self) -> bool: - """ - Create a backup of the database to the configured backup server. - - :param: backup_directory: Name of directory where backup will be stored. Optional. - :type: backup_directory: Optional[str] - - :param: backup_file_name: Name of file where backup will be stored. Optional. - :type: backup_file_name: Optional[str] - """ + """Create a backup of the database to the configured backup server.""" # check if the backup server was configured if self.backup_server is None: self.sys_log.error(f"{self.name} - {self.sys_log.hostname}: not configured.") @@ -120,15 +112,7 @@ class DatabaseService(Service): return False def restore_backup(self) -> bool: - """ - Restore a backup from backup server. - - :param: backup_directory: Name of directory where backup will be stored. Optional. - :type: backup_directory: Optional[str] - - :param: backup_file_name: Name of file where backup will be stored. Optional. - :type: backup_file_name: Optional[str] - """ + """Restore a backup from backup server.""" software_manager: SoftwareManager = self.software_manager ftp_client_service: FTPClient = software_manager.software["FTPClient"] diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index c22f704b..648b2494 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -107,7 +107,7 @@ class FTPClient(FTPServiceABC): payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port ) if payload.status_code == FTPStatusCode.OK: - self.connected = None + self.connected = False return True return False @@ -162,13 +162,16 @@ class FTPClient(FTPServiceABC): else: self.sys_log.info(f"Sending file {src_folder_name}/{src_file_name} to {str(dest_ip_address)}") # send STOR request - return self._send_data( + if self._send_data( file=file_to_transfer, dest_folder_name=dest_folder_name, dest_file_name=dest_file_name, dest_ip_address=dest_ip_address, dest_port=dest_port, - ) + ): + return self._disconnect_from_server(dest_ip_address=dest_ip_address, dest_port=dest_port) + + return False def request_file( self, diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py index d86791cd..31718387 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py @@ -3,6 +3,8 @@ from ipaddress import IPv4Address import pytest from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.dns import DNSPacket, DNSReply, DNSRequest from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -12,7 +14,9 @@ from primaite.simulator.system.services.dns.dns_server import DNSServer @pytest.fixture(scope="function") def dns_server() -> Node: - node = Node(hostname="dns_server") + node = Server( + hostname="dns_server", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" + ) node.software_manager.install(software_class=DNSServer) node.software_manager.software["DNSServer"].start() return node @@ -20,7 +24,9 @@ def dns_server() -> Node: @pytest.fixture(scope="function") def dns_client() -> Node: - node = Node(hostname="dns_client") + node = Computer( + hostname="dns_client", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" + ) return node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py index fce4a487..3ccb0c99 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py @@ -3,6 +3,8 @@ from ipaddress import IPv4Address import pytest from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -12,7 +14,9 @@ from primaite.simulator.system.services.ftp.ftp_server import FTPServer @pytest.fixture(scope="function") def ftp_server() -> Node: - node = Node(hostname="ftp_server") + node = Server( + hostname="ftp_server", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" + ) node.software_manager.install(software_class=FTPServer) node.software_manager.software["FTPServer"].start() return node @@ -20,7 +24,9 @@ def ftp_server() -> Node: @pytest.fixture(scope="function") def ftp_client() -> Node: - node = Node(hostname="ftp_client") + node = Computer( + hostname="ftp_client", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" + ) return node