Merged PR 355: Document request success
## Summary Add a doc page and a notebook to go over the request manager. ## Test process Notebook runs. ## Checklist - [x] PR is linked to a **work item** - [x] **acceptance criteria** of linked ticket are met - [x] performed **self-review** of the code - [na] written **tests** for any new functionality added with this PR - [x] updated the **documentation** if this PR changes or adds functionality - [x] written/updated **design docs** if this PR implements new functionality - [na] updated the **change log** - [x] ran **pre-commit** checks for code style - [x] attended to any **TO-DOs** left in the code Related work items: #2514
This commit is contained in:
@@ -41,6 +41,20 @@ Just like other aspects of SimComponent, the request types are not managed centr
|
||||
- ``context`` detail
|
||||
The context is not used by any of the currently implemented components or requests.
|
||||
|
||||
- Request response
|
||||
When the simulator receives a request, it returns a response with a success status. The possible statuses are:
|
||||
|
||||
* **success**: The request was received and successfully executed.
|
||||
* For example, the agent tries to add an ACL rule and specifies correct parameters, and the ACL rule is added successfully.
|
||||
|
||||
* **failure**: The request was received, but it could not be executed, or it failed while executing.
|
||||
* For example, the agent tries to execute the ``WebBrowser`` application, but the webpage wasn't retrieved because the DNS server is not setup on the node.
|
||||
|
||||
* **unreachable**: The request was sent to a simulation component that does not exist.
|
||||
* For example, the agent tries to scan a file that has not been created yet.
|
||||
|
||||
For more information, please refer to the ``Requests-and-Responses.ipynb`` jupyter notebook
|
||||
|
||||
Technical Detail
|
||||
================
|
||||
|
||||
@@ -64,9 +78,9 @@ A simple example without chaining can be seen in the :py:class:`primaite.simulat
|
||||
...
|
||||
def _init_request_manager(self):
|
||||
...
|
||||
request_manager.add_request("scan", RequestType(func=lambda request, context: self.scan()))
|
||||
request_manager.add_request("repair", RequestType(func=lambda request, context: self.repair()))
|
||||
request_manager.add_request("restore", RequestType(func=lambda request, context: self.restore()))
|
||||
request_manager.add_request("scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan())))
|
||||
request_manager.add_request("repair", RequestType(func=lambda request, context: RequestResponse.from_bool(self.repair())))
|
||||
request_manager.add_request("restore", RequestType(func=lambda request, context: RequestResponse.from_bool(self.restore())))
|
||||
|
||||
*ellipses (``...``) used to omit code impertinent to this explanation*
|
||||
|
||||
@@ -115,8 +129,8 @@ Requests that are specified without a validator automatically get assigned an ``
|
||||
Request Response
|
||||
----------------
|
||||
|
||||
The :py:class:`primaite.interface.request.RequestResponse<RequestResponse>` is a data transfer object that carries response data between the simulator and the game layer. The ``status`` field reports on the success or failure, and the ``data`` field is for any additional data. The most common way that this class is initiated is by its ``from_bool`` method. This way, given a True or False, a successful or failed request response is generated, respectively (with an empty data field).
|
||||
The :py:class:`primaite.interface.request.RequestResponse<RequestResponse>` carries response data between the simulator and the game layer. The ``status`` field reports on the success or failure, and the ``data`` field is for any additional data. The most common way that this class is used is by the ``from_bool`` method. This way, given a True or False, a successful or failed request response is generated, respectively (with an empty data field).
|
||||
|
||||
For instance, the ``execute`` action on a :py:class:`primaite.simulator.system.applications.web_browser.WebBrowser<WebBrowser>` calls the ``get_webpage()`` method of the ``WebBrowser``. ``get_webpage()`` returns a True if the webpage was successfully retrieved, and False if unsuccessful for any reason, such as being blocked by an ACL, or if the database server is unresponsive. The boolean returned from ``get_webpage()`` is used to create the request response.
|
||||
For instance, the ``execute`` action on a :py:class:`primaite.simulator.system.applications.web_browser.WebBrowser<WebBrowser>` calls the ``get_webpage()`` method. ``get_webpage()`` returns a True if the webpage was successfully retrieved, and False if unsuccessful for any reason, such as being blocked by an ACL, or if the database server is unresponsive. The boolean returned from ``get_webpage()`` is used to create the request response with ``from_bool()``.
|
||||
|
||||
Just as the requests themselves were passed from owner to component, the request response is bubbled back up from component to owner until it arrives at the game layer.
|
||||
|
||||
221
src/primaite/notebooks/Requests-and-Responses.ipynb
Normal file
221
src/primaite/notebooks/Requests-and-Responses.ipynb
Normal file
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Requests and Responses\n",
|
||||
"\n",
|
||||
"Agents interact with the PrimAITE simulation via the Request system.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Sending a request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's set up a minimal network simulation and send some requests to see how it works."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState\n",
|
||||
"from primaite.simulator.network.hardware.nodes.host.host_node import HostNode\n",
|
||||
"from primaite.simulator.sim_container import Simulation\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"sim = Simulation()\n",
|
||||
"sim.network.add_node(\n",
|
||||
" HostNode(\n",
|
||||
" hostname=\"client\",\n",
|
||||
" ip_address='10.0.0.1',\n",
|
||||
" subnet_mask='255.255.255.0',\n",
|
||||
" operating_state=NodeOperatingState.ON)\n",
|
||||
")\n",
|
||||
"client = sim.network.get_node_by_hostname('client')\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"A request is structured in a similar way to a command line interface - a list of strings with positional args. It's also possible to supply an optional `context` dictionary. We will craft a request that stops the pre-installed DNSClient service on the client node.\n",
|
||||
"\n",
|
||||
"First let's verify that the DNS Client is running on the client.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"client.software_manager.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Send a request to the simulator to stop the DNSClient."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"response = sim.apply_request(\n",
|
||||
" request=[\"network\", \"node\", \"client\", \"service\", \"DNSClient\", \"stop\"],\n",
|
||||
" context={}\n",
|
||||
" )\n",
|
||||
"print(response)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"\n",
|
||||
"The request returns a `RequestResponse` object which tells us that the request was successfully executed. Let's verify that the DNS client is in a stopped state now."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(f\"DNS Client state: {client.software_manager.software.get('DNSClient').operating_state.name}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Unreachable requests"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If we attempt to send a request to something that doesn't exist, we will get an unreachable request status."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"response = sim.apply_request(\n",
|
||||
" request=[\"network\", \"node\", \"client\", \"service\", \"NonExistentApplication\", \"stop\"],\n",
|
||||
" context={}\n",
|
||||
" )\n",
|
||||
"print(response)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Failed requests\n",
|
||||
"\n",
|
||||
"Sometimes requests cannot be executed by the simulation. For example if we turn off the client node, we cannot execute the software that is running on it."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"response = sim.apply_request(\n",
|
||||
" request = [\"network\", \"node\", \"client\", \"shutdown\"],\n",
|
||||
" context = {}\n",
|
||||
")\n",
|
||||
"print(response)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We need to apply timestep a few times for the client to go from `SHUTTING_DOWN` to `OFF` state."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(f\"client is in state: {client.operating_state.name}\")\n",
|
||||
"sim.apply_timestep(1)\n",
|
||||
"sim.apply_timestep(2)\n",
|
||||
"sim.apply_timestep(3)\n",
|
||||
"sim.apply_timestep(4)\n",
|
||||
"print(f\"client is in state: {client.operating_state.name}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now, if we try to start the DNSClient back up, we get a failure because we cannot start software on a node that is turned off."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"response = sim.apply_request(\n",
|
||||
" request=[\"network\", \"node\", \"client\", \"service\", \"DNSClient\", \"start\"],\n",
|
||||
" context={}\n",
|
||||
" )\n",
|
||||
"print(response)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
# flake8: noqa
|
||||
"""Core of the PrimAITE Simulator."""
|
||||
import warnings
|
||||
from abc import abstractmethod
|
||||
from typing import Callable, Dict, List, Literal, Optional, Union
|
||||
from uuid import uuid4
|
||||
@@ -26,6 +27,12 @@ class RequestPermissionValidator(BaseModel):
|
||||
"""Use the request and context parameters to decide whether the request should be permitted."""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def fail_message(self) -> str:
|
||||
"""Message that is reported when a request is rejected by this validator."""
|
||||
return "request rejected"
|
||||
|
||||
|
||||
class AllowAllValidator(RequestPermissionValidator):
|
||||
"""Always allows the request."""
|
||||
@@ -34,6 +41,16 @@ class AllowAllValidator(RequestPermissionValidator):
|
||||
"""Always allow the request."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def fail_message(self) -> str:
|
||||
"""
|
||||
Message that is reported when a request is rejected by this validator.
|
||||
|
||||
This method should really never be called because this validator never rejects requests.
|
||||
"""
|
||||
warnings.warn("Something went wrong - AllowAllValidator rejected a request.")
|
||||
return super().fail_message
|
||||
|
||||
|
||||
class RequestType(BaseModel):
|
||||
"""
|
||||
@@ -99,7 +116,7 @@ class RequestManager(BaseModel):
|
||||
|
||||
if not request_type.validator(request_options, context):
|
||||
_LOGGER.debug(f"Request {request} was denied due to insufficient permissions")
|
||||
return RequestResponse(status="failure", data={"reason": "request validation failed"})
|
||||
return RequestResponse(status="failure", data={"reason": request_type.validator.fail_message})
|
||||
|
||||
return request_type.func(request_options, context)
|
||||
|
||||
|
||||
@@ -57,6 +57,11 @@ class GroupMembershipValidator(RequestPermissionValidator):
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def fail_message(self) -> str:
|
||||
"""Message that is reported when a request is rejected by this validator."""
|
||||
return "User does not belong to group"
|
||||
|
||||
|
||||
class DomainController(SimComponent):
|
||||
"""Main object for controlling the domain."""
|
||||
|
||||
@@ -798,6 +798,11 @@ class Node(SimComponent):
|
||||
"""Return whether the node is on or off."""
|
||||
return self.node.operating_state == NodeOperatingState.ON
|
||||
|
||||
@property
|
||||
def fail_message(self) -> str:
|
||||
"""Message that is reported when a request is rejected by this validator."""
|
||||
return f"Cannot perform request on node '{self.node.hostname}' because it is not turned on."
|
||||
|
||||
def _init_request_manager(self) -> RequestManager:
|
||||
"""
|
||||
Initialise the request manager.
|
||||
|
||||
Reference in New Issue
Block a user