#2912 - Mid-day commit. Actions moving across from actions.py to game.agent.actions
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
"""yaml example
|
||||
|
||||
agents:
|
||||
@@ -10,20 +11,22 @@ agents:
|
||||
action_map:
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from primaite.game.game import PrimaiteGame
|
||||
from primaite.interface.request import RequestFormat
|
||||
from __future__ import annotations
|
||||
from gymnasium import spaces
|
||||
|
||||
|
||||
import itertools
|
||||
from typing import Any, ClassVar, Dict, List, Literal, Tuple, Type
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Type
|
||||
|
||||
from gymnasium import spaces
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from primaite.game.game import _LOGGER, PrimaiteGame
|
||||
from primaite.interface.request import RequestFormat
|
||||
|
||||
|
||||
class AbstractAction(BaseModel):
|
||||
"""Base class for actions."""
|
||||
|
||||
# notes:
|
||||
# we actually don't need to hold any state in actions, so there's no need to define any __init__ logic.
|
||||
# all the init methods in the old actions are just used for holding a verb and shape, which are not really used.
|
||||
@@ -31,30 +34,32 @@ class AbstractAction(BaseModel):
|
||||
# (therefore there's no need for creating action instances, just the action class contains logic for converting
|
||||
# CAOS actions to requests for simulator. Similar to the network node adder, that class also doesn't need to be
|
||||
# instantiated.)
|
||||
class ConfigSchema(BaseModel, ABC): # TODO: not sure if this better named something like `Options`
|
||||
class ConfigSchema(BaseModel, ABC): # TODO: not sure if this better named something like `Options`
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
type: str
|
||||
|
||||
_registry: ClassVar[Dict[str,Type[AbstractAction]]] = {}
|
||||
_registry: ClassVar[Dict[str, Type[AbstractAction]]] = {}
|
||||
|
||||
def __init_subclass__(cls, identifier:str, **kwargs: Any) -> None:
|
||||
def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None:
|
||||
super().__init_subclass__(**kwargs)
|
||||
if identifier in cls._registry:
|
||||
raise ValueError(f"Cannot create new action under reserved name {identifier}")
|
||||
cls._registry[identifier] = cls
|
||||
|
||||
@classmethod
|
||||
def form_request(self, config:ConfigSchema) -> RequestFormat:
|
||||
def form_request(self, config: ConfigSchema) -> RequestFormat:
|
||||
"""Return the action formatted as a request which can be ingested by the PrimAITE simulation."""
|
||||
return []
|
||||
|
||||
|
||||
class DoNothingAction(AbstractAction):
|
||||
class ConfigSchema(AbstractAction.ConfigSchema):
|
||||
type: Literal["do_nothing"] = "do_nothing"
|
||||
|
||||
def form_request(self, options:ConfigSchema) -> RequestFormat:
|
||||
def form_request(self, options: ConfigSchema) -> RequestFormat:
|
||||
return ["do_nothing"]
|
||||
|
||||
|
||||
class ActionManager:
|
||||
"""Class which manages the action space for an agent."""
|
||||
|
||||
@@ -131,53 +136,53 @@ class ActionManager:
|
||||
"""
|
||||
|
||||
# Populate lists of apps, services, files, folders, etc on nodes.
|
||||
for node in nodes:
|
||||
app_list = [a["application_name"] for a in node.get("applications", [])]
|
||||
while len(app_list) < max_applications_per_node:
|
||||
app_list.append(None)
|
||||
self.application_names.append(app_list)
|
||||
# for node in nodes:
|
||||
# app_list = [a["application_name"] for a in node.get("applications", [])]
|
||||
# while len(app_list) < max_applications_per_node:
|
||||
# app_list.append(None)
|
||||
# self.application_names.append(app_list)
|
||||
|
||||
svc_list = [s["service_name"] for s in node.get("services", [])]
|
||||
while len(svc_list) < max_services_per_node:
|
||||
svc_list.append(None)
|
||||
self.service_names.append(svc_list)
|
||||
# svc_list = [s["service_name"] for s in node.get("services", [])]
|
||||
# while len(svc_list) < max_services_per_node:
|
||||
# svc_list.append(None)
|
||||
# self.service_names.append(svc_list)
|
||||
|
||||
folder_list = [f["folder_name"] for f in node.get("folders", [])]
|
||||
while len(folder_list) < max_folders_per_node:
|
||||
folder_list.append(None)
|
||||
self.folder_names.append(folder_list)
|
||||
# folder_list = [f["folder_name"] for f in node.get("folders", [])]
|
||||
# while len(folder_list) < max_folders_per_node:
|
||||
# folder_list.append(None)
|
||||
# self.folder_names.append(folder_list)
|
||||
|
||||
file_sublist = []
|
||||
for folder in node.get("folders", [{"files": []}]):
|
||||
file_list = [f["file_name"] for f in folder.get("files", [])]
|
||||
while len(file_list) < max_files_per_folder:
|
||||
file_list.append(None)
|
||||
file_sublist.append(file_list)
|
||||
while len(file_sublist) < max_folders_per_node:
|
||||
file_sublist.append([None] * max_files_per_folder)
|
||||
self.file_names.append(file_sublist)
|
||||
self.protocols: List[str] = protocols
|
||||
self.ports: List[str] = ports
|
||||
# file_sublist = []
|
||||
# for folder in node.get("folders", [{"files": []}]):
|
||||
# file_list = [f["file_name"] for f in folder.get("files", [])]
|
||||
# while len(file_list) < max_files_per_folder:
|
||||
# file_list.append(None)
|
||||
# file_sublist.append(file_list)
|
||||
# while len(file_sublist) < max_folders_per_node:
|
||||
# file_sublist.append([None] * max_files_per_folder)
|
||||
# self.file_names.append(file_sublist)
|
||||
# self.protocols: List[str] = protocols
|
||||
# self.ports: List[str] = ports
|
||||
|
||||
self.ip_address_list: List[str] = ip_list
|
||||
self.wildcard_list: List[str] = wildcard_list
|
||||
if self.wildcard_list == []:
|
||||
self.wildcard_list = ["NONE"]
|
||||
# action_args are settings which are applied to the action space as a whole.
|
||||
global_action_args = {
|
||||
"num_nodes": len(self.node_names),
|
||||
"num_folders": max_folders_per_node,
|
||||
"num_files": max_files_per_folder,
|
||||
"num_services": max_services_per_node,
|
||||
"num_applications": max_applications_per_node,
|
||||
"num_nics": max_nics_per_node,
|
||||
"num_acl_rules": max_acl_rules,
|
||||
"num_protocols": len(self.protocols),
|
||||
"num_ports": len(self.protocols),
|
||||
"num_ips": len(self.ip_address_list),
|
||||
"max_acl_rules": max_acl_rules,
|
||||
"max_nics_per_node": max_nics_per_node,
|
||||
}
|
||||
# self.ip_address_list: List[str] = ip_list
|
||||
# self.wildcard_list: List[str] = wildcard_list
|
||||
# if self.wildcard_list == []:
|
||||
# self.wildcard_list = ["NONE"]
|
||||
# # action_args are settings which are applied to the action space as a whole.
|
||||
# global_action_args = {
|
||||
# "num_nodes": len(self.node_names),
|
||||
# "num_folders": max_folders_per_node,
|
||||
# "num_files": max_files_per_folder,
|
||||
# "num_services": max_services_per_node,
|
||||
# "num_applications": max_applications_per_node,
|
||||
# "num_nics": max_nics_per_node,
|
||||
# "num_acl_rules": max_acl_rules,
|
||||
# "num_protocols": len(self.protocols),
|
||||
# "num_ports": len(self.protocols),
|
||||
# "num_ips": len(self.ip_address_list),
|
||||
# "max_acl_rules": max_acl_rules,
|
||||
# "max_nics_per_node": max_nics_per_node,
|
||||
# }
|
||||
self.actions: Dict[str, AbstractAction] = {}
|
||||
for act_spec in actions:
|
||||
# each action is provided into the action space config like this:
|
||||
@@ -260,191 +265,191 @@ class ActionManager:
|
||||
"""Return the gymnasium action space for this agent."""
|
||||
return spaces.Discrete(len(self.action_map))
|
||||
|
||||
def get_node_name_by_idx(self, node_idx: int) -> str:
|
||||
"""
|
||||
Get the node name corresponding to the given index.
|
||||
# def get_node_name_by_idx(self, node_idx: int) -> str:
|
||||
# """
|
||||
# Get the node name corresponding to the given index.
|
||||
|
||||
:param node_idx: The index of the node to retrieve.
|
||||
:type node_idx: int
|
||||
:return: The node hostname.
|
||||
:rtype: str
|
||||
"""
|
||||
if not node_idx < len(self.node_names):
|
||||
msg = (
|
||||
f"Error: agent attempted to perform an action on node {node_idx}, but its action space only"
|
||||
f"has {len(self.node_names)} nodes."
|
||||
)
|
||||
_LOGGER.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
return self.node_names[node_idx]
|
||||
# :param node_idx: The index of the node to retrieve.
|
||||
# :type node_idx: int
|
||||
# :return: The node hostname.
|
||||
# :rtype: str
|
||||
# """
|
||||
# if not node_idx < len(self.node_names):
|
||||
# msg = (
|
||||
# f"Error: agent attempted to perform an action on node {node_idx}, but its action space only"
|
||||
# f"has {len(self.node_names)} nodes."
|
||||
# )
|
||||
# _LOGGER.error(msg)
|
||||
# raise RuntimeError(msg)
|
||||
# return self.node_names[node_idx]
|
||||
|
||||
def get_folder_name_by_idx(self, node_idx: int, folder_idx: int) -> Optional[str]:
|
||||
"""
|
||||
Get the folder name corresponding to the given node and folder indices.
|
||||
# def get_folder_name_by_idx(self, node_idx: int, folder_idx: int) -> Optional[str]:
|
||||
# """
|
||||
# Get the folder name corresponding to the given node and folder indices.
|
||||
|
||||
:param node_idx: The index of the node.
|
||||
:type node_idx: int
|
||||
:param folder_idx: The index of the folder on the node.
|
||||
:type folder_idx: int
|
||||
:return: The name of the folder. Or None if the node has fewer folders than the given index.
|
||||
:rtype: Optional[str]
|
||||
"""
|
||||
if node_idx >= len(self.folder_names) or folder_idx >= len(self.folder_names[node_idx]):
|
||||
msg = (
|
||||
f"Error: agent attempted to perform an action on node {node_idx} and folder {folder_idx}, but this"
|
||||
f" is out of range for its action space. Folder on each node: {self.folder_names}"
|
||||
)
|
||||
_LOGGER.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
return self.folder_names[node_idx][folder_idx]
|
||||
# :param node_idx: The index of the node.
|
||||
# :type node_idx: int
|
||||
# :param folder_idx: The index of the folder on the node.
|
||||
# :type folder_idx: int
|
||||
# :return: The name of the folder. Or None if the node has fewer folders than the given index.
|
||||
# :rtype: Optional[str]
|
||||
# """
|
||||
# if node_idx >= len(self.folder_names) or folder_idx >= len(self.folder_names[node_idx]):
|
||||
# msg = (
|
||||
# f"Error: agent attempted to perform an action on node {node_idx} and folder {folder_idx}, but this"
|
||||
# f" is out of range for its action space. Folder on each node: {self.folder_names}"
|
||||
# )
|
||||
# _LOGGER.error(msg)
|
||||
# raise RuntimeError(msg)
|
||||
# return self.folder_names[node_idx][folder_idx]
|
||||
|
||||
def get_file_name_by_idx(self, node_idx: int, folder_idx: int, file_idx: int) -> Optional[str]:
|
||||
"""Get the file name corresponding to the given node, folder, and file indices.
|
||||
# def get_file_name_by_idx(self, node_idx: int, folder_idx: int, file_idx: int) -> Optional[str]:
|
||||
# """Get the file name corresponding to the given node, folder, and file indices.
|
||||
|
||||
:param node_idx: The index of the node.
|
||||
:type node_idx: int
|
||||
:param folder_idx: The index of the folder on the node.
|
||||
:type folder_idx: int
|
||||
:param file_idx: The index of the file in the folder.
|
||||
:type file_idx: int
|
||||
:return: The name of the file. Or None if the node has fewer folders than the given index, or the folder has
|
||||
fewer files than the given index.
|
||||
:rtype: Optional[str]
|
||||
"""
|
||||
if (
|
||||
node_idx >= len(self.file_names)
|
||||
or folder_idx >= len(self.file_names[node_idx])
|
||||
or file_idx >= len(self.file_names[node_idx][folder_idx])
|
||||
):
|
||||
msg = (
|
||||
f"Error: agent attempted to perform an action on node {node_idx} folder {folder_idx} file {file_idx}"
|
||||
f" but this is out of range for its action space. Files on each node: {self.file_names}"
|
||||
)
|
||||
_LOGGER.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
return self.file_names[node_idx][folder_idx][file_idx]
|
||||
# :param node_idx: The index of the node.
|
||||
# :type node_idx: int
|
||||
# :param folder_idx: The index of the folder on the node.
|
||||
# :type folder_idx: int
|
||||
# :param file_idx: The index of the file in the folder.
|
||||
# :type file_idx: int
|
||||
# :return: The name of the file. Or None if the node has fewer folders than the given index, or the folder has
|
||||
# fewer files than the given index.
|
||||
# :rtype: Optional[str]
|
||||
# """
|
||||
# if (
|
||||
# node_idx >= len(self.file_names)
|
||||
# or folder_idx >= len(self.file_names[node_idx])
|
||||
# or file_idx >= len(self.file_names[node_idx][folder_idx])
|
||||
# ):
|
||||
# msg = (
|
||||
# f"Error: agent attempted to perform an action on node {node_idx} folder {folder_idx} file {file_idx}"
|
||||
# f" but this is out of range for its action space. Files on each node: {self.file_names}"
|
||||
# )
|
||||
# _LOGGER.error(msg)
|
||||
# raise RuntimeError(msg)
|
||||
# return self.file_names[node_idx][folder_idx][file_idx]
|
||||
|
||||
def get_service_name_by_idx(self, node_idx: int, service_idx: int) -> Optional[str]:
|
||||
"""Get the service name corresponding to the given node and service indices.
|
||||
# def get_service_name_by_idx(self, node_idx: int, service_idx: int) -> Optional[str]:
|
||||
# """Get the service name corresponding to the given node and service indices.
|
||||
|
||||
:param node_idx: The index of the node.
|
||||
:type node_idx: int
|
||||
:param service_idx: The index of the service on the node.
|
||||
:type service_idx: int
|
||||
:return: The name of the service. Or None if the node has fewer services than the given index.
|
||||
:rtype: Optional[str]
|
||||
"""
|
||||
if node_idx >= len(self.service_names) or service_idx >= len(self.service_names[node_idx]):
|
||||
msg = (
|
||||
f"Error: agent attempted to perform an action on node {node_idx} and service {service_idx}, but this"
|
||||
f" is out of range for its action space. Services on each node: {self.service_names}"
|
||||
)
|
||||
_LOGGER.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
return self.service_names[node_idx][service_idx]
|
||||
# :param node_idx: The index of the node.
|
||||
# :type node_idx: int
|
||||
# :param service_idx: The index of the service on the node.
|
||||
# :type service_idx: int
|
||||
# :return: The name of the service. Or None if the node has fewer services than the given index.
|
||||
# :rtype: Optional[str]
|
||||
# """
|
||||
# if node_idx >= len(self.service_names) or service_idx >= len(self.service_names[node_idx]):
|
||||
# msg = (
|
||||
# f"Error: agent attempted to perform an action on node {node_idx} and service {service_idx}, but this"
|
||||
# f" is out of range for its action space. Services on each node: {self.service_names}"
|
||||
# )
|
||||
# _LOGGER.error(msg)
|
||||
# raise RuntimeError(msg)
|
||||
# return self.service_names[node_idx][service_idx]
|
||||
|
||||
def get_application_name_by_idx(self, node_idx: int, application_idx: int) -> Optional[str]:
|
||||
"""Get the application name corresponding to the given node and service indices.
|
||||
# def get_application_name_by_idx(self, node_idx: int, application_idx: int) -> Optional[str]:
|
||||
# """Get the application name corresponding to the given node and service indices.
|
||||
|
||||
:param node_idx: The index of the node.
|
||||
:type node_idx: int
|
||||
:param application_idx: The index of the service on the node.
|
||||
:type application_idx: int
|
||||
:return: The name of the service. Or None if the node has fewer services than the given index.
|
||||
:rtype: Optional[str]
|
||||
"""
|
||||
if node_idx >= len(self.application_names) or application_idx >= len(self.application_names[node_idx]):
|
||||
msg = (
|
||||
f"Error: agent attempted to perform an action on node {node_idx} and app {application_idx}, but "
|
||||
f"this is out of range for its action space. Applications on each node: {self.application_names}"
|
||||
)
|
||||
_LOGGER.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
return self.application_names[node_idx][application_idx]
|
||||
# :param node_idx: The index of the node.
|
||||
# :type node_idx: int
|
||||
# :param application_idx: The index of the service on the node.
|
||||
# :type application_idx: int
|
||||
# :return: The name of the service. Or None if the node has fewer services than the given index.
|
||||
# :rtype: Optional[str]
|
||||
# """
|
||||
# if node_idx >= len(self.application_names) or application_idx >= len(self.application_names[node_idx]):
|
||||
# msg = (
|
||||
# f"Error: agent attempted to perform an action on node {node_idx} and app {application_idx}, but "
|
||||
# f"this is out of range for its action space. Applications on each node: {self.application_names}"
|
||||
# )
|
||||
# _LOGGER.error(msg)
|
||||
# raise RuntimeError(msg)
|
||||
# return self.application_names[node_idx][application_idx]
|
||||
|
||||
def get_internet_protocol_by_idx(self, protocol_idx: int) -> str:
|
||||
"""Get the internet protocol corresponding to the given index.
|
||||
# def get_internet_protocol_by_idx(self, protocol_idx: int) -> str:
|
||||
# """Get the internet protocol corresponding to the given index.
|
||||
|
||||
:param protocol_idx: The index of the protocol to retrieve.
|
||||
:type protocol_idx: int
|
||||
:return: The protocol.
|
||||
:rtype: str
|
||||
"""
|
||||
if protocol_idx >= len(self.protocols):
|
||||
msg = (
|
||||
f"Error: agent attempted to perform an action on protocol {protocol_idx} but this"
|
||||
f" is out of range for its action space. Protocols: {self.protocols}"
|
||||
)
|
||||
_LOGGER.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
return self.protocols[protocol_idx]
|
||||
# :param protocol_idx: The index of the protocol to retrieve.
|
||||
# :type protocol_idx: int
|
||||
# :return: The protocol.
|
||||
# :rtype: str
|
||||
# """
|
||||
# if protocol_idx >= len(self.protocols):
|
||||
# msg = (
|
||||
# f"Error: agent attempted to perform an action on protocol {protocol_idx} but this"
|
||||
# f" is out of range for its action space. Protocols: {self.protocols}"
|
||||
# )
|
||||
# _LOGGER.error(msg)
|
||||
# raise RuntimeError(msg)
|
||||
# return self.protocols[protocol_idx]
|
||||
|
||||
def get_ip_address_by_idx(self, ip_idx: int) -> str:
|
||||
"""
|
||||
Get the IP address corresponding to the given index.
|
||||
# def get_ip_address_by_idx(self, ip_idx: int) -> str:
|
||||
# """
|
||||
# Get the IP address corresponding to the given index.
|
||||
|
||||
:param ip_idx: The index of the IP address to retrieve.
|
||||
:type ip_idx: int
|
||||
:return: The IP address.
|
||||
:rtype: str
|
||||
"""
|
||||
if ip_idx >= len(self.ip_address_list):
|
||||
msg = (
|
||||
f"Error: agent attempted to perform an action on ip address {ip_idx} but this"
|
||||
f" is out of range for its action space. IP address list: {self.ip_address_list}"
|
||||
)
|
||||
_LOGGER.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
return self.ip_address_list[ip_idx]
|
||||
# :param ip_idx: The index of the IP address to retrieve.
|
||||
# :type ip_idx: int
|
||||
# :return: The IP address.
|
||||
# :rtype: str
|
||||
# """
|
||||
# if ip_idx >= len(self.ip_address_list):
|
||||
# msg = (
|
||||
# f"Error: agent attempted to perform an action on ip address {ip_idx} but this"
|
||||
# f" is out of range for its action space. IP address list: {self.ip_address_list}"
|
||||
# )
|
||||
# _LOGGER.error(msg)
|
||||
# raise RuntimeError(msg)
|
||||
# return self.ip_address_list[ip_idx]
|
||||
|
||||
def get_wildcard_by_idx(self, wildcard_idx: int) -> str:
|
||||
"""
|
||||
Get the IP wildcard corresponding to the given index.
|
||||
# def get_wildcard_by_idx(self, wildcard_idx: int) -> str:
|
||||
# """
|
||||
# Get the IP wildcard corresponding to the given index.
|
||||
|
||||
:param ip_idx: The index of the IP wildcard to retrieve.
|
||||
:type ip_idx: int
|
||||
:return: The wildcard address.
|
||||
:rtype: str
|
||||
"""
|
||||
if wildcard_idx >= len(self.wildcard_list):
|
||||
msg = (
|
||||
f"Error: agent attempted to perform an action on ip wildcard {wildcard_idx} but this"
|
||||
f" is out of range for its action space. Wildcard list: {self.wildcard_list}"
|
||||
)
|
||||
_LOGGER.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
return self.wildcard_list[wildcard_idx]
|
||||
# :param ip_idx: The index of the IP wildcard to retrieve.
|
||||
# :type ip_idx: int
|
||||
# :return: The wildcard address.
|
||||
# :rtype: str
|
||||
# """
|
||||
# if wildcard_idx >= len(self.wildcard_list):
|
||||
# msg = (
|
||||
# f"Error: agent attempted to perform an action on ip wildcard {wildcard_idx} but this"
|
||||
# f" is out of range for its action space. Wildcard list: {self.wildcard_list}"
|
||||
# )
|
||||
# _LOGGER.error(msg)
|
||||
# raise RuntimeError(msg)
|
||||
# return self.wildcard_list[wildcard_idx]
|
||||
|
||||
def get_port_by_idx(self, port_idx: int) -> str:
|
||||
"""
|
||||
Get the port corresponding to the given index.
|
||||
# def get_port_by_idx(self, port_idx: int) -> str:
|
||||
# """
|
||||
# Get the port corresponding to the given index.
|
||||
|
||||
:param port_idx: The index of the port to retrieve.
|
||||
:type port_idx: int
|
||||
:return: The port.
|
||||
:rtype: str
|
||||
"""
|
||||
if port_idx >= len(self.ports):
|
||||
msg = (
|
||||
f"Error: agent attempted to perform an action on port {port_idx} but this"
|
||||
f" is out of range for its action space. Port list: {self.ip_address_list}"
|
||||
)
|
||||
_LOGGER.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
return self.ports[port_idx]
|
||||
# :param port_idx: The index of the port to retrieve.
|
||||
# :type port_idx: int
|
||||
# :return: The port.
|
||||
# :rtype: str
|
||||
# """
|
||||
# if port_idx >= len(self.ports):
|
||||
# msg = (
|
||||
# f"Error: agent attempted to perform an action on port {port_idx} but this"
|
||||
# f" is out of range for its action space. Port list: {self.ip_address_list}"
|
||||
# )
|
||||
# _LOGGER.error(msg)
|
||||
# raise RuntimeError(msg)
|
||||
# return self.ports[port_idx]
|
||||
|
||||
def get_nic_num_by_idx(self, node_idx: int, nic_idx: int) -> int:
|
||||
"""
|
||||
Get the NIC number corresponding to the given node and NIC indices.
|
||||
# def get_nic_num_by_idx(self, node_idx: int, nic_idx: int) -> int:
|
||||
# """
|
||||
# Get the NIC number corresponding to the given node and NIC indices.
|
||||
|
||||
:param node_idx: The index of the node.
|
||||
:type node_idx: int
|
||||
:param nic_idx: The index of the NIC on the node.
|
||||
:type nic_idx: int
|
||||
:return: The NIC number.
|
||||
:rtype: int
|
||||
"""
|
||||
return nic_idx + 1
|
||||
# :param node_idx: The index of the node.
|
||||
# :type node_idx: int
|
||||
# :param nic_idx: The index of the NIC on the node.
|
||||
# :type nic_idx: int
|
||||
# :return: The NIC number.
|
||||
# :rtype: int
|
||||
# """
|
||||
# return nic_idx + 1
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager":
|
||||
|
||||
Reference in New Issue
Block a user