Merged PR 439: #2641: Added a check for software health state in db service + tests

## Summary
Added a check in the database service process_sql method that checks if the Database Service is in a GOOD state (mainly so that the database service being in a fixed state returns an error)

## Test process
https://dev.azure.com/ma-dev-uk/PrimAITE/_git/PrimAITE/pullrequest/439?_a=files&path=/tests/integration_tests/system/test_database_on_node.py

https://dev.azure.com/ma-dev-uk/PrimAITE/_git/PrimAITE/pullrequest/439?_a=files&path=/tests/integration_tests/system/test_web_client_server_and_database.py

## Checklist
- [X] PR is linked to a **work item**
- [X] **acceptance criteria** of linked ticket are met
- [X] performed **self-review** of the code
- [X] 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**
- [X] ran **pre-commit** checks for code style
- [ ] attended to any **TO-DOs** left in the code

#2641: Added a check for software health state in db service + tests

Related work items: #2641
This commit is contained in:
Czar Echavez
2024-07-01 15:22:22 +00:00
3 changed files with 138 additions and 1 deletions

View File

@@ -197,7 +197,11 @@ class DatabaseService(Service):
status_code = 503 # service unavailable status_code = 503 # service unavailable
if self.health_state_actual == SoftwareHealthState.OVERWHELMED: if self.health_state_actual == SoftwareHealthState.OVERWHELMED:
self.sys_log.error(f"{self.name}: Connect request for {src_ip=} declined. Service is at capacity.") self.sys_log.error(f"{self.name}: Connect request for {src_ip=} declined. Service is at capacity.")
if self.health_state_actual == SoftwareHealthState.GOOD: if self.health_state_actual in [
SoftwareHealthState.GOOD,
SoftwareHealthState.FIXING,
SoftwareHealthState.COMPROMISED,
]:
if self.password == password: if self.password == password:
status_code = 200 # ok status_code = 200 # ok
connection_id = self._generate_connection_id() connection_id = self._generate_connection_id()
@@ -244,6 +248,10 @@ class DatabaseService(Service):
self.sys_log.error(f"{self.name}: Failed to run {query} because the database file is missing.") self.sys_log.error(f"{self.name}: Failed to run {query} because the database file is missing.")
return {"status_code": 404, "type": "sql", "data": False} return {"status_code": 404, "type": "sql", "data": False}
if self.health_state_actual is not SoftwareHealthState.GOOD:
self.sys_log.error(f"{self.name}: Failed to run {query} because the database service is unavailable.")
return {"status_code": 500, "type": "sql", "data": False}
if query == "SELECT": if query == "SELECT":
if self.db_file.health_status == FileSystemItemHealthStatus.CORRUPT: if self.db_file.health_status == FileSystemItemHealthStatus.CORRUPT:
return { return {

View File

@@ -14,6 +14,7 @@ from primaite.simulator.system.applications.database_client import DatabaseClien
from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.database.database_service import DatabaseService
from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.ftp.ftp_server import FTPServer
from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.service import ServiceOperatingState
from primaite.simulator.system.software import SoftwareHealthState
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@@ -213,6 +214,110 @@ def test_restore_backup_after_deleting_file_without_updating_scan(uc2_network):
assert db_service.db_file.visible_health_status == FileSystemItemHealthStatus.GOOD # now looks good assert db_service.db_file.visible_health_status == FileSystemItemHealthStatus.GOOD # now looks good
def test_database_service_fix(uc2_network):
"""Test that the software fix applies to database service."""
db_server: Server = uc2_network.get_node_by_hostname("database_server")
db_service: DatabaseService = db_server.software_manager.software["DatabaseService"]
assert db_service.backup_database() is True
# delete database locally
db_service.file_system.delete_file(folder_name="database", file_name="database.db")
# db file is gone, reduced to atoms
assert db_service.db_file is None
db_service.fix() # fix the database service
assert db_service.health_state_actual == SoftwareHealthState.FIXING
# apply timestep until the fix is applied
for i in range(db_service.fixing_duration + 1):
uc2_network.apply_timestep(i)
assert db_service.db_file.health_status == FileSystemItemHealthStatus.GOOD
assert db_service.health_state_actual == SoftwareHealthState.GOOD
def test_database_cannot_be_queried_while_fixing(uc2_network):
"""Tests that the database service cannot be queried if the service is being fixed."""
db_server: Server = uc2_network.get_node_by_hostname("database_server")
db_service: DatabaseService = db_server.software_manager.software["DatabaseService"]
web_server: Server = uc2_network.get_node_by_hostname("web_server")
db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"]
db_connection: DatabaseClientConnection = db_client.get_new_connection()
assert db_connection.query(sql="SELECT")
assert db_service.backup_database() is True
# delete database locally
db_service.file_system.delete_file(folder_name="database", file_name="database.db")
# db file is gone, reduced to atoms
assert db_service.db_file is None
db_service.fix() # fix the database service
assert db_service.health_state_actual == SoftwareHealthState.FIXING
# fails to query because database is in FIXING state
assert db_connection.query(sql="SELECT") is False
# apply timestep until the fix is applied
for i in range(db_service.fixing_duration + 1):
uc2_network.apply_timestep(i)
assert db_service.health_state_actual == SoftwareHealthState.GOOD
assert db_service.db_file.health_status == FileSystemItemHealthStatus.GOOD
assert db_connection.query(sql="SELECT")
def test_database_can_create_connection_while_fixing(uc2_network):
"""Tests that connections cannot be created while the database is being fixed."""
db_server: Server = uc2_network.get_node_by_hostname("database_server")
db_service: DatabaseService = db_server.software_manager.software["DatabaseService"]
client_2: Server = uc2_network.get_node_by_hostname("client_2")
db_client: DatabaseClient = client_2.software_manager.software["DatabaseClient"]
db_connection: DatabaseClientConnection = db_client.get_new_connection()
assert db_connection.query(sql="SELECT")
assert db_service.backup_database() is True
# delete database locally
db_service.file_system.delete_file(folder_name="database", file_name="database.db")
# db file is gone, reduced to atoms
assert db_service.db_file is None
db_service.fix() # fix the database service
assert db_service.health_state_actual == SoftwareHealthState.FIXING
# fails to query because database is in FIXING state
assert db_connection.query(sql="SELECT") is False
# should be able to create a new connection
new_db_connection: DatabaseClientConnection = db_client.get_new_connection()
assert new_db_connection is not None
assert new_db_connection.query(sql="SELECT") is False # still should fail to query because FIXING
# apply timestep until the fix is applied
for i in range(db_service.fixing_duration + 1):
uc2_network.apply_timestep(i)
assert db_service.health_state_actual == SoftwareHealthState.GOOD
assert db_service.db_file.health_status == FileSystemItemHealthStatus.GOOD
assert db_connection.query(sql="SELECT")
assert new_db_connection.query(sql="SELECT")
def test_database_client_cannot_query_offline_database_server(uc2_network): def test_database_client_cannot_query_offline_database_server(uc2_network):
"""Tests DB query across the network returns HTTP status 404 when db server is offline.""" """Tests DB query across the network returns HTTP status 404 when db server is offline."""
db_server: Server = uc2_network.get_node_by_hostname("database_server") db_server: Server = uc2_network.get_node_by_hostname("database_server")

View File

@@ -16,6 +16,7 @@ from primaite.simulator.system.services.database.database_service import Databas
from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.dns.dns_client import DNSClient
from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.dns.dns_server import DNSServer
from primaite.simulator.system.services.web_server.web_server import WebServer from primaite.simulator.system.services.web_server.web_server import WebServer
from primaite.simulator.system.software import SoftwareHealthState
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@@ -110,6 +111,29 @@ def test_web_client_requests_users(web_client_web_server_database):
assert web_browser.get_webpage() assert web_browser.get_webpage()
def test_database_fix_disrupts_web_client(uc2_network):
"""Tests that the database service being in fixed state disrupts the web client."""
computer: Computer = uc2_network.get_node_by_hostname("client_1")
db_server: Server = uc2_network.get_node_by_hostname("database_server")
web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser")
database_service: DatabaseService = db_server.software_manager.software.get("DatabaseService")
# fix the database service
database_service.fix()
assert database_service.health_state_actual == SoftwareHealthState.FIXING
assert web_browser.get_webpage() is False
for i in range(database_service.fixing_duration + 1):
uc2_network.apply_timestep(i)
assert database_service.health_state_actual == SoftwareHealthState.GOOD
assert web_browser.get_webpage()
class TestWebBrowserHistory: class TestWebBrowserHistory:
def test_populating_history(self, web_client_web_server_database): def test_populating_history(self, web_client_web_server_database):
network, computer, _, _ = web_client_web_server_database network, computer, _, _ = web_client_web_server_database