Merge pull request #807 from coreemu/develop

CORE 9.0.3
This commit is contained in:
bharnden 2023-08-01 09:37:38 -07:00 committed by GitHub
commit c37fa33ffe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
223 changed files with 6865 additions and 2674 deletions

21
.github/workflows/documentation.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: documentation
on:
push:
branches:
- master
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.x
- uses: actions/cache@v2
with:
key: ${{ github.ref }}
path: .cache
- run: pip install mkdocs-material
- run: mkdocs gh-deploy --force

3
.gitignore vendored
View file

@ -18,6 +18,9 @@ configure~
debian debian
stamp-h1 stamp-h1
# python virtual environments
venv
# generated protobuf files # generated protobuf files
*_pb2.py *_pb2.py
*_pb2_grpc.py *_pb2_grpc.py

View file

@ -1,3 +1,26 @@
## 2023-08-01 CORE 9.0.3
* Installation
* updated various dependencies
* Documentation
* improved GUI docs to include node interaction and note xhost usage
* \#780 - fixed gRPC examples
* \#787 - complete documentation revamp to leverage mkdocs material
* \#790 - fixed custom emane model example
* core-daemon
* update type hinting to avoid deprecated imports
* updated commands ran within docker based nodes to have proper environment variables
* fixed issue improperly setting session options over gRPC
* \#668 - add fedora sbin path to frr service
* \#774 - fixed pcap configservice
* \#805 - fixed radvd configservice template error
* core-gui
* update type hinting to avoid deprecated imports
* fixed issue allowing duplicate named hook scripts
* fixed issue joining sessions with RJ45 nodes
* utility scripts
* fixed issue in core-cleanup for removing devices
## 2023-03-02 CORE 9.0.2 ## 2023-03-02 CORE 9.0.2
* Installation * Installation
@ -12,11 +35,10 @@
* fixed issue for LXC nodes to properly use a configured image name and write it to XML * fixed issue for LXC nodes to properly use a configured image name and write it to XML
* \#742 - fixed issue with bad wlan node id being used * \#742 - fixed issue with bad wlan node id being used
* \#744 - fixed issue not properly setting broadcast address * \#744 - fixed issue not properly setting broadcast address
* core-gui
## core-gui * fixed sample1.xml to remove SSH service
* fixed sample1.xml to remove SSH service * fixed emane demo examples
* fixed emane demo examples * fixed issue displaying emane configs generally configured for a node
* fixed issue displaying emane configs generally configured for a node
## 2022-11-28 CORE 9.0.1 ## 2022-11-28 CORE 9.0.1

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 9.0.2) AC_INIT(core, 9.0.3)
# autoconf and automake initialization # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) AC_CONFIG_SRCDIR([netns/version.h.in])

View file

@ -4,10 +4,11 @@ gRpc client for interfacing with CORE.
import logging import logging
import threading import threading
from collections.abc import Callable, Generator, Iterable
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from queue import Queue from queue import Queue
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple from typing import Any, Optional
import grpc import grpc
@ -235,7 +236,7 @@ class CoreGrpcClient:
def start_session( def start_session(
self, session: wrappers.Session, definition: bool = False self, session: wrappers.Session, definition: bool = False
) -> Tuple[bool, List[str]]: ) -> tuple[bool, list[str]]:
""" """
Start a session. Start a session.
@ -285,7 +286,7 @@ class CoreGrpcClient:
response = self.stub.DeleteSession(request) response = self.stub.DeleteSession(request)
return response.result return response.result
def get_sessions(self) -> List[wrappers.SessionSummary]: def get_sessions(self) -> list[wrappers.SessionSummary]:
""" """
Retrieves all currently known sessions. Retrieves all currently known sessions.
@ -354,7 +355,7 @@ class CoreGrpcClient:
self, self,
session_id: int, session_id: int,
handler: Callable[[wrappers.Event], None], handler: Callable[[wrappers.Event], None],
events: List[wrappers.EventType] = None, events: list[wrappers.EventType] = None,
) -> grpc.Future: ) -> grpc.Future:
""" """
Listen for session events. Listen for session events.
@ -428,7 +429,7 @@ class CoreGrpcClient:
def get_node( def get_node(
self, session_id: int, node_id: int self, session_id: int, node_id: int
) -> Tuple[wrappers.Node, List[wrappers.Interface], List[wrappers.Link]]: ) -> tuple[wrappers.Node, list[wrappers.Interface], list[wrappers.Link]]:
""" """
Get node details. Get node details.
@ -536,7 +537,7 @@ class CoreGrpcClient:
command: str, command: str,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
) -> Tuple[int, str]: ) -> tuple[int, str]:
""" """
Send command to a node and get the output. Send command to a node and get the output.
@ -575,7 +576,7 @@ class CoreGrpcClient:
def add_link( def add_link(
self, session_id: int, link: wrappers.Link, source: str = None self, session_id: int, link: wrappers.Link, source: str = None
) -> Tuple[bool, wrappers.Interface, wrappers.Interface]: ) -> tuple[bool, wrappers.Interface, wrappers.Interface]:
""" """
Add a link between nodes. Add a link between nodes.
@ -646,7 +647,7 @@ class CoreGrpcClient:
def get_mobility_config( def get_mobility_config(
self, session_id: int, node_id: int self, session_id: int, node_id: int
) -> Dict[str, wrappers.ConfigOption]: ) -> dict[str, wrappers.ConfigOption]:
""" """
Get mobility configuration for a node. Get mobility configuration for a node.
@ -660,7 +661,7 @@ class CoreGrpcClient:
return wrappers.ConfigOption.from_dict(response.config) return wrappers.ConfigOption.from_dict(response.config)
def set_mobility_config( def set_mobility_config(
self, session_id: int, node_id: int, config: Dict[str, str] self, session_id: int, node_id: int, config: dict[str, str]
) -> bool: ) -> bool:
""" """
Set mobility configuration for a node. Set mobility configuration for a node.
@ -706,7 +707,7 @@ class CoreGrpcClient:
response = self.stub.GetConfig(request) response = self.stub.GetConfig(request)
return wrappers.CoreConfig.from_proto(response) return wrappers.CoreConfig.from_proto(response)
def get_service_defaults(self, session_id: int) -> List[wrappers.ServiceDefault]: def get_service_defaults(self, session_id: int) -> list[wrappers.ServiceDefault]:
""" """
Get default services for different default node models. Get default services for different default node models.
@ -723,7 +724,7 @@ class CoreGrpcClient:
return defaults return defaults
def set_service_defaults( def set_service_defaults(
self, session_id: int, service_defaults: Dict[str, List[str]] self, session_id: int, service_defaults: dict[str, list[str]]
) -> bool: ) -> bool:
""" """
Set default services for node models. Set default services for node models.
@ -829,7 +830,7 @@ class CoreGrpcClient:
def get_wlan_config( def get_wlan_config(
self, session_id: int, node_id: int self, session_id: int, node_id: int
) -> Dict[str, wrappers.ConfigOption]: ) -> dict[str, wrappers.ConfigOption]:
""" """
Get wlan configuration for a node. Get wlan configuration for a node.
@ -843,7 +844,7 @@ class CoreGrpcClient:
return wrappers.ConfigOption.from_dict(response.config) return wrappers.ConfigOption.from_dict(response.config)
def set_wlan_config( def set_wlan_config(
self, session_id: int, node_id: int, config: Dict[str, str] self, session_id: int, node_id: int, config: dict[str, str]
) -> bool: ) -> bool:
""" """
Set wlan configuration for a node. Set wlan configuration for a node.
@ -861,7 +862,7 @@ class CoreGrpcClient:
def get_emane_model_config( def get_emane_model_config(
self, session_id: int, node_id: int, model: str, iface_id: int = -1 self, session_id: int, node_id: int, model: str, iface_id: int = -1
) -> Dict[str, wrappers.ConfigOption]: ) -> dict[str, wrappers.ConfigOption]:
""" """
Get emane model configuration for a node or a node's interface. Get emane model configuration for a node or a node's interface.
@ -909,7 +910,7 @@ class CoreGrpcClient:
with open(file_path, "w") as xml_file: with open(file_path, "w") as xml_file:
xml_file.write(response.data) xml_file.write(response.data)
def open_xml(self, file_path: Path, start: bool = False) -> Tuple[bool, int]: def open_xml(self, file_path: Path, start: bool = False) -> tuple[bool, int]:
""" """
Load a local scenario XML file to open as a new session. Load a local scenario XML file to open as a new session.
@ -940,7 +941,7 @@ class CoreGrpcClient:
response = self.stub.EmaneLink(request) response = self.stub.EmaneLink(request)
return response.result return response.result
def get_ifaces(self) -> List[str]: def get_ifaces(self) -> list[str]:
""" """
Retrieves a list of interfaces available on the host machine that are not Retrieves a list of interfaces available on the host machine that are not
a part of a CORE session. a part of a CORE session.
@ -951,20 +952,26 @@ class CoreGrpcClient:
response = self.stub.GetInterfaces(request) response = self.stub.GetInterfaces(request)
return list(response.ifaces) return list(response.ifaces)
def get_config_service_defaults(self, name: str) -> wrappers.ConfigServiceDefaults: def get_config_service_defaults(
self, session_id: int, node_id: int, name: str
) -> wrappers.ConfigServiceDefaults:
""" """
Retrieves config service default values. Retrieves config service default values.
:param session_id: session id to get node from
:param node_id: node id to get service data from
:param name: name of service to get defaults for :param name: name of service to get defaults for
:return: config service defaults :return: config service defaults
""" """
request = GetConfigServiceDefaultsRequest(name=name) request = GetConfigServiceDefaultsRequest(
name=name, session_id=session_id, node_id=node_id
)
response = self.stub.GetConfigServiceDefaults(request) response = self.stub.GetConfigServiceDefaults(request)
return wrappers.ConfigServiceDefaults.from_proto(response) return wrappers.ConfigServiceDefaults.from_proto(response)
def get_node_config_service( def get_node_config_service(
self, session_id: int, node_id: int, name: str self, session_id: int, node_id: int, name: str
) -> Dict[str, str]: ) -> dict[str, str]:
""" """
Retrieves information for a specific config service on a node. Retrieves information for a specific config service on a node.
@ -982,7 +989,7 @@ class CoreGrpcClient:
def get_config_service_rendered( def get_config_service_rendered(
self, session_id: int, node_id: int, name: str self, session_id: int, node_id: int, name: str
) -> Dict[str, str]: ) -> dict[str, str]:
""" """
Retrieve the rendered config service files for a node. Retrieve the rendered config service files for a node.
@ -1129,7 +1136,7 @@ class CoreGrpcClient:
def get_wireless_config( def get_wireless_config(
self, session_id: int, node_id: int self, session_id: int, node_id: int
) -> Dict[str, wrappers.ConfigOption]: ) -> dict[str, wrappers.ConfigOption]:
request = GetWirelessConfigRequest(session_id=session_id, node_id=node_id) request = GetWirelessConfigRequest(session_id=session_id, node_id=node_id)
response = self.stub.GetWirelessConfig(request) response = self.stub.GetWirelessConfig(request)
return wrappers.ConfigOption.from_dict(response.config) return wrappers.ConfigOption.from_dict(response.config)
@ -1156,7 +1163,7 @@ class CoreGrpcClient:
self.channel = None self.channel = None
@contextmanager @contextmanager
def context_connect(self) -> Generator: def context_connect(self) -> Generator[None, None, None]:
""" """
Makes a context manager based connection to the server, will close after Makes a context manager based connection to the server, will close after
context ends. context ends.

View file

@ -1,6 +1,7 @@
import logging import logging
from collections.abc import Iterable
from queue import Empty, Queue from queue import Empty, Queue
from typing import Iterable, Optional from typing import Optional
from core.api.grpc import core_pb2, grpcutils from core.api.grpc import core_pb2, grpcutils
from core.api.grpc.grpcutils import convert_link_data from core.api.grpc.grpcutils import convert_link_data

View file

@ -1,7 +1,7 @@
import logging import logging
import time import time
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Type, Union from typing import Any, Optional, Union
import grpc import grpc
from grpc import ServicerContext from grpc import ServicerContext
@ -36,6 +36,7 @@ from core.nodes.docker import DockerNode, DockerOptions
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode, LxcOptions from core.nodes.lxd import LxcNode, LxcOptions
from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode
from core.nodes.podman import PodmanNode, PodmanOptions
from core.nodes.wireless import WirelessNode from core.nodes.wireless import WirelessNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
@ -63,8 +64,8 @@ class CpuUsage:
def add_node_data( def add_node_data(
_class: Type[NodeBase], node_proto: core_pb2.Node _class: type[NodeBase], node_proto: core_pb2.Node
) -> Tuple[Position, NodeOptions]: ) -> tuple[Position, NodeOptions]:
""" """
Convert node protobuf message to data for creating a node. Convert node protobuf message to data for creating a node.
@ -81,7 +82,7 @@ def add_node_data(
options.config_services = node_proto.config_services options.config_services = node_proto.config_services
if isinstance(options, EmaneOptions): if isinstance(options, EmaneOptions):
options.emane_model = node_proto.emane options.emane_model = node_proto.emane
if isinstance(options, (DockerOptions, LxcOptions)): if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)):
options.image = node_proto.image options.image = node_proto.image
position = Position() position = Position()
position.set(node_proto.position.x, node_proto.position.y) position.set(node_proto.position.x, node_proto.position.y)
@ -118,7 +119,7 @@ def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
def add_link_data( def add_link_data(
link_proto: core_pb2.Link, link_proto: core_pb2.Link,
) -> Tuple[InterfaceData, InterfaceData, LinkOptions]: ) -> tuple[InterfaceData, InterfaceData, LinkOptions]:
""" """
Convert link proto to link interfaces and options data. Convert link proto to link interfaces and options data.
@ -145,8 +146,8 @@ def add_link_data(
def create_nodes( def create_nodes(
session: Session, node_protos: List[core_pb2.Node] session: Session, node_protos: list[core_pb2.Node]
) -> Tuple[List[NodeBase], List[Exception]]: ) -> tuple[list[NodeBase], list[Exception]]:
""" """
Create nodes using a thread pool and wait for completion. Create nodes using a thread pool and wait for completion.
@ -176,8 +177,8 @@ def create_nodes(
def create_links( def create_links(
session: Session, link_protos: List[core_pb2.Link] session: Session, link_protos: list[core_pb2.Link]
) -> Tuple[List[NodeBase], List[Exception]]: ) -> tuple[list[NodeBase], list[Exception]]:
""" """
Create links using a thread pool and wait for completion. Create links using a thread pool and wait for completion.
@ -200,8 +201,8 @@ def create_links(
def edit_links( def edit_links(
session: Session, link_protos: List[core_pb2.Link] session: Session, link_protos: list[core_pb2.Link]
) -> Tuple[List[None], List[Exception]]: ) -> tuple[list[None], list[Exception]]:
""" """
Edit links using a thread pool and wait for completion. Edit links using a thread pool and wait for completion.
@ -235,7 +236,7 @@ def convert_value(value: Any) -> str:
return value return value
def convert_session_options(session: Session) -> Dict[str, common_pb2.ConfigOption]: def convert_session_options(session: Session) -> dict[str, common_pb2.ConfigOption]:
config_options = {} config_options = {}
for option in session.options.options: for option in session.options.options:
value = session.options.get(option.id) value = session.options.get(option.id)
@ -252,9 +253,9 @@ def convert_session_options(session: Session) -> Dict[str, common_pb2.ConfigOpti
def get_config_options( def get_config_options(
config: Dict[str, str], config: dict[str, str],
configurable_options: Union[ConfigurableOptions, Type[ConfigurableOptions]], configurable_options: Union[ConfigurableOptions, type[ConfigurableOptions]],
) -> Dict[str, common_pb2.ConfigOption]: ) -> dict[str, common_pb2.ConfigOption]:
""" """
Retrieve configuration options in a form that is used by the grpc server. Retrieve configuration options in a form that is used by the grpc server.
@ -283,7 +284,7 @@ def get_config_options(
def get_node_proto( def get_node_proto(
session: Session, node: NodeBase, emane_configs: List[NodeEmaneConfig] session: Session, node: NodeBase, emane_configs: list[NodeEmaneConfig]
) -> core_pb2.Node: ) -> core_pb2.Node:
""" """
Convert CORE node to protobuf representation. Convert CORE node to protobuf representation.
@ -313,7 +314,7 @@ def get_node_proto(
if isinstance(node, EmaneNet): if isinstance(node, EmaneNet):
emane_model = node.wireless_model.name emane_model = node.wireless_model.name
image = None image = None
if isinstance(node, (DockerNode, LxcNode)): if isinstance(node, (DockerNode, LxcNode, PodmanNode)):
image = node.image image = node.image
# check for wlan config # check for wlan config
wlan_config = session.mobility.get_configs( wlan_config = session.mobility.get_configs(
@ -390,7 +391,7 @@ def get_node_proto(
) )
def get_links(session: Session, node: NodeBase) -> List[core_pb2.Link]: def get_links(session: Session, node: NodeBase) -> list[core_pb2.Link]:
""" """
Retrieve a list of links for grpc to use. Retrieve a list of links for grpc to use.
@ -435,7 +436,7 @@ def convert_iface(iface: CoreInterface) -> core_pb2.Interface:
) )
def convert_core_link(core_link: CoreLink) -> List[core_pb2.Link]: def convert_core_link(core_link: CoreLink) -> list[core_pb2.Link]:
""" """
Convert core link to protobuf data. Convert core link to protobuf data.
@ -581,7 +582,7 @@ def convert_link(
) )
def parse_proc_net_dev(lines: List[str]) -> Dict[str, Any]: def parse_proc_net_dev(lines: list[str]) -> dict[str, dict[str, float]]:
""" """
Parse lines of output from /proc/net/dev. Parse lines of output from /proc/net/dev.
@ -599,7 +600,7 @@ def parse_proc_net_dev(lines: List[str]) -> Dict[str, Any]:
return stats return stats
def get_net_stats() -> Dict[str, Dict]: def get_net_stats() -> dict[str, dict[str, float]]:
""" """
Retrieve status about the current interfaces in the system Retrieve status about the current interfaces in the system
@ -728,7 +729,7 @@ def get_nem_id(
return nem_id return nem_id
def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]: def get_emane_model_configs_dict(session: Session) -> dict[int, list[NodeEmaneConfig]]:
""" """
Get emane model configuration protobuf data. Get emane model configuration protobuf data.
@ -751,7 +752,7 @@ def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneCo
return configs return configs
def get_hooks(session: Session) -> List[core_pb2.Hook]: def get_hooks(session: Session) -> list[core_pb2.Hook]:
""" """
Retrieve hook protobuf data for a session. Retrieve hook protobuf data for a session.
@ -767,7 +768,7 @@ def get_hooks(session: Session) -> List[core_pb2.Hook]:
return hooks return hooks
def get_default_services(session: Session) -> List[ServiceDefaults]: def get_default_services(session: Session) -> list[ServiceDefaults]:
""" """
Retrieve the default service sets for a given session. Retrieve the default service sets for a given session.

View file

@ -5,9 +5,11 @@ import signal
import sys import sys
import tempfile import tempfile
import time import time
from collections.abc import Iterable
from concurrent import futures from concurrent import futures
from pathlib import Path from pathlib import Path
from typing import Iterable, Optional, Pattern, Type from re import Pattern
from typing import Optional
import grpc import grpc
from grpc import ServicerContext from grpc import ServicerContext
@ -105,7 +107,7 @@ from core.services.coreservices import ServiceManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ONE_DAY_IN_SECONDS: int = 60 * 60 * 24 _ONE_DAY_IN_SECONDS: int = 60 * 60 * 24
_INTERFACE_REGEX: Pattern = re.compile(r"beth(?P<node>[0-9a-fA-F]+)") _INTERFACE_REGEX: Pattern[str] = re.compile(r"beth(?P<node>[0-9a-fA-F]+)")
_MAX_WORKERS = 1000 _MAX_WORKERS = 1000
@ -171,7 +173,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
return session return session
def get_node( def get_node(
self, session: Session, node_id: int, context: ServicerContext, _class: Type[NT] self, session: Session, node_id: int, context: ServicerContext, _class: type[NT]
) -> NT: ) -> NT:
""" """
Retrieve node given session and node id Retrieve node given session and node id
@ -210,7 +212,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
def validate_service( def validate_service(
self, name: str, context: ServicerContext self, name: str, context: ServicerContext
) -> Type[ConfigService]: ) -> type[ConfigService]:
""" """
Validates a configuration service is a valid known service. Validates a configuration service is a valid known service.
@ -282,7 +284,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
# session options # session options
for option in request.session.options.values(): for option in request.session.options.values():
session.options.set(option.name, option.value) if option.value:
session.options.set(option.name, option.value)
session.metadata = dict(request.session.metadata) session.metadata = dict(request.session.metadata)
# add servers # add servers
@ -1103,7 +1106,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
node_id = request.wlan_config.node_id node_id = request.wlan_config.node_id
config = request.wlan_config.config config = request.wlan_config.config
session.mobility.set_model_config(node_id, BasicRangeModel.name, config) session.mobility.set_model_config(node_id, BasicRangeModel.name, config)
if session.state == EventTypes.RUNTIME_STATE: if session.is_running():
node = self.get_node(session, node_id, context, WlanNode) node = self.get_node(session, node_id, context, WlanNode)
node.updatemodel(config) node.updatemodel(config)
return SetWlanConfigResponse(result=True) return SetWlanConfigResponse(result=True)
@ -1176,7 +1179,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logger.debug("open xml: %s", request) logger.debug("open xml: %s", request)
session = self.coreemu.create_session() session = self.coreemu.create_session()
temp = tempfile.NamedTemporaryFile(delete=False) temp = tempfile.NamedTemporaryFile(delete=False)
temp.write(request.data.encode("utf-8")) temp.write(request.data.encode())
temp.close() temp.close()
temp_path = Path(temp.name) temp_path = Path(temp.name)
file_path = Path(request.file) file_path = Path(request.file)
@ -1282,8 +1285,10 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:param context: grpc context :param context: grpc context
:return: get config service defaults response :return: get config service defaults response
""" """
session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context, CoreNode)
service_class = self.validate_service(request.name, context) service_class = self.validate_service(request.name, context)
service = service_class(None) service = service_class(node)
templates = service.get_templates() templates = service.get_templates()
config = {} config = {}
for configuration in service.default_configs: for configuration in service.default_configs:

View file

@ -1,7 +1,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple from typing import Any, Optional
from core.api.grpc import ( from core.api.grpc import (
common_pb2, common_pb2,
@ -68,6 +68,7 @@ class NodeType(Enum):
DOCKER = 15 DOCKER = 15
LXC = 16 LXC = 16
WIRELESS = 17 WIRELESS = 17
PODMAN = 18
class LinkType(Enum): class LinkType(Enum):
@ -114,13 +115,13 @@ class EventType:
class ConfigService: class ConfigService:
group: str group: str
name: str name: str
executables: List[str] executables: list[str]
dependencies: List[str] dependencies: list[str]
directories: List[str] directories: list[str]
files: List[str] files: list[str]
startup: List[str] startup: list[str]
validate: List[str] validate: list[str]
shutdown: List[str] shutdown: list[str]
validation_mode: ConfigServiceValidationMode validation_mode: ConfigServiceValidationMode
validation_timer: int validation_timer: int
validation_period: float validation_period: float
@ -147,8 +148,8 @@ class ConfigService:
class ConfigServiceConfig: class ConfigServiceConfig:
node_id: int node_id: int
name: str name: str
templates: Dict[str, str] templates: dict[str, str]
config: Dict[str, str] config: dict[str, str]
@classmethod @classmethod
def from_proto( def from_proto(
@ -164,15 +165,15 @@ class ConfigServiceConfig:
@dataclass @dataclass
class ConfigServiceData: class ConfigServiceData:
templates: Dict[str, str] = field(default_factory=dict) templates: dict[str, str] = field(default_factory=dict)
config: Dict[str, str] = field(default_factory=dict) config: dict[str, str] = field(default_factory=dict)
@dataclass @dataclass
class ConfigServiceDefaults: class ConfigServiceDefaults:
templates: Dict[str, str] templates: dict[str, str]
config: Dict[str, "ConfigOption"] config: dict[str, "ConfigOption"]
modes: Dict[str, Dict[str, str]] modes: dict[str, dict[str, str]]
@classmethod @classmethod
def from_proto( def from_proto(
@ -211,7 +212,7 @@ class Service:
@dataclass @dataclass
class ServiceDefault: class ServiceDefault:
model: str model: str
services: List[str] services: list[str]
@classmethod @classmethod
def from_proto(cls, proto: services_pb2.ServiceDefaults) -> "ServiceDefault": def from_proto(cls, proto: services_pb2.ServiceDefaults) -> "ServiceDefault":
@ -220,15 +221,15 @@ class ServiceDefault:
@dataclass @dataclass
class NodeServiceData: class NodeServiceData:
executables: List[str] = field(default_factory=list) executables: list[str] = field(default_factory=list)
dependencies: List[str] = field(default_factory=list) dependencies: list[str] = field(default_factory=list)
dirs: List[str] = field(default_factory=list) dirs: list[str] = field(default_factory=list)
configs: List[str] = field(default_factory=list) configs: list[str] = field(default_factory=list)
startup: List[str] = field(default_factory=list) startup: list[str] = field(default_factory=list)
validate: List[str] = field(default_factory=list) validate: list[str] = field(default_factory=list)
validation_mode: ServiceValidationMode = ServiceValidationMode.NON_BLOCKING validation_mode: ServiceValidationMode = ServiceValidationMode.NON_BLOCKING
validation_timer: int = 5 validation_timer: int = 5
shutdown: List[str] = field(default_factory=list) shutdown: list[str] = field(default_factory=list)
meta: str = None meta: str = None
@classmethod @classmethod
@ -266,7 +267,7 @@ class NodeServiceConfig:
node_id: int node_id: int
service: str service: str
data: NodeServiceData data: NodeServiceData
files: Dict[str, str] = field(default_factory=dict) files: dict[str, str] = field(default_factory=dict)
@classmethod @classmethod
def from_proto(cls, proto: services_pb2.NodeServiceConfig) -> "NodeServiceConfig": def from_proto(cls, proto: services_pb2.NodeServiceConfig) -> "NodeServiceConfig":
@ -282,11 +283,11 @@ class NodeServiceConfig:
class ServiceConfig: class ServiceConfig:
node_id: int node_id: int
service: str service: str
files: List[str] = None files: list[str] = None
directories: List[str] = None directories: list[str] = None
startup: List[str] = None startup: list[str] = None
validate: List[str] = None validate: list[str] = None
shutdown: List[str] = None shutdown: list[str] = None
def to_proto(self) -> services_pb2.ServiceConfig: def to_proto(self) -> services_pb2.ServiceConfig:
return services_pb2.ServiceConfig( return services_pb2.ServiceConfig(
@ -339,8 +340,8 @@ class InterfaceThroughput:
@dataclass @dataclass
class ThroughputsEvent: class ThroughputsEvent:
session_id: int session_id: int
bridge_throughputs: List[BridgeThroughput] bridge_throughputs: list[BridgeThroughput]
iface_throughputs: List[InterfaceThroughput] iface_throughputs: list[InterfaceThroughput]
@classmethod @classmethod
def from_proto(cls, proto: core_pb2.ThroughputsEvent) -> "ThroughputsEvent": def from_proto(cls, proto: core_pb2.ThroughputsEvent) -> "ThroughputsEvent":
@ -428,19 +429,19 @@ class ConfigOption:
label: str = None label: str = None
type: ConfigOptionType = None type: ConfigOptionType = None
group: str = None group: str = None
select: List[str] = None select: list[str] = None
@classmethod @classmethod
def from_dict( def from_dict(
cls, config: Dict[str, common_pb2.ConfigOption] cls, config: dict[str, common_pb2.ConfigOption]
) -> Dict[str, "ConfigOption"]: ) -> dict[str, "ConfigOption"]:
d = {} d = {}
for key, value in config.items(): for key, value in config.items():
d[key] = ConfigOption.from_proto(value) d[key] = ConfigOption.from_proto(value)
return d return d
@classmethod @classmethod
def to_dict(cls, config: Dict[str, "ConfigOption"]) -> Dict[str, str]: def to_dict(cls, config: dict[str, "ConfigOption"]) -> dict[str, str]:
return {k: v.value for k, v in config.items()} return {k: v.value for k, v in config.items()}
@classmethod @classmethod
@ -671,7 +672,7 @@ class EmaneModelConfig:
node_id: int node_id: int
model: str model: str
iface_id: int = -1 iface_id: int = -1
config: Dict[str, ConfigOption] = None config: dict[str, ConfigOption] = None
@classmethod @classmethod
def from_proto(cls, proto: emane_pb2.GetEmaneModelConfig) -> "EmaneModelConfig": def from_proto(cls, proto: emane_pb2.GetEmaneModelConfig) -> "EmaneModelConfig":
@ -725,8 +726,8 @@ class Node:
type: NodeType = NodeType.DEFAULT type: NodeType = NodeType.DEFAULT
model: str = None model: str = None
position: Position = Position(x=0, y=0) position: Position = Position(x=0, y=0)
services: Set[str] = field(default_factory=set) services: set[str] = field(default_factory=set)
config_services: Set[str] = field(default_factory=set) config_services: set[str] = field(default_factory=set)
emane: str = None emane: str = None
icon: str = None icon: str = None
image: str = None image: str = None
@ -737,19 +738,19 @@ class Node:
canvas: int = None canvas: int = None
# configurations # configurations
emane_model_configs: Dict[ emane_model_configs: dict[
Tuple[str, Optional[int]], Dict[str, ConfigOption] tuple[str, Optional[int]], dict[str, ConfigOption]
] = field(default_factory=dict, repr=False) ] = field(default_factory=dict, repr=False)
wlan_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) wlan_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False)
wireless_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) wireless_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False)
mobility_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) mobility_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False)
service_configs: Dict[str, NodeServiceData] = field( service_configs: dict[str, NodeServiceData] = field(
default_factory=dict, repr=False default_factory=dict, repr=False
) )
service_file_configs: Dict[str, Dict[str, str]] = field( service_file_configs: dict[str, dict[str, str]] = field(
default_factory=dict, repr=False default_factory=dict, repr=False
) )
config_service_configs: Dict[str, ConfigServiceData] = field( config_service_configs: dict[str, ConfigServiceData] = field(
default_factory=dict, repr=False default_factory=dict, repr=False
) )
@ -849,18 +850,18 @@ class Node:
wireless_config={k: v.to_proto() for k, v in self.wireless_config.items()}, wireless_config={k: v.to_proto() for k, v in self.wireless_config.items()},
) )
def set_wlan(self, config: Dict[str, str]) -> None: def set_wlan(self, config: dict[str, str]) -> None:
for key, value in config.items(): for key, value in config.items():
option = ConfigOption(name=key, value=value) option = ConfigOption(name=key, value=value)
self.wlan_config[key] = option self.wlan_config[key] = option
def set_mobility(self, config: Dict[str, str]) -> None: def set_mobility(self, config: dict[str, str]) -> None:
for key, value in config.items(): for key, value in config.items():
option = ConfigOption(name=key, value=value) option = ConfigOption(name=key, value=value)
self.mobility_config[key] = option self.mobility_config[key] = option
def set_emane_model( def set_emane_model(
self, model: str, config: Dict[str, str], iface_id: int = None self, model: str, config: dict[str, str], iface_id: int = None
) -> None: ) -> None:
key = (model, iface_id) key = (model, iface_id)
config_options = self.emane_model_configs.setdefault(key, {}) config_options = self.emane_model_configs.setdefault(key, {})
@ -873,23 +874,23 @@ class Node:
class Session: class Session:
id: int = None id: int = None
state: SessionState = SessionState.DEFINITION state: SessionState = SessionState.DEFINITION
nodes: Dict[int, Node] = field(default_factory=dict) nodes: dict[int, Node] = field(default_factory=dict)
links: List[Link] = field(default_factory=list) links: list[Link] = field(default_factory=list)
dir: str = None dir: str = None
user: str = None user: str = None
default_services: Dict[str, Set[str]] = field(default_factory=dict) default_services: dict[str, set[str]] = field(default_factory=dict)
location: SessionLocation = SessionLocation( location: SessionLocation = SessionLocation(
x=0.0, y=0.0, z=0.0, lat=47.57917, lon=-122.13232, alt=2.0, scale=150.0 x=0.0, y=0.0, z=0.0, lat=47.57917, lon=-122.13232, alt=2.0, scale=150.0
) )
hooks: Dict[str, Hook] = field(default_factory=dict) hooks: dict[str, Hook] = field(default_factory=dict)
metadata: Dict[str, str] = field(default_factory=dict) metadata: dict[str, str] = field(default_factory=dict)
file: Path = None file: Path = None
options: Dict[str, ConfigOption] = field(default_factory=dict) options: dict[str, ConfigOption] = field(default_factory=dict)
servers: List[Server] = field(default_factory=list) servers: list[Server] = field(default_factory=list)
@classmethod @classmethod
def from_proto(cls, proto: core_pb2.Session) -> "Session": def from_proto(cls, proto: core_pb2.Session) -> "Session":
nodes: Dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes} nodes: dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes}
links = [Link.from_proto(x) for x in proto.links] links = [Link.from_proto(x) for x in proto.links]
default_services = {x.model: set(x.services) for x in proto.default_services} default_services = {x.model: set(x.services) for x in proto.default_services}
hooks = {x.file: Hook.from_proto(x) for x in proto.hooks} hooks = {x.file: Hook.from_proto(x) for x in proto.hooks}
@ -987,7 +988,7 @@ class Session:
self.links.append(link) self.links.append(link)
return link return link
def set_options(self, config: Dict[str, str]) -> None: def set_options(self, config: dict[str, str]) -> None:
for key, value in config.items(): for key, value in config.items():
option = ConfigOption(name=key, value=value) option = ConfigOption(name=key, value=value)
self.options[key] = option self.options[key] = option
@ -995,9 +996,9 @@ class Session:
@dataclass @dataclass
class CoreConfig: class CoreConfig:
services: List[Service] = field(default_factory=list) services: list[Service] = field(default_factory=list)
config_services: List[ConfigService] = field(default_factory=list) config_services: list[ConfigService] = field(default_factory=list)
emane_models: List[str] = field(default_factory=list) emane_models: list[str] = field(default_factory=list)
@classmethod @classmethod
def from_proto(cls, proto: core_pb2.GetConfigResponse) -> "CoreConfig": def from_proto(cls, proto: core_pb2.GetConfigResponse) -> "CoreConfig":
@ -1088,7 +1089,7 @@ class ConfigEvent:
node_id: int node_id: int
object: str object: str
type: int type: int
data_types: List[int] data_types: list[int]
data_values: str data_values: str
captions: str captions: str
bitmap: str bitmap: str

View file

@ -5,7 +5,7 @@ Common support for configurable CORE objects.
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Type, Union from typing import TYPE_CHECKING, Any, Optional, Union
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.enumerations import ConfigDataTypes from core.emulator.enumerations import ConfigDataTypes
@ -17,9 +17,9 @@ logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.location.mobility import WirelessModel from core.location.mobility import WirelessModel
WirelessModelType = Type[WirelessModel] WirelessModelType = type[WirelessModel]
_BOOL_OPTIONS: Set[str] = {"0", "1"} _BOOL_OPTIONS: set[str] = {"0", "1"}
@dataclass @dataclass
@ -43,7 +43,7 @@ class Configuration:
type: ConfigDataTypes type: ConfigDataTypes
label: str = None label: str = None
default: str = "" default: str = ""
options: List[str] = field(default_factory=list) options: list[str] = field(default_factory=list)
group: str = "Configuration" group: str = "Configuration"
def __post_init__(self) -> None: def __post_init__(self) -> None:
@ -118,10 +118,10 @@ class ConfigurableOptions:
""" """
name: Optional[str] = None name: Optional[str] = None
options: List[Configuration] = [] options: list[Configuration] = []
@classmethod @classmethod
def configurations(cls) -> List[Configuration]: def configurations(cls) -> list[Configuration]:
""" """
Provides the configurations for this class. Provides the configurations for this class.
@ -130,7 +130,7 @@ class ConfigurableOptions:
return cls.options return cls.options
@classmethod @classmethod
def config_groups(cls) -> List[ConfigGroup]: def config_groups(cls) -> list[ConfigGroup]:
""" """
Defines how configurations are grouped. Defines how configurations are grouped.
@ -139,7 +139,7 @@ class ConfigurableOptions:
return [ConfigGroup("Options", 1, len(cls.configurations()))] return [ConfigGroup("Options", 1, len(cls.configurations()))]
@classmethod @classmethod
def default_values(cls) -> Dict[str, str]: def default_values(cls) -> dict[str, str]:
""" """
Provides an ordered mapping of configuration keys to default values. Provides an ordered mapping of configuration keys to default values.
@ -165,7 +165,7 @@ class ConfigurableManager:
""" """
self.node_configurations = {} self.node_configurations = {}
def nodes(self) -> List[int]: def nodes(self) -> list[int]:
""" """
Retrieves the ids of all node configurations known by this manager. Retrieves the ids of all node configurations known by this manager.
@ -208,7 +208,7 @@ class ConfigurableManager:
def set_configs( def set_configs(
self, self,
config: Dict[str, str], config: dict[str, str],
node_id: int = _default_node, node_id: int = _default_node,
config_type: str = _default_type, config_type: str = _default_type,
) -> None: ) -> None:
@ -250,7 +250,7 @@ class ConfigurableManager:
def get_configs( def get_configs(
self, node_id: int = _default_node, config_type: str = _default_type self, node_id: int = _default_node, config_type: str = _default_type
) -> Optional[Dict[str, str]]: ) -> Optional[dict[str, str]]:
""" """
Retrieve configurations for a node and configuration type. Retrieve configurations for a node and configuration type.
@ -264,7 +264,7 @@ class ConfigurableManager:
result = node_configs.get(config_type) result = node_configs.get(config_type)
return result return result
def get_all_configs(self, node_id: int = _default_node) -> Dict[str, Any]: def get_all_configs(self, node_id: int = _default_node) -> dict[str, Any]:
""" """
Retrieve all current configuration types for a node. Retrieve all current configuration types for a node.
@ -284,11 +284,11 @@ class ModelManager(ConfigurableManager):
Creates a ModelManager object. Creates a ModelManager object.
""" """
super().__init__() super().__init__()
self.models: Dict[str, Any] = {} self.models: dict[str, Any] = {}
self.node_models: Dict[int, str] = {} self.node_models: dict[int, str] = {}
def set_model_config( def set_model_config(
self, node_id: int, model_name: str, config: Dict[str, str] = None self, node_id: int, model_name: str, config: dict[str, str] = None
) -> None: ) -> None:
""" """
Set configuration data for a model. Set configuration data for a model.
@ -317,7 +317,7 @@ class ModelManager(ConfigurableManager):
# set configuration # set configuration
self.set_configs(model_config, node_id=node_id, config_type=model_name) self.set_configs(model_config, node_id=node_id, config_type=model_name)
def get_model_config(self, node_id: int, model_name: str) -> Dict[str, str]: def get_model_config(self, node_id: int, model_name: str) -> dict[str, str]:
""" """
Retrieve configuration data for a model. Retrieve configuration data for a model.
@ -342,7 +342,7 @@ class ModelManager(ConfigurableManager):
self, self,
node: Union[WlanNode, EmaneNet], node: Union[WlanNode, EmaneNet],
model_class: "WirelessModelType", model_class: "WirelessModelType",
config: Dict[str, str] = None, config: dict[str, str] = None,
) -> None: ) -> None:
""" """
Set model and model configuration for node. Set model and model configuration for node.
@ -361,7 +361,7 @@ class ModelManager(ConfigurableManager):
def get_models( def get_models(
self, node: Union[WlanNode, EmaneNet] self, node: Union[WlanNode, EmaneNet]
) -> List[Tuple[Type, Dict[str, str]]]: ) -> list[tuple[type, dict[str, str]]]:
""" """
Return a list of model classes and values for a net if one has been Return a list of model classes and values for a net if one has been
configured. This is invoked when exporting a session to XML. configured. This is invoked when exporting a session to XML.

View file

@ -5,7 +5,7 @@ import logging
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Optional
from mako import exceptions from mako import exceptions
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
@ -67,7 +67,7 @@ class ConfigService(abc.ABC):
validation_timer: int = 5 validation_timer: int = 5
# directories to shadow and copy files from # directories to shadow and copy files from
shadow_directories: List[ShadowDir] = [] shadow_directories: list[ShadowDir] = []
def __init__(self, node: CoreNode) -> None: def __init__(self, node: CoreNode) -> None:
""" """
@ -79,9 +79,9 @@ class ConfigService(abc.ABC):
class_file = inspect.getfile(self.__class__) class_file = inspect.getfile(self.__class__)
templates_path = Path(class_file).parent.joinpath(TEMPLATES_DIR) templates_path = Path(class_file).parent.joinpath(TEMPLATES_DIR)
self.templates: TemplateLookup = TemplateLookup(directories=templates_path) self.templates: TemplateLookup = TemplateLookup(directories=templates_path)
self.config: Dict[str, Configuration] = {} self.config: dict[str, Configuration] = {}
self.custom_templates: Dict[str, str] = {} self.custom_templates: dict[str, str] = {}
self.custom_config: Dict[str, str] = {} self.custom_config: dict[str, str] = {}
configs = self.default_configs[:] configs = self.default_configs[:]
self._define_config(configs) self._define_config(configs)
@ -108,47 +108,47 @@ class ConfigService(abc.ABC):
@property @property
@abc.abstractmethod @abc.abstractmethod
def directories(self) -> List[str]: def directories(self) -> list[str]:
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def files(self) -> List[str]: def files(self) -> list[str]:
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def default_configs(self) -> List[Configuration]: def default_configs(self) -> list[Configuration]:
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def modes(self) -> Dict[str, Dict[str, str]]: def modes(self) -> dict[str, dict[str, str]]:
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def executables(self) -> List[str]: def executables(self) -> list[str]:
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def dependencies(self) -> List[str]: def dependencies(self) -> list[str]:
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def startup(self) -> List[str]: def startup(self) -> list[str]:
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def validate(self) -> List[str]: def validate(self) -> list[str]:
raise NotImplementedError raise NotImplementedError
@property @property
@abc.abstractmethod @abc.abstractmethod
def shutdown(self) -> List[str]: def shutdown(self) -> list[str]:
raise NotImplementedError raise NotImplementedError
@property @property
@ -276,7 +276,7 @@ class ConfigService(abc.ABC):
f"failure to create service directory: {directory}" f"failure to create service directory: {directory}"
) )
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
""" """
Returns key/value data, used when rendering file templates. Returns key/value data, used when rendering file templates.
@ -303,7 +303,7 @@ class ConfigService(abc.ABC):
""" """
raise CoreError(f"service({self.name}) unknown template({name})") raise CoreError(f"service({self.name}) unknown template({name})")
def get_templates(self) -> Dict[str, str]: def get_templates(self) -> dict[str, str]:
""" """
Retrieves mapping of file names to templates for all cases, which Retrieves mapping of file names to templates for all cases, which
includes custom templates, file templates, and text templates. includes custom templates, file templates, and text templates.
@ -331,7 +331,7 @@ class ConfigService(abc.ABC):
templates[file] = template templates[file] = template
return templates return templates
def get_rendered_templates(self) -> Dict[str, str]: def get_rendered_templates(self) -> dict[str, str]:
templates = {} templates = {}
data = self.data() data = self.data()
for file in sorted(self.files): for file in sorted(self.files):
@ -339,7 +339,7 @@ class ConfigService(abc.ABC):
templates[file] = rendered templates[file] = rendered
return templates return templates
def _get_rendered_template(self, file: str, data: Dict[str, Any]) -> str: def _get_rendered_template(self, file: str, data: dict[str, Any]) -> str:
file_path = Path(file) file_path = Path(file)
template_path = get_template_path(file_path) template_path = get_template_path(file_path)
if file in self.custom_templates: if file in self.custom_templates:
@ -426,7 +426,7 @@ class ConfigService(abc.ABC):
f"node({self.node.name}) service({self.name}) failed to validate" f"node({self.node.name}) service({self.name}) failed to validate"
) )
def _render(self, template: Template, data: Dict[str, Any] = None) -> str: def _render(self, template: Template, data: dict[str, Any] = None) -> str:
""" """
Renders template providing all associated data to template. Renders template providing all associated data to template.
@ -440,7 +440,7 @@ class ConfigService(abc.ABC):
node=self.node, config=self.render_config(), **data node=self.node, config=self.render_config(), **data
) )
def render_text(self, text: str, data: Dict[str, Any] = None) -> str: def render_text(self, text: str, data: dict[str, Any] = None) -> str:
""" """
Renders text based template providing all associated data to template. Renders text based template providing all associated data to template.
@ -458,7 +458,7 @@ class ConfigService(abc.ABC):
f"{exceptions.text_error_template().render_unicode()}" f"{exceptions.text_error_template().render_unicode()}"
) )
def render_template(self, template_path: str, data: Dict[str, Any] = None) -> str: def render_template(self, template_path: str, data: dict[str, Any] = None) -> str:
""" """
Renders file based template providing all associated data to template. Renders file based template providing all associated data to template.
@ -475,7 +475,7 @@ class ConfigService(abc.ABC):
f"{exceptions.text_error_template().render_unicode()}" f"{exceptions.text_error_template().render_unicode()}"
) )
def _define_config(self, configs: List[Configuration]) -> None: def _define_config(self, configs: list[Configuration]) -> None:
""" """
Initializes default configuration data. Initializes default configuration data.
@ -485,7 +485,7 @@ class ConfigService(abc.ABC):
for config in configs: for config in configs:
self.config[config.id] = config self.config[config.id] = config
def render_config(self) -> Dict[str, str]: def render_config(self) -> dict[str, str]:
""" """
Returns configuration data key/value pairs for rendering a template. Returns configuration data key/value pairs for rendering a template.
@ -496,7 +496,7 @@ class ConfigService(abc.ABC):
else: else:
return {k: v.default for k, v in self.config.items()} return {k: v.default for k, v in self.config.items()}
def set_config(self, data: Dict[str, str]) -> None: def set_config(self, data: dict[str, str]) -> None:
""" """
Set configuration data from key/value pairs. Set configuration data from key/value pairs.

View file

@ -1,5 +1,5 @@
import logging import logging
from typing import TYPE_CHECKING, Dict, List, Set from typing import TYPE_CHECKING
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -12,16 +12,16 @@ class ConfigServiceDependencies:
Generates sets of services to start in order of their dependencies. Generates sets of services to start in order of their dependencies.
""" """
def __init__(self, services: Dict[str, "ConfigService"]) -> None: def __init__(self, services: dict[str, "ConfigService"]) -> None:
""" """
Create a ConfigServiceDependencies instance. Create a ConfigServiceDependencies instance.
:param services: services for determining dependency sets :param services: services for determining dependency sets
""" """
# helpers to check validity # helpers to check validity
self.dependents: Dict[str, Set[str]] = {} self.dependents: dict[str, set[str]] = {}
self.started: Set[str] = set() self.started: set[str] = set()
self.node_services: Dict[str, "ConfigService"] = {} self.node_services: dict[str, "ConfigService"] = {}
for service in services.values(): for service in services.values():
self.node_services[service.name] = service self.node_services[service.name] = service
for dependency in service.dependencies: for dependency in service.dependencies:
@ -29,11 +29,11 @@ class ConfigServiceDependencies:
dependents.add(service.name) dependents.add(service.name)
# used to find paths # used to find paths
self.path: List["ConfigService"] = [] self.path: list["ConfigService"] = []
self.visited: Set[str] = set() self.visited: set[str] = set()
self.visiting: Set[str] = set() self.visiting: set[str] = set()
def startup_paths(self) -> List[List["ConfigService"]]: def startup_paths(self) -> list[list["ConfigService"]]:
""" """
Find startup path sets based on service dependencies. Find startup path sets based on service dependencies.
@ -54,8 +54,8 @@ class ConfigServiceDependencies:
if self.started != set(self.node_services): if self.started != set(self.node_services):
raise ValueError( raise ValueError(
"failure to start all services: %s != %s" f"failure to start all services: {self.started} != "
% (self.started, self.node_services.keys()) f"{self.node_services.keys()}"
) )
return paths return paths
@ -70,7 +70,7 @@ class ConfigServiceDependencies:
self.visited.clear() self.visited.clear()
self.visiting.clear() self.visiting.clear()
def _start(self, service: "ConfigService") -> List["ConfigService"]: def _start(self, service: "ConfigService") -> list["ConfigService"]:
""" """
Starts a oath for checking dependencies for a given service. Starts a oath for checking dependencies for a given service.
@ -81,7 +81,7 @@ class ConfigServiceDependencies:
self._reset() self._reset()
return self._visit(service) return self._visit(service)
def _visit(self, current_service: "ConfigService") -> List["ConfigService"]: def _visit(self, current_service: "ConfigService") -> list["ConfigService"]:
""" """
Visits a service when discovering dependency chains for service. Visits a service when discovering dependency chains for service.
@ -96,14 +96,14 @@ class ConfigServiceDependencies:
for service_name in current_service.dependencies: for service_name in current_service.dependencies:
if service_name not in self.node_services: if service_name not in self.node_services:
raise ValueError( raise ValueError(
"required dependency was not included in node services: %s" "required dependency was not included in node "
% service_name f"services: {service_name}"
) )
if service_name in self.visiting: if service_name in self.visiting:
raise ValueError( raise ValueError(
"cyclic dependency at service(%s): %s" f"cyclic dependency at service({current_service.name}): "
% (current_service.name, service_name) f"{service_name}"
) )
if service_name not in self.visited: if service_name not in self.visited:

View file

@ -2,7 +2,6 @@ import logging
import pathlib import pathlib
import pkgutil import pkgutil
from pathlib import Path from pathlib import Path
from typing import Dict, List, Type
from core import configservices, utils from core import configservices, utils
from core.configservice.base import ConfigService from core.configservice.base import ConfigService
@ -20,9 +19,9 @@ class ConfigServiceManager:
""" """
Create a ConfigServiceManager instance. Create a ConfigServiceManager instance.
""" """
self.services: Dict[str, Type[ConfigService]] = {} self.services: dict[str, type[ConfigService]] = {}
def get_service(self, name: str) -> Type[ConfigService]: def get_service(self, name: str) -> type[ConfigService]:
""" """
Retrieve a service by name. Retrieve a service by name.
@ -35,7 +34,7 @@ class ConfigServiceManager:
raise CoreError(f"service does not exist {name}") raise CoreError(f"service does not exist {name}")
return service_class return service_class
def add(self, service: Type[ConfigService]) -> None: def add(self, service: type[ConfigService]) -> None:
""" """
Add service to manager, checking service requirements have been met. Add service to manager, checking service requirements have been met.
@ -62,7 +61,7 @@ class ConfigServiceManager:
# make service available # make service available
self.services[name] = service self.services[name] = service
def load_locals(self) -> List[str]: def load_locals(self) -> list[str]:
""" """
Search and add config service from local core module. Search and add config service from local core module.
@ -81,7 +80,7 @@ class ConfigServiceManager:
logger.debug("not loading config service(%s): %s", service.name, e) logger.debug("not loading config service(%s): %s", service.name, e)
return errors return errors
def load(self, path: Path) -> List[str]: def load(self, path: Path) -> list[str]:
""" """
Search path provided for config services and add them for being managed. Search path provided for config services and add them for being managed.

View file

@ -1,5 +1,5 @@
import abc import abc
from typing import Any, Dict, List from typing import Any
from core.config import Configuration from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
@ -82,29 +82,30 @@ def rj45_check(iface: CoreInterface) -> bool:
class FRRZebra(ConfigService): class FRRZebra(ConfigService):
name: str = "FRRzebra" name: str = "FRRzebra"
group: str = GROUP group: str = GROUP
directories: List[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"] directories: list[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
files: List[str] = [ files: list[str] = [
"/usr/local/etc/frr/frr.conf", "/usr/local/etc/frr/frr.conf",
"frrboot.sh", "frrboot.sh",
"/usr/local/etc/frr/vtysh.conf", "/usr/local/etc/frr/vtysh.conf",
"/usr/local/etc/frr/daemons", "/usr/local/etc/frr/daemons",
] ]
executables: List[str] = ["zebra"] executables: list[str] = ["zebra"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash frrboot.sh zebra"] startup: list[str] = ["bash frrboot.sh zebra"]
validate: List[str] = ["pidof zebra"] validate: list[str] = ["pidof zebra"]
shutdown: List[str] = ["killall zebra"] shutdown: list[str] = ["killall zebra"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
frr_conf = self.files[0] frr_conf = self.files[0]
frr_bin_search = self.node.session.options.get( frr_bin_search = self.node.session.options.get(
"frr_bin_search", default="/usr/local/bin /usr/bin /usr/lib/frr" "frr_bin_search", default="/usr/local/bin /usr/bin /usr/lib/frr"
).strip('"') ).strip('"')
frr_sbin_search = self.node.session.options.get( frr_sbin_search = self.node.session.options.get(
"frr_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/frr" "frr_sbin_search",
default="/usr/local/sbin /usr/sbin /usr/lib/frr /usr/libexec/frr",
).strip('"') ).strip('"')
services = [] services = []
@ -145,16 +146,16 @@ class FRRZebra(ConfigService):
class FrrService(abc.ABC): class FrrService(abc.ABC):
group: str = GROUP group: str = GROUP
directories: List[str] = [] directories: list[str] = []
files: List[str] = [] files: list[str] = []
executables: List[str] = [] executables: list[str] = []
dependencies: List[str] = ["FRRzebra"] dependencies: list[str] = ["FRRzebra"]
startup: List[str] = [] startup: list[str] = []
validate: List[str] = [] validate: list[str] = []
shutdown: List[str] = [] shutdown: list[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
ipv4_routing: bool = False ipv4_routing: bool = False
ipv6_routing: bool = False ipv6_routing: bool = False
@ -175,8 +176,8 @@ class FRROspfv2(FrrService, ConfigService):
""" """
name: str = "FRROSPFv2" name: str = "FRROSPFv2"
shutdown: List[str] = ["killall ospfd"] shutdown: list[str] = ["killall ospfd"]
validate: List[str] = ["pidof ospfd"] validate: list[str] = ["pidof ospfd"]
ipv4_routing: bool = True ipv4_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
@ -227,8 +228,8 @@ class FRROspfv3(FrrService, ConfigService):
""" """
name: str = "FRROSPFv3" name: str = "FRROSPFv3"
shutdown: List[str] = ["killall ospf6d"] shutdown: list[str] = ["killall ospf6d"]
validate: List[str] = ["pidof ospf6d"] validate: list[str] = ["pidof ospf6d"]
ipv4_routing: bool = True ipv4_routing: bool = True
ipv6_routing: bool = True ipv6_routing: bool = True
@ -264,8 +265,8 @@ class FRRBgp(FrrService, ConfigService):
""" """
name: str = "FRRBGP" name: str = "FRRBGP"
shutdown: List[str] = ["killall bgpd"] shutdown: list[str] = ["killall bgpd"]
validate: List[str] = ["pidof bgpd"] validate: list[str] = ["pidof bgpd"]
custom_needed: bool = True custom_needed: bool = True
ipv4_routing: bool = True ipv4_routing: bool = True
ipv6_routing: bool = True ipv6_routing: bool = True
@ -294,8 +295,8 @@ class FRRRip(FrrService, ConfigService):
""" """
name: str = "FRRRIP" name: str = "FRRRIP"
shutdown: List[str] = ["killall ripd"] shutdown: list[str] = ["killall ripd"]
validate: List[str] = ["pidof ripd"] validate: list[str] = ["pidof ripd"]
ipv4_routing: bool = True ipv4_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
@ -319,8 +320,8 @@ class FRRRipng(FrrService, ConfigService):
""" """
name: str = "FRRRIPNG" name: str = "FRRRIPNG"
shutdown: List[str] = ["killall ripngd"] shutdown: list[str] = ["killall ripngd"]
validate: List[str] = ["pidof ripngd"] validate: list[str] = ["pidof ripngd"]
ipv6_routing: bool = True ipv6_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
@ -345,8 +346,8 @@ class FRRBabel(FrrService, ConfigService):
""" """
name: str = "FRRBabel" name: str = "FRRBabel"
shutdown: List[str] = ["killall babeld"] shutdown: list[str] = ["killall babeld"]
validate: List[str] = ["pidof babeld"] validate: list[str] = ["pidof babeld"]
ipv6_routing: bool = True ipv6_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
@ -385,8 +386,8 @@ class FRRpimd(FrrService, ConfigService):
""" """
name: str = "FRRpimd" name: str = "FRRpimd"
shutdown: List[str] = ["killall pimd"] shutdown: list[str] = ["killall pimd"]
validate: List[str] = ["pidof pimd"] validate: list[str] = ["pidof pimd"]
ipv4_routing: bool = True ipv4_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:

View file

@ -1,4 +1,4 @@
from typing import Any, Dict, List from typing import Any
from core import utils from core import utils
from core.config import Configuration from core.config import Configuration
@ -10,18 +10,18 @@ GROUP: str = "ProtoSvc"
class MgenSinkService(ConfigService): class MgenSinkService(ConfigService):
name: str = "MGEN_Sink" name: str = "MGEN_Sink"
group: str = GROUP group: str = GROUP
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["mgensink.sh", "sink.mgen"] files: list[str] = ["mgensink.sh", "sink.mgen"]
executables: List[str] = ["mgen"] executables: list[str] = ["mgen"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash mgensink.sh"] startup: list[str] = ["bash mgensink.sh"]
validate: List[str] = ["pidof mgen"] validate: list[str] = ["pidof mgen"]
shutdown: List[str] = ["killall mgen"] shutdown: list[str] = ["killall mgen"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
ifnames = [] ifnames = []
for iface in self.node.get_ifaces(): for iface in self.node.get_ifaces():
name = utils.sysctl_devname(iface.name) name = utils.sysctl_devname(iface.name)
@ -32,18 +32,18 @@ class MgenSinkService(ConfigService):
class NrlNhdp(ConfigService): class NrlNhdp(ConfigService):
name: str = "NHDP" name: str = "NHDP"
group: str = GROUP group: str = GROUP
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["nrlnhdp.sh"] files: list[str] = ["nrlnhdp.sh"]
executables: List[str] = ["nrlnhdp"] executables: list[str] = ["nrlnhdp"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash nrlnhdp.sh"] startup: list[str] = ["bash nrlnhdp.sh"]
validate: List[str] = ["pidof nrlnhdp"] validate: list[str] = ["pidof nrlnhdp"]
shutdown: List[str] = ["killall nrlnhdp"] shutdown: list[str] = ["killall nrlnhdp"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
ifnames = [] ifnames = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
@ -54,18 +54,18 @@ class NrlNhdp(ConfigService):
class NrlSmf(ConfigService): class NrlSmf(ConfigService):
name: str = "SMF" name: str = "SMF"
group: str = GROUP group: str = GROUP
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["startsmf.sh"] files: list[str] = ["startsmf.sh"]
executables: List[str] = ["nrlsmf", "killall"] executables: list[str] = ["nrlsmf", "killall"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash startsmf.sh"] startup: list[str] = ["bash startsmf.sh"]
validate: List[str] = ["pidof nrlsmf"] validate: list[str] = ["pidof nrlsmf"]
shutdown: List[str] = ["killall nrlsmf"] shutdown: list[str] = ["killall nrlsmf"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
has_nhdp = "NHDP" in self.node.config_services has_nhdp = "NHDP" in self.node.config_services
has_olsr = "OLSR" in self.node.config_services has_olsr = "OLSR" in self.node.config_services
ifnames = [] ifnames = []
@ -84,18 +84,18 @@ class NrlSmf(ConfigService):
class NrlOlsr(ConfigService): class NrlOlsr(ConfigService):
name: str = "OLSR" name: str = "OLSR"
group: str = GROUP group: str = GROUP
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["nrlolsrd.sh"] files: list[str] = ["nrlolsrd.sh"]
executables: List[str] = ["nrlolsrd"] executables: list[str] = ["nrlolsrd"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash nrlolsrd.sh"] startup: list[str] = ["bash nrlolsrd.sh"]
validate: List[str] = ["pidof nrlolsrd"] validate: list[str] = ["pidof nrlolsrd"]
shutdown: List[str] = ["killall nrlolsrd"] shutdown: list[str] = ["killall nrlolsrd"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
has_zebra = "zebra" in self.node.config_services has_zebra = "zebra" in self.node.config_services
ifname = None ifname = None
@ -108,18 +108,18 @@ class NrlOlsr(ConfigService):
class NrlOlsrv2(ConfigService): class NrlOlsrv2(ConfigService):
name: str = "OLSRv2" name: str = "OLSRv2"
group: str = GROUP group: str = GROUP
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["nrlolsrv2.sh"] files: list[str] = ["nrlolsrv2.sh"]
executables: List[str] = ["nrlolsrv2"] executables: list[str] = ["nrlolsrv2"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash nrlolsrv2.sh"] startup: list[str] = ["bash nrlolsrv2.sh"]
validate: List[str] = ["pidof nrlolsrv2"] validate: list[str] = ["pidof nrlolsrv2"]
shutdown: List[str] = ["killall nrlolsrv2"] shutdown: list[str] = ["killall nrlolsrv2"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
ifnames = [] ifnames = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
@ -130,18 +130,18 @@ class NrlOlsrv2(ConfigService):
class OlsrOrg(ConfigService): class OlsrOrg(ConfigService):
name: str = "OLSRORG" name: str = "OLSRORG"
group: str = GROUP group: str = GROUP
directories: List[str] = ["/etc/olsrd"] directories: list[str] = ["/etc/olsrd"]
files: List[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"] files: list[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
executables: List[str] = ["olsrd"] executables: list[str] = ["olsrd"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash olsrd.sh"] startup: list[str] = ["bash olsrd.sh"]
validate: List[str] = ["pidof olsrd"] validate: list[str] = ["pidof olsrd"]
shutdown: List[str] = ["killall olsrd"] shutdown: list[str] = ["killall olsrd"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
ifnames = [] ifnames = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
@ -152,13 +152,13 @@ class OlsrOrg(ConfigService):
class MgenActor(ConfigService): class MgenActor(ConfigService):
name: str = "MgenActor" name: str = "MgenActor"
group: str = GROUP group: str = GROUP
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["start_mgen_actor.sh"] files: list[str] = ["start_mgen_actor.sh"]
executables: List[str] = ["mgen"] executables: list[str] = ["mgen"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash start_mgen_actor.sh"] startup: list[str] = ["bash start_mgen_actor.sh"]
validate: List[str] = ["pidof mgen"] validate: list[str] = ["pidof mgen"]
shutdown: List[str] = ["killall mgen"] shutdown: list[str] = ["killall mgen"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}

View file

@ -1,6 +1,6 @@
import abc import abc
import logging import logging
from typing import Any, Dict, List from typing import Any
from core.config import Configuration from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
@ -84,22 +84,22 @@ def rj45_check(iface: CoreInterface) -> bool:
class Zebra(ConfigService): class Zebra(ConfigService):
name: str = "zebra" name: str = "zebra"
group: str = GROUP group: str = GROUP
directories: List[str] = ["/usr/local/etc/quagga", "/var/run/quagga"] directories: list[str] = ["/usr/local/etc/quagga", "/var/run/quagga"]
files: List[str] = [ files: list[str] = [
"/usr/local/etc/quagga/Quagga.conf", "/usr/local/etc/quagga/Quagga.conf",
"quaggaboot.sh", "quaggaboot.sh",
"/usr/local/etc/quagga/vtysh.conf", "/usr/local/etc/quagga/vtysh.conf",
] ]
executables: List[str] = ["zebra"] executables: list[str] = ["zebra"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash quaggaboot.sh zebra"] startup: list[str] = ["bash quaggaboot.sh zebra"]
validate: List[str] = ["pidof zebra"] validate: list[str] = ["pidof zebra"]
shutdown: List[str] = ["killall zebra"] shutdown: list[str] = ["killall zebra"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
quagga_bin_search = self.node.session.options.get( quagga_bin_search = self.node.session.options.get(
"quagga_bin_search", default="/usr/local/bin /usr/bin /usr/lib/quagga" "quagga_bin_search", default="/usr/local/bin /usr/bin /usr/lib/quagga"
).strip('"') ).strip('"')
@ -153,16 +153,16 @@ class Zebra(ConfigService):
class QuaggaService(abc.ABC): class QuaggaService(abc.ABC):
group: str = GROUP group: str = GROUP
directories: List[str] = [] directories: list[str] = []
files: List[str] = [] files: list[str] = []
executables: List[str] = [] executables: list[str] = []
dependencies: List[str] = ["zebra"] dependencies: list[str] = ["zebra"]
startup: List[str] = [] startup: list[str] = []
validate: List[str] = [] validate: list[str] = []
shutdown: List[str] = [] shutdown: list[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
ipv4_routing: bool = False ipv4_routing: bool = False
ipv6_routing: bool = False ipv6_routing: bool = False
@ -183,8 +183,8 @@ class Ospfv2(QuaggaService, ConfigService):
""" """
name: str = "OSPFv2" name: str = "OSPFv2"
validate: List[str] = ["pidof ospfd"] validate: list[str] = ["pidof ospfd"]
shutdown: List[str] = ["killall ospfd"] shutdown: list[str] = ["killall ospfd"]
ipv4_routing: bool = True ipv4_routing: bool = True
def quagga_iface_config(self, iface: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
@ -234,8 +234,8 @@ class Ospfv3(QuaggaService, ConfigService):
""" """
name: str = "OSPFv3" name: str = "OSPFv3"
shutdown: List[str] = ["killall ospf6d"] shutdown: list[str] = ["killall ospf6d"]
validate: List[str] = ["pidof ospf6d"] validate: list[str] = ["pidof ospf6d"]
ipv4_routing: bool = True ipv4_routing: bool = True
ipv6_routing: bool = True ipv6_routing: bool = True
@ -300,8 +300,8 @@ class Bgp(QuaggaService, ConfigService):
""" """
name: str = "BGP" name: str = "BGP"
shutdown: List[str] = ["killall bgpd"] shutdown: list[str] = ["killall bgpd"]
validate: List[str] = ["pidof bgpd"] validate: list[str] = ["pidof bgpd"]
ipv4_routing: bool = True ipv4_routing: bool = True
ipv6_routing: bool = True ipv6_routing: bool = True
@ -329,8 +329,8 @@ class Rip(QuaggaService, ConfigService):
""" """
name: str = "RIP" name: str = "RIP"
shutdown: List[str] = ["killall ripd"] shutdown: list[str] = ["killall ripd"]
validate: List[str] = ["pidof ripd"] validate: list[str] = ["pidof ripd"]
ipv4_routing: bool = True ipv4_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
@ -354,8 +354,8 @@ class Ripng(QuaggaService, ConfigService):
""" """
name: str = "RIPNG" name: str = "RIPNG"
shutdown: List[str] = ["killall ripngd"] shutdown: list[str] = ["killall ripngd"]
validate: List[str] = ["pidof ripngd"] validate: list[str] = ["pidof ripngd"]
ipv6_routing: bool = True ipv6_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
@ -380,8 +380,8 @@ class Babel(QuaggaService, ConfigService):
""" """
name: str = "Babel" name: str = "Babel"
shutdown: List[str] = ["killall babeld"] shutdown: list[str] = ["killall babeld"]
validate: List[str] = ["pidof babeld"] validate: list[str] = ["pidof babeld"]
ipv6_routing: bool = True ipv6_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
@ -420,8 +420,8 @@ class Xpimd(QuaggaService, ConfigService):
""" """
name: str = "Xpimd" name: str = "Xpimd"
shutdown: List[str] = ["killall xpimd"] shutdown: list[str] = ["killall xpimd"]
validate: List[str] = ["pidof xpimd"] validate: list[str] = ["pidof xpimd"]
ipv4_routing: bool = True ipv4_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:

View file

@ -1,4 +1,4 @@
from typing import Any, Dict, List from typing import Any
from core.config import ConfigString, Configuration from core.config import ConfigString, Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
@ -9,41 +9,41 @@ GROUP_NAME: str = "Security"
class VpnClient(ConfigService): class VpnClient(ConfigService):
name: str = "VPNClient" name: str = "VPNClient"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["vpnclient.sh"] files: list[str] = ["vpnclient.sh"]
executables: List[str] = ["openvpn", "ip", "killall"] executables: list[str] = ["openvpn", "ip", "killall"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash vpnclient.sh"] startup: list[str] = ["bash vpnclient.sh"]
validate: List[str] = ["pidof openvpn"] validate: list[str] = ["pidof openvpn"]
shutdown: List[str] = ["killall openvpn"] shutdown: list[str] = ["killall openvpn"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [ default_configs: list[Configuration] = [
ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"), ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"),
ConfigString(id="keyname", label="Key Name", default="client1"), ConfigString(id="keyname", label="Key Name", default="client1"),
ConfigString(id="server", label="Server", default="10.0.2.10"), ConfigString(id="server", label="Server", default="10.0.2.10"),
] ]
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
class VpnServer(ConfigService): class VpnServer(ConfigService):
name: str = "VPNServer" name: str = "VPNServer"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["vpnserver.sh"] files: list[str] = ["vpnserver.sh"]
executables: List[str] = ["openvpn", "ip", "killall"] executables: list[str] = ["openvpn", "ip", "killall"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash vpnserver.sh"] startup: list[str] = ["bash vpnserver.sh"]
validate: List[str] = ["pidof openvpn"] validate: list[str] = ["pidof openvpn"]
shutdown: List[str] = ["killall openvpn"] shutdown: list[str] = ["killall openvpn"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [ default_configs: list[Configuration] = [
ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"), ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"),
ConfigString(id="keyname", label="Key Name", default="server"), ConfigString(id="keyname", label="Key Name", default="server"),
ConfigString(id="subnet", label="Subnet", default="10.0.200.0"), ConfigString(id="subnet", label="Subnet", default="10.0.200.0"),
] ]
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
address = None address = None
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
ip4 = iface.get_ip4() ip4 = iface.get_ip4()
@ -56,48 +56,48 @@ class VpnServer(ConfigService):
class IPsec(ConfigService): class IPsec(ConfigService):
name: str = "IPsec" name: str = "IPsec"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["ipsec.sh"] files: list[str] = ["ipsec.sh"]
executables: List[str] = ["racoon", "ip", "setkey", "killall"] executables: list[str] = ["racoon", "ip", "setkey", "killall"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash ipsec.sh"] startup: list[str] = ["bash ipsec.sh"]
validate: List[str] = ["pidof racoon"] validate: list[str] = ["pidof racoon"]
shutdown: List[str] = ["killall racoon"] shutdown: list[str] = ["killall racoon"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
class Firewall(ConfigService): class Firewall(ConfigService):
name: str = "Firewall" name: str = "Firewall"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["firewall.sh"] files: list[str] = ["firewall.sh"]
executables: List[str] = ["iptables"] executables: list[str] = ["iptables"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash firewall.sh"] startup: list[str] = ["bash firewall.sh"]
validate: List[str] = [] validate: list[str] = []
shutdown: List[str] = [] shutdown: list[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
class Nat(ConfigService): class Nat(ConfigService):
name: str = "NAT" name: str = "NAT"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["nat.sh"] files: list[str] = ["nat.sh"]
executables: List[str] = ["iptables"] executables: list[str] = ["iptables"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash nat.sh"] startup: list[str] = ["bash nat.sh"]
validate: List[str] = [] validate: list[str] = []
shutdown: List[str] = [] shutdown: list[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
ifnames = [] ifnames = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
ifnames.append(iface.name) ifnames.append(iface.name)

View file

@ -1,4 +1,4 @@
from typing import Any, Dict, List from typing import Any
import netaddr import netaddr
@ -12,18 +12,18 @@ GROUP_NAME = "Utility"
class DefaultRouteService(ConfigService): class DefaultRouteService(ConfigService):
name: str = "DefaultRoute" name: str = "DefaultRoute"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["defaultroute.sh"] files: list[str] = ["defaultroute.sh"]
executables: List[str] = ["ip"] executables: list[str] = ["ip"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash defaultroute.sh"] startup: list[str] = ["bash defaultroute.sh"]
validate: List[str] = [] validate: list[str] = []
shutdown: List[str] = [] shutdown: list[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
# only add default routes for linked routing nodes # only add default routes for linked routing nodes
routes = [] routes = []
ifaces = self.node.get_ifaces() ifaces = self.node.get_ifaces()
@ -40,18 +40,18 @@ class DefaultRouteService(ConfigService):
class DefaultMulticastRouteService(ConfigService): class DefaultMulticastRouteService(ConfigService):
name: str = "DefaultMulticastRoute" name: str = "DefaultMulticastRoute"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["defaultmroute.sh"] files: list[str] = ["defaultmroute.sh"]
executables: List[str] = [] executables: list[str] = []
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash defaultmroute.sh"] startup: list[str] = ["bash defaultmroute.sh"]
validate: List[str] = [] validate: list[str] = []
shutdown: List[str] = [] shutdown: list[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
ifname = None ifname = None
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
ifname = iface.name ifname = iface.name
@ -62,18 +62,18 @@ class DefaultMulticastRouteService(ConfigService):
class StaticRouteService(ConfigService): class StaticRouteService(ConfigService):
name: str = "StaticRoute" name: str = "StaticRoute"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["staticroute.sh"] files: list[str] = ["staticroute.sh"]
executables: List[str] = [] executables: list[str] = []
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash staticroute.sh"] startup: list[str] = ["bash staticroute.sh"]
validate: List[str] = [] validate: list[str] = []
shutdown: List[str] = [] shutdown: list[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
routes = [] routes = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
for ip in iface.ips(): for ip in iface.ips():
@ -90,18 +90,18 @@ class StaticRouteService(ConfigService):
class IpForwardService(ConfigService): class IpForwardService(ConfigService):
name: str = "IPForward" name: str = "IPForward"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["ipforward.sh"] files: list[str] = ["ipforward.sh"]
executables: List[str] = ["sysctl"] executables: list[str] = ["sysctl"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash ipforward.sh"] startup: list[str] = ["bash ipforward.sh"]
validate: List[str] = [] validate: list[str] = []
shutdown: List[str] = [] shutdown: list[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
devnames = [] devnames = []
for iface in self.node.get_ifaces(): for iface in self.node.get_ifaces():
devname = utils.sysctl_devname(iface.name) devname = utils.sysctl_devname(iface.name)
@ -112,18 +112,18 @@ class IpForwardService(ConfigService):
class SshService(ConfigService): class SshService(ConfigService):
name: str = "SSH" name: str = "SSH"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = ["/etc/ssh", "/var/run/sshd"] directories: list[str] = ["/etc/ssh", "/var/run/sshd"]
files: List[str] = ["startsshd.sh", "/etc/ssh/sshd_config"] files: list[str] = ["startsshd.sh", "/etc/ssh/sshd_config"]
executables: List[str] = ["sshd"] executables: list[str] = ["sshd"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash startsshd.sh"] startup: list[str] = ["bash startsshd.sh"]
validate: List[str] = [] validate: list[str] = []
shutdown: List[str] = ["killall sshd"] shutdown: list[str] = ["killall sshd"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
return dict( return dict(
sshcfgdir=self.directories[0], sshcfgdir=self.directories[0],
sshstatedir=self.directories[1], sshstatedir=self.directories[1],
@ -134,18 +134,18 @@ class SshService(ConfigService):
class DhcpService(ConfigService): class DhcpService(ConfigService):
name: str = "DHCP" name: str = "DHCP"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = ["/etc/dhcp", "/var/lib/dhcp"] directories: list[str] = ["/etc/dhcp", "/var/lib/dhcp"]
files: List[str] = ["/etc/dhcp/dhcpd.conf"] files: list[str] = ["/etc/dhcp/dhcpd.conf"]
executables: List[str] = ["dhcpd"] executables: list[str] = ["dhcpd"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"] startup: list[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
validate: List[str] = ["pidof dhcpd"] validate: list[str] = ["pidof dhcpd"]
shutdown: List[str] = ["killall dhcpd"] shutdown: list[str] = ["killall dhcpd"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
subnets = [] subnets = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
for ip4 in iface.ip4s: for ip4 in iface.ip4s:
@ -162,18 +162,18 @@ class DhcpService(ConfigService):
class DhcpClientService(ConfigService): class DhcpClientService(ConfigService):
name: str = "DHCPClient" name: str = "DHCPClient"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["startdhcpclient.sh"] files: list[str] = ["startdhcpclient.sh"]
executables: List[str] = ["dhclient"] executables: list[str] = ["dhclient"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash startdhcpclient.sh"] startup: list[str] = ["bash startdhcpclient.sh"]
validate: List[str] = ["pidof dhclient"] validate: list[str] = ["pidof dhclient"]
shutdown: List[str] = ["killall dhclient"] shutdown: list[str] = ["killall dhclient"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
ifnames = [] ifnames = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
ifnames.append(iface.name) ifnames.append(iface.name)
@ -183,56 +183,56 @@ class DhcpClientService(ConfigService):
class FtpService(ConfigService): class FtpService(ConfigService):
name: str = "FTP" name: str = "FTP"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = ["/var/run/vsftpd/empty", "/var/ftp"] directories: list[str] = ["/var/run/vsftpd/empty", "/var/ftp"]
files: List[str] = ["vsftpd.conf"] files: list[str] = ["vsftpd.conf"]
executables: List[str] = ["vsftpd"] executables: list[str] = ["vsftpd"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["vsftpd ./vsftpd.conf"] startup: list[str] = ["vsftpd ./vsftpd.conf"]
validate: List[str] = ["pidof vsftpd"] validate: list[str] = ["pidof vsftpd"]
shutdown: List[str] = ["killall vsftpd"] shutdown: list[str] = ["killall vsftpd"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
class PcapService(ConfigService): class PcapService(ConfigService):
name: str = "pcap" name: str = "pcap"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [] directories: list[str] = []
files: List[str] = ["pcap.sh"] files: list[str] = ["pcap.sh"]
executables: List[str] = ["tcpdump"] executables: list[str] = ["tcpdump"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash pcap.sh start"] startup: list[str] = ["bash pcap.sh start"]
validate: List[str] = ["pidof tcpdump"] validate: list[str] = ["pidof tcpdump"]
shutdown: List[str] = ["bash pcap.sh stop"] shutdown: list[str] = ["bash pcap.sh stop"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
ifnames = [] ifnames = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
ifnames.append(iface.name) ifnames.append(iface.name)
return dict() return dict(ifnames=ifnames)
class RadvdService(ConfigService): class RadvdService(ConfigService):
name: str = "radvd" name: str = "radvd"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = ["/etc/radvd", "/var/run/radvd"] directories: list[str] = ["/etc/radvd", "/var/run/radvd"]
files: List[str] = ["/etc/radvd/radvd.conf"] files: list[str] = ["/etc/radvd/radvd.conf"]
executables: List[str] = ["radvd"] executables: list[str] = ["radvd"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = [ startup: list[str] = [
"radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log" "radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"
] ]
validate: List[str] = ["pidof radvd"] validate: list[str] = ["pidof radvd"]
shutdown: List[str] = ["pkill radvd"] shutdown: list[str] = ["pkill radvd"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
ifaces = [] ifaces = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
prefixes = [] prefixes = []
@ -247,22 +247,22 @@ class RadvdService(ConfigService):
class AtdService(ConfigService): class AtdService(ConfigService):
name: str = "atd" name: str = "atd"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"] directories: list[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
files: List[str] = ["startatd.sh"] files: list[str] = ["startatd.sh"]
executables: List[str] = ["atd"] executables: list[str] = ["atd"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["bash startatd.sh"] startup: list[str] = ["bash startatd.sh"]
validate: List[str] = ["pidof atd"] validate: list[str] = ["pidof atd"]
shutdown: List[str] = ["pkill atd"] shutdown: list[str] = ["pkill atd"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
class HttpService(ConfigService): class HttpService(ConfigService):
name: str = "HTTP" name: str = "HTTP"
group: str = GROUP_NAME group: str = GROUP_NAME
directories: List[str] = [ directories: list[str] = [
"/etc/apache2", "/etc/apache2",
"/var/run/apache2", "/var/run/apache2",
"/var/log/apache2", "/var/log/apache2",
@ -270,21 +270,21 @@ class HttpService(ConfigService):
"/var/lock/apache2", "/var/lock/apache2",
"/var/www", "/var/www",
] ]
files: List[str] = [ files: list[str] = [
"/etc/apache2/apache2.conf", "/etc/apache2/apache2.conf",
"/etc/apache2/envvars", "/etc/apache2/envvars",
"/var/www/index.html", "/var/www/index.html",
] ]
executables: List[str] = ["apache2ctl"] executables: list[str] = ["apache2ctl"]
dependencies: List[str] = [] dependencies: list[str] = []
startup: List[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"] startup: list[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"]
validate: List[str] = ["pidof apache2"] validate: list[str] = ["pidof apache2"]
shutdown: List[str] = ["apache2ctl stop"] shutdown: list[str] = ["apache2ctl stop"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: list[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: dict[str, dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> dict[str, Any]:
ifaces = [] ifaces = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
ifaces.append(iface) ifaces.append(iface)

View file

@ -1,5 +1,5 @@
# auto-generated by RADVD service (utility.py) # auto-generated by RADVD service (utility.py)
% for ifname, prefixes in values: % for ifname, prefixes in ifaces:
interface ${ifname} interface ${ifname}
{ {
AdvSendAdvert on; AdvSendAdvert on;

View file

@ -13,4 +13,5 @@ sysctl -w net.ipv4.conf.default.rp_filter=0
sysctl -w net.ipv4.conf.${devname}.forwarding=1 sysctl -w net.ipv4.conf.${devname}.forwarding=1
sysctl -w net.ipv4.conf.${devname}.send_redirects=0 sysctl -w net.ipv4.conf.${devname}.send_redirects=0
sysctl -w net.ipv4.conf.${devname}.rp_filter=0 sysctl -w net.ipv4.conf.${devname}.rp_filter=0
sysctl -w net.ipv6.conf.${devname}.forwarding=1
% endfor % endfor

View file

@ -3,7 +3,7 @@
# (-s snap length, -C limit pcap file length, -n disable name resolution) # (-s snap length, -C limit pcap file length, -n disable name resolution)
if [ "x$1" = "xstart" ]; then if [ "x$1" = "xstart" ]; then
% for ifname in ifnames: % for ifname in ifnames:
tcpdump -s 12288 -C 10 -n -w ${node.name}.${ifname}.pcap -i ${ifname} < /dev/null & tcpdump -s 12288 -C 10 -n -w ${node.name}.${ifname}.pcap -i ${ifname} > /dev/null 2>&1 &
% endfor % endfor
elif [ "x$1" = "xstop" ]; then elif [ "x$1" = "xstop" ]; then
mkdir -p $SESSION_DIR/pcap mkdir -p $SESSION_DIR/pcap

View file

@ -6,7 +6,7 @@ import logging
import os import os
import threading import threading
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union from typing import TYPE_CHECKING, Optional, Union
from core import utils from core import utils
from core.emane.emanemodel import EmaneModel from core.emane.emanemodel import EmaneModel
@ -126,9 +126,9 @@ class EmaneManager:
""" """
super().__init__() super().__init__()
self.session: "Session" = session self.session: "Session" = session
self.nems_to_ifaces: Dict[int, CoreInterface] = {} self.nems_to_ifaces: dict[int, CoreInterface] = {}
self.ifaces_to_nems: Dict[CoreInterface, int] = {} self.ifaces_to_nems: dict[CoreInterface, int] = {}
self._emane_nets: Dict[int, EmaneNet] = {} self._emane_nets: dict[int, EmaneNet] = {}
self._emane_node_lock: threading.Lock = threading.Lock() self._emane_node_lock: threading.Lock = threading.Lock()
# port numbers are allocated from these counters # port numbers are allocated from these counters
self.platformport: int = self.session.options.get_int( self.platformport: int = self.session.options.get_int(
@ -141,14 +141,14 @@ class EmaneManager:
self.eventmonthread: Optional[threading.Thread] = None self.eventmonthread: Optional[threading.Thread] = None
# model for global EMANE configuration options # model for global EMANE configuration options
self.node_configs: Dict[int, Dict[str, Dict[str, str]]] = {} self.node_configs: dict[int, dict[str, dict[str, str]]] = {}
self.node_models: Dict[int, str] = {} self.node_models: dict[int, str] = {}
# link monitor # link monitor
self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self) self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self)
# emane event monitoring # emane event monitoring
self.services: Dict[str, EmaneEventService] = {} self.services: dict[str, EmaneEventService] = {}
self.nem_service: Dict[int, EmaneEventService] = {} self.nem_service: dict[int, EmaneEventService] = {}
def next_nem_id(self, iface: CoreInterface) -> int: def next_nem_id(self, iface: CoreInterface) -> int:
nem_id = self.session.options.get_int("nem_id_start") nem_id = self.session.options.get_int("nem_id_start")
@ -161,7 +161,7 @@ class EmaneManager:
def get_config( def get_config(
self, key: int, model: str, default: bool = True self, key: int, model: str, default: bool = True
) -> Optional[Dict[str, str]]: ) -> Optional[dict[str, str]]:
""" """
Get the current or default configuration for an emane model. Get the current or default configuration for an emane model.
@ -181,7 +181,7 @@ class EmaneManager:
config = model_class.default_values() config = model_class.default_values()
return config return config
def set_config(self, key: int, model: str, config: Dict[str, str] = None) -> None: def set_config(self, key: int, model: str, config: dict[str, str] = None) -> None:
""" """
Sets and update the provided configuration against the default model Sets and update the provided configuration against the default model
or currently set emane model configuration. or currently set emane model configuration.
@ -199,7 +199,7 @@ class EmaneManager:
model_configs = self.node_configs.setdefault(key, {}) model_configs = self.node_configs.setdefault(key, {})
model_configs[model] = model_config model_configs[model] = model_config
def get_model(self, model_name: str) -> Type[EmaneModel]: def get_model(self, model_name: str) -> type[EmaneModel]:
""" """
Convenience method for getting globally loaded emane models. Convenience method for getting globally loaded emane models.
@ -211,7 +211,7 @@ class EmaneManager:
def get_iface_config( def get_iface_config(
self, emane_net: EmaneNet, iface: CoreInterface self, emane_net: EmaneNet, iface: CoreInterface
) -> Dict[str, str]: ) -> dict[str, str]:
""" """
Retrieve configuration for a given interface, first checking for interface Retrieve configuration for a given interface, first checking for interface
specific config, node specific config, network specific config, and finally specific config, node specific config, network specific config, and finally
@ -260,7 +260,7 @@ class EmaneManager:
) )
self._emane_nets[emane_net.id] = emane_net self._emane_nets[emane_net.id] = emane_net
def getnodes(self) -> Set[CoreNode]: def getnodes(self) -> set[CoreNode]:
""" """
Return a set of CoreNodes that are linked to an EMANE network, Return a set of CoreNodes that are linked to an EMANE network,
e.g. containers having one or more radio interfaces. e.g. containers having one or more radio interfaces.
@ -335,7 +335,7 @@ class EmaneManager:
self.start_daemon(iface) self.start_daemon(iface)
self.install_iface(iface, config) self.install_iface(iface, config)
def get_ifaces(self) -> List[Tuple[EmaneNet, TunTap]]: def get_ifaces(self) -> list[tuple[EmaneNet, TunTap]]:
ifaces = [] ifaces = []
for emane_net in self._emane_nets.values(): for emane_net in self._emane_nets.values():
if not emane_net.wireless_model: if not emane_net.wireless_model:
@ -354,7 +354,7 @@ class EmaneManager:
return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].id)) return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].id))
def setup_control_channels( def setup_control_channels(
self, nem_id: int, iface: CoreInterface, config: Dict[str, str] self, nem_id: int, iface: CoreInterface, config: dict[str, str]
) -> None: ) -> None:
node = iface.node node = iface.node
# setup ota device # setup ota device
@ -419,7 +419,7 @@ class EmaneManager:
def get_nem_position( def get_nem_position(
self, iface: CoreInterface self, iface: CoreInterface
) -> Optional[Tuple[int, float, float, int]]: ) -> Optional[tuple[int, float, float, int]]:
""" """
Retrieves nem position for a given interface. Retrieves nem position for a given interface.
@ -453,7 +453,7 @@ class EmaneManager:
event.append(nemid, latitude=lat, longitude=lon, altitude=alt) event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.publish_event(nemid, event, send_all=True) self.publish_event(nemid, event, send_all=True)
def set_nem_positions(self, moved_ifaces: List[CoreInterface]) -> None: def set_nem_positions(self, moved_ifaces: list[CoreInterface]) -> None:
""" """
Several NEMs have moved, from e.g. a WaypointMobilityModel Several NEMs have moved, from e.g. a WaypointMobilityModel
calculation. Generate an EMANE Location Event having several calculation. Generate an EMANE Location Event having several
@ -480,7 +480,7 @@ class EmaneManager:
try: try:
with path.open("a") as f: with path.open("a") as f:
f.write(f"{iface.node.name} {iface.name} {nem_id}\n") f.write(f"{iface.node.name} {iface.name} {nem_id}\n")
except IOError: except OSError:
logger.exception("error writing to emane nem file") logger.exception("error writing to emane nem file")
def links_enabled(self) -> bool: def links_enabled(self) -> bool:
@ -624,7 +624,7 @@ class EmaneManager:
args = f"{emanecmd} -f {log_file} {platform_xml}" args = f"{emanecmd} -f {log_file} {platform_xml}"
node.host_cmd(args, cwd=self.session.directory) node.host_cmd(args, cwd=self.session.directory)
def install_iface(self, iface: TunTap, config: Dict[str, str]) -> None: def install_iface(self, iface: TunTap, config: dict[str, str]) -> None:
external = config.get("external", "0") external = config.get("external", "0")
if external == "0": if external == "0":
iface.set_ips() iface.set_ips()

View file

@ -1,6 +1,5 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict, List
from core.config import Configuration from core.config import Configuration
from core.emulator.enumerations import ConfigDataTypes from core.emulator.enumerations import ConfigDataTypes
@ -33,7 +32,7 @@ def _type_value(config_type: str) -> ConfigDataTypes:
return ConfigDataTypes[config_type] return ConfigDataTypes[config_type]
def _get_possible(config_type: str, config_regex: str) -> List[str]: def _get_possible(config_type: str, config_regex: str) -> list[str]:
""" """
Retrieve possible config value options based on emane regexes. Retrieve possible config value options based on emane regexes.
@ -51,7 +50,7 @@ def _get_possible(config_type: str, config_regex: str) -> List[str]:
return [] return []
def _get_default(config_type_name: str, config_value: List[str]) -> str: def _get_default(config_type_name: str, config_value: list[str]) -> str:
""" """
Convert default configuration values to one used by core. Convert default configuration values to one used by core.
@ -74,7 +73,7 @@ def _get_default(config_type_name: str, config_value: List[str]) -> str:
return config_default return config_default
def parse(manifest_path: Path, defaults: Dict[str, str]) -> List[Configuration]: def parse(manifest_path: Path, defaults: dict[str, str]) -> list[Configuration]:
""" """
Parses a valid emane manifest file and converts the provided configuration values Parses a valid emane manifest file and converts the provided configuration values
into ones used by core. into ones used by core.

View file

@ -3,7 +3,7 @@ Defines Emane Models used within CORE.
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Set from typing import Optional
from core.config import ConfigBool, ConfigGroup, ConfigString, Configuration from core.config import ConfigBool, ConfigGroup, ConfigString, Configuration
from core.emane import emanemanifest from core.emane import emanemanifest
@ -28,38 +28,38 @@ class EmaneModel(WirelessModel):
# default platform configuration settings # default platform configuration settings
platform_controlport: str = "controlportendpoint" platform_controlport: str = "controlportendpoint"
platform_xml: str = "nemmanager.xml" platform_xml: str = "nemmanager.xml"
platform_defaults: Dict[str, str] = { platform_defaults: dict[str, str] = {
"eventservicedevice": DEFAULT_DEV, "eventservicedevice": DEFAULT_DEV,
"eventservicegroup": "224.1.2.8:45703", "eventservicegroup": "224.1.2.8:45703",
"otamanagerdevice": DEFAULT_DEV, "otamanagerdevice": DEFAULT_DEV,
"otamanagergroup": "224.1.2.8:45702", "otamanagergroup": "224.1.2.8:45702",
} }
platform_config: List[Configuration] = [] platform_config: list[Configuration] = []
# default mac configuration settings # default mac configuration settings
mac_library: Optional[str] = None mac_library: Optional[str] = None
mac_xml: Optional[str] = None mac_xml: Optional[str] = None
mac_defaults: Dict[str, str] = {} mac_defaults: dict[str, str] = {}
mac_config: List[Configuration] = [] mac_config: list[Configuration] = []
# default phy configuration settings, using the universal model # default phy configuration settings, using the universal model
phy_library: Optional[str] = None phy_library: Optional[str] = None
phy_xml: str = "emanephy.xml" phy_xml: str = "emanephy.xml"
phy_defaults: Dict[str, str] = { phy_defaults: dict[str, str] = {
"subid": "1", "subid": "1",
"propagationmodel": "2ray", "propagationmodel": "2ray",
"noisemode": "none", "noisemode": "none",
} }
phy_config: List[Configuration] = [] phy_config: list[Configuration] = []
# support for external configurations # support for external configurations
external_config: List[Configuration] = [ external_config: list[Configuration] = [
ConfigBool(id="external", default="0"), ConfigBool(id="external", default="0"),
ConfigString(id="platformendpoint", default="127.0.0.1:40001"), ConfigString(id="platformendpoint", default="127.0.0.1:40001"),
ConfigString(id="transportendpoint", default="127.0.0.1:50002"), ConfigString(id="transportendpoint", default="127.0.0.1:50002"),
] ]
config_ignore: Set[str] = set() config_ignore: set[str] = set()
@classmethod @classmethod
def load(cls, emane_prefix: Path) -> None: def load(cls, emane_prefix: Path) -> None:
@ -94,7 +94,7 @@ class EmaneModel(WirelessModel):
cls.platform_config.pop(controlport_index) cls.platform_config.pop(controlport_index)
@classmethod @classmethod
def configurations(cls) -> List[Configuration]: def configurations(cls) -> list[Configuration]:
""" """
Returns the combination all all configurations (mac, phy, and external). Returns the combination all all configurations (mac, phy, and external).
@ -105,7 +105,7 @@ class EmaneModel(WirelessModel):
) )
@classmethod @classmethod
def config_groups(cls) -> List[ConfigGroup]: def config_groups(cls) -> list[ConfigGroup]:
""" """
Returns the defined configuration groups. Returns the defined configuration groups.
@ -122,7 +122,7 @@ class EmaneModel(WirelessModel):
ConfigGroup("External Parameters", phy_len + 1, config_len), ConfigGroup("External Parameters", phy_len + 1, config_len),
] ]
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None: def build_xml_files(self, config: dict[str, str], iface: CoreInterface) -> None:
""" """
Builds xml files for this emane model. Creates a nem.xml file that points to Builds xml files for this emane model. Creates a nem.xml file that points to
both mac.xml and phy.xml definitions. both mac.xml and phy.xml definitions.
@ -146,7 +146,7 @@ class EmaneModel(WirelessModel):
""" """
logger.debug("emane model(%s) has no post setup tasks", self.name) logger.debug("emane model(%s) has no post setup tasks", self.name)
def update(self, moved_ifaces: List[CoreInterface]) -> None: def update(self, moved_ifaces: list[CoreInterface]) -> None:
""" """
Invoked from MobilityModel when nodes are moved; this causes Invoked from MobilityModel when nodes are moved; this causes
emane location events to be generated for the nodes in the moved emane location events to be generated for the nodes in the moved

View file

@ -2,7 +2,7 @@ import logging
import sched import sched
import threading import threading
import time import time
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Optional
from lxml import etree from lxml import etree
@ -34,10 +34,10 @@ NEM_SELF: int = 65535
class LossTable: class LossTable:
def __init__(self, losses: Dict[float, float]) -> None: def __init__(self, losses: dict[float, float]) -> None:
self.losses: Dict[float, float] = losses self.losses: dict[float, float] = losses
self.sinrs: List[float] = sorted(self.losses.keys()) self.sinrs: list[float] = sorted(self.losses.keys())
self.loss_lookup: Dict[int, float] = {} self.loss_lookup: dict[int, float] = {}
for index, value in enumerate(self.sinrs): for index, value in enumerate(self.sinrs):
self.loss_lookup[index] = self.losses[value] self.loss_lookup[index] = self.losses[value]
self.mac_id: Optional[str] = None self.mac_id: Optional[str] = None
@ -84,7 +84,7 @@ class EmaneClient:
self.client: shell.ControlPortClient = shell.ControlPortClient( self.client: shell.ControlPortClient = shell.ControlPortClient(
self.address, port self.address, port
) )
self.nems: Dict[int, LossTable] = {} self.nems: dict[int, LossTable] = {}
self.setup() self.setup()
def setup(self) -> None: def setup(self) -> None:
@ -110,7 +110,7 @@ class EmaneClient:
self.nems[nem_id] = loss_table self.nems[nem_id] = loss_table
def check_links( def check_links(
self, links: Dict[Tuple[int, int], EmaneLink], loss_threshold: int self, links: dict[tuple[int, int], EmaneLink], loss_threshold: int
) -> None: ) -> None:
for from_nem, loss_table in self.nems.items(): for from_nem, loss_table in self.nems.items():
tables = self.client.getStatisticTable(loss_table.mac_id, (SINR_TABLE,)) tables = self.client.getStatisticTable(loss_table.mac_id, (SINR_TABLE,))
@ -138,11 +138,11 @@ class EmaneClient:
link = EmaneLink(from_nem, to_nem, sinr) link = EmaneLink(from_nem, to_nem, sinr)
links[link_key] = link links[link_key] = link
def handle_tdma(self, config: Dict[str, Tuple]): def handle_tdma(self, config: dict[str, tuple]):
pcr = config["pcrcurveuri"][0][0] pcr = config["pcrcurveuri"][0][0]
logger.debug("tdma pcr: %s", pcr) logger.debug("tdma pcr: %s", pcr)
def handle_80211(self, config: Dict[str, Tuple]) -> LossTable: def handle_80211(self, config: dict[str, tuple]) -> LossTable:
unicastrate = config["unicastrate"][0][0] unicastrate = config["unicastrate"][0][0]
pcr = config["pcrcurveuri"][0][0] pcr = config["pcrcurveuri"][0][0]
logger.debug("80211 pcr: %s", pcr) logger.debug("80211 pcr: %s", pcr)
@ -159,7 +159,7 @@ class EmaneClient:
losses[sinr] = por losses[sinr] = por
return LossTable(losses) return LossTable(losses)
def handle_rfpipe(self, config: Dict[str, Tuple]) -> LossTable: def handle_rfpipe(self, config: dict[str, tuple]) -> LossTable:
pcr = config["pcrcurveuri"][0][0] pcr = config["pcrcurveuri"][0][0]
logger.debug("rfpipe pcr: %s", pcr) logger.debug("rfpipe pcr: %s", pcr)
tree = etree.parse(pcr) tree = etree.parse(pcr)
@ -179,9 +179,9 @@ class EmaneClient:
class EmaneLinkMonitor: class EmaneLinkMonitor:
def __init__(self, emane_manager: "EmaneManager") -> None: def __init__(self, emane_manager: "EmaneManager") -> None:
self.emane_manager: "EmaneManager" = emane_manager self.emane_manager: "EmaneManager" = emane_manager
self.clients: List[EmaneClient] = [] self.clients: list[EmaneClient] = []
self.links: Dict[Tuple[int, int], EmaneLink] = {} self.links: dict[tuple[int, int], EmaneLink] = {}
self.complete_links: Set[Tuple[int, int]] = set() self.complete_links: set[tuple[int, int]] = set()
self.loss_threshold: Optional[int] = None self.loss_threshold: Optional[int] = None
self.link_interval: Optional[int] = None self.link_interval: Optional[int] = None
self.link_timeout: Optional[int] = None self.link_timeout: Optional[int] = None
@ -210,7 +210,7 @@ class EmaneLinkMonitor:
if client.nems: if client.nems:
self.clients.append(client) self.clients.append(client)
def get_addresses(self) -> List[Tuple[str, int]]: def get_addresses(self) -> list[tuple[str, int]]:
addresses = [] addresses = []
nodes = self.emane_manager.getnodes() nodes = self.emane_manager.getnodes()
for node in nodes: for node in nodes:
@ -273,25 +273,25 @@ class EmaneLinkMonitor:
if self.running: if self.running:
self.scheduler.enter(self.link_interval, 0, self.check_links) self.scheduler.enter(self.link_interval, 0, self.check_links)
def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]: def get_complete_id(self, link_id: tuple[int, int]) -> tuple[int, int]:
value1, value2 = link_id value1, value2 = link_id
if value1 < value2: if value1 < value2:
return value1, value2 return value1, value2
else: else:
return value2, value1 return value2, value1
def is_complete_link(self, link_id: Tuple[int, int]) -> bool: def is_complete_link(self, link_id: tuple[int, int]) -> bool:
reverse_id = link_id[1], link_id[0] reverse_id = link_id[1], link_id[0]
return link_id in self.links and reverse_id in self.links return link_id in self.links and reverse_id in self.links
def get_link_label(self, link_id: Tuple[int, int]) -> str: def get_link_label(self, link_id: tuple[int, int]) -> str:
source_id = tuple(sorted(link_id)) source_id = tuple(sorted(link_id))
source_link = self.links[source_id] source_link = self.links[source_id]
dest_id = link_id[::-1] dest_id = link_id[::-1]
dest_link = self.links[dest_id] dest_link = self.links[dest_id]
return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}" return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}"
def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None: def send_link(self, message_type: MessageFlags, link_id: tuple[int, int]) -> None:
nem1, nem2 = link_id nem1, nem2 = link_id
link = self.emane_manager.get_nem_link(nem1, nem2, message_type) link = self.emane_manager.get_nem_link(nem1, nem2, message_type)
if link: if link:

View file

@ -1,7 +1,6 @@
import logging import logging
import pkgutil import pkgutil
from pathlib import Path from pathlib import Path
from typing import Dict, List, Type
from core import utils from core import utils
from core.emane import models as emane_models from core.emane import models as emane_models
@ -12,10 +11,10 @@ logger = logging.getLogger(__name__)
class EmaneModelManager: class EmaneModelManager:
models: Dict[str, Type[EmaneModel]] = {} models: dict[str, type[EmaneModel]] = {}
@classmethod @classmethod
def load_locals(cls, emane_prefix: Path) -> List[str]: def load_locals(cls, emane_prefix: Path) -> list[str]:
""" """
Load local core emane models and make them available. Load local core emane models and make them available.
@ -38,7 +37,7 @@ class EmaneModelManager:
return errors return errors
@classmethod @classmethod
def load(cls, path: Path, emane_prefix: Path) -> List[str]: def load(cls, path: Path, emane_prefix: Path) -> list[str]:
""" """
Search and load custom emane models and make them available. Search and load custom emane models and make them available.
@ -63,7 +62,7 @@ class EmaneModelManager:
return errors return errors
@classmethod @classmethod
def get(cls, name: str) -> Type[EmaneModel]: def get(cls, name: str) -> type[EmaneModel]:
model = cls.models.get(name) model = cls.models.get(name)
if model is None: if model is None:
raise CoreError(f"emame model does not exist {name}") raise CoreError(f"emame model does not exist {name}")

View file

@ -2,7 +2,6 @@
EMANE Bypass model for CORE EMANE Bypass model for CORE
""" """
from pathlib import Path from pathlib import Path
from typing import List, Set
from core.config import ConfigBool, Configuration from core.config import ConfigBool, Configuration
from core.emane import emanemodel from core.emane import emanemodel
@ -12,11 +11,11 @@ class EmaneBypassModel(emanemodel.EmaneModel):
name: str = "emane_bypass" name: str = "emane_bypass"
# values to ignore, when writing xml files # values to ignore, when writing xml files
config_ignore: Set[str] = {"none"} config_ignore: set[str] = {"none"}
# mac definitions # mac definitions
mac_library: str = "bypassmaclayer" mac_library: str = "bypassmaclayer"
mac_config: List[Configuration] = [ mac_config: list[Configuration] = [
ConfigBool( ConfigBool(
id="none", id="none",
default="0", default="0",
@ -26,7 +25,7 @@ class EmaneBypassModel(emanemodel.EmaneModel):
# phy definitions # phy definitions
phy_library: str = "bypassphylayer" phy_library: str = "bypassphylayer"
phy_config: List[Configuration] = [] phy_config: list[Configuration] = []
@classmethod @classmethod
def load(cls, emane_prefix: Path) -> None: def load(cls, emane_prefix: Path) -> None:

View file

@ -4,7 +4,6 @@ commeffect.py: EMANE CommEffect model for CORE
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict, List
from lxml import etree from lxml import etree
@ -42,12 +41,12 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
name: str = "emane_commeffect" name: str = "emane_commeffect"
shim_library: str = "commeffectshim" shim_library: str = "commeffectshim"
shim_xml: str = "commeffectshim.xml" shim_xml: str = "commeffectshim.xml"
shim_defaults: Dict[str, str] = {} shim_defaults: dict[str, str] = {}
config_shim: List[Configuration] = [] config_shim: list[Configuration] = []
# comm effect does not need the default phy and external configurations # comm effect does not need the default phy and external configurations
phy_config: List[Configuration] = [] phy_config: list[Configuration] = []
external_config: List[Configuration] = [] external_config: list[Configuration] = []
@classmethod @classmethod
def load(cls, emane_prefix: Path) -> None: def load(cls, emane_prefix: Path) -> None:
@ -56,11 +55,11 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
cls.config_shim = emanemanifest.parse(shim_xml_path, cls.shim_defaults) cls.config_shim = emanemanifest.parse(shim_xml_path, cls.shim_defaults)
@classmethod @classmethod
def configurations(cls) -> List[Configuration]: def configurations(cls) -> list[Configuration]:
return cls.platform_config + cls.config_shim return cls.platform_config + cls.config_shim
@classmethod @classmethod
def config_groups(cls) -> List[ConfigGroup]: def config_groups(cls) -> list[ConfigGroup]:
platform_len = len(cls.platform_config) platform_len = len(cls.platform_config)
return [ return [
ConfigGroup("Platform Parameters", 1, platform_len), ConfigGroup("Platform Parameters", 1, platform_len),
@ -71,7 +70,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
), ),
] ]
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None: def build_xml_files(self, config: dict[str, str], iface: CoreInterface) -> None:
""" """
Build the necessary nem and commeffect XMLs in the given path. Build the necessary nem and commeffect XMLs in the given path.
If an individual NEM has a nonstandard config, we need to build If an individual NEM has a nonstandard config, we need to build

View file

@ -4,7 +4,6 @@ tdma.py: EMANE TDMA model bindings for CORE
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Set
from core import constants, utils from core import constants, utils
from core.config import ConfigString from core.config import ConfigString
@ -28,7 +27,7 @@ class EmaneTdmaModel(emanemodel.EmaneModel):
default_schedule: Path = ( default_schedule: Path = (
constants.CORE_DATA_DIR / "examples" / "tdma" / "schedule.xml" constants.CORE_DATA_DIR / "examples" / "tdma" / "schedule.xml"
) )
config_ignore: Set[str] = {schedule_name} config_ignore: set[str] = {schedule_name}
@classmethod @classmethod
def load(cls, emane_prefix: Path) -> None: def load(cls, emane_prefix: Path) -> None:

View file

@ -6,11 +6,11 @@ share the same MAC+PHY model.
import logging import logging
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type, Union from typing import TYPE_CHECKING, Callable, Optional, Union
from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.data import InterfaceData, LinkData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.enumerations import EventTypes, MessageFlags, RegisterTlvs from core.emulator.enumerations import MessageFlags, RegisterTlvs
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNetworkBase, CoreNode, NodeOptions from core.nodes.base import CoreNetworkBase, CoreNode, NodeOptions
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
@ -167,7 +167,7 @@ class EmaneNet(CoreNetworkBase):
self.mobility: Optional[WayPointMobility] = None self.mobility: Optional[WayPointMobility] = None
model_class = self.session.emane.get_model(options.emane_model) model_class = self.session.emane.get_model(options.emane_model)
self.wireless_model: Optional["EmaneModel"] = model_class(self.session, self.id) self.wireless_model: Optional["EmaneModel"] = model_class(self.session, self.id)
if self.session.state == EventTypes.RUNTIME_STATE: if self.session.is_running():
self.session.emane.add_node(self) self.session.emane.add_node(self)
@classmethod @classmethod
@ -196,7 +196,7 @@ class EmaneNet(CoreNetworkBase):
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass pass
def updatemodel(self, config: Dict[str, str]) -> None: def updatemodel(self, config: dict[str, str]) -> None:
""" """
Update configuration for the current model. Update configuration for the current model.
@ -212,8 +212,8 @@ class EmaneNet(CoreNetworkBase):
def setmodel( def setmodel(
self, self,
model: Union[Type["EmaneModel"], Type["WayPointMobility"]], model: Union[type["EmaneModel"], type["WayPointMobility"]],
config: Dict[str, str], config: dict[str, str],
) -> None: ) -> None:
""" """
set the EmaneModel associated with this node set the EmaneModel associated with this node
@ -225,7 +225,7 @@ class EmaneNet(CoreNetworkBase):
self.mobility = model(session=self.session, _id=self.id) self.mobility = model(session=self.session, _id=self.id)
self.mobility.update_config(config) self.mobility.update_config(config)
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]:
links = [] links = []
emane_manager = self.session.emane emane_manager = self.session.emane
# gather current emane links # gather current emane links
@ -280,7 +280,7 @@ class EmaneNet(CoreNetworkBase):
self.attach(iface) self.attach(iface)
if self.up: if self.up:
iface.startup() iface.startup()
if self.session.state == EventTypes.RUNTIME_STATE: if self.session.is_running():
self.session.emane.start_iface(self, iface) self.session.emane.start_iface(self, iface)
return iface return iface

View file

@ -0,0 +1,67 @@
from collections.abc import Callable
from typing import TypeVar, Union
from core.emulator.data import (
ConfigData,
EventData,
ExceptionData,
FileData,
LinkData,
NodeData,
)
from core.errors import CoreError
T = TypeVar(
"T", bound=Union[EventData, ExceptionData, NodeData, LinkData, FileData, ConfigData]
)
class BroadcastManager:
def __init__(self) -> None:
"""
Creates a BroadcastManager instance.
"""
self.handlers: dict[type[T], set[Callable[[T], None]]] = {}
def send(self, data: T) -> None:
"""
Retrieve handlers for data, and run all current handlers.
:param data: data to provide to handlers
:return: nothing
"""
handlers = self.handlers.get(type(data), set())
for handler in handlers:
handler(data)
def add_handler(self, data_type: type[T], handler: Callable[[T], None]) -> None:
"""
Add a handler for a given data type.
:param data_type: type of data to add handler for
:param handler: handler to add
:return: nothing
"""
handlers = self.handlers.setdefault(data_type, set())
if handler in handlers:
raise CoreError(
f"cannot add data({data_type}) handler({repr(handler)}), "
f"already exists"
)
handlers.add(handler)
def remove_handler(self, data_type: type[T], handler: Callable[[T], None]) -> None:
"""
Remove a handler for a given data type.
:param data_type: type of data to remove handler for
:param handler: handler to remove
:return: nothing
"""
handlers = self.handlers.get(data_type, set())
if handler not in handlers:
raise CoreError(
f"cannot remove data({data_type}) handler({repr(handler)}), "
f"does not exist"
)
handlers.remove(handler)

View file

@ -0,0 +1,239 @@
import logging
from typing import TYPE_CHECKING, Optional
from core import utils
from core.emulator.data import InterfaceData
from core.errors import CoreError
from core.nodes.base import CoreNode
from core.nodes.interface import DEFAULT_MTU
from core.nodes.network import CtrlNet
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from core.emulator.session import Session
CTRL_NET_ID: int = 9001
ETC_HOSTS_PATH: str = "/etc/hosts"
class ControlNetManager:
def __init__(self, session: "Session") -> None:
self.session: "Session" = session
self.etc_hosts_header: str = f"CORE session {self.session.id} host entries"
def _etc_hosts_enabled(self) -> bool:
"""
Determines if /etc/hosts should be configured.
:return: True if /etc/hosts should be configured, False otherwise
"""
return self.session.options.get_bool("update_etc_hosts", False)
def _get_server_ifaces(
self,
) -> tuple[None, Optional[str], Optional[str], Optional[str]]:
"""
Retrieve control net server interfaces.
:return: control net server interfaces
"""
d0 = self.session.options.get("controlnetif0")
if d0:
logger.error("controlnet0 cannot be assigned with a host interface")
d1 = self.session.options.get("controlnetif1")
d2 = self.session.options.get("controlnetif2")
d3 = self.session.options.get("controlnetif3")
return None, d1, d2, d3
def _get_prefixes(
self,
) -> tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
"""
Retrieve control net prefixes.
:return: control net prefixes
"""
p = self.session.options.get("controlnet")
p0 = self.session.options.get("controlnet0")
p1 = self.session.options.get("controlnet1")
p2 = self.session.options.get("controlnet2")
p3 = self.session.options.get("controlnet3")
if not p0 and p:
p0 = p
return p0, p1, p2, p3
def update_etc_hosts(self) -> None:
"""
Add the IP addresses of control interfaces to the /etc/hosts file.
:return: nothing
"""
if not self._etc_hosts_enabled():
return
control_net = self.get_control_net(0)
entries = ""
for iface in control_net.get_ifaces():
name = iface.node.name
for ip in iface.ips():
entries += f"{ip.ip} {name}\n"
logger.info("adding entries to /etc/hosts")
utils.file_munge(ETC_HOSTS_PATH, self.etc_hosts_header, entries)
def clear_etc_hosts(self) -> None:
"""
Clear IP addresses of control interfaces from the /etc/hosts file.
:return: nothing
"""
if not self._etc_hosts_enabled():
return
logger.info("removing /etc/hosts file entries")
utils.file_demunge(ETC_HOSTS_PATH, self.etc_hosts_header)
def get_control_net_index(self, dev: str) -> int:
"""
Retrieve control net index.
:param dev: device to get control net index for
:return: control net index, -1 otherwise
"""
if dev[0:4] == "ctrl" and int(dev[4]) in (0, 1, 2, 3):
index = int(dev[4])
if index == 0:
return index
if index < 4 and self._get_prefixes()[index] is not None:
return index
return -1
def get_control_net(self, index: int) -> Optional[CtrlNet]:
"""
Retrieve a control net based on index.
:param index: control net index
:return: control net when available, None otherwise
"""
try:
return self.session.get_node(CTRL_NET_ID + index, CtrlNet)
except CoreError:
return None
def add_control_net(
self, index: int, conf_required: bool = True
) -> Optional[CtrlNet]:
"""
Create a control network bridge as necessary. The conf_reqd flag,
when False, causes a control network bridge to be added even if
one has not been configured.
:param index: network index to add
:param conf_required: flag to check if conf is required
:return: control net node
"""
logger.info(
"checking to add control net index(%s) conf_required(%s)",
index,
conf_required,
)
# check for valid index
if not (0 <= index <= 3):
raise CoreError(f"invalid control net index({index})")
# return any existing control net bridge
control_net = self.get_control_net(index)
if control_net:
logger.info("control net index(%s) already exists", index)
return control_net
# retrieve prefix for current index
index_prefix = self._get_prefixes()[index]
if not index_prefix:
if conf_required:
return None
else:
index_prefix = CtrlNet.DEFAULT_PREFIX_LIST[index]
# retrieve valid prefix from old style values
prefixes = index_prefix.split()
if len(prefixes) > 1:
# a list of per-host prefixes is provided
try:
prefix = prefixes[0].split(":", 1)[1]
except IndexError:
prefix = prefixes[0]
else:
prefix = prefixes[0]
# use the updown script for control net 0 only
updown_script = None
if index == 0:
updown_script = self.session.options.get("controlnet_updown_script")
# build a new controlnet bridge
_id = CTRL_NET_ID + index
server_iface = self._get_server_ifaces()[index]
logger.info(
"adding controlnet(%s) prefix(%s) updown(%s) server interface(%s)",
_id,
prefix,
updown_script,
server_iface,
)
options = CtrlNet.create_options()
options.prefix = prefix
options.updown_script = updown_script
options.serverintf = server_iface
control_net = self.session.create_node(CtrlNet, False, _id, options=options)
control_net.brname = f"ctrl{index}.{self.session.short_session_id()}"
control_net.startup()
return control_net
def remove_control_net(self, index: int) -> None:
"""
Removes control net.
:param index: index of control net to remove
:return: nothing
"""
control_net = self.get_control_net(index)
if control_net:
logger.info("removing control net index(%s)", index)
self.session.delete_node(control_net.id)
def add_control_iface(self, node: CoreNode, index: int) -> None:
"""
Adds a control net interface to a node.
:param node: node to add control net interface to
:param index: index of control net to add interface to
:return: nothing
:raises CoreError: if control net doesn't exist, interface already exists,
or there is an error creating the interface
"""
control_net = self.get_control_net(index)
if not control_net:
raise CoreError(f"control net index({index}) does not exist")
iface_id = control_net.CTRLIF_IDX_BASE + index
if node.ifaces.get(iface_id):
raise CoreError(f"control iface({iface_id}) already exists")
try:
logger.info(
"node(%s) adding control net index(%s) interface(%s)",
node.name,
index,
iface_id,
)
ip4 = control_net.prefix[node.id]
ip4_mask = control_net.prefix.prefixlen
iface_data = InterfaceData(
id=iface_id,
name=f"ctrl{index}",
mac=utils.random_mac(),
ip4=ip4,
ip4_mask=ip4_mask,
mtu=DEFAULT_MTU,
)
iface = node.create_iface(iface_data)
control_net.attach(iface)
iface.control = True
except ValueError:
raise CoreError(
f"error adding control net interface to node({node.id}), "
f"invalid control net prefix({control_net.prefix}), "
"a longer prefix length may be required"
)

View file

@ -1,7 +1,6 @@
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from typing import Dict, List, Type
from core import utils from core import utils
from core.configservice.manager import ConfigServiceManager from core.configservice.manager import ConfigServiceManager
@ -20,7 +19,7 @@ class CoreEmu:
Provides logic for creating and configuring CORE sessions and the nodes within them. Provides logic for creating and configuring CORE sessions and the nodes within them.
""" """
def __init__(self, config: Dict[str, str] = None) -> None: def __init__(self, config: dict[str, str] = None) -> None:
""" """
Create a CoreEmu object. Create a CoreEmu object.
@ -31,13 +30,13 @@ class CoreEmu:
# configuration # configuration
config = config if config else {} config = config if config else {}
self.config: Dict[str, str] = config self.config: dict[str, str] = config
# session management # session management
self.sessions: Dict[int, Session] = {} self.sessions: dict[int, Session] = {}
# load services # load services
self.service_errors: List[str] = [] self.service_errors: list[str] = []
self.service_manager: ConfigServiceManager = ConfigServiceManager() self.service_manager: ConfigServiceManager = ConfigServiceManager()
self._load_services() self._load_services()
@ -119,7 +118,7 @@ class CoreEmu:
_, session = self.sessions.popitem() _, session = self.sessions.popitem()
session.shutdown() session.shutdown()
def create_session(self, _id: int = None, _cls: Type[Session] = Session) -> Session: def create_session(self, _id: int = None, _cls: type[Session] = Session) -> Session:
""" """
Create a new CORE session. Create a new CORE session.

View file

@ -2,7 +2,7 @@
CORE data objects. CORE data objects.
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, List, Optional, Tuple from typing import TYPE_CHECKING, Any, Optional
import netaddr import netaddr
@ -24,7 +24,7 @@ class ConfigData:
node: int = None node: int = None
object: str = None object: str = None
type: int = None type: int = None
data_types: Tuple[int] = None data_types: tuple[int] = None
data_values: str = None data_values: str = None
captions: str = None captions: str = None
bitmap: str = None bitmap: str = None
@ -81,8 +81,8 @@ class NodeOptions:
model: Optional[str] = "PC" model: Optional[str] = "PC"
canvas: int = None canvas: int = None
icon: str = None icon: str = None
services: List[str] = field(default_factory=list) services: list[str] = field(default_factory=list)
config_services: List[str] = field(default_factory=list) config_services: list[str] = field(default_factory=list)
x: float = None x: float = None
y: float = None y: float = None
lat: float = None lat: float = None
@ -93,9 +93,9 @@ class NodeOptions:
emane: str = None emane: str = None
legacy: bool = False legacy: bool = False
# src, dst # src, dst
binds: List[Tuple[str, str]] = field(default_factory=list) binds: list[tuple[str, str]] = field(default_factory=list)
# src, dst, unique, delete # src, dst, unique, delete
volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list) volumes: list[tuple[str, str, bool, bool]] = field(default_factory=list)
def set_position(self, x: float, y: float) -> None: def set_position(self, x: float, y: float) -> None:
""" """
@ -148,7 +148,7 @@ class InterfaceData:
ip6_mask: int = None ip6_mask: int = None
mtu: int = None mtu: int = None
def get_ips(self) -> List[str]: def get_ips(self) -> list[str]:
""" """
Returns a list of ip4 and ip6 addresses when present. Returns a list of ip4 and ip6 addresses when present.

View file

@ -8,7 +8,7 @@ import threading
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Callable, Dict, Tuple from typing import TYPE_CHECKING, Callable
import netaddr import netaddr
from fabric import Connection from fabric import Connection
@ -48,7 +48,7 @@ class DistributedServer:
self.lock: threading.Lock = threading.Lock() self.lock: threading.Lock = threading.Lock()
def remote_cmd( def remote_cmd(
self, cmd: str, env: Dict[str, str] = None, cwd: str = None, wait: bool = True self, cmd: str, env: dict[str, str] = None, cwd: str = None, wait: bool = True
) -> str: ) -> str:
""" """
Run command remotely using server connection. Run command remotely using server connection.
@ -105,7 +105,7 @@ class DistributedServer:
""" """
with self.lock: with self.lock:
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
temp.write(data.encode("utf-8")) temp.write(data.encode())
temp.close() temp.close()
self.conn.put(temp.name, str(dst_path)) self.conn.put(temp.name, str(dst_path))
os.unlink(temp.name) os.unlink(temp.name)
@ -123,8 +123,8 @@ class DistributedController:
:param session: session :param session: session
""" """
self.session: "Session" = session self.session: "Session" = session
self.servers: Dict[str, DistributedServer] = OrderedDict() self.servers: dict[str, DistributedServer] = OrderedDict()
self.tunnels: Dict[int, Tuple[GreTap, GreTap]] = {} self.tunnels: dict[int, tuple[GreTap, GreTap]] = {}
self.address: str = self.session.options.get("distributed_address") self.address: str = self.session.options.get("distributed_address")
def add_server(self, name: str, host: str) -> None: def add_server(self, name: str, host: str) -> None:
@ -187,8 +187,7 @@ class DistributedController:
:return: nothing :return: nothing
""" """
mtu = self.session.options.get_int("mtu") mtu = self.session.options.get_int("mtu")
for node_id in self.session.nodes: for node in self.session.nodes.values():
node = self.session.nodes[node_id]
if not isinstance(node, CtrlNet) or node.serverintf is not None: if not isinstance(node, CtrlNet) or node.serverintf is not None:
continue continue
for name in self.servers: for name in self.servers:
@ -214,7 +213,7 @@ class DistributedController:
def create_gre_tunnel( def create_gre_tunnel(
self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool
) -> Tuple[GreTap, GreTap]: ) -> tuple[GreTap, GreTap]:
""" """
Create gre tunnel using a pair of gre taps between the local and remote server. Create gre tunnel using a pair of gre taps between the local and remote server.

View file

@ -50,6 +50,7 @@ class NodeTypes(Enum):
DOCKER = 15 DOCKER = 15
LXC = 16 LXC = 16
WIRELESS = 17 WIRELESS = 17
PODMAN = 18
class LinkTypes(Enum): class LinkTypes(Enum):

View file

@ -0,0 +1,145 @@
import logging
import subprocess
from collections.abc import Callable
from pathlib import Path
from core.emulator.enumerations import EventTypes
from core.errors import CoreError
logger = logging.getLogger(__name__)
class HookManager:
"""
Provides functionality for managing and running script/callback hooks.
"""
def __init__(self) -> None:
"""
Create a HookManager instance.
"""
self.script_hooks: dict[EventTypes, dict[str, str]] = {}
self.callback_hooks: dict[EventTypes, list[Callable[[], None]]] = {}
def reset(self) -> None:
"""
Clear all current hooks.
:return: nothing
"""
self.script_hooks.clear()
self.callback_hooks.clear()
def add_script_hook(self, state: EventTypes, file_name: str, data: str) -> None:
"""
Add a hook script to run for a given state.
:param state: state to run hook on
:param file_name: hook file name
:param data: file data
:return: nothing
"""
logger.info("setting state hook: %s - %s", state, file_name)
state_hooks = self.script_hooks.setdefault(state, {})
if file_name in state_hooks:
raise CoreError(
f"adding duplicate state({state.name}) hook script({file_name})"
)
state_hooks[file_name] = data
def delete_script_hook(self, state: EventTypes, file_name: str) -> None:
"""
Delete a script hook from a given state.
:param state: state to delete script hook from
:param file_name: name of script to delete
:return: nothing
"""
state_hooks = self.script_hooks.get(state, {})
if file_name not in state_hooks:
raise CoreError(
f"deleting state({state.name}) hook script({file_name}) "
"that does not exist"
)
del state_hooks[file_name]
def add_callback_hook(
self, state: EventTypes, hook: Callable[[EventTypes], None]
) -> None:
"""
Add a hook callback to run for a state.
:param state: state to add hook for
:param hook: callback to run
:return: nothing
"""
hooks = self.callback_hooks.setdefault(state, [])
if hook in hooks:
name = getattr(callable, "__name__", repr(hook))
raise CoreError(
f"adding duplicate state({state.name}) hook callback({name})"
)
hooks.append(hook)
def delete_callback_hook(
self, state: EventTypes, hook: Callable[[EventTypes], None]
) -> None:
"""
Delete a state hook.
:param state: state to delete hook for
:param hook: hook to delete
:return: nothing
"""
hooks = self.callback_hooks.get(state, [])
if hook not in hooks:
name = getattr(callable, "__name__", repr(hook))
raise CoreError(
f"deleting state({state.name}) hook callback({name}) "
"that does not exist"
)
hooks.remove(hook)
def run_hooks(
self, state: EventTypes, directory: Path, env: dict[str, str]
) -> None:
"""
Run all hooks for the current state.
:param state: state to run hooks for
:param directory: directory to run script hooks within
:param env: environment to run script hooks with
:return: nothing
"""
for state_hooks in self.script_hooks.get(state, {}):
for file_name, data in state_hooks.items():
logger.info("running hook %s", file_name)
file_path = directory / file_name
log_path = directory / f"{file_name}.log"
try:
with file_path.open("w") as f:
f.write(data)
with log_path.open("w") as f:
args = ["/bin/sh", file_name]
subprocess.check_call(
args,
stdout=f,
stderr=subprocess.STDOUT,
close_fds=True,
cwd=directory,
env=env,
)
except (OSError, subprocess.CalledProcessError) as e:
raise CoreError(
f"failure running state({state.name}) "
f"hook script({file_name}): {e}"
)
for hook in self.callback_hooks.get(state, []):
try:
hook()
except Exception as e:
name = getattr(callable, "__name__", repr(hook))
raise CoreError(
f"failure running state({state.name}) "
f"hook callback({name}): {e}"
)

View file

@ -4,8 +4,9 @@ for a session.
""" """
import logging import logging
from collections.abc import ValuesView
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, Optional, Tuple, ValuesView from typing import Optional
from core.emulator.data import LinkData, LinkOptions from core.emulator.data import LinkData, LinkOptions
from core.emulator.enumerations import LinkTypes, MessageFlags from core.emulator.enumerations import LinkTypes, MessageFlags
@ -15,7 +16,7 @@ from core.nodes.interface import CoreInterface
from core.nodes.network import PtpNet from core.nodes.network import PtpNet
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
LinkKeyType = Tuple[int, Optional[int], int, Optional[int]] LinkKeyType = tuple[int, Optional[int], int, Optional[int]]
def create_key( def create_key(
@ -145,8 +146,8 @@ class LinkManager:
""" """
Create a LinkManager instance. Create a LinkManager instance.
""" """
self._links: Dict[LinkKeyType, CoreLink] = {} self._links: dict[LinkKeyType, CoreLink] = {}
self._node_links: Dict[int, Dict[LinkKeyType, CoreLink]] = {} self._node_links: dict[int, dict[LinkKeyType, CoreLink]] = {}
def add(self, core_link: CoreLink) -> None: def add(self, core_link: CoreLink) -> None:
""" """

View file

@ -14,7 +14,7 @@ import tempfile
import threading import threading
import time import time
from pathlib import Path from pathlib import Path
from typing import Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union from typing import Callable, Optional, TypeVar, Union
from core import constants, utils from core import constants, utils
from core.configservice.manager import ConfigServiceManager from core.configservice.manager import ConfigServiceManager
@ -57,6 +57,7 @@ from core.nodes.network import (
WlanNode, WlanNode,
) )
from core.nodes.physical import PhysicalNode, Rj45Node from core.nodes.physical import PhysicalNode, Rj45Node
from core.nodes.podman import PodmanNode
from core.nodes.wireless import WirelessNode from core.nodes.wireless import WirelessNode
from core.plugins.sdt import Sdt from core.plugins.sdt import Sdt
from core.services.coreservices import CoreServices from core.services.coreservices import CoreServices
@ -66,7 +67,7 @@ from core.xml.corexml import CoreXmlReader, CoreXmlWriter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# maps for converting from API call node type values to classes and vice versa # maps for converting from API call node type values to classes and vice versa
NODES: Dict[NodeTypes, Type[NodeBase]] = { NODES: dict[NodeTypes, type[NodeBase]] = {
NodeTypes.DEFAULT: CoreNode, NodeTypes.DEFAULT: CoreNode,
NodeTypes.PHYSICAL: PhysicalNode, NodeTypes.PHYSICAL: PhysicalNode,
NodeTypes.SWITCH: SwitchNode, NodeTypes.SWITCH: SwitchNode,
@ -81,13 +82,13 @@ NODES: Dict[NodeTypes, Type[NodeBase]] = {
NodeTypes.DOCKER: DockerNode, NodeTypes.DOCKER: DockerNode,
NodeTypes.LXC: LxcNode, NodeTypes.LXC: LxcNode,
NodeTypes.WIRELESS: WirelessNode, NodeTypes.WIRELESS: WirelessNode,
NodeTypes.PODMAN: PodmanNode,
} }
NODES_TYPE: Dict[Type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES} NODES_TYPE: dict[type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES}
CONTAINER_NODES: Set[Type[NodeBase]] = {DockerNode, LxcNode}
CTRL_NET_ID: int = 9001 CTRL_NET_ID: int = 9001
LINK_COLORS: List[str] = ["green", "blue", "orange", "purple", "turquoise"] LINK_COLORS: list[str] = ["green", "blue", "orange", "purple", "turquoise"]
NT: TypeVar = TypeVar("NT", bound=NodeBase) NT: TypeVar = TypeVar("NT", bound=NodeBase)
WIRELESS_TYPE: Tuple[Type[WlanNode], Type[EmaneNet], Type[WirelessNode]] = ( WIRELESS_TYPE: tuple[type[WlanNode], type[EmaneNet], type[WirelessNode]] = (
WlanNode, WlanNode,
EmaneNet, EmaneNet,
WirelessNode, WirelessNode,
@ -100,7 +101,7 @@ class Session:
""" """
def __init__( def __init__(
self, _id: int, config: Dict[str, str] = None, mkdir: bool = True self, _id: int, config: dict[str, str] = None, mkdir: bool = True
) -> None: ) -> None:
""" """
Create a Session instance. Create a Session instance.
@ -121,33 +122,33 @@ class Session:
self.thumbnail: Optional[Path] = None self.thumbnail: Optional[Path] = None
self.user: Optional[str] = None self.user: Optional[str] = None
self.event_loop: EventLoop = EventLoop() self.event_loop: EventLoop = EventLoop()
self.link_colors: Dict[int, str] = {} self.link_colors: dict[int, str] = {}
# dict of nodes: all nodes and nets # dict of nodes: all nodes and nets
self.nodes: Dict[int, NodeBase] = {} self.nodes: dict[int, NodeBase] = {}
self.nodes_lock: threading.Lock = threading.Lock() self.nodes_lock: threading.Lock = threading.Lock()
self.link_manager: LinkManager = LinkManager() self.link_manager: LinkManager = LinkManager()
# states and hooks handlers # states and hooks handlers
self.state: EventTypes = EventTypes.DEFINITION_STATE self.state: EventTypes = EventTypes.DEFINITION_STATE
self.state_time: float = time.monotonic() self.state_time: float = time.monotonic()
self.hooks: Dict[EventTypes, List[Tuple[str, str]]] = {} self.hooks: dict[EventTypes, list[tuple[str, str]]] = {}
self.state_hooks: Dict[EventTypes, List[Callable[[EventTypes], None]]] = {} self.state_hooks: dict[EventTypes, list[Callable[[EventTypes], None]]] = {}
self.add_state_hook( self.add_state_hook(
state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook
) )
# handlers for broadcasting information # handlers for broadcasting information
self.event_handlers: List[Callable[[EventData], None]] = [] self.event_handlers: list[Callable[[EventData], None]] = []
self.exception_handlers: List[Callable[[ExceptionData], None]] = [] self.exception_handlers: list[Callable[[ExceptionData], None]] = []
self.node_handlers: List[Callable[[NodeData], None]] = [] self.node_handlers: list[Callable[[NodeData], None]] = []
self.link_handlers: List[Callable[[LinkData], None]] = [] self.link_handlers: list[Callable[[LinkData], None]] = []
self.file_handlers: List[Callable[[FileData], None]] = [] self.file_handlers: list[Callable[[FileData], None]] = []
self.config_handlers: List[Callable[[ConfigData], None]] = [] self.config_handlers: list[Callable[[ConfigData], None]] = []
# session options/metadata # session options/metadata
self.options: SessionConfig = SessionConfig(config) self.options: SessionConfig = SessionConfig(config)
self.metadata: Dict[str, str] = {} self.metadata: dict[str, str] = {}
# distributed support and logic # distributed support and logic
self.distributed: DistributedController = DistributedController(self) self.distributed: DistributedController = DistributedController(self)
@ -163,7 +164,7 @@ class Session:
self.service_manager: Optional[ConfigServiceManager] = None self.service_manager: Optional[ConfigServiceManager] = None
@classmethod @classmethod
def get_node_class(cls, _type: NodeTypes) -> Type[NodeBase]: def get_node_class(cls, _type: NodeTypes) -> type[NodeBase]:
""" """
Retrieve the class for a given node type. Retrieve the class for a given node type.
@ -176,7 +177,7 @@ class Session:
return node_class return node_class
@classmethod @classmethod
def get_node_type(cls, _class: Type[NodeBase]) -> NodeTypes: def get_node_type(cls, _class: type[NodeBase]) -> NodeTypes:
""" """
Retrieve node type for a given node class. Retrieve node type for a given node class.
@ -238,7 +239,7 @@ class Session:
iface1_data: InterfaceData = None, iface1_data: InterfaceData = None,
iface2_data: InterfaceData = None, iface2_data: InterfaceData = None,
options: LinkOptions = None, options: LinkOptions = None,
) -> Tuple[Optional[CoreInterface], Optional[CoreInterface]]: ) -> tuple[Optional[CoreInterface], Optional[CoreInterface]]:
""" """
Add a link between nodes. Add a link between nodes.
@ -345,7 +346,7 @@ class Session:
iface1_data: InterfaceData = None, iface1_data: InterfaceData = None,
iface2_data: InterfaceData = None, iface2_data: InterfaceData = None,
options: LinkOptions = None, options: LinkOptions = None,
) -> Tuple[CoreInterface, CoreInterface]: ) -> tuple[CoreInterface, CoreInterface]:
""" """
Create a wired link between two nodes. Create a wired link between two nodes.
@ -476,7 +477,7 @@ class Session:
def add_node( def add_node(
self, self,
_class: Type[NT], _class: type[NT],
_id: int = None, _id: int = None,
name: str = None, name: str = None,
server: str = None, server: str = None,
@ -520,8 +521,7 @@ class Session:
if isinstance(node, WlanNode): if isinstance(node, WlanNode):
self.mobility.set_model_config(node.id, BasicRangeModel.name) self.mobility.set_model_config(node.id, BasicRangeModel.name)
# boot core nodes after runtime # boot core nodes after runtime
is_runtime = self.state == EventTypes.RUNTIME_STATE if self.is_running() and isinstance(node, CoreNode):
if is_runtime and isinstance(node, CoreNode):
self.add_remove_control_iface(node, remove=False) self.add_remove_control_iface(node, remove=False)
self.boot_node(node) self.boot_node(node)
self.sdt.add_node(node) self.sdt.add_node(node)
@ -746,7 +746,7 @@ class Session:
for hook in hooks: for hook in hooks:
self.run_hook(hook) self.run_hook(hook)
def run_hook(self, hook: Tuple[str, str]) -> None: def run_hook(self, hook: tuple[str, str]) -> None:
""" """
Run a hook. Run a hook.
@ -770,7 +770,7 @@ class Session:
cwd=self.directory, cwd=self.directory,
env=self.get_environment(), env=self.get_environment(),
) )
except (IOError, subprocess.CalledProcessError): except (OSError, subprocess.CalledProcessError):
logger.exception("error running hook: %s", file_path) logger.exception("error running hook: %s", file_path)
def run_state_hooks(self, state: EventTypes) -> None: def run_state_hooks(self, state: EventTypes) -> None:
@ -836,7 +836,7 @@ class Session:
xml_file_path = self.directory / "session-deployed.xml" xml_file_path = self.directory / "session-deployed.xml"
xml_writer.write(xml_file_path) xml_writer.write(xml_file_path)
def get_environment(self, state: bool = True) -> Dict[str, str]: def get_environment(self, state: bool = True) -> dict[str, str]:
""" """
Get an environment suitable for a subprocess.Popen call. Get an environment suitable for a subprocess.Popen call.
This is the current process environment with some session-specific This is the current process environment with some session-specific
@ -871,7 +871,7 @@ class Session:
if path.is_file(): if path.is_file():
try: try:
utils.load_config(path, env) utils.load_config(path, env)
except IOError: except OSError:
logger.exception("error reading environment file: %s", path) logger.exception("error reading environment file: %s", path)
return env return env
@ -888,12 +888,12 @@ class Session:
uid = pwd.getpwnam(user).pw_uid uid = pwd.getpwnam(user).pw_uid
gid = self.directory.stat().st_gid gid = self.directory.stat().st_gid
os.chown(self.directory, uid, gid) os.chown(self.directory, uid, gid)
except IOError: except OSError:
logger.exception("failed to set permission on %s", self.directory) logger.exception("failed to set permission on %s", self.directory)
def create_node( def create_node(
self, self,
_class: Type[NT], _class: type[NT],
start: bool, start: bool,
_id: int = None, _id: int = None,
name: str = None, name: str = None,
@ -929,7 +929,7 @@ class Session:
node.startup() node.startup()
return node return node
def get_node(self, _id: int, _class: Type[NT]) -> NT: def get_node(self, _id: int, _class: type[NT]) -> NT:
""" """
Get a session node. Get a session node.
@ -1003,7 +1003,7 @@ class Session:
) )
self.broadcast_exception(exception_data) self.broadcast_exception(exception_data)
def instantiate(self) -> List[Exception]: def instantiate(self) -> list[Exception]:
""" """
We have entered the instantiation state, invoke startup methods We have entered the instantiation state, invoke startup methods
of various managers and boot the nodes. Validate nodes and check of various managers and boot the nodes. Validate nodes and check
@ -1011,7 +1011,7 @@ class Session:
:return: list of service boot errors during startup :return: list of service boot errors during startup
""" """
if self.state == EventTypes.RUNTIME_STATE: if self.is_running():
logger.warning("ignoring instantiate, already in runtime state") logger.warning("ignoring instantiate, already in runtime state")
return [] return []
# create control net interfaces and network tunnels # create control net interfaces and network tunnels
@ -1083,6 +1083,7 @@ class Session:
if isinstance(node, CoreNodeBase) and node.up: if isinstance(node, CoreNodeBase) and node.up:
args = (node,) args = (node,)
funcs.append((self.services.stop_services, args, {})) funcs.append((self.services.stop_services, args, {}))
funcs.append((node.stop_config_services, (), {}))
utils.threadpool(funcs) utils.threadpool(funcs)
# shutdown emane # shutdown emane
@ -1122,7 +1123,7 @@ class Session:
self.services.boot_services(node) self.services.boot_services(node)
node.start_config_services() node.start_config_services()
def boot_nodes(self) -> List[Exception]: def boot_nodes(self) -> list[Exception]:
""" """
Invoke the boot() procedure for all nodes and send back node Invoke the boot() procedure for all nodes and send back node
messages to the GUI for node messages that had the status messages to the GUI for node messages that had the status
@ -1144,7 +1145,7 @@ class Session:
self.update_control_iface_hosts() self.update_control_iface_hosts()
return exceptions return exceptions
def get_control_net_prefixes(self) -> List[str]: def get_control_net_prefixes(self) -> list[str]:
""" """
Retrieve control net prefixes. Retrieve control net prefixes.
@ -1159,7 +1160,7 @@ class Session:
p0 = p p0 = p
return [p0, p1, p2, p3] return [p0, p1, p2, p3]
def get_control_net_server_ifaces(self) -> List[str]: def get_control_net_server_ifaces(self) -> list[str]:
""" """
Retrieve control net server interfaces. Retrieve control net server interfaces.
@ -1365,7 +1366,7 @@ class Session:
Return the current time we have been in the runtime state, or zero Return the current time we have been in the runtime state, or zero
if not in runtime. if not in runtime.
""" """
if self.state == EventTypes.RUNTIME_STATE: if self.is_running():
return time.monotonic() - self.state_time return time.monotonic() - self.state_time
else: else:
return 0.0 return 0.0
@ -1442,3 +1443,11 @@ class Session:
color = LINK_COLORS[index] color = LINK_COLORS[index]
self.link_colors[network_id] = color self.link_colors[network_id] = color
return color return color
def is_running(self) -> bool:
"""
Convenience for checking if this session is in the runtime state.
:return: True if in the runtime state, False otherwise
"""
return self.state == EventTypes.RUNTIME_STATE

View file

@ -1,4 +1,4 @@
from typing import Dict, List, Optional from typing import Optional
from core.config import ConfigBool, ConfigInt, ConfigString, Configuration from core.config import ConfigBool, ConfigInt, ConfigString, Configuration
from core.errors import CoreError from core.errors import CoreError
@ -10,7 +10,7 @@ class SessionConfig:
Provides session configuration. Provides session configuration.
""" """
options: List[Configuration] = [ options: list[Configuration] = [
ConfigString(id="controlnet", label="Control Network"), ConfigString(id="controlnet", label="Control Network"),
ConfigString(id="controlnet0", label="Control Network 0"), ConfigString(id="controlnet0", label="Control Network 0"),
ConfigString(id="controlnet1", label="Control Network 1"), ConfigString(id="controlnet1", label="Control Network 1"),
@ -35,16 +35,16 @@ class SessionConfig:
ConfigInt(id="mtu", default="0", label="MTU for All Devices"), ConfigInt(id="mtu", default="0", label="MTU for All Devices"),
] ]
def __init__(self, config: Dict[str, str] = None) -> None: def __init__(self, config: dict[str, str] = None) -> None:
""" """
Create a SessionConfig instance. Create a SessionConfig instance.
:param config: configuration to initialize with :param config: configuration to initialize with
""" """
self._config: Dict[str, str] = {x.id: x.default for x in self.options} self._config: dict[str, str] = {x.id: x.default for x in self.options}
self._config.update(config or {}) self._config.update(config or {})
def update(self, config: Dict[str, str]) -> None: def update(self, config: dict[str, str]) -> None:
""" """
Update current configuration with provided values. Update current configuration with provided values.
@ -73,7 +73,7 @@ class SessionConfig:
""" """
return self._config.get(name, default) return self._config.get(name, default)
def all(self) -> Dict[str, str]: def all(self) -> dict[str, str]:
""" """
Retrieve all configuration options. Retrieve all configuration options.

View file

@ -1,5 +1,3 @@
from typing import List
BASH: str = "bash" BASH: str = "bash"
ETHTOOL: str = "ethtool" ETHTOOL: str = "ethtool"
IP: str = "ip" IP: str = "ip"
@ -13,7 +11,7 @@ UMOUNT: str = "umount"
VCMD: str = "vcmd" VCMD: str = "vcmd"
VNODED: str = "vnoded" VNODED: str = "vnoded"
COMMON_REQUIREMENTS: List[str] = [ COMMON_REQUIREMENTS: list[str] = [
BASH, BASH,
ETHTOOL, ETHTOOL,
IP, IP,
@ -26,10 +24,10 @@ COMMON_REQUIREMENTS: List[str] = [
VCMD, VCMD,
VNODED, VNODED,
] ]
OVS_REQUIREMENTS: List[str] = [OVS_VSCTL] OVS_REQUIREMENTS: list[str] = [OVS_VSCTL]
def get_requirements(use_ovs: bool) -> List[str]: def get_requirements(use_ovs: bool) -> list[str]:
""" """
Retrieve executable requirements needed to run CORE. Retrieve executable requirements needed to run CORE.

View file

@ -3,7 +3,7 @@ import math
import tkinter as tk import tkinter as tk
from tkinter import PhotoImage, font, messagebox, ttk from tkinter import PhotoImage, font, messagebox, ttk
from tkinter.ttk import Progressbar from tkinter.ttk import Progressbar
from typing import Any, Dict, Optional, Type from typing import Any, Optional
import grpc import grpc
@ -45,7 +45,7 @@ class Application(ttk.Frame):
self.show_infobar: tk.BooleanVar = tk.BooleanVar(value=False) self.show_infobar: tk.BooleanVar = tk.BooleanVar(value=False)
# fonts # fonts
self.fonts_size: Dict[str, int] = {} self.fonts_size: dict[str, int] = {}
self.icon_text_font: Optional[font.Font] = None self.icon_text_font: Optional[font.Font] = None
self.edge_font: Optional[font.Font] = None self.edge_font: Optional[font.Font] = None
@ -145,7 +145,7 @@ class Application(ttk.Frame):
self.statusbar = StatusBar(self.right_frame, self) self.statusbar = StatusBar(self.right_frame, self)
self.statusbar.grid(sticky=tk.EW, columnspan=2) self.statusbar.grid(sticky=tk.EW, columnspan=2)
def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None: def display_info(self, frame_class: type[InfoFrameBase], **kwargs: Any) -> None:
if not self.show_infobar.get(): if not self.show_infobar.get():
return return
self.clear_info() self.clear_info()

View file

@ -1,7 +1,7 @@
import os import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Type from typing import Optional
import yaml import yaml
@ -26,7 +26,7 @@ LOCAL_XMLS_PATH: Path = DATA_PATH.joinpath("xmls").absolute()
LOCAL_MOBILITY_PATH: Path = DATA_PATH.joinpath("mobility").absolute() LOCAL_MOBILITY_PATH: Path = DATA_PATH.joinpath("mobility").absolute()
# configuration data # configuration data
TERMINALS: Dict[str, str] = { TERMINALS: dict[str, str] = {
"xterm": "xterm -e", "xterm": "xterm -e",
"aterm": "aterm -e", "aterm": "aterm -e",
"eterm": "eterm -e", "eterm": "eterm -e",
@ -36,7 +36,7 @@ TERMINALS: Dict[str, str] = {
"xfce4-terminal": "xfce4-terminal -x", "xfce4-terminal": "xfce4-terminal -x",
"gnome-terminal": "gnome-terminal --window --", "gnome-terminal": "gnome-terminal --window --",
} }
EDITORS: List[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] EDITORS: list[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
class IndentDumper(yaml.Dumper): class IndentDumper(yaml.Dumper):
@ -46,17 +46,17 @@ class IndentDumper(yaml.Dumper):
class CustomNode(yaml.YAMLObject): class CustomNode(yaml.YAMLObject):
yaml_tag: str = "!CustomNode" yaml_tag: str = "!CustomNode"
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
def __init__(self, name: str, image: str, services: List[str]) -> None: def __init__(self, name: str, image: str, services: list[str]) -> None:
self.name: str = name self.name: str = name
self.image: str = image self.image: str = image
self.services: List[str] = services self.services: list[str] = services
class CoreServer(yaml.YAMLObject): class CoreServer(yaml.YAMLObject):
yaml_tag: str = "!CoreServer" yaml_tag: str = "!CoreServer"
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
def __init__(self, name: str, address: str) -> None: def __init__(self, name: str, address: str) -> None:
self.name: str = name self.name: str = name
@ -65,7 +65,7 @@ class CoreServer(yaml.YAMLObject):
class Observer(yaml.YAMLObject): class Observer(yaml.YAMLObject):
yaml_tag: str = "!Observer" yaml_tag: str = "!Observer"
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
def __init__(self, name: str, cmd: str) -> None: def __init__(self, name: str, cmd: str) -> None:
self.name: str = name self.name: str = name
@ -74,7 +74,7 @@ class Observer(yaml.YAMLObject):
class PreferencesConfig(yaml.YAMLObject): class PreferencesConfig(yaml.YAMLObject):
yaml_tag: str = "!PreferencesConfig" yaml_tag: str = "!PreferencesConfig"
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
@ -95,7 +95,7 @@ class PreferencesConfig(yaml.YAMLObject):
class LocationConfig(yaml.YAMLObject): class LocationConfig(yaml.YAMLObject):
yaml_tag: str = "!LocationConfig" yaml_tag: str = "!LocationConfig"
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
@ -118,17 +118,17 @@ class LocationConfig(yaml.YAMLObject):
class IpConfigs(yaml.YAMLObject): class IpConfigs(yaml.YAMLObject):
yaml_tag: str = "!IpConfigs" yaml_tag: str = "!IpConfigs"
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
self.__setstate__(kwargs) self.__setstate__(kwargs)
def __setstate__(self, kwargs): def __setstate__(self, kwargs):
self.ip4s: List[str] = kwargs.get( self.ip4s: list[str] = kwargs.get(
"ip4s", ["10.0.0.0", "192.168.0.0", "172.16.0.0"] "ip4s", ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
) )
self.ip4: str = kwargs.get("ip4", self.ip4s[0]) self.ip4: str = kwargs.get("ip4", self.ip4s[0])
self.ip6s: List[str] = kwargs.get("ip6s", ["2001::", "2002::", "a::"]) self.ip6s: list[str] = kwargs.get("ip6s", ["2001::", "2002::", "a::"])
self.ip6: str = kwargs.get("ip6", self.ip6s[0]) self.ip6: str = kwargs.get("ip6", self.ip6s[0])
self.enable_ip4: bool = kwargs.get("enable_ip4", True) self.enable_ip4: bool = kwargs.get("enable_ip4", True)
self.enable_ip6: bool = kwargs.get("enable_ip6", True) self.enable_ip6: bool = kwargs.get("enable_ip6", True)
@ -136,16 +136,16 @@ class IpConfigs(yaml.YAMLObject):
class GuiConfig(yaml.YAMLObject): class GuiConfig(yaml.YAMLObject):
yaml_tag: str = "!GuiConfig" yaml_tag: str = "!GuiConfig"
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
preferences: PreferencesConfig = None, preferences: PreferencesConfig = None,
location: LocationConfig = None, location: LocationConfig = None,
servers: List[CoreServer] = None, servers: list[CoreServer] = None,
nodes: List[CustomNode] = None, nodes: list[CustomNode] = None,
recentfiles: List[str] = None, recentfiles: list[str] = None,
observers: List[Observer] = None, observers: list[Observer] = None,
scale: float = 1.0, scale: float = 1.0,
ips: IpConfigs = None, ips: IpConfigs = None,
mac: str = "00:00:00:aa:00:00", mac: str = "00:00:00:aa:00:00",
@ -158,16 +158,16 @@ class GuiConfig(yaml.YAMLObject):
self.location: LocationConfig = location self.location: LocationConfig = location
if servers is None: if servers is None:
servers = [] servers = []
self.servers: List[CoreServer] = servers self.servers: list[CoreServer] = servers
if nodes is None: if nodes is None:
nodes = [] nodes = []
self.nodes: List[CustomNode] = nodes self.nodes: list[CustomNode] = nodes
if recentfiles is None: if recentfiles is None:
recentfiles = [] recentfiles = []
self.recentfiles: List[str] = recentfiles self.recentfiles: list[str] = recentfiles
if observers is None: if observers is None:
observers = [] observers = []
self.observers: List[Observer] = observers self.observers: list[Observer] = observers
self.scale: float = scale self.scale: float = scale
if ips is None: if ips is None:
ips = IpConfigs() ips = IpConfigs()

View file

@ -6,9 +6,10 @@ import json
import logging import logging
import os import os
import tkinter as tk import tkinter as tk
from collections.abc import Iterable
from pathlib import Path from pathlib import Path
from tkinter import messagebox from tkinter import messagebox
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Optional
import grpc import grpc
@ -16,6 +17,7 @@ from core.api.grpc import client, configservices_pb2, core_pb2
from core.api.grpc.wrappers import ( from core.api.grpc.wrappers import (
ConfigOption, ConfigOption,
ConfigService, ConfigService,
ConfigServiceDefaults,
EmaneModelConfig, EmaneModelConfig,
Event, Event,
ExceptionEvent, ExceptionEvent,
@ -55,7 +57,7 @@ GUI_SOURCE = "gui"
CPU_USAGE_DELAY = 3 CPU_USAGE_DELAY = 3
def to_dict(config: Dict[str, ConfigOption]) -> Dict[str, str]: def to_dict(config: dict[str, ConfigOption]) -> dict[str, str]:
return {x: y.value for x, y in config.items()} return {x: y.value for x, y in config.items()}
@ -74,26 +76,26 @@ class CoreClient:
self.show_throughputs: tk.BooleanVar = tk.BooleanVar(value=False) self.show_throughputs: tk.BooleanVar = tk.BooleanVar(value=False)
# global service settings # global service settings
self.services: Dict[str, Set[str]] = {} self.services: dict[str, set[str]] = {}
self.config_services_groups: Dict[str, Set[str]] = {} self.config_services_groups: dict[str, set[str]] = {}
self.config_services: Dict[str, ConfigService] = {} self.config_services: dict[str, ConfigService] = {}
# loaded configuration data # loaded configuration data
self.emane_models: List[str] = [] self.emane_models: list[str] = []
self.servers: Dict[str, CoreServer] = {} self.servers: dict[str, CoreServer] = {}
self.custom_nodes: Dict[str, NodeDraw] = {} self.custom_nodes: dict[str, NodeDraw] = {}
self.custom_observers: Dict[str, Observer] = {} self.custom_observers: dict[str, Observer] = {}
self.read_config() self.read_config()
# helpers # helpers
self.iface_to_edge: Dict[Tuple[int, ...], CanvasEdge] = {} self.iface_to_edge: dict[tuple[int, ...], CanvasEdge] = {}
self.ifaces_manager: InterfaceManager = InterfaceManager(self.app) self.ifaces_manager: InterfaceManager = InterfaceManager(self.app)
self.observer: Optional[str] = None self.observer: Optional[str] = None
# session data # session data
self.mobility_players: Dict[int, MobilityPlayer] = {} self.mobility_players: dict[int, MobilityPlayer] = {}
self.canvas_nodes: Dict[int, CanvasNode] = {} self.canvas_nodes: dict[int, CanvasNode] = {}
self.links: Dict[str, CanvasEdge] = {} self.links: dict[str, CanvasEdge] = {}
self.handling_throughputs: Optional[grpc.Future] = None self.handling_throughputs: Optional[grpc.Future] = None
self.handling_cpu_usage: Optional[grpc.Future] = None self.handling_cpu_usage: Optional[grpc.Future] = None
self.handling_events: Optional[grpc.Future] = None self.handling_events: Optional[grpc.Future] = None
@ -372,7 +374,7 @@ class CoreClient:
# existing session # existing session
sessions = self.client.get_sessions() sessions = self.client.get_sessions()
if session_id: if session_id:
session_ids = set(x.id for x in sessions) session_ids = {x.id for x in sessions}
if session_id not in session_ids: if session_id not in session_ids:
self.app.show_error( self.app.show_error(
"Join Session Error", "Join Session Error",
@ -401,7 +403,7 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Edit Node Error", e) self.app.show_grpc_exception("Edit Node Error", e)
def get_links(self, definition: bool = False) -> List[Link]: def get_links(self, definition: bool = False) -> list[Link]:
if not definition: if not definition:
self.ifaces_manager.set_macs([x.link for x in self.links.values()]) self.ifaces_manager.set_macs([x.link for x in self.links.values()])
links = [] links = []
@ -419,7 +421,7 @@ class CoreClient:
links.append(edge.asymmetric_link) links.append(edge.asymmetric_link)
return links return links
def start_session(self, definition: bool = False) -> Tuple[bool, List[str]]: def start_session(self, definition: bool = False) -> tuple[bool, list[str]]:
self.session.links = self.get_links(definition) self.session.links = self.get_links(definition)
self.session.metadata = self.get_metadata() self.session.metadata = self.get_metadata()
self.session.servers.clear() self.session.servers.clear()
@ -461,7 +463,7 @@ class CoreClient:
self.mobility_players[node.id] = mobility_player self.mobility_players[node.id] = mobility_player
mobility_player.show() mobility_player.show()
def get_metadata(self) -> Dict[str, str]: def get_metadata(self) -> dict[str, str]:
# create canvas data # create canvas data
canvas_config = self.app.manager.get_metadata() canvas_config = self.app.manager.get_metadata()
canvas_config = json.dumps(canvas_config) canvas_config = json.dumps(canvas_config)
@ -652,7 +654,7 @@ class CoreClient:
self.session.nodes[node.id] = node self.session.nodes[node.id] = node
return node return node
def deleted_canvas_nodes(self, canvas_nodes: List[CanvasNode]) -> None: def deleted_canvas_nodes(self, canvas_nodes: list[CanvasNode]) -> None:
""" """
remove the nodes selected by the user and anything related to that node remove the nodes selected by the user and anything related to that node
such as link, configurations, interfaces such as link, configurations, interfaces
@ -680,7 +682,7 @@ class CoreClient:
dst_iface_id = edge.link.iface2.id dst_iface_id = edge.link.iface2.id
self.iface_to_edge[(dst_node.id, dst_iface_id)] = edge self.iface_to_edge[(dst_node.id, dst_iface_id)] = edge
def get_wlan_configs(self) -> List[Tuple[int, Dict[str, str]]]: def get_wlan_configs(self) -> list[tuple[int, dict[str, str]]]:
configs = [] configs = []
for node in self.session.nodes.values(): for node in self.session.nodes.values():
if node.type != NodeType.WIRELESS_LAN: if node.type != NodeType.WIRELESS_LAN:
@ -691,7 +693,7 @@ class CoreClient:
configs.append((node.id, config)) configs.append((node.id, config))
return configs return configs
def get_mobility_configs(self) -> List[Tuple[int, Dict[str, str]]]: def get_mobility_configs(self) -> list[tuple[int, dict[str, str]]]:
configs = [] configs = []
for node in self.session.nodes.values(): for node in self.session.nodes.values():
if not nutils.is_mobility(node): if not nutils.is_mobility(node):
@ -702,7 +704,7 @@ class CoreClient:
configs.append((node.id, config)) configs.append((node.id, config))
return configs return configs
def get_emane_model_configs(self) -> List[EmaneModelConfig]: def get_emane_model_configs(self) -> list[EmaneModelConfig]:
configs = [] configs = []
for node in self.session.nodes.values(): for node in self.session.nodes.values():
for key, config in node.emane_model_configs.items(): for key, config in node.emane_model_configs.items():
@ -716,7 +718,7 @@ class CoreClient:
configs.append(config) configs.append(config)
return configs return configs
def get_service_configs(self) -> List[ServiceConfig]: def get_service_configs(self) -> list[ServiceConfig]:
configs = [] configs = []
for node in self.session.nodes.values(): for node in self.session.nodes.values():
if not nutils.is_container(node): if not nutils.is_container(node):
@ -736,7 +738,7 @@ class CoreClient:
configs.append(config) configs.append(config)
return configs return configs
def get_service_file_configs(self) -> List[ServiceFileConfig]: def get_service_file_configs(self) -> list[ServiceFileConfig]:
configs = [] configs = []
for node in self.session.nodes.values(): for node in self.session.nodes.values():
if not nutils.is_container(node): if not nutils.is_container(node):
@ -749,12 +751,17 @@ class CoreClient:
configs.append(config) configs.append(config)
return configs return configs
def get_config_service_rendered(self, node_id: int, name: str) -> Dict[str, str]: def get_config_service_rendered(self, node_id: int, name: str) -> dict[str, str]:
return self.client.get_config_service_rendered(self.session.id, node_id, name) return self.client.get_config_service_rendered(self.session.id, node_id, name)
def get_config_service_defaults(
self, node_id: int, name: str
) -> ConfigServiceDefaults:
return self.client.get_config_service_defaults(self.session.id, node_id, name)
def get_config_service_configs_proto( def get_config_service_configs_proto(
self, self,
) -> List[configservices_pb2.ConfigServiceConfig]: ) -> list[configservices_pb2.ConfigServiceConfig]:
config_service_protos = [] config_service_protos = []
for node in self.session.nodes.values(): for node in self.session.nodes.values():
if not nutils.is_container(node): if not nutils.is_container(node):
@ -776,7 +783,7 @@ class CoreClient:
_, output = self.client.node_command(self.session.id, node_id, self.observer) _, output = self.client.node_command(self.session.id, node_id, self.observer)
return output return output
def get_wlan_config(self, node_id: int) -> Dict[str, ConfigOption]: def get_wlan_config(self, node_id: int) -> dict[str, ConfigOption]:
config = self.client.get_wlan_config(self.session.id, node_id) config = self.client.get_wlan_config(self.session.id, node_id)
logger.debug( logger.debug(
"get wlan configuration from node %s, result configuration: %s", "get wlan configuration from node %s, result configuration: %s",
@ -785,10 +792,10 @@ class CoreClient:
) )
return config return config
def get_wireless_config(self, node_id: int) -> Dict[str, ConfigOption]: def get_wireless_config(self, node_id: int) -> dict[str, ConfigOption]:
return self.client.get_wireless_config(self.session.id, node_id) return self.client.get_wireless_config(self.session.id, node_id)
def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]: def get_mobility_config(self, node_id: int) -> dict[str, ConfigOption]:
config = self.client.get_mobility_config(self.session.id, node_id) config = self.client.get_mobility_config(self.session.id, node_id)
logger.debug( logger.debug(
"get mobility config from node %s, result configuration: %s", "get mobility config from node %s, result configuration: %s",
@ -799,7 +806,7 @@ class CoreClient:
def get_emane_model_config( def get_emane_model_config(
self, node_id: int, model: str, iface_id: int = None self, node_id: int, model: str, iface_id: int = None
) -> Dict[str, ConfigOption]: ) -> dict[str, ConfigOption]:
if iface_id is None: if iface_id is None:
iface_id = -1 iface_id = -1
config = self.client.get_emane_model_config( config = self.client.get_emane_model_config(

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -3,7 +3,7 @@ check engine light
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional from typing import TYPE_CHECKING, Optional
from core.api.grpc.wrappers import ExceptionEvent, ExceptionLevel from core.api.grpc.wrappers import ExceptionEvent, ExceptionLevel
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -19,7 +19,7 @@ class AlertsDialog(Dialog):
super().__init__(app, "Alerts") super().__init__(app, "Alerts")
self.tree: Optional[ttk.Treeview] = None self.tree: Optional[ttk.Treeview] = None
self.codetext: Optional[CodeText] = None self.codetext: Optional[CodeText] = None
self.alarm_map: Dict[int, ExceptionEvent] = {} self.alarm_map: dict[int, ExceptionEvent] = {}
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:

View file

@ -4,7 +4,7 @@ set wallpaper
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, Optional
from core.gui import images from core.gui import images
from core.gui.appconfig import BACKGROUNDS_PATH from core.gui.appconfig import BACKGROUNDS_PATH
@ -32,7 +32,7 @@ class CanvasWallpaperDialog(Dialog):
) )
self.filename: tk.StringVar = tk.StringVar(value=self.canvas.wallpaper_file) self.filename: tk.StringVar = tk.StringVar(value=self.canvas.wallpaper_file)
self.image_label: Optional[ttk.Label] = None self.image_label: Optional[ttk.Label] = None
self.options: List[ttk.Radiobutton] = [] self.options: list[ttk.Radiobutton] = []
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:

View file

@ -3,7 +3,7 @@ custom color picker
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Optional, Tuple from typing import TYPE_CHECKING, Optional
from core.gui import validation from core.gui import validation
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -13,6 +13,36 @@ if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
def get_rgb(red: int, green: int, blue: int) -> str:
"""
Convert rgb integers to an rgb hex code (#<red><green><blue>).
:param red: red value
:param green: green value
:param blue: blue value
:return: rgb hex code
"""
return f"#{red:02x}{green:02x}{blue:02x}"
def get_rgb_values(hex_code: str) -> tuple[int, int, int]:
"""
Convert a valid rgb hex code (#<red><green><blue>) to rgb integers.
:param hex_code: valid rgb hex code
:return: a tuple of red, blue, and green values
"""
if len(hex_code) == 4:
red = hex_code[1]
green = hex_code[2]
blue = hex_code[3]
else:
red = hex_code[1:3]
green = hex_code[3:5]
blue = hex_code[5:]
return int(red, 16), int(green, 16), int(blue, 16)
class ColorPickerDialog(Dialog): class ColorPickerDialog(Dialog):
def __init__( def __init__(
self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000" self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000"
@ -27,7 +57,7 @@ class ColorPickerDialog(Dialog):
self.blue_label: Optional[ttk.Label] = None self.blue_label: Optional[ttk.Label] = None
self.display: Optional[tk.Frame] = None self.display: Optional[tk.Frame] = None
self.color: str = initcolor self.color: str = initcolor
red, green, blue = self.get_rgb(initcolor) red, green, blue = get_rgb_values(initcolor)
self.red: tk.IntVar = tk.IntVar(value=red) self.red: tk.IntVar = tk.IntVar(value=red)
self.blue: tk.IntVar = tk.IntVar(value=blue) self.blue: tk.IntVar = tk.IntVar(value=blue)
self.green: tk.IntVar = tk.IntVar(value=green) self.green: tk.IntVar = tk.IntVar(value=green)
@ -66,7 +96,7 @@ class ColorPickerDialog(Dialog):
) )
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
self.red_label = ttk.Label( self.red_label = ttk.Label(
frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5 frame, background=get_rgb(self.red.get(), 0, 0), width=5
) )
self.red_label.grid(row=0, column=3, sticky=tk.EW) self.red_label.grid(row=0, column=3, sticky=tk.EW)
@ -89,7 +119,7 @@ class ColorPickerDialog(Dialog):
) )
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
self.green_label = ttk.Label( self.green_label = ttk.Label(
frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5 frame, background=get_rgb(0, self.green.get(), 0), width=5
) )
self.green_label.grid(row=0, column=3, sticky=tk.EW) self.green_label.grid(row=0, column=3, sticky=tk.EW)
@ -112,7 +142,7 @@ class ColorPickerDialog(Dialog):
) )
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
self.blue_label = ttk.Label( self.blue_label = ttk.Label(
frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5 frame, background=get_rgb(0, 0, self.blue.get()), width=5
) )
self.blue_label.grid(row=0, column=3, sticky=tk.EW) self.blue_label.grid(row=0, column=3, sticky=tk.EW)
@ -150,39 +180,27 @@ class ColorPickerDialog(Dialog):
self.color = self.hex.get() self.color = self.hex.get()
self.destroy() self.destroy()
def get_hex(self) -> str:
"""
convert current RGB values into hex color
"""
red = self.red_entry.get()
blue = self.blue_entry.get()
green = self.green_entry.get()
return "#%02x%02x%02x" % (int(red), int(green), int(blue))
def current_focus(self, focus: str) -> None: def current_focus(self, focus: str) -> None:
self.focus = focus self.focus = focus
def update_color(self, arg1=None, arg2=None, arg3=None) -> None: def update_color(self, arg1=None, arg2=None, arg3=None) -> None:
if self.focus == "rgb": if self.focus == "rgb":
red = self.red_entry.get() red = int(self.red_entry.get() or 0)
blue = self.blue_entry.get() blue = int(self.blue_entry.get() or 0)
green = self.green_entry.get() green = int(self.green_entry.get() or 0)
self.set_scale(red, green, blue) self.set_scale(red, green, blue)
if red and blue and green: hex_code = get_rgb(red, green, blue)
hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue)) self.hex.set(hex_code)
self.hex.set(hex_code) self.display.config(background=hex_code)
self.display.config(background=hex_code) self.set_label(red, green, blue)
self.set_label(red, green, blue)
elif self.focus == "hex": elif self.focus == "hex":
hex_code = self.hex.get() hex_code = self.hex.get()
if len(hex_code) == 4 or len(hex_code) == 7: if len(hex_code) == 4 or len(hex_code) == 7:
red, green, blue = self.get_rgb(hex_code) red, green, blue = get_rgb_values(hex_code)
else: self.set_entry(red, green, blue)
return self.set_scale(red, green, blue)
self.set_entry(red, green, blue) self.display.config(background=hex_code)
self.set_scale(red, green, blue) self.set_label(red, green, blue)
self.display.config(background=hex_code)
self.set_label(str(red), str(green), str(blue))
def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar) -> None: def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar) -> None:
color_var.set(var.get()) color_var.set(var.get())
@ -199,21 +217,7 @@ class ColorPickerDialog(Dialog):
self.green.set(green) self.green.set(green)
self.blue.set(blue) self.blue.set(blue)
def set_label(self, red: str, green: str, blue: str) -> None: def set_label(self, red: int, green: int, blue: int) -> None:
self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0)) self.red_label.configure(background=get_rgb(red, 0, 0))
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0)) self.green_label.configure(background=get_rgb(0, green, 0))
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue))) self.blue_label.configure(background=get_rgb(0, 0, blue))
def get_rgb(self, hex_code: str) -> Tuple[int, int, int]:
"""
convert a valid hex code to RGB values
"""
if len(hex_code) == 4:
red = hex_code[1]
green = hex_code[2]
blue = hex_code[3]
else:
red = hex_code[1:3]
green = hex_code[3:5]
blue = hex_code[5:]
return int(red, 16), int(green, 16), int(blue, 16)

View file

@ -4,7 +4,7 @@ Service configuration dialog
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Dict, List, Optional, Set from typing import TYPE_CHECKING, Optional
import grpc import grpc
@ -35,22 +35,22 @@ class ConfigServiceConfigDialog(Dialog):
self.node: Node = node self.node: Node = node
self.service_name: str = service_name self.service_name: str = service_name
self.radiovar: tk.IntVar = tk.IntVar(value=2) self.radiovar: tk.IntVar = tk.IntVar(value=2)
self.directories: List[str] = [] self.directories: list[str] = []
self.templates: List[str] = [] self.templates: list[str] = []
self.rendered: Dict[str, str] = {} self.rendered: dict[str, str] = {}
self.dependencies: List[str] = [] self.dependencies: list[str] = []
self.executables: List[str] = [] self.executables: list[str] = []
self.startup_commands: List[str] = [] self.startup_commands: list[str] = []
self.validation_commands: List[str] = [] self.validation_commands: list[str] = []
self.shutdown_commands: List[str] = [] self.shutdown_commands: list[str] = []
self.default_startup: List[str] = [] self.default_startup: list[str] = []
self.default_validate: List[str] = [] self.default_validate: list[str] = []
self.default_shutdown: List[str] = [] self.default_shutdown: list[str] = []
self.validation_mode: Optional[ServiceValidationMode] = None self.validation_mode: Optional[ServiceValidationMode] = None
self.validation_time: Optional[int] = None self.validation_time: Optional[int] = None
self.validation_period: tk.DoubleVar = tk.DoubleVar() self.validation_period: tk.DoubleVar = tk.DoubleVar()
self.modes: List[str] = [] self.modes: list[str] = []
self.mode_configs: Dict[str, Dict[str, str]] = {} self.mode_configs: dict[str, dict[str, str]] = {}
self.notebook: Optional[ttk.Notebook] = None self.notebook: Optional[ttk.Notebook] = None
self.templates_combobox: Optional[ttk.Combobox] = None self.templates_combobox: Optional[ttk.Combobox] = None
self.modes_combobox: Optional[ttk.Combobox] = None self.modes_combobox: Optional[ttk.Combobox] = None
@ -62,12 +62,12 @@ class ConfigServiceConfigDialog(Dialog):
self.template_text: Optional[CodeText] = None self.template_text: Optional[CodeText] = None
self.rendered_text: Optional[CodeText] = None self.rendered_text: Optional[CodeText] = None
self.validation_period_entry: Optional[ttk.Entry] = None self.validation_period_entry: Optional[ttk.Entry] = None
self.original_service_files: Dict[str, str] = {} self.original_service_files: dict[str, str] = {}
self.temp_service_files: Dict[str, str] = {} self.temp_service_files: dict[str, str] = {}
self.modified_files: Set[str] = set() self.modified_files: set[str] = set()
self.config_frame: Optional[ConfigFrame] = None self.config_frame: Optional[ConfigFrame] = None
self.default_config: Dict[str, str] = {} self.default_config: dict[str, str] = {}
self.config: Dict[str, ConfigOption] = {} self.config: dict[str, ConfigOption] = {}
self.has_error: bool = False self.has_error: bool = False
self.load() self.load()
if not self.has_error: if not self.has_error:
@ -87,7 +87,9 @@ class ConfigServiceConfigDialog(Dialog):
self.validation_mode = service.validation_mode self.validation_mode = service.validation_mode
self.validation_time = service.validation_timer self.validation_time = service.validation_timer
self.validation_period.set(service.validation_period) self.validation_period.set(service.validation_period)
defaults = self.core.client.get_config_service_defaults(self.service_name) defaults = self.core.get_config_service_defaults(
self.node.id, self.service_name
)
self.original_service_files = defaults.templates self.original_service_files = defaults.templates
self.temp_service_files = dict(self.original_service_files) self.temp_service_files = dict(self.original_service_files)
self.modes = sorted(defaults.modes) self.modes = sorted(defaults.modes)
@ -405,7 +407,7 @@ class ConfigServiceConfigDialog(Dialog):
pass pass
def append_commands( def append_commands(
self, commands: List[str], listbox: tk.Listbox, to_add: List[str] self, commands: list[str], listbox: tk.Listbox, to_add: list[str]
) -> None: ) -> None:
for cmd in to_add: for cmd in to_add:
commands.append(cmd) commands.append(cmd)

View file

@ -4,7 +4,7 @@ copy service config dialog
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional from typing import TYPE_CHECKING, Optional
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
@ -29,7 +29,7 @@ class CopyServiceConfigDialog(Dialog):
self.service: str = service self.service: str = service
self.file_name: str = file_name self.file_name: str = file_name
self.listbox: Optional[tk.Listbox] = None self.listbox: Optional[tk.Listbox] = None
self.nodes: Dict[str, int] = {} self.nodes: dict[str, int] = {}
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:

View file

@ -2,7 +2,7 @@ import logging
import tkinter as tk import tkinter as tk
from pathlib import Path from pathlib import Path
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Optional, Set from typing import TYPE_CHECKING, Optional
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
@ -21,13 +21,13 @@ if TYPE_CHECKING:
class ServicesSelectDialog(Dialog): class ServicesSelectDialog(Dialog):
def __init__( def __init__(
self, master: tk.BaseWidget, app: "Application", current_services: Set[str] self, master: tk.BaseWidget, app: "Application", current_services: set[str]
) -> None: ) -> None:
super().__init__(app, "Node Config Services", master=master) super().__init__(app, "Node Config Services", master=master)
self.groups: Optional[ListboxScroll] = None self.groups: Optional[ListboxScroll] = None
self.services: Optional[CheckboxList] = None self.services: Optional[CheckboxList] = None
self.current: Optional[ListboxScroll] = None self.current: Optional[ListboxScroll] = None
self.current_services: Set[str] = current_services self.current_services: set[str] = current_services
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:
@ -114,7 +114,7 @@ class CustomNodesDialog(Dialog):
self.image_button: Optional[ttk.Button] = None self.image_button: Optional[ttk.Button] = None
self.image: Optional[PhotoImage] = None self.image: Optional[PhotoImage] = None
self.image_file: Optional[str] = None self.image_file: Optional[str] = None
self.services: Set[str] = set() self.services: set[str] = set()
self.selected: Optional[str] = None self.selected: Optional[str] = None
self.selected_index: Optional[int] = None self.selected_index: Optional[int] = None
self.draw() self.draw()

View file

@ -4,7 +4,7 @@ emane configuration
import tkinter as tk import tkinter as tk
import webbrowser import webbrowser
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Dict, List, Optional from typing import TYPE_CHECKING, Optional
import grpc import grpc
@ -43,7 +43,7 @@ class EmaneModelDialog(Dialog):
config = self.app.core.get_emane_model_config( config = self.app.core.get_emane_model_config(
self.node.id, self.model, self.iface_id self.node.id, self.model, self.iface_id
) )
self.config: Dict[str, ConfigOption] = config self.config: dict[str, ConfigOption] = config
self.draw() self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Get EMANE Config Error", e) self.app.show_grpc_exception("Get EMANE Config Error", e)
@ -82,7 +82,7 @@ class EmaneConfigDialog(Dialog):
self.node: Node = node self.node: Node = node
self.radiovar: tk.IntVar = tk.IntVar() self.radiovar: tk.IntVar = tk.IntVar()
self.radiovar.set(1) self.radiovar.set(1)
self.emane_models: List[str] = [ self.emane_models: list[str] = [
x.split("_")[1] for x in self.app.core.emane_models x.split("_")[1] for x in self.app.core.emane_models
] ]
model = self.node.emane.split("_")[1] model = self.node.emane.split("_")[1]

View file

@ -1,5 +1,5 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from core.api.grpc.wrappers import Hook, SessionState from core.api.grpc.wrappers import Hook, SessionState
@ -91,6 +91,13 @@ class HookDialog(Dialog):
self.hook.file = file_name self.hook.file = file_name
self.hook.data = data self.hook.data = data
else: else:
if file_name in self.app.core.session.hooks:
messagebox.showerror(
"Hook Error",
f"Hook {file_name} already exists!",
parent=self.master,
)
return
self.hook = Hook(state=state, file=file_name, data=data) self.hook = Hook(state=state, file=file_name, data=data)
self.destroy() self.destroy()

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, Optional
import netaddr import netaddr
@ -17,8 +17,8 @@ class IpConfigDialog(Dialog):
super().__init__(app, "IP Configuration") super().__init__(app, "IP Configuration")
self.ip4: str = self.app.guiconfig.ips.ip4 self.ip4: str = self.app.guiconfig.ips.ip4
self.ip6: str = self.app.guiconfig.ips.ip6 self.ip6: str = self.app.guiconfig.ips.ip6
self.ip4s: List[str] = self.app.guiconfig.ips.ip4s self.ip4s: list[str] = self.app.guiconfig.ips.ip4s
self.ip6s: List[str] = self.app.guiconfig.ips.ip6s self.ip6s: list[str] = self.app.guiconfig.ips.ip6s
self.ip4_entry: Optional[ttk.Entry] = None self.ip4_entry: Optional[ttk.Entry] = None
self.ip4_listbox: Optional[ListboxScroll] = None self.ip4_listbox: Optional[ListboxScroll] = None
self.ip6_entry: Optional[ttk.Entry] = None self.ip6_entry: Optional[ttk.Entry] = None

View file

@ -3,7 +3,7 @@ mobility configuration
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional from typing import TYPE_CHECKING, Optional
import grpc import grpc
@ -26,7 +26,7 @@ class MobilityConfigDialog(Dialog):
config = self.node.mobility_config config = self.node.mobility_config
if not config: if not config:
config = self.app.core.get_mobility_config(self.node.id) config = self.app.core.get_mobility_config(self.node.id)
self.config: Dict[str, ConfigOption] = config self.config: dict[str, ConfigOption] = config
self.draw() self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Get Mobility Config Error", e) self.app.show_grpc_exception("Get Mobility Config Error", e)

View file

@ -2,7 +2,7 @@ import logging
import tkinter as tk import tkinter as tk
from functools import partial from functools import partial
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, Dict, Optional from typing import TYPE_CHECKING, Optional
import netaddr import netaddr
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
@ -190,7 +190,7 @@ class NodeConfigDialog(Dialog):
if self.node.server: if self.node.server:
server = self.node.server server = self.node.server
self.server: tk.StringVar = tk.StringVar(value=server) self.server: tk.StringVar = tk.StringVar(value=server)
self.ifaces: Dict[int, InterfaceData] = {} self.ifaces: dict[int, InterfaceData] = {}
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:

View file

@ -4,7 +4,7 @@ core node services
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, Optional, Set from typing import TYPE_CHECKING, Optional
from core.api.grpc.wrappers import Node from core.api.grpc.wrappers import Node
from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog
@ -20,7 +20,7 @@ if TYPE_CHECKING:
class NodeConfigServiceDialog(Dialog): class NodeConfigServiceDialog(Dialog):
def __init__( def __init__(
self, app: "Application", node: Node, services: Set[str] = None self, app: "Application", node: Node, services: set[str] = None
) -> None: ) -> None:
title = f"{node.name} Config Services" title = f"{node.name} Config Services"
super().__init__(app, title) super().__init__(app, title)
@ -30,7 +30,7 @@ class NodeConfigServiceDialog(Dialog):
self.current: Optional[ListboxScroll] = None self.current: Optional[ListboxScroll] = None
if services is None: if services is None:
services = set(node.config_services) services = set(node.config_services)
self.current_services: Set[str] = services self.current_services: set[str] = services
self.protocol("WM_DELETE_WINDOW", self.click_cancel) self.protocol("WM_DELETE_WINDOW", self.click_cancel)
self.draw() self.draw()

View file

@ -3,7 +3,7 @@ core node services
""" """
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, Optional, Set from typing import TYPE_CHECKING, Optional
from core.api.grpc.wrappers import Node from core.api.grpc.wrappers import Node
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -24,7 +24,7 @@ class NodeServiceDialog(Dialog):
self.services: Optional[CheckboxList] = None self.services: Optional[CheckboxList] = None
self.current: Optional[ListboxScroll] = None self.current: Optional[ListboxScroll] = None
services = set(node.services) services = set(node.services)
self.current_services: Set[str] = services self.current_services: set[str] = services
self.protocol("WM_DELETE_WINDOW", self.click_cancel) self.protocol("WM_DELETE_WINDOW", self.click_cancel)
self.draw() self.draw()

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional from typing import TYPE_CHECKING, Optional
from core.gui import nodeutils as nutils from core.gui import nodeutils as nutils
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -17,7 +17,7 @@ class RunToolDialog(Dialog):
self.cmd: tk.StringVar = tk.StringVar(value="ps ax") self.cmd: tk.StringVar = tk.StringVar(value="ps ax")
self.result: Optional[CodeText] = None self.result: Optional[CodeText] = None
self.node_list: Optional[ListboxScroll] = None self.node_list: Optional[ListboxScroll] = None
self.executable_nodes: Dict[str, int] = {} self.executable_nodes: dict[str, int] = {}
self.store_nodes() self.store_nodes()
self.draw() self.draw()

View file

@ -2,7 +2,7 @@ import logging
import tkinter as tk import tkinter as tk
from pathlib import Path from pathlib import Path
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Optional
import grpc import grpc
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
@ -35,21 +35,21 @@ class ServiceConfigDialog(Dialog):
self.service_name: str = service_name self.service_name: str = service_name
self.radiovar: tk.IntVar = tk.IntVar(value=2) self.radiovar: tk.IntVar = tk.IntVar(value=2)
self.metadata: str = "" self.metadata: str = ""
self.filenames: List[str] = [] self.filenames: list[str] = []
self.dependencies: List[str] = [] self.dependencies: list[str] = []
self.executables: List[str] = [] self.executables: list[str] = []
self.startup_commands: List[str] = [] self.startup_commands: list[str] = []
self.validation_commands: List[str] = [] self.validation_commands: list[str] = []
self.shutdown_commands: List[str] = [] self.shutdown_commands: list[str] = []
self.default_startup: List[str] = [] self.default_startup: list[str] = []
self.default_validate: List[str] = [] self.default_validate: list[str] = []
self.default_shutdown: List[str] = [] self.default_shutdown: list[str] = []
self.validation_mode: Optional[ServiceValidationMode] = None self.validation_mode: Optional[ServiceValidationMode] = None
self.validation_time: Optional[int] = None self.validation_time: Optional[int] = None
self.validation_period: Optional[float] = None self.validation_period: Optional[float] = None
self.directory_entry: Optional[ttk.Entry] = None self.directory_entry: Optional[ttk.Entry] = None
self.default_directories: List[str] = [] self.default_directories: list[str] = []
self.temp_directories: List[str] = [] self.temp_directories: list[str] = []
self.documentnew_img: PhotoImage = self.app.get_enum_icon( self.documentnew_img: PhotoImage = self.app.get_enum_icon(
ImageEnum.DOCUMENTNEW, width=ICON_SIZE ImageEnum.DOCUMENTNEW, width=ICON_SIZE
) )
@ -67,10 +67,10 @@ class ServiceConfigDialog(Dialog):
self.validation_mode_entry: Optional[ttk.Entry] = None self.validation_mode_entry: Optional[ttk.Entry] = None
self.service_file_data: Optional[CodeText] = None self.service_file_data: Optional[CodeText] = None
self.validation_period_entry: Optional[ttk.Entry] = None self.validation_period_entry: Optional[ttk.Entry] = None
self.original_service_files: Dict[str, str] = {} self.original_service_files: dict[str, str] = {}
self.default_config: Optional[NodeServiceData] = None self.default_config: Optional[NodeServiceData] = None
self.temp_service_files: Dict[str, str] = {} self.temp_service_files: dict[str, str] = {}
self.modified_files: Set[str] = set() self.modified_files: set[str] = set()
self.has_error: bool = False self.has_error: bool = False
self.load() self.load()
if not self.has_error: if not self.has_error:
@ -558,13 +558,13 @@ class ServiceConfigDialog(Dialog):
@classmethod @classmethod
def append_commands( def append_commands(
cls, commands: List[str], listbox: tk.Listbox, to_add: List[str] cls, commands: list[str], listbox: tk.Listbox, to_add: list[str]
) -> None: ) -> None:
for cmd in to_add: for cmd in to_add:
commands.append(cmd) commands.append(cmd)
listbox.insert(tk.END, cmd) listbox.insert(tk.END, cmd)
def get_commands(self) -> Tuple[List[str], List[str], List[str]]: def get_commands(self) -> tuple[list[str], list[str], list[str]]:
startup = self.startup_commands_listbox.get(0, "end") startup = self.startup_commands_listbox.get(0, "end")
shutdown = self.shutdown_commands_listbox.get(0, "end") shutdown = self.shutdown_commands_listbox.get(0, "end")
validate = self.validate_commands_listbox.get(0, "end") validate = self.validate_commands_listbox.get(0, "end")

View file

@ -1,7 +1,7 @@
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, Optional
import grpc import grpc
@ -30,7 +30,7 @@ class SessionsDialog(Dialog):
self.protocol("WM_DELETE_WINDOW", self.on_closing) self.protocol("WM_DELETE_WINDOW", self.on_closing)
self.draw() self.draw()
def get_sessions(self) -> List[SessionSummary]: def get_sessions(self) -> list[SessionSummary]:
try: try:
sessions = self.app.core.client.get_sessions() sessions = self.app.core.client.get_sessions()
logger.info("sessions: %s", sessions) logger.info("sessions: %s", sessions)

View file

@ -3,7 +3,7 @@ shape input dialog
""" """
import tkinter as tk import tkinter as tk
from tkinter import font, ttk from tkinter import font, ttk
from typing import TYPE_CHECKING, List, Optional, Union from typing import TYPE_CHECKING, Optional, Union
from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -16,8 +16,8 @@ if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
from core.gui.graph.shape import Shape from core.gui.graph.shape import Shape
FONT_SIZES: List[int] = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] FONT_SIZES: list[int] = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
BORDER_WIDTH: List[int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] BORDER_WIDTH: list[int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
class ShapeDialog(Dialog): class ShapeDialog(Dialog):
@ -168,7 +168,7 @@ class ShapeDialog(Dialog):
self.add_text() self.add_text()
self.destroy() self.destroy()
def make_font(self) -> List[Union[int, str]]: def make_font(self) -> list[Union[int, str]]:
""" """
create font for text or shape label create font for text or shape label
""" """

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional from typing import TYPE_CHECKING, Optional
import grpc import grpc
@ -19,12 +19,12 @@ class WirelessConfigDialog(Dialog):
super().__init__(app, f"Wireless Configuration - {canvas_node.core_node.name}") super().__init__(app, f"Wireless Configuration - {canvas_node.core_node.name}")
self.node: Node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.config_frame: Optional[ConfigFrame] = None self.config_frame: Optional[ConfigFrame] = None
self.config: Dict[str, ConfigOption] = {} self.config: dict[str, ConfigOption] = {}
try: try:
config = self.node.wireless_config config = self.node.wireless_config
if not config: if not config:
config = self.app.core.get_wireless_config(self.node.id) config = self.app.core.get_wireless_config(self.node.id)
self.config: Dict[str, ConfigOption] = config self.config: dict[str, ConfigOption] = config
self.draw() self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Wireless Config Error", e) self.app.show_grpc_exception("Wireless Config Error", e)

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional from typing import TYPE_CHECKING, Optional
import grpc import grpc
@ -27,13 +27,13 @@ class WlanConfigDialog(Dialog):
self.config_frame: Optional[ConfigFrame] = None self.config_frame: Optional[ConfigFrame] = None
self.range_entry: Optional[ttk.Entry] = None self.range_entry: Optional[ttk.Entry] = None
self.has_error: bool = False self.has_error: bool = False
self.ranges: Dict[int, int] = {} self.ranges: dict[int, int] = {}
self.positive_int: int = self.app.master.register(self.validate_and_update) self.positive_int: int = self.app.master.register(self.validate_and_update)
try: try:
config = self.node.wlan_config config = self.node.wlan_config
if not config: if not config:
config = self.app.core.get_wlan_config(self.node.id) config = self.app.core.get_wlan_config(self.node.id)
self.config: Dict[str, ConfigOption] = config self.config: dict[str, ConfigOption] = config
self.init_draw_range() self.init_draw_range()
self.draw() self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:

View file

@ -2,10 +2,10 @@ import functools
import logging import logging
import math import math
import tkinter as tk import tkinter as tk
from typing import TYPE_CHECKING, Optional, Tuple, Union from typing import TYPE_CHECKING, Optional, Union
from core.api.grpc.wrappers import Interface, Link from core.api.grpc.wrappers import Interface, Link
from core.gui import themes from core.gui import nodeutils, themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
from core.gui.graph import tags from core.gui.graph import tags
@ -54,7 +54,7 @@ def create_edge_token(link: Link) -> str:
def node_label_positions( def node_label_positions(
src_x: int, src_y: int, dst_x: int, dst_y: int src_x: int, src_y: int, dst_x: int, dst_y: int
) -> Tuple[Tuple[float, float], Tuple[float, float]]: ) -> tuple[tuple[float, float], tuple[float, float]]:
v_x, v_y = dst_x - src_x, dst_y - src_y v_x, v_y = dst_x - src_x, dst_y - src_y
v_len = math.sqrt(v_x**2 + v_y**2) v_len = math.sqrt(v_x**2 + v_y**2)
if v_len == 0: if v_len == 0:
@ -128,8 +128,8 @@ class Edge:
return self.width * self.app.app_scale return self.width * self.app.app_scale
def _get_arcpoint( def _get_arcpoint(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] self, src_pos: tuple[float, float], dst_pos: tuple[float, float]
) -> Tuple[float, float]: ) -> tuple[float, float]:
src_x, src_y = src_pos src_x, src_y = src_pos
dst_x, dst_y = dst_pos dst_x, dst_y = dst_pos
mp_x = (src_x + dst_x) / 2 mp_x = (src_x + dst_x) / 2
@ -317,7 +317,7 @@ class Edge:
if self.dst_label2: if self.dst_label2:
self.dst.canvas.itemconfig(self.dst_label2, text=text) self.dst.canvas.itemconfig(self.dst_label2, text=text)
def drawing(self, pos: Tuple[float, float]) -> None: def drawing(self, pos: tuple[float, float]) -> None:
src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id) src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id)
src_pos = src_x, src_y src_pos = src_x, src_y
self.moved(src_pos, pos) self.moved(src_pos, pos)
@ -368,7 +368,7 @@ class Edge:
dst_pos = dst_x, dst_y dst_pos = dst_x, dst_y
self.moved(self.src.position(), dst_pos) self.moved(self.src.position(), dst_pos)
def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None: def moved(self, src_pos: tuple[float, float], dst_pos: tuple[float, float]) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos) arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.src.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos) self.src.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos)
if self.middle_label: if self.middle_label:
@ -381,7 +381,7 @@ class Edge:
self.src.canvas.coords(self.dst_label, *dst_pos) self.src.canvas.coords(self.dst_label, *dst_pos)
def moved2( def moved2(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] self, src_pos: tuple[float, float], dst_pos: tuple[float, float]
) -> None: ) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos) arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.dst.canvas.coords(self.id2, *src_pos, *arc_pos, *dst_pos) self.dst.canvas.coords(self.id2, *src_pos, *arc_pos, *dst_pos)
@ -568,7 +568,7 @@ class CanvasEdge(Edge):
label += f"{iface.ip6}/{iface.ip6_mask}" label += f"{iface.ip6}/{iface.ip6_mask}"
return label return label
def create_node_labels(self) -> Tuple[str, str]: def create_node_labels(self) -> tuple[str, str]:
label1 = None label1 = None
if self.link.iface1: if self.link.iface1:
label1 = self.iface_label(self.link.iface1) label1 = self.iface_label(self.link.iface1)
@ -638,10 +638,10 @@ class CanvasEdge(Edge):
self.check_wireless() self.check_wireless()
if link is None: if link is None:
link = self.app.core.ifaces_manager.create_link(self) link = self.app.core.ifaces_manager.create_link(self)
if link.iface1: if link.iface1 and not nodeutils.is_rj45(self.src.core_node):
iface1 = link.iface1 iface1 = link.iface1
self.src.ifaces[iface1.id] = iface1 self.src.ifaces[iface1.id] = iface1
if link.iface2: if link.iface2 and not nodeutils.is_rj45(self.dst.core_node):
iface2 = link.iface2 iface2 = link.iface2
self.dst.ifaces[iface2.id] = iface2 self.dst.ifaces[iface2.id] = iface2
self.token = create_edge_token(link) self.token = create_edge_token(link)
@ -751,9 +751,9 @@ class CanvasEdge(Edge):
self.src.edges.discard(self) self.src.edges.discard(self)
if self.dst: if self.dst:
self.dst.edges.discard(self) self.dst.edges.discard(self)
if self.link.iface1: if self.link.iface1 and not nodeutils.is_rj45(self.src.core_node):
del self.src.ifaces[self.link.iface1.id] del self.src.ifaces[self.link.iface1.id]
if self.link.iface2: if self.link.iface2 and not nodeutils.is_rj45(self.dst.core_node):
del self.dst.ifaces[self.link.iface2.id] del self.dst.ifaces[self.link.iface2.id]
if self.src.is_wireless(): if self.src.is_wireless():
self.dst.delete_antenna() self.dst.delete_antenna()

View file

@ -2,7 +2,7 @@ import logging
import tkinter as tk import tkinter as tk
from copy import deepcopy from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Any, Optional
from PIL import Image from PIL import Image
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
@ -27,8 +27,8 @@ if TYPE_CHECKING:
ZOOM_IN: float = 1.1 ZOOM_IN: float = 1.1
ZOOM_OUT: float = 0.9 ZOOM_OUT: float = 0.9
MOVE_NODE_MODES: Set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT} MOVE_NODE_MODES: set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT}
MOVE_SHAPE_MODES: Set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT} MOVE_SHAPE_MODES: set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT}
BACKGROUND_COLOR: str = "#cccccc" BACKGROUND_COLOR: str = "#cccccc"
@ -40,32 +40,32 @@ class CanvasGraph(tk.Canvas):
manager: "CanvasManager", manager: "CanvasManager",
core: "CoreClient", core: "CoreClient",
_id: int, _id: int,
dimensions: Tuple[int, int], dimensions: tuple[int, int],
) -> None: ) -> None:
super().__init__(master, highlightthickness=0, background=BACKGROUND_COLOR) super().__init__(master, highlightthickness=0, background=BACKGROUND_COLOR)
self.id: int = _id self.id: int = _id
self.app: "Application" = app self.app: "Application" = app
self.manager: "CanvasManager" = manager self.manager: "CanvasManager" = manager
self.core: "CoreClient" = core self.core: "CoreClient" = core
self.selection: Dict[int, int] = {} self.selection: dict[int, int] = {}
self.select_box: Optional[Shape] = None self.select_box: Optional[Shape] = None
self.selected: Optional[int] = None self.selected: Optional[int] = None
self.nodes: Dict[int, CanvasNode] = {} self.nodes: dict[int, CanvasNode] = {}
self.shadow_nodes: Dict[int, ShadowNode] = {} self.shadow_nodes: dict[int, ShadowNode] = {}
self.shapes: Dict[int, Shape] = {} self.shapes: dict[int, Shape] = {}
self.shadow_core_nodes: Dict[int, ShadowNode] = {} self.shadow_core_nodes: dict[int, ShadowNode] = {}
# map wireless/EMANE node to the set of MDRs connected to that node # map wireless/EMANE node to the set of MDRs connected to that node
self.wireless_network: Dict[int, Set[int]] = {} self.wireless_network: dict[int, set[int]] = {}
self.drawing_edge: Optional[CanvasEdge] = None self.drawing_edge: Optional[CanvasEdge] = None
self.rect: Optional[int] = None self.rect: Optional[int] = None
self.shape_drawing: bool = False self.shape_drawing: bool = False
self.current_dimensions: Tuple[int, int] = dimensions self.current_dimensions: tuple[int, int] = dimensions
self.ratio: float = 1.0 self.ratio: float = 1.0
self.offset: Tuple[int, int] = (0, 0) self.offset: tuple[int, int] = (0, 0)
self.cursor: Tuple[int, int] = (0, 0) self.cursor: tuple[int, int] = (0, 0)
self.to_copy: List[CanvasNode] = [] self.to_copy: list[CanvasNode] = []
# background related # background related
self.wallpaper_id: Optional[int] = None self.wallpaper_id: Optional[int] = None
@ -82,7 +82,7 @@ class CanvasGraph(tk.Canvas):
self.draw_canvas() self.draw_canvas()
self.draw_grid() self.draw_grid()
def draw_canvas(self, dimensions: Tuple[int, int] = None) -> None: def draw_canvas(self, dimensions: tuple[int, int] = None) -> None:
if self.rect is not None: if self.rect is not None:
self.delete(self.rect) self.delete(self.rect)
if not dimensions: if not dimensions:
@ -126,23 +126,23 @@ class CanvasGraph(tk.Canvas):
shadow_node = ShadowNode(self.app, self, node) shadow_node = ShadowNode(self.app, self, node)
return shadow_node return shadow_node
def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]: def get_actual_coords(self, x: float, y: float) -> tuple[float, float]:
actual_x = (x - self.offset[0]) / self.ratio actual_x = (x - self.offset[0]) / self.ratio
actual_y = (y - self.offset[1]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio
return actual_x, actual_y return actual_x, actual_y
def get_scaled_coords(self, x: float, y: float) -> Tuple[float, float]: def get_scaled_coords(self, x: float, y: float) -> tuple[float, float]:
scaled_x = (x * self.ratio) + self.offset[0] scaled_x = (x * self.ratio) + self.offset[0]
scaled_y = (y * self.ratio) + self.offset[1] scaled_y = (y * self.ratio) + self.offset[1]
return scaled_x, scaled_y return scaled_x, scaled_y
def inside_canvas(self, x: float, y: float) -> Tuple[bool, bool]: def inside_canvas(self, x: float, y: float) -> tuple[bool, bool]:
x1, y1, x2, y2 = self.bbox(self.rect) x1, y1, x2, y2 = self.bbox(self.rect)
valid_x = x1 <= x <= x2 valid_x = x1 <= x <= x2
valid_y = y1 <= y <= y2 valid_y = y1 <= y <= y2
return valid_x and valid_y return valid_x and valid_y
def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> Tuple[bool, bool]: def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> tuple[bool, bool]:
valid_topleft = self.inside_canvas(x1, y1) valid_topleft = self.inside_canvas(x1, y1)
valid_bottomright = self.inside_canvas(x2, y2) valid_bottomright = self.inside_canvas(x2, y2)
return valid_topleft and valid_bottomright return valid_topleft and valid_bottomright
@ -161,7 +161,7 @@ class CanvasGraph(tk.Canvas):
self.tag_lower(tags.GRIDLINE) self.tag_lower(tags.GRIDLINE)
self.tag_lower(self.rect) self.tag_lower(self.rect)
def canvas_xy(self, event: tk.Event) -> Tuple[float, float]: def canvas_xy(self, event: tk.Event) -> tuple[float, float]:
""" """
Convert window coordinate to canvas coordinate Convert window coordinate to canvas coordinate
""" """
@ -516,7 +516,7 @@ class CanvasGraph(tk.Canvas):
self.nodes[node.id] = node self.nodes[node.id] = node
self.core.set_canvas_node(core_node, node) self.core.set_canvas_node(core_node, node)
def width_and_height(self) -> Tuple[int, int]: def width_and_height(self) -> tuple[int, int]:
""" """
retrieve canvas width and height in pixels retrieve canvas width and height in pixels
""" """
@ -601,7 +601,7 @@ class CanvasGraph(tk.Canvas):
self.redraw_canvas((image.width(), image.height())) self.redraw_canvas((image.width(), image.height()))
self.draw_wallpaper(image) self.draw_wallpaper(image)
def redraw_canvas(self, dimensions: Tuple[int, int] = None) -> None: def redraw_canvas(self, dimensions: tuple[int, int] = None) -> None:
logger.debug("redrawing canvas to dimensions: %s", dimensions) logger.debug("redrawing canvas to dimensions: %s", dimensions)
# reset scale and move back to original position # reset scale and move back to original position
@ -814,7 +814,7 @@ class CanvasGraph(tk.Canvas):
for edge_id in self.find_withtag(tags.EDGE): for edge_id in self.find_withtag(tags.EDGE):
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale)) self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale))
def get_metadata(self) -> Dict[str, Any]: def get_metadata(self) -> dict[str, Any]:
wallpaper_path = None wallpaper_path = None
if self.wallpaper_file: if self.wallpaper_file:
wallpaper = Path(self.wallpaper_file) wallpaper = Path(self.wallpaper_file)
@ -830,7 +830,7 @@ class CanvasGraph(tk.Canvas):
dimensions=self.current_dimensions, dimensions=self.current_dimensions,
) )
def parse_metadata(self, config: Dict[str, Any]) -> None: def parse_metadata(self, config: dict[str, Any]) -> None:
fit_image = config.get("fit_image", False) fit_image = config.get("fit_image", False)
self.adjust_to_dim.set(fit_image) self.adjust_to_dim.set(fit_image)
wallpaper_style = config.get("wallpaper_style", 1) wallpaper_style = config.get("wallpaper_style", 1)

View file

@ -1,9 +1,10 @@
import json import json
import logging import logging
import tkinter as tk import tkinter as tk
from collections.abc import ValuesView
from copy import deepcopy from copy import deepcopy
from tkinter import BooleanVar, messagebox, ttk from tkinter import BooleanVar, messagebox, ttk
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, ValuesView from typing import TYPE_CHECKING, Any, Literal, Optional
from core.api.grpc.wrappers import Link, LinkType, Node, Session, ThroughputsEvent from core.api.grpc.wrappers import Link, LinkType, Node, Session, ThroughputsEvent
from core.gui import nodeutils as nutils from core.gui import nodeutils as nutils
@ -34,7 +35,7 @@ class ShowVar(BooleanVar):
self.manager: "CanvasManager" = manager self.manager: "CanvasManager" = manager
self.tag: str = tag self.tag: str = tag
def state(self) -> str: def state(self) -> Literal["normal", "hidden"]:
return tk.NORMAL if self.get() else tk.HIDDEN return tk.NORMAL if self.get() else tk.HIDDEN
def click_handler(self) -> None: def click_handler(self) -> None:
@ -78,14 +79,14 @@ class CanvasManager:
self.mode: GraphMode = GraphMode.SELECT self.mode: GraphMode = GraphMode.SELECT
self.annotation_type: Optional[ShapeType] = None self.annotation_type: Optional[ShapeType] = None
self.node_draw: Optional[NodeDraw] = None self.node_draw: Optional[NodeDraw] = None
self.canvases: Dict[int, CanvasGraph] = {} self.canvases: dict[int, CanvasGraph] = {}
# global edge management # global edge management
self.edges: Dict[str, CanvasEdge] = {} self.edges: dict[str, CanvasEdge] = {}
self.wireless_edges: Dict[str, CanvasWirelessEdge] = {} self.wireless_edges: dict[str, CanvasWirelessEdge] = {}
# global canvas settings # global canvas settings
self.default_dimensions: Tuple[int, int] = ( self.default_dimensions: tuple[int, int] = (
self.app.guiconfig.preferences.width, self.app.guiconfig.preferences.width,
self.app.guiconfig.preferences.height, self.app.guiconfig.preferences.height,
) )
@ -111,8 +112,8 @@ class CanvasManager:
# widget # widget
self.notebook: Optional[ttk.Notebook] = None self.notebook: Optional[ttk.Notebook] = None
self.canvas_ids: Dict[str, int] = {} self.canvas_ids: dict[str, int] = {}
self.unique_ids: Dict[int, str] = {} self.unique_ids: dict[int, str] = {}
self.draw() self.draw()
self.setup_bindings() self.setup_bindings()
@ -273,17 +274,17 @@ class CanvasManager:
if not self.canvases: if not self.canvases:
self.add_canvas() self.add_canvas()
def redraw_canvas(self, dimensions: Tuple[int, int]) -> None: def redraw_canvas(self, dimensions: tuple[int, int]) -> None:
canvas = self.current() canvas = self.current()
canvas.redraw_canvas(dimensions) canvas.redraw_canvas(dimensions)
if canvas.wallpaper: if canvas.wallpaper:
canvas.redraw_wallpaper() canvas.redraw_wallpaper()
def get_metadata(self) -> Dict[str, Any]: def get_metadata(self) -> dict[str, Any]:
canvases = [x.get_metadata() for x in self.all()] canvases = [x.get_metadata() for x in self.all()]
return dict(gridlines=self.show_grid.get(), canvases=canvases) return dict(gridlines=self.show_grid.get(), canvases=canvases)
def parse_metadata_canvas(self, metadata: Dict[str, Any]) -> None: def parse_metadata_canvas(self, metadata: dict[str, Any]) -> None:
# canvas setting # canvas setting
canvas_config = metadata.get("canvas") canvas_config = metadata.get("canvas")
logger.debug("canvas metadata: %s", canvas_config) logger.debug("canvas metadata: %s", canvas_config)
@ -303,7 +304,7 @@ class CanvasManager:
canvas = self.get(canvas_id) canvas = self.get(canvas_id)
canvas.parse_metadata(canvas_config) canvas.parse_metadata(canvas_config)
def parse_metadata_shapes(self, metadata: Dict[str, Any]) -> None: def parse_metadata_shapes(self, metadata: dict[str, Any]) -> None:
# load saved shapes # load saved shapes
shapes_config = metadata.get("shapes") shapes_config = metadata.get("shapes")
if not shapes_config: if not shapes_config:
@ -313,7 +314,7 @@ class CanvasManager:
logger.debug("loading shape: %s", shape_config) logger.debug("loading shape: %s", shape_config)
Shape.from_metadata(self.app, shape_config) Shape.from_metadata(self.app, shape_config)
def parse_metadata_edges(self, metadata: Dict[str, Any]) -> None: def parse_metadata_edges(self, metadata: dict[str, Any]) -> None:
# load edges config # load edges config
edges_config = metadata.get("edges") edges_config = metadata.get("edges")
if not edges_config: if not edges_config:
@ -330,7 +331,7 @@ class CanvasManager:
else: else:
logger.warning("invalid edge token to configure: %s", edge_token) logger.warning("invalid edge token to configure: %s", edge_token)
def parse_metadata_hidden(self, metadata: Dict[str, Any]) -> None: def parse_metadata_hidden(self, metadata: dict[str, Any]) -> None:
# read hidden nodes # read hidden nodes
hidden_config = metadata.get("hidden") hidden_config = metadata.get("hidden")
if not hidden_config: if not hidden_config:

View file

@ -2,7 +2,7 @@ import functools
import logging import logging
import tkinter as tk import tkinter as tk
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Optional
import grpc import grpc
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
@ -62,17 +62,17 @@ class CanvasNode:
state=self.app.manager.show_node_labels.state(), state=self.app.manager.show_node_labels.state(),
) )
self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas) self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas)
self.edges: Set[CanvasEdge] = set() self.edges: set[CanvasEdge] = set()
self.ifaces: Dict[int, Interface] = {} self.ifaces: dict[int, Interface] = {}
self.wireless_edges: Set[CanvasWirelessEdge] = set() self.wireless_edges: set[CanvasWirelessEdge] = set()
self.antennas: List[int] = [] self.antennas: list[int] = []
self.antenna_images: Dict[int, PhotoImage] = {} self.antenna_images: dict[int, PhotoImage] = {}
self.hidden: bool = False self.hidden: bool = False
self.setup_bindings() self.setup_bindings()
self.context: tk.Menu = tk.Menu(self.canvas) self.context: tk.Menu = tk.Menu(self.canvas)
themes.style_menu(self.context) themes.style_menu(self.context)
def position(self) -> Tuple[int, int]: def position(self) -> tuple[int, int]:
return self.canvas.coords(self.id) return self.canvas.coords(self.id)
def next_iface_id(self) -> int: def next_iface_id(self) -> int:
@ -543,7 +543,7 @@ class ShadowNode:
self.canvas.shadow_nodes[self.id] = self self.canvas.shadow_nodes[self.id] = self
self.canvas.shadow_core_nodes[self.node.core_node.id] = self self.canvas.shadow_core_nodes[self.node.core_node.id] = self
def position(self) -> Tuple[int, int]: def position(self) -> tuple[int, int]:
return self.canvas.coords(self.id) return self.canvas.coords(self.id)
def should_delete(self) -> bool: def should_delete(self) -> bool:

View file

@ -1,5 +1,5 @@
import logging import logging
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from typing import TYPE_CHECKING, Any, Optional, Union
from core.gui.dialogs.shapemod import ShapeDialog from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags from core.gui.graph import tags
@ -72,7 +72,7 @@ class Shape:
self.draw() self.draw()
@classmethod @classmethod
def from_metadata(cls, app: "Application", config: Dict[str, Any]) -> None: def from_metadata(cls, app: "Application", config: dict[str, Any]) -> None:
shape_type = config["type"] shape_type = config["type"]
try: try:
shape_type = ShapeType(shape_type) shape_type = ShapeType(shape_type)
@ -144,7 +144,7 @@ class Shape:
logger.error("unknown shape type: %s", self.shape_type) logger.error("unknown shape type: %s", self.shape_type)
self.created = True self.created = True
def get_font(self) -> List[Union[int, str]]: def get_font(self) -> list[Union[int, str]]:
font = [self.shape_data.font, self.shape_data.font_size] font = [self.shape_data.font, self.shape_data.font_size]
if self.shape_data.bold: if self.shape_data.bold:
font.append("bold") font.append("bold")
@ -198,7 +198,7 @@ class Shape:
self.canvas.delete(self.id) self.canvas.delete(self.id)
self.canvas.delete(self.text_id) self.canvas.delete(self.text_id)
def metadata(self) -> Dict[str, Union[str, int, bool]]: def metadata(self) -> dict[str, Union[str, int, bool]]:
coords = self.canvas.coords(self.id) coords = self.canvas.coords(self.id)
# update coords to actual positions # update coords to actual positions
if len(coords) == 4: if len(coords) == 4:

View file

@ -1,5 +1,4 @@
import enum import enum
from typing import Set
class ShapeType(enum.Enum): class ShapeType(enum.Enum):
@ -9,7 +8,7 @@ class ShapeType(enum.Enum):
TEXT = "text" TEXT = "text"
SHAPES: Set[ShapeType] = {ShapeType.OVAL, ShapeType.RECTANGLE} SHAPES: set[ShapeType] = {ShapeType.OVAL, ShapeType.RECTANGLE}
def is_draw_shape(shape_type: ShapeType) -> bool: def is_draw_shape(shape_type: ShapeType) -> bool:

View file

@ -1,5 +1,3 @@
from typing import List
ANNOTATION: str = "annotation" ANNOTATION: str = "annotation"
GRIDLINE: str = "gridline" GRIDLINE: str = "gridline"
SHAPE: str = "shape" SHAPE: str = "shape"
@ -15,7 +13,7 @@ WALLPAPER: str = "wallpaper"
SELECTION: str = "selectednodes" SELECTION: str = "selectednodes"
MARKER: str = "marker" MARKER: str = "marker"
HIDDEN: str = "hidden" HIDDEN: str = "hidden"
ORGANIZE_TAGS: List[str] = [ ORGANIZE_TAGS: list[str] = [
WALLPAPER, WALLPAPER,
GRIDLINE, GRIDLINE,
SHAPE, SHAPE,
@ -29,7 +27,7 @@ ORGANIZE_TAGS: List[str] = [
SELECTION, SELECTION,
MARKER, MARKER,
] ]
RESET_TAGS: List[str] = [ RESET_TAGS: list[str] = [
EDGE, EDGE,
NODE, NODE,
NODE_LABEL, NODE_LABEL,

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Optional, Tuple from typing import TYPE_CHECKING, Optional
from core.gui.themes import Styles from core.gui.themes import Styles
@ -27,9 +27,9 @@ class CanvasTooltip:
self, self,
canvas: "CanvasGraph", canvas: "CanvasGraph",
*, *,
pad: Tuple[int, int, int, int] = (5, 3, 5, 3), pad: tuple[int, int, int, int] = (5, 3, 5, 3),
waittime: int = 400, waittime: int = 400,
wraplength: int = 600 wraplength: int = 600,
) -> None: ) -> None:
# in miliseconds, originally 500 # in miliseconds, originally 500
self.waittime: int = waittime self.waittime: int = waittime
@ -37,7 +37,7 @@ class CanvasTooltip:
self.wraplength: int = wraplength self.wraplength: int = wraplength
self.canvas: "CanvasGraph" = canvas self.canvas: "CanvasGraph" = canvas
self.text: tk.StringVar = tk.StringVar() self.text: tk.StringVar = tk.StringVar()
self.pad: Tuple[int, int, int, int] = pad self.pad: tuple[int, int, int, int] = pad
self.id: Optional[str] = None self.id: Optional[str] = None
self.tw: Optional[tk.Toplevel] = None self.tw: Optional[tk.Toplevel] = None
@ -63,8 +63,8 @@ class CanvasTooltip:
canvas: "CanvasGraph", canvas: "CanvasGraph",
label: ttk.Label, label: ttk.Label,
*, *,
tip_delta: Tuple[int, int] = (10, 5), tip_delta: tuple[int, int] = (10, 5),
pad: Tuple[int, int, int, int] = (5, 3, 5, 3) pad: tuple[int, int, int, int] = (5, 3, 5, 3),
): ):
c = canvas c = canvas
s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight() s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight()
@ -112,7 +112,7 @@ class CanvasTooltip:
) )
label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW)
x, y = tip_pos_calculator(canvas, label, pad=pad) x, y = tip_pos_calculator(canvas, label, pad=pad)
self.tw.wm_geometry("+%d+%d" % (x, y)) self.tw.wm_geometry(f"+{x:d}+{y:d}")
def hide(self) -> None: def hide(self) -> None:
if self.tw: if self.tw:

View file

@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from typing import Dict, Optional, Tuple from typing import Optional
from PIL import Image from PIL import Image
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
@ -12,7 +12,7 @@ ANTENNA_SIZE: int = 32
BUTTON_SIZE: int = 16 BUTTON_SIZE: int = 16
ERROR_SIZE: int = 24 ERROR_SIZE: int = 24
DIALOG_SIZE: int = 16 DIALOG_SIZE: int = 16
IMAGES: Dict[str, str] = {} IMAGES: dict[str, str] = {}
def load_all() -> None: def load_all() -> None:
@ -78,6 +78,7 @@ class ImageEnum(Enum):
EDITDELETE = "edit-delete" EDITDELETE = "edit-delete"
ANTENNA = "antenna" ANTENNA = "antenna"
DOCKER = "docker" DOCKER = "docker"
PODMAN = "podman"
LXC = "lxc" LXC = "lxc"
ALERT = "alert" ALERT = "alert"
DELETE = "delete" DELETE = "delete"
@ -87,7 +88,7 @@ class ImageEnum(Enum):
SHADOW = "shadow" SHADOW = "shadow"
TYPE_MAP: Dict[Tuple[NodeType, str], ImageEnum] = { TYPE_MAP: dict[tuple[NodeType, str], ImageEnum] = {
(NodeType.DEFAULT, "router"): ImageEnum.ROUTER, (NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
(NodeType.DEFAULT, "PC"): ImageEnum.PC, (NodeType.DEFAULT, "PC"): ImageEnum.PC,
(NodeType.DEFAULT, "host"): ImageEnum.HOST, (NodeType.DEFAULT, "host"): ImageEnum.HOST,
@ -101,6 +102,7 @@ TYPE_MAP: Dict[Tuple[NodeType, str], ImageEnum] = {
(NodeType.RJ45, None): ImageEnum.RJ45, (NodeType.RJ45, None): ImageEnum.RJ45,
(NodeType.TUNNEL, None): ImageEnum.TUNNEL, (NodeType.TUNNEL, None): ImageEnum.TUNNEL,
(NodeType.DOCKER, None): ImageEnum.DOCKER, (NodeType.DOCKER, None): ImageEnum.DOCKER,
(NodeType.PODMAN, None): ImageEnum.PODMAN,
(NodeType.LXC, None): ImageEnum.LXC, (NodeType.LXC, None): ImageEnum.LXC,
} }

View file

@ -1,5 +1,5 @@
import logging import logging
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Any, Optional
import netaddr import netaddr
from netaddr import EUI, IPNetwork from netaddr import EUI, IPNetwork
@ -43,7 +43,7 @@ class Subnets:
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self.key()) return hash(self.key())
def key(self) -> Tuple[IPNetwork, IPNetwork]: def key(self) -> tuple[IPNetwork, IPNetwork]:
return self.ip4, self.ip6 return self.ip4, self.ip6
def next(self) -> "Subnets": def next(self) -> "Subnets":
@ -61,8 +61,8 @@ class InterfaceManager:
self.mac: EUI = EUI(mac, dialect=netaddr.mac_unix_expanded) self.mac: EUI = EUI(mac, dialect=netaddr.mac_unix_expanded)
self.current_mac: Optional[EUI] = None self.current_mac: Optional[EUI] = None
self.current_subnets: Optional[Subnets] = None self.current_subnets: Optional[Subnets] = None
self.used_subnets: Dict[Tuple[IPNetwork, IPNetwork], Subnets] = {} self.used_subnets: dict[tuple[IPNetwork, IPNetwork], Subnets] = {}
self.used_macs: Set[str] = set() self.used_macs: set[str] = set()
def update_ips(self, ip4: str, ip6: str) -> None: def update_ips(self, ip4: str, ip6: str) -> None:
self.reset() self.reset()
@ -91,7 +91,7 @@ class InterfaceManager:
self.current_subnets = None self.current_subnets = None
self.used_subnets.clear() self.used_subnets.clear()
def removed(self, links: List[Link]) -> None: def removed(self, links: list[Link]) -> None:
# get remaining subnets # get remaining subnets
remaining_subnets = set() remaining_subnets = set()
for edge in self.app.core.links.values(): for edge in self.app.core.links.values():
@ -121,7 +121,7 @@ class InterfaceManager:
subnets.used_indexes.discard(index) subnets.used_indexes.discard(index)
self.current_subnets = None self.current_subnets = None
def set_macs(self, links: List[Link]) -> None: def set_macs(self, links: list[Link]) -> None:
self.current_mac = self.mac self.current_mac = self.mac
self.used_macs.clear() self.used_macs.clear()
for link in links: for link in links:
@ -130,7 +130,7 @@ class InterfaceManager:
if link.iface2: if link.iface2:
self.used_macs.add(link.iface2.mac) self.used_macs.add(link.iface2.mac)
def joined(self, links: List[Link]) -> None: def joined(self, links: list[Link]) -> None:
ifaces = [] ifaces = []
for link in links: for link in links:
if link.iface1: if link.iface1:
@ -208,7 +208,7 @@ class InterfaceManager:
logger.info("ignoring subnet change for link between network nodes") logger.info("ignoring subnet change for link between network nodes")
def find_subnets( def find_subnets(
self, canvas_node: CanvasNode, visited: Set[int] = None self, canvas_node: CanvasNode, visited: set[int] = None
) -> Optional[IPNetwork]: ) -> Optional[IPNetwork]:
logger.info("finding subnet for node: %s", canvas_node.core_node.name) logger.info("finding subnet for node: %s", canvas_node.core_node.name)
subnets = None subnets = None

View file

@ -1,5 +1,5 @@
import logging import logging
from typing import TYPE_CHECKING, List, Optional, Set from typing import TYPE_CHECKING, Optional
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
@ -13,22 +13,27 @@ logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
NODES: List["NodeDraw"] = [] NODES: list["NodeDraw"] = []
NETWORK_NODES: List["NodeDraw"] = [] NETWORK_NODES: list["NodeDraw"] = []
NODE_ICONS = {} NODE_ICONS = {}
CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} CONTAINER_NODES: set[NodeType] = {
IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC} NodeType.DEFAULT,
WIRELESS_NODES: Set[NodeType] = { NodeType.DOCKER,
NodeType.LXC,
NodeType.PODMAN,
}
IMAGE_NODES: set[NodeType] = {NodeType.DOCKER, NodeType.LXC, NodeType.PODMAN}
WIRELESS_NODES: set[NodeType] = {
NodeType.WIRELESS_LAN, NodeType.WIRELESS_LAN,
NodeType.EMANE, NodeType.EMANE,
NodeType.WIRELESS, NodeType.WIRELESS,
} }
RJ45_NODES: Set[NodeType] = {NodeType.RJ45} RJ45_NODES: set[NodeType] = {NodeType.RJ45}
BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH} BRIDGE_NODES: set[NodeType] = {NodeType.HUB, NodeType.SWITCH}
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET} IGNORE_NODES: set[NodeType] = {NodeType.CONTROL_NET}
MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} MOBILITY_NODES: set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
NODE_MODELS: Set[str] = {"router", "PC", "mdr", "prouter"} NODE_MODELS: set[str] = {"router", "PC", "mdr", "prouter"}
ROUTER_NODES: Set[str] = {"router", "mdr"} ROUTER_NODES: set[str] = {"router", "mdr"}
ANTENNA_ICON: Optional[PhotoImage] = None ANTENNA_ICON: Optional[PhotoImage] = None
@ -41,6 +46,7 @@ def setup() -> None:
(ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"), (ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"),
(ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None), (ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None),
(ImageEnum.LXC, NodeType.LXC, "LXC", None), (ImageEnum.LXC, NodeType.LXC, "LXC", None),
(ImageEnum.PODMAN, NodeType.PODMAN, "Podman", None),
] ]
for image_enum, node_type, label, model in nodes: for image_enum, node_type, label, model in nodes:
node_draw = NodeDraw.from_setup(image_enum, node_type, label, model) node_draw = NodeDraw.from_setup(image_enum, node_type, label, model)
@ -106,7 +112,7 @@ def is_iface_node(node: Node) -> bool:
return is_container(node) or is_bridge(node) return is_container(node) or is_bridge(node)
def get_custom_services(gui_config: GuiConfig, name: str) -> List[str]: def get_custom_services(gui_config: GuiConfig, name: str) -> list[str]:
for custom_node in gui_config.nodes: for custom_node in gui_config.nodes:
if custom_node.name == name: if custom_node.name == name:
return custom_node.services return custom_node.services
@ -154,7 +160,7 @@ class NodeDraw:
self.image_file: Optional[str] = None self.image_file: Optional[str] = None
self.node_type: Optional[NodeType] = None self.node_type: Optional[NodeType] = None
self.model: Optional[str] = None self.model: Optional[str] = None
self.services: Set[str] = set() self.services: set[str] = set()
self.label: Optional[str] = None self.label: Optional[str] = None
@classmethod @classmethod

View file

@ -1,13 +1,13 @@
import tkinter as tk import tkinter as tk
from functools import partial from functools import partial
from typing import TYPE_CHECKING, Dict from typing import TYPE_CHECKING
from core.gui.dialogs.observers import ObserverDialog from core.gui.dialogs.observers import ObserverDialog
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
OBSERVERS: Dict[str, str] = { OBSERVERS: dict[str, str] = {
"List Processes": "ps", "List Processes": "ps",
"Show Interfaces": "ip address", "Show Interfaces": "ip address",
"IPV4 Routes": "ip -4 route", "IPV4 Routes": "ip -4 route",

View file

@ -3,7 +3,7 @@ status bar
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, Optional
from core.api.grpc.wrappers import ExceptionEvent, ExceptionLevel from core.api.grpc.wrappers import ExceptionEvent, ExceptionLevel
from core.gui.dialogs.alerts import AlertsDialog from core.gui.dialogs.alerts import AlertsDialog
@ -24,7 +24,7 @@ class StatusBar(ttk.Frame):
self.alerts_button: Optional[ttk.Button] = None self.alerts_button: Optional[ttk.Button] = None
self.alert_style = Styles.no_alert self.alert_style = Styles.no_alert
self.running: bool = False self.running: bool = False
self.core_alarms: List[ExceptionEvent] = [] self.core_alarms: list[ExceptionEvent] = []
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:

View file

@ -2,7 +2,7 @@ import logging
import threading import threading
import time import time
import tkinter as tk import tkinter as tk
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple from typing import TYPE_CHECKING, Any, Callable, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,7 +17,7 @@ class ProgressTask:
title: str, title: str,
task: Callable, task: Callable,
callback: Callable = None, callback: Callable = None,
args: Tuple[Any] = None, args: tuple[Any] = None,
): ):
self.app: "Application" = app self.app: "Application" = app
self.title: str = title self.title: str = title
@ -25,7 +25,7 @@ class ProgressTask:
self.callback: Callable = callback self.callback: Callable = callback
if args is None: if args is None:
args = () args = ()
self.args: Tuple[Any] = args self.args: tuple[Any] = args
self.time: Optional[float] = None self.time: Optional[float] = None
def start(self) -> None: def start(self) -> None:

View file

@ -1,10 +1,9 @@
import tkinter as tk import tkinter as tk
from tkinter import font, ttk from tkinter import font, ttk
from typing import Dict, Tuple
THEME_DARK: str = "black" THEME_DARK: str = "black"
PADX: Tuple[int, int] = (0, 5) PADX: tuple[int, int] = (0, 5)
PADY: Tuple[int, int] = (0, 5) PADY: tuple[int, int] = (0, 5)
FRAME_PAD: int = 5 FRAME_PAD: int = 5
DIALOG_PAD: int = 5 DIALOG_PAD: int = 5
@ -201,7 +200,7 @@ def theme_change(event: tk.Event) -> None:
_alert_style(style, Styles.red_alert, "red") _alert_style(style, Styles.red_alert, "red")
def scale_fonts(fonts_size: Dict[str, int], scale: float) -> None: def scale_fonts(fonts_size: dict[str, int], scale: float) -> None:
for name in font.names(): for name in font.names():
f = font.nametofont(name) f = font.nametofont(name)
if name in fonts_size: if name in fonts_size:

View file

@ -3,7 +3,7 @@ import tkinter as tk
from enum import Enum from enum import Enum
from functools import partial from functools import partial
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Callable, List, Optional from typing import TYPE_CHECKING, Callable, Optional
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
@ -90,7 +90,7 @@ class ButtonBar(ttk.Frame):
def __init__(self, master: tk.Widget, app: "Application") -> None: def __init__(self, master: tk.Widget, app: "Application") -> None:
super().__init__(master) super().__init__(master)
self.app: "Application" = app self.app: "Application" = app
self.radio_buttons: List[ttk.Button] = [] self.radio_buttons: list[ttk.Button] = []
def create_button( def create_button(
self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False
@ -303,7 +303,7 @@ class Toolbar(ttk.Frame):
) )
task.start() task.start()
def start_callback(self, result: bool, exceptions: List[str]) -> None: def start_callback(self, result: bool, exceptions: list[str]) -> None:
self.set_runtime() self.set_runtime()
self.app.core.show_mobility_players() self.app.core.show_mobility_players()
if not result and exceptions: if not result and exceptions:

View file

@ -5,7 +5,7 @@ from typing import Optional
from core.gui.themes import Styles from core.gui.themes import Styles
class Tooltip(object): class Tooltip:
""" """
Create tool tip for a given widget Create tool tip for a given widget
""" """
@ -42,7 +42,7 @@ class Tooltip(object):
y += self.widget.winfo_rooty() + 32 y += self.widget.winfo_rooty() + 32
self.tw = tk.Toplevel(self.widget) self.tw = tk.Toplevel(self.widget)
self.tw.wm_overrideredirect(True) self.tw.wm_overrideredirect(True)
self.tw.wm_geometry("+%d+%d" % (x, y)) self.tw.wm_geometry(f"+{x:d}+{y:d}")
self.tw.rowconfigure(0, weight=1) self.tw.rowconfigure(0, weight=1)
self.tw.columnconfigure(0, weight=1) self.tw.columnconfigure(0, weight=1)
frame = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3) frame = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3)

View file

@ -3,8 +3,9 @@ input validation
""" """
import re import re
import tkinter as tk import tkinter as tk
from re import Pattern
from tkinter import ttk from tkinter import ttk
from typing import Any, Optional, Pattern from typing import Any, Optional
SMALLEST_SCALE: float = 0.5 SMALLEST_SCALE: float = 0.5
LARGEST_SCALE: float = 5.0 LARGEST_SCALE: float = 5.0

View file

@ -3,7 +3,7 @@ import tkinter as tk
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
from tkinter import filedialog, font, ttk from tkinter import filedialog, font, ttk
from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type from typing import TYPE_CHECKING, Any, Callable
from core.api.grpc.wrappers import ConfigOption, ConfigOptionType from core.api.grpc.wrappers import ConfigOption, ConfigOptionType
from core.gui import appconfig, themes, validation from core.gui import appconfig, themes, validation
@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
INT_TYPES: Set[ConfigOptionType] = { INT_TYPES: set[ConfigOptionType] = {
ConfigOptionType.UINT8, ConfigOptionType.UINT8,
ConfigOptionType.UINT16, ConfigOptionType.UINT16,
ConfigOptionType.UINT32, ConfigOptionType.UINT32,
@ -40,7 +40,7 @@ class FrameScroll(ttk.Frame):
self, self,
master: tk.Widget, master: tk.Widget,
app: "Application", app: "Application",
_cls: Type[ttk.Frame] = ttk.Frame, _cls: type[ttk.Frame] = ttk.Frame,
**kw: Any **kw: Any
) -> None: ) -> None:
super().__init__(master, **kw) super().__init__(master, **kw)
@ -86,14 +86,14 @@ class ConfigFrame(ttk.Notebook):
self, self,
master: tk.Widget, master: tk.Widget,
app: "Application", app: "Application",
config: Dict[str, ConfigOption], config: dict[str, ConfigOption],
enabled: bool = True, enabled: bool = True,
**kw: Any **kw: Any
) -> None: ) -> None:
super().__init__(master, **kw) super().__init__(master, **kw)
self.app: "Application" = app self.app: "Application" = app
self.config: Dict[str, ConfigOption] = config self.config: dict[str, ConfigOption] = config
self.values: Dict[str, tk.StringVar] = {} self.values: dict[str, tk.StringVar] = {}
self.enabled: bool = enabled self.enabled: bool = enabled
def draw_config(self) -> None: def draw_config(self) -> None:
@ -166,7 +166,7 @@ class ConfigFrame(ttk.Notebook):
logger.error("unhandled config option type: %s", option.type) logger.error("unhandled config option type: %s", option.type)
self.values[option.name] = value self.values[option.name] = value
def parse_config(self) -> Dict[str, str]: def parse_config(self) -> dict[str, str]:
for key in self.config: for key in self.config:
option = self.config[key] option = self.config[key]
value = self.values[key] value = self.values[key]
@ -180,7 +180,7 @@ class ConfigFrame(ttk.Notebook):
option.value = config_value option.value = config_value
return {x: self.config[x].value for x in self.config} return {x: self.config[x].value for x in self.config}
def set_values(self, config: Dict[str, str]) -> None: def set_values(self, config: dict[str, str]) -> None:
for name, data in config.items(): for name, data in config.items():
option = self.config[name] option = self.config[name]
value = self.values[name] value = self.values[name]

View file

@ -6,7 +6,7 @@ import heapq
import threading import threading
import time import time
from functools import total_ordering from functools import total_ordering
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Optional
class Timer(threading.Thread): class Timer(threading.Thread):
@ -19,8 +19,8 @@ class Timer(threading.Thread):
self, self,
interval: float, interval: float,
func: Callable[..., None], func: Callable[..., None],
args: Tuple[Any] = None, args: tuple[Any] = None,
kwargs: Dict[Any, Any] = None, kwargs: dict[Any, Any] = None,
) -> None: ) -> None:
""" """
Create a Timer instance. Create a Timer instance.
@ -38,11 +38,11 @@ class Timer(threading.Thread):
# validate arguments were provided # validate arguments were provided
if args is None: if args is None:
args = () args = ()
self.args: Tuple[Any] = args self.args: tuple[Any] = args
# validate keyword arguments were provided # validate keyword arguments were provided
if kwargs is None: if kwargs is None:
kwargs = {} kwargs = {}
self.kwargs: Dict[Any, Any] = kwargs self.kwargs: dict[Any, Any] = kwargs
def cancel(self) -> bool: def cancel(self) -> bool:
""" """
@ -96,8 +96,8 @@ class Event:
self.eventnum: int = eventnum self.eventnum: int = eventnum
self.time: float = event_time self.time: float = event_time
self.func: Callable[..., None] = func self.func: Callable[..., None] = func
self.args: Tuple[Any] = args self.args: tuple[Any] = args
self.kwds: Dict[Any, Any] = kwds self.kwds: dict[Any, Any] = kwds
self.canceled: bool = False self.canceled: bool = False
def __lt__(self, other: "Event") -> bool: def __lt__(self, other: "Event") -> bool:
@ -135,7 +135,7 @@ class EventLoop:
Creates a EventLoop instance. Creates a EventLoop instance.
""" """
self.lock: threading.RLock = threading.RLock() self.lock: threading.RLock = threading.RLock()
self.queue: List[Event] = [] self.queue: list[Event] = []
self.eventnum: int = 0 self.eventnum: int = 0
self.timer: Optional[Timer] = None self.timer: Optional[Timer] = None
self.running: bool = False self.running: bool = False

View file

@ -3,7 +3,6 @@ Provides conversions from x,y,z to lon,lat,alt.
""" """
import logging import logging
from typing import Tuple
import pyproj import pyproj
from pyproj import Transformer from pyproj import Transformer
@ -35,9 +34,9 @@ class GeoLocation:
self.to_geo: Transformer = pyproj.Transformer.from_crs( self.to_geo: Transformer = pyproj.Transformer.from_crs(
CRS_PROJ, CRS_WGS84, always_xy=True CRS_PROJ, CRS_WGS84, always_xy=True
) )
self.refproj: Tuple[float, float, float] = (0.0, 0.0, 0.0) self.refproj: tuple[float, float, float] = (0.0, 0.0, 0.0)
self.refgeo: Tuple[float, float, float] = (0.0, 0.0, 0.0) self.refgeo: tuple[float, float, float] = (0.0, 0.0, 0.0)
self.refxyz: Tuple[float, float, float] = (0.0, 0.0, 0.0) self.refxyz: tuple[float, float, float] = (0.0, 0.0, 0.0)
self.refscale: float = 1.0 self.refscale: float = 1.0
def setrefgeo(self, lat: float, lon: float, alt: float) -> None: def setrefgeo(self, lat: float, lon: float, alt: float) -> None:
@ -84,7 +83,7 @@ class GeoLocation:
return 0.0 return 0.0
return SCALE_FACTOR * (value / self.refscale) return SCALE_FACTOR * (value / self.refscale)
def getxyz(self, lat: float, lon: float, alt: float) -> Tuple[float, float, float]: def getxyz(self, lat: float, lon: float, alt: float) -> tuple[float, float, float]:
""" """
Convert provided lon,lat,alt to x,y,z. Convert provided lon,lat,alt to x,y,z.
@ -104,7 +103,7 @@ class GeoLocation:
logger.debug("result x,y,z(%s, %s, %s)", x, y, z) logger.debug("result x,y,z(%s, %s, %s)", x, y, z)
return x, y, z return x, y, z
def getgeo(self, x: float, y: float, z: float) -> Tuple[float, float, float]: def getgeo(self, x: float, y: float, z: float) -> tuple[float, float, float]:
""" """
Convert provided x,y,z to lon,lat,alt. Convert provided x,y,z to lon,lat,alt.

View file

@ -9,7 +9,7 @@ import threading
import time import time
from functools import total_ordering from functools import total_ordering
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union from typing import TYPE_CHECKING, Callable, Optional, Union
from core import utils from core import utils
from core.config import ( from core.config import (
@ -47,7 +47,7 @@ def get_mobility_node(session: "Session", node_id: int) -> Union[WlanNode, Emane
return session.get_node(node_id, EmaneNet) return session.get_node(node_id, EmaneNet)
def get_config_int(current: int, config: Dict[str, str], name: str) -> Optional[int]: def get_config_int(current: int, config: dict[str, str], name: str) -> Optional[int]:
""" """
Convenience function to get config values as int. Convenience function to get config values as int.
@ -63,7 +63,7 @@ def get_config_int(current: int, config: Dict[str, str], name: str) -> Optional[
def get_config_float( def get_config_float(
current: Union[int, float], config: Dict[str, str], name: str current: Union[int, float], config: dict[str, str], name: str
) -> Optional[float]: ) -> Optional[float]:
""" """
Convenience function to get config values as float. Convenience function to get config values as float.
@ -112,7 +112,7 @@ class MobilityManager(ModelManager):
""" """
self.config_reset() self.config_reset()
def startup(self, node_ids: List[int] = None) -> None: def startup(self, node_ids: list[int] = None) -> None:
""" """
Session is transitioning from instantiation to runtime state. Session is transitioning from instantiation to runtime state.
Instantiate any mobility models that have been configured for a WLAN. Instantiate any mobility models that have been configured for a WLAN.
@ -237,7 +237,7 @@ class WirelessModel(ConfigurableOptions):
self.session: "Session" = session self.session: "Session" = session
self.id: int = _id self.id: int = _id
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]:
""" """
May be used if the model can populate the GUI with wireless (green) May be used if the model can populate the GUI with wireless (green)
link lines. link lines.
@ -247,7 +247,7 @@ class WirelessModel(ConfigurableOptions):
""" """
return [] return []
def update(self, moved_ifaces: List[CoreInterface]) -> None: def update(self, moved_ifaces: list[CoreInterface]) -> None:
""" """
Update this wireless model. Update this wireless model.
@ -256,7 +256,7 @@ class WirelessModel(ConfigurableOptions):
""" """
raise NotImplementedError raise NotImplementedError
def update_config(self, config: Dict[str, str]) -> None: def update_config(self, config: dict[str, str]) -> None:
""" """
For run-time updates of model config. Returns True when position callback and For run-time updates of model config. Returns True when position callback and
set link parameters should be invoked. set link parameters should be invoked.
@ -275,7 +275,7 @@ class BasicRangeModel(WirelessModel):
""" """
name: str = "basic_range" name: str = "basic_range"
options: List[Configuration] = [ options: list[Configuration] = [
ConfigInt(id="range", default="275", label="wireless range (pixels)"), ConfigInt(id="range", default="275", label="wireless range (pixels)"),
ConfigInt(id="bandwidth", default="54000000", label="bandwidth (bps)"), ConfigInt(id="bandwidth", default="54000000", label="bandwidth (bps)"),
ConfigInt(id="jitter", default="0", label="transmission jitter (usec)"), ConfigInt(id="jitter", default="0", label="transmission jitter (usec)"),
@ -298,7 +298,7 @@ class BasicRangeModel(WirelessModel):
super().__init__(session, _id) super().__init__(session, _id)
self.session: "Session" = session self.session: "Session" = session
self.wlan: WlanNode = session.get_node(_id, WlanNode) self.wlan: WlanNode = session.get_node(_id, WlanNode)
self.iface_to_pos: Dict[CoreInterface, Tuple[float, float, float]] = {} self.iface_to_pos: dict[CoreInterface, tuple[float, float, float]] = {}
self.iface_lock: threading.Lock = threading.Lock() self.iface_lock: threading.Lock = threading.Lock()
self.range: int = 0 self.range: int = 0
self.bw: Optional[int] = None self.bw: Optional[int] = None
@ -323,7 +323,7 @@ class BasicRangeModel(WirelessModel):
iface.options.update(options) iface.options.update(options)
iface.set_config() iface.set_config()
def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]: def get_position(self, iface: CoreInterface) -> tuple[float, float, float]:
""" """
Retrieve network interface position. Retrieve network interface position.
@ -352,7 +352,7 @@ class BasicRangeModel(WirelessModel):
position_callback = set_position position_callback = set_position
def update(self, moved_ifaces: List[CoreInterface]) -> None: def update(self, moved_ifaces: list[CoreInterface]) -> None:
""" """
Node positions have changed without recalc. Update positions from Node positions have changed without recalc. Update positions from
node.position, then re-calculate links for those that have moved. node.position, then re-calculate links for those that have moved.
@ -412,7 +412,7 @@ class BasicRangeModel(WirelessModel):
@staticmethod @staticmethod
def calcdistance( def calcdistance(
p1: Tuple[float, float, float], p2: Tuple[float, float, float] p1: tuple[float, float, float], p2: tuple[float, float, float]
) -> float: ) -> float:
""" """
Calculate the distance between two three-dimensional points. Calculate the distance between two three-dimensional points.
@ -428,7 +428,7 @@ class BasicRangeModel(WirelessModel):
c = p1[2] - p2[2] c = p1[2] - p2[2]
return math.hypot(math.hypot(a, b), c) return math.hypot(math.hypot(a, b), c)
def update_config(self, config: Dict[str, str]) -> None: def update_config(self, config: dict[str, str]) -> None:
""" """
Configuration has changed during runtime. Configuration has changed during runtime.
@ -487,7 +487,7 @@ class BasicRangeModel(WirelessModel):
link_data = self.create_link_data(iface, iface2, message_type) link_data = self.create_link_data(iface, iface2, message_type)
self.session.broadcast_link(link_data) self.session.broadcast_link(link_data)
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]:
""" """
Return a list of wireless link messages for when the GUI reconnects. Return a list of wireless link messages for when the GUI reconnects.
@ -513,7 +513,7 @@ class WayPoint:
self, self,
_time: float, _time: float,
node_id: int, node_id: int,
coords: Tuple[float, float, Optional[float]], coords: tuple[float, float, Optional[float]],
speed: float, speed: float,
) -> None: ) -> None:
""" """
@ -526,7 +526,7 @@ class WayPoint:
""" """
self.time: float = _time self.time: float = _time
self.node_id: int = node_id self.node_id: int = node_id
self.coords: Tuple[float, float, Optional[float]] = coords self.coords: tuple[float, float, Optional[float]] = coords
self.speed: float = speed self.speed: float = speed
def __eq__(self, other: "WayPoint") -> bool: def __eq__(self, other: "WayPoint") -> bool:
@ -563,10 +563,10 @@ class WayPointMobility(WirelessModel):
""" """
super().__init__(session=session, _id=_id) super().__init__(session=session, _id=_id)
self.state: int = self.STATE_STOPPED self.state: int = self.STATE_STOPPED
self.queue: List[WayPoint] = [] self.queue: list[WayPoint] = []
self.queue_copy: List[WayPoint] = [] self.queue_copy: list[WayPoint] = []
self.points: Dict[int, WayPoint] = {} self.points: dict[int, WayPoint] = {}
self.initial: Dict[int, WayPoint] = {} self.initial: dict[int, WayPoint] = {}
self.lasttime: Optional[float] = None self.lasttime: Optional[float] = None
self.endtime: Optional[int] = None self.endtime: Optional[int] = None
self.timezero: float = 0.0 self.timezero: float = 0.0
@ -855,7 +855,7 @@ class Ns2ScriptedMobility(WayPointMobility):
""" """
name: str = "ns2script" name: str = "ns2script"
options: List[Configuration] = [ options: list[Configuration] = [
ConfigString(id="file", label="mobility script file"), ConfigString(id="file", label="mobility script file"),
ConfigInt(id="refresh_ms", default="50", label="refresh time (ms)"), ConfigInt(id="refresh_ms", default="50", label="refresh time (ms)"),
ConfigBool(id="loop", default="1", label="loop"), ConfigBool(id="loop", default="1", label="loop"),
@ -867,7 +867,7 @@ class Ns2ScriptedMobility(WayPointMobility):
] ]
@classmethod @classmethod
def config_groups(cls) -> List[ConfigGroup]: def config_groups(cls) -> list[ConfigGroup]:
return [ return [
ConfigGroup("ns-2 Mobility Script Parameters", 1, len(cls.configurations())) ConfigGroup("ns-2 Mobility Script Parameters", 1, len(cls.configurations()))
] ]
@ -882,12 +882,12 @@ class Ns2ScriptedMobility(WayPointMobility):
super().__init__(session, _id) super().__init__(session, _id)
self.file: Optional[Path] = None self.file: Optional[Path] = None
self.autostart: Optional[str] = None self.autostart: Optional[str] = None
self.nodemap: Dict[int, int] = {} self.nodemap: dict[int, int] = {}
self.script_start: Optional[str] = None self.script_start: Optional[str] = None
self.script_pause: Optional[str] = None self.script_pause: Optional[str] = None
self.script_stop: Optional[str] = None self.script_stop: Optional[str] = None
def update_config(self, config: Dict[str, str]) -> None: def update_config(self, config: dict[str, str]) -> None:
self.file = Path(config["file"]) self.file = Path(config["file"])
logger.info( logger.info(
"ns-2 scripted mobility configured for WLAN %d using file: %s", "ns-2 scripted mobility configured for WLAN %d using file: %s",
@ -916,7 +916,7 @@ class Ns2ScriptedMobility(WayPointMobility):
file_path = self.findfile(self.file) file_path = self.findfile(self.file)
try: try:
f = file_path.open("r") f = file_path.open("r")
except IOError: except OSError:
logger.exception( logger.exception(
"ns-2 scripted mobility failed to load file: %s", self.file "ns-2 scripted mobility failed to load file: %s", self.file
) )

View file

@ -9,7 +9,7 @@ import threading
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from threading import RLock from threading import RLock
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union from typing import TYPE_CHECKING, Optional, Union
import netaddr import netaddr
@ -29,10 +29,10 @@ if TYPE_CHECKING:
from core.configservice.base import ConfigService from core.configservice.base import ConfigService
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
CoreServices = List[Union[CoreService, Type[CoreService]]] CoreServices = list[Union[CoreService, type[CoreService]]]
ConfigServiceType = Type[ConfigService] ConfigServiceType = type[ConfigService]
PRIVATE_DIRS: List[Path] = [Path("/var/run"), Path("/var/log")] PRIVATE_DIRS: list[Path] = [Path("/var/run"), Path("/var/log")]
@dataclass @dataclass
@ -64,7 +64,7 @@ class Position:
self.z = z self.z = z
return True return True
def get(self) -> Tuple[float, float, float]: def get(self) -> tuple[float, float, float]:
""" """
Retrieve x,y,z position. Retrieve x,y,z position.
@ -88,7 +88,7 @@ class Position:
self.lat = lat self.lat = lat
self.alt = alt self.alt = alt
def get_geo(self) -> Tuple[float, float, float]: def get_geo(self) -> tuple[float, float, float]:
""" """
Retrieve current geo position lon, lat, alt. Retrieve current geo position lon, lat, alt.
@ -113,9 +113,9 @@ class NodeOptions:
class CoreNodeOptions(NodeOptions): class CoreNodeOptions(NodeOptions):
model: str = "PC" model: str = "PC"
"""model is used for providing a default set of services""" """model is used for providing a default set of services"""
services: List[str] = field(default_factory=list) services: list[str] = field(default_factory=list)
"""services to start within node""" """services to start within node"""
config_services: List[str] = field(default_factory=list) config_services: list[str] = field(default_factory=list)
"""config services to start within node""" """config services to start within node"""
directory: Path = None directory: Path = None
"""directory to define node, defaults to path under the session directory""" """directory to define node, defaults to path under the session directory"""
@ -152,7 +152,7 @@ class NodeBase(abc.ABC):
self.server: "DistributedServer" = server self.server: "DistributedServer" = server
self.model: Optional[str] = None self.model: Optional[str] = None
self.services: CoreServices = [] self.services: CoreServices = []
self.ifaces: Dict[int, CoreInterface] = {} self.ifaces: dict[int, CoreInterface] = {}
self.iface_id: int = 0 self.iface_id: int = 0
self.position: Position = Position() self.position: Position = Position()
self.up: bool = False self.up: bool = False
@ -201,7 +201,7 @@ class NodeBase(abc.ABC):
def host_cmd( def host_cmd(
self, self,
args: str, args: str,
env: Dict[str, str] = None, env: dict[str, str] = None,
cwd: Path = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
@ -246,7 +246,7 @@ class NodeBase(abc.ABC):
""" """
return self.position.set(x=x, y=y, z=z) return self.position.set(x=x, y=y, z=z)
def getposition(self) -> Tuple[float, float, float]: def getposition(self) -> tuple[float, float, float]:
""" """
Return an (x,y,z) tuple representing this object's position. Return an (x,y,z) tuple representing this object's position.
@ -331,7 +331,7 @@ class NodeBase(abc.ABC):
raise CoreError(f"node({self.name}) does not have interface({iface_id})") raise CoreError(f"node({self.name}) does not have interface({iface_id})")
return self.ifaces[iface_id] return self.ifaces[iface_id]
def get_ifaces(self, control: bool = True) -> List[CoreInterface]: def get_ifaces(self, control: bool = True) -> list[CoreInterface]:
""" """
Retrieve sorted list of interfaces, optionally do not include control Retrieve sorted list of interfaces, optionally do not include control
interfaces. interfaces.
@ -395,7 +395,7 @@ class CoreNodeBase(NodeBase):
will run on, default is None for localhost will run on, default is None for localhost
""" """
super().__init__(session, _id, name, server, options) super().__init__(session, _id, name, server, options)
self.config_services: Dict[str, "ConfigService"] = {} self.config_services: dict[str, "ConfigService"] = {}
self.directory: Optional[Path] = None self.directory: Optional[Path] = None
self.tmpnodedir: bool = False self.tmpnodedir: bool = False
@ -481,7 +481,7 @@ class CoreNodeBase(NodeBase):
raise CoreError(f"node({self.name}) already has service({name})") raise CoreError(f"node({self.name}) already has service({name})")
self.config_services[name] = service_class(self) self.config_services[name] = service_class(self)
def set_service_config(self, name: str, data: Dict[str, str]) -> None: def set_service_config(self, name: str, data: dict[str, str]) -> None:
""" """
Sets configuration service custom config data. Sets configuration service custom config data.
@ -506,6 +506,15 @@ class CoreNodeBase(NodeBase):
for service in startup_path: for service in startup_path:
service.start() service.start()
def stop_config_services(self) -> None:
"""
Stop all configuration services.
:return: nothing
"""
for service in self.config_services.values():
service.stop()
def makenodedir(self) -> None: def makenodedir(self) -> None:
""" """
Create the node directory. Create the node directory.
@ -574,7 +583,7 @@ class CoreNode(CoreNodeBase):
self.directory: Optional[Path] = options.directory self.directory: Optional[Path] = options.directory
self.ctrlchnlname: Path = self.session.directory / self.name self.ctrlchnlname: Path = self.session.directory / self.name
self.pid: Optional[int] = None self.pid: Optional[int] = None
self._mounts: List[Tuple[Path, Path]] = [] self._mounts: list[tuple[Path, Path]] = []
self.node_net_client: LinuxNetClient = self.create_node_net_client( self.node_net_client: LinuxNetClient = self.create_node_net_client(
self.session.use_ovs() self.session.use_ovs()
) )
@ -928,7 +937,7 @@ class CoreNetworkBase(NodeBase):
mtu = self.session.options.get_int("mtu") mtu = self.session.options.get_int("mtu")
self.mtu: int = mtu if mtu > 0 else DEFAULT_MTU self.mtu: int = mtu if mtu > 0 else DEFAULT_MTU
self.brname: Optional[str] = None self.brname: Optional[str] = None
self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {} self.linked: dict[CoreInterface, dict[CoreInterface, bool]] = {}
self.linked_lock: threading.Lock = threading.Lock() self.linked_lock: threading.Lock = threading.Lock()
def attach(self, iface: CoreInterface) -> None: def attach(self, iface: CoreInterface) -> None:

View file

@ -4,8 +4,9 @@ import shlex
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Dict, List, Tuple from typing import TYPE_CHECKING
from core import utils
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import BASH from core.executables import BASH
@ -23,9 +24,9 @@ DOCKER: str = "docker"
class DockerOptions(CoreNodeOptions): class DockerOptions(CoreNodeOptions):
image: str = "ubuntu" image: str = "ubuntu"
"""image used when creating container""" """image used when creating container"""
binds: List[Tuple[str, str]] = field(default_factory=list) binds: list[tuple[str, str]] = field(default_factory=list)
"""bind mount source and destinations to setup within container""" """bind mount source and destinations to setup within container"""
volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list) volumes: list[tuple[str, str, bool, bool]] = field(default_factory=list)
""" """
volume mount source, destination, unique, delete to setup within container volume mount source, destination, unique, delete to setup within container
@ -74,8 +75,9 @@ class DockerNode(CoreNode):
options = options or DockerOptions() options = options or DockerOptions()
super().__init__(session, _id, name, server, options) super().__init__(session, _id, name, server, options)
self.image: str = options.image self.image: str = options.image
self.binds: List[Tuple[str, str]] = options.binds self.binds: list[tuple[str, str]] = options.binds
self.volumes: Dict[str, DockerVolume] = {} self.volumes: dict[str, DockerVolume] = {}
self.env: dict[str, str] = {}
for src, dst, unique, delete in options.volumes: for src, dst, unique, delete in options.volumes:
src_name = self._unique_name(src) if unique else src src_name = self._unique_name(src) if unique else src
self.volumes[src] = DockerVolume(src_name, dst, unique, delete) self.volumes[src] = DockerVolume(src_name, dst, unique, delete)
@ -99,7 +101,24 @@ class DockerNode(CoreNode):
""" """
if shell: if shell:
args = f"{BASH} -c {shlex.quote(args)}" args = f"{BASH} -c {shlex.quote(args)}"
return f"nsenter -t {self.pid} -m -u -i -p -n {args}" return f"nsenter -t {self.pid} -m -u -i -p -n -- {args}"
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
"""
Runs a command that is used to configure and setup the network within a
node.
:param args: command to run
:param wait: True to wait for status, False otherwise
:param shell: True to use shell, False otherwise
:return: combined stdout and stderr
:raises CoreCommandError: when a non-zero exit status occurs
"""
args = self.create_cmd(args, shell)
if self.server is None:
return utils.cmd(args, wait=wait, shell=shell, env=self.env)
else:
return self.server.remote_cmd(args, wait=wait, env=self.env)
def _unique_name(self, name: str) -> str: def _unique_name(self, name: str) -> str:
""" """
@ -133,7 +152,9 @@ class DockerNode(CoreNode):
with self.lock: with self.lock:
if self.up: if self.up:
raise CoreError(f"starting node({self.name}) that is already up") raise CoreError(f"starting node({self.name}) that is already up")
# create node directory
self.makenodedir() self.makenodedir()
# setup commands for creating bind/volume mounts
binds = "" binds = ""
for src, dst in self.binds: for src, dst in self.binds:
binds += f"--mount type=bind,source={src},target={dst} " binds += f"--mount type=bind,source={src},target={dst} "
@ -142,16 +163,26 @@ class DockerNode(CoreNode):
volumes += ( volumes += (
f"--mount type=volume," f"source={volume.src},target={volume.dst} " f"--mount type=volume," f"source={volume.src},target={volume.dst} "
) )
# normalize hostname
hostname = self.name.replace("_", "-") hostname = self.name.replace("_", "-")
# create container and retrieve the created containers PID
self.host_cmd( self.host_cmd(
f"{DOCKER} run -td --init --net=none --hostname {hostname} " f"{DOCKER} run -td --init --net=none --hostname {hostname} "
f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 " f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 "
f"{binds} {volumes} " f"{binds} {volumes} "
f"--privileged {self.image} tail -f /dev/null" f"--privileged {self.image} tail -f /dev/null"
) )
# retrieve pid and process environment for use in nsenter commands
self.pid = self.host_cmd( self.pid = self.host_cmd(
f"{DOCKER} inspect -f '{{{{.State.Pid}}}}' {self.name}" f"{DOCKER} inspect -f '{{{{.State.Pid}}}}' {self.name}"
) )
output = self.host_cmd(f"cat /proc/{self.pid}/environ")
for line in output.split("\x00"):
if not line:
continue
key, value = line.split("=")
self.env[key] = value
# setup symlinks for bind and volume mounts within
for src, dst in self.binds: for src, dst in self.binds:
link_path = self.host_path(Path(dst), True) link_path = self.host_path(Path(dst), True)
self.host_cmd(f"ln -s {src} {link_path}") self.host_cmd(f"ln -s {src} {link_path}")
@ -227,7 +258,7 @@ class DockerNode(CoreNode):
""" """
logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode) logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode)
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
temp.write(contents.encode("utf-8")) temp.write(contents.encode())
temp.close() temp.close()
temp_path = Path(temp.name) temp_path = Path(temp.name)
directory = file_path.parent directory = file_path.parent

View file

@ -5,7 +5,7 @@ virtual ethernet classes that implement the interfaces available under Linux.
import logging import logging
import math import math
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Callable, Dict, List, Optional from typing import TYPE_CHECKING, Callable, Optional
import netaddr import netaddr
@ -114,8 +114,8 @@ class CoreInterface:
self.up: bool = False self.up: bool = False
self.mtu: int = mtu self.mtu: int = mtu
self.net: Optional[CoreNetworkBase] = None self.net: Optional[CoreNetworkBase] = None
self.ip4s: List[netaddr.IPNetwork] = [] self.ip4s: list[netaddr.IPNetwork] = []
self.ip6s: List[netaddr.IPNetwork] = [] self.ip6s: list[netaddr.IPNetwork] = []
self.mac: Optional[netaddr.EUI] = None self.mac: Optional[netaddr.EUI] = None
# placeholder position hook # placeholder position hook
self.poshook: Callable[[CoreInterface], None] = lambda x: None self.poshook: Callable[[CoreInterface], None] = lambda x: None
@ -133,7 +133,7 @@ class CoreInterface:
def host_cmd( def host_cmd(
self, self,
args: str, args: str,
env: Dict[str, str] = None, env: dict[str, str] = None,
cwd: Path = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
@ -235,7 +235,7 @@ class CoreInterface:
""" """
return next(iter(self.ip6s), None) return next(iter(self.ip6s), None)
def ips(self) -> List[netaddr.IPNetwork]: def ips(self) -> list[netaddr.IPNetwork]:
""" """
Retrieve a list of all ip4 and ip6 addresses combined. Retrieve a list of all ip4 and ip6 addresses combined.

View file

@ -5,7 +5,7 @@ import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Dict, List, Tuple from typing import TYPE_CHECKING
from core.emulator.data import InterfaceData, LinkOptions from core.emulator.data import InterfaceData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
@ -24,9 +24,9 @@ if TYPE_CHECKING:
class LxcOptions(CoreNodeOptions): class LxcOptions(CoreNodeOptions):
image: str = "ubuntu" image: str = "ubuntu"
"""image used when creating container""" """image used when creating container"""
binds: List[Tuple[str, str]] = field(default_factory=list) binds: list[tuple[str, str]] = field(default_factory=list)
"""bind mount source and destinations to setup within container""" """bind mount source and destinations to setup within container"""
volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list) volumes: list[tuple[str, str, bool, bool]] = field(default_factory=list)
""" """
volume mount source, destination, unique, delete to setup within container volume mount source, destination, unique, delete to setup within container
@ -74,7 +74,7 @@ class LxcNode(CoreNode):
args = f"{BASH} -c {shlex.quote(args)}" args = f"{BASH} -c {shlex.quote(args)}"
return f"nsenter -t {self.pid} -m -u -i -p -n {args}" return f"nsenter -t {self.pid} -m -u -i -p -n {args}"
def _get_info(self) -> Dict: def _get_info(self) -> dict:
args = f"lxc list {self.name} --format json" args = f"lxc list {self.name} --format json"
output = self.host_cmd(args) output = self.host_cmd(args)
data = json.loads(output) data = json.loads(output)
@ -170,7 +170,7 @@ class LxcNode(CoreNode):
""" """
logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode) logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode)
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
temp.write(contents.encode("utf-8")) temp.write(contents.encode())
temp.close() temp.close()
temp_path = Path(temp.name) temp_path = Path(temp.name)
directory = file_path.parent directory = file_path.parent

View file

@ -6,7 +6,7 @@ import logging
import threading import threading
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Type from typing import TYPE_CHECKING, Optional
import netaddr import netaddr
@ -51,7 +51,7 @@ class NftablesQueue:
# this lock protects cmds and updates lists # this lock protects cmds and updates lists
self.lock: threading.Lock = threading.Lock() self.lock: threading.Lock = threading.Lock()
# list of pending nftables commands # list of pending nftables commands
self.cmds: List[str] = [] self.cmds: list[str] = []
# list of WLANs requiring update # list of WLANs requiring update
self.updates: utils.SetQueue = utils.SetQueue() self.updates: utils.SetQueue = utils.SetQueue()
@ -226,7 +226,7 @@ class CoreNetwork(CoreNetworkBase):
def host_cmd( def host_cmd(
self, self,
args: str, args: str,
env: Dict[str, str] = None, env: dict[str, str] = None,
cwd: Path = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
@ -448,7 +448,7 @@ class GreTapBridge(CoreNetwork):
self.gretap = None self.gretap = None
super().shutdown() super().shutdown()
def add_ips(self, ips: List[str]) -> None: def add_ips(self, ips: list[str]) -> None:
""" """
Set the remote tunnel endpoint. This is a one-time method for Set the remote tunnel endpoint. This is a one-time method for
creating the GreTap device, which requires the remoteip at startup. creating the GreTap device, which requires the remoteip at startup.
@ -512,7 +512,7 @@ class CtrlNet(CoreNetwork):
policy: NetworkPolicy = NetworkPolicy.ACCEPT policy: NetworkPolicy = NetworkPolicy.ACCEPT
# base control interface index # base control interface index
CTRLIF_IDX_BASE: int = 99 CTRLIF_IDX_BASE: int = 99
DEFAULT_PREFIX_LIST: List[str] = [ DEFAULT_PREFIX_LIST: list[str] = [
"172.16.0.0/24 172.16.1.0/24 172.16.2.0/24 172.16.3.0/24 172.16.4.0/24", "172.16.0.0/24 172.16.1.0/24 172.16.2.0/24 172.16.3.0/24 172.16.4.0/24",
"172.17.0.0/24 172.17.1.0/24 172.17.2.0/24 172.17.3.0/24 172.17.4.0/24", "172.17.0.0/24 172.17.1.0/24 172.17.2.0/24 172.17.3.0/24 172.17.4.0/24",
"172.18.0.0/24 172.18.1.0/24 172.18.2.0/24 172.18.3.0/24 172.18.4.0/24", "172.18.0.0/24 172.18.1.0/24 172.18.2.0/24 172.18.3.0/24 172.18.4.0/24",
@ -734,7 +734,7 @@ class WlanNode(CoreNetwork):
iface.poshook = self.wireless_model.position_callback iface.poshook = self.wireless_model.position_callback
iface.setposition() iface.setposition()
def setmodel(self, wireless_model: Type["WirelessModel"], config: Dict[str, str]): def setmodel(self, wireless_model: type["WirelessModel"], config: dict[str, str]):
""" """
Sets the mobility and wireless model. Sets the mobility and wireless model.
@ -753,12 +753,12 @@ class WlanNode(CoreNetwork):
self.mobility = wireless_model(session=self.session, _id=self.id) self.mobility = wireless_model(session=self.session, _id=self.id)
self.mobility.update_config(config) self.mobility.update_config(config)
def update_mobility(self, config: Dict[str, str]) -> None: def update_mobility(self, config: dict[str, str]) -> None:
if not self.mobility: if not self.mobility:
raise CoreError(f"no mobility set to update for node({self.name})") raise CoreError(f"no mobility set to update for node({self.name})")
self.mobility.update_config(config) self.mobility.update_config(config)
def updatemodel(self, config: Dict[str, str]) -> None: def updatemodel(self, config: dict[str, str]) -> None:
if not self.wireless_model: if not self.wireless_model:
raise CoreError(f"no model set to update for node({self.name})") raise CoreError(f"no model set to update for node({self.name})")
logger.debug( logger.debug(
@ -768,7 +768,7 @@ class WlanNode(CoreNetwork):
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.setposition() iface.setposition()
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]:
""" """
Retrieve all link data. Retrieve all link data.

View file

@ -4,7 +4,7 @@ PhysicalNode class for including real systems in the emulated network.
import logging import logging
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Tuple from typing import TYPE_CHECKING, Optional
import netaddr import netaddr
@ -52,7 +52,7 @@ class Rj45Node(CoreNodeBase):
) )
self.iface.transport_type = TransportType.RAW self.iface.transport_type = TransportType.RAW
self.old_up: bool = False self.old_up: bool = False
self.old_addrs: List[Tuple[str, Optional[str]]] = [] self.old_addrs: list[tuple[str, Optional[str]]] = []
def startup(self) -> None: def startup(self) -> None:
""" """
@ -159,7 +159,7 @@ class Rj45Node(CoreNodeBase):
""" """
# TODO: save/restore the PROMISC flag # TODO: save/restore the PROMISC flag
self.old_up = False self.old_up = False
self.old_addrs: List[Tuple[str, Optional[str]]] = [] self.old_addrs: list[tuple[str, Optional[str]]] = []
localname = self.iface.localname localname = self.iface.localname
output = self.net_client.address_show(localname) output = self.net_client.address_show(localname)
for line in output.split("\n"): for line in output.split("\n"):
@ -171,7 +171,10 @@ class Rj45Node(CoreNodeBase):
if "UP" in flags: if "UP" in flags:
self.old_up = True self.old_up = True
elif items[0] == "inet": elif items[0] == "inet":
self.old_addrs.append((items[1], items[3])) broadcast = None
if items[2] == "brd":
broadcast = items[3]
self.old_addrs.append((items[1], broadcast))
elif items[0] == "inet6": elif items[0] == "inet6":
if items[1][:4] == "fe80": if items[1][:4] == "fe80":
continue continue

271
daemon/core/nodes/podman.py Normal file
View file

@ -0,0 +1,271 @@
import json
import logging
import shlex
from dataclasses import dataclass, field
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING
from core.emulator.distributed import DistributedServer
from core.errors import CoreCommandError, CoreError
from core.executables import BASH
from core.nodes.base import CoreNode, CoreNodeOptions
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from core.emulator.session import Session
PODMAN: str = "podman"
@dataclass
class PodmanOptions(CoreNodeOptions):
image: str = "ubuntu"
"""image used when creating container"""
binds: list[tuple[str, str]] = field(default_factory=list)
"""bind mount source and destinations to setup within container"""
volumes: list[tuple[str, str, bool, bool]] = field(default_factory=list)
"""
volume mount source, destination, unique, delete to setup within container
unique is True for node unique volume naming
delete is True for deleting volume mount during shutdown
"""
@dataclass
class VolumeMount:
src: str
"""volume mount name"""
dst: str
"""volume mount destination directory"""
unique: bool = True
"""True to create a node unique prefixed name for this volume"""
delete: bool = True
"""True to delete the volume during shutdown"""
path: str = None
"""path to the volume on the host"""
class PodmanNode(CoreNode):
"""
Provides logic for creating a Podman based node.
"""
def __init__(
self,
session: "Session",
_id: int = None,
name: str = None,
server: DistributedServer = None,
options: PodmanOptions = None,
) -> None:
"""
Create a PodmanNode instance.
:param session: core session instance
:param _id: node id
:param name: node name
:param server: remote server node
will run on, default is None for localhost
:param options: options for creating node
"""
options = options or PodmanOptions()
super().__init__(session, _id, name, server, options)
self.image: str = options.image
self.binds: list[tuple[str, str]] = options.binds
self.volumes: dict[str, VolumeMount] = {}
for src, dst, unique, delete in options.volumes:
src_name = self._unique_name(src) if unique else src
self.volumes[src] = VolumeMount(src_name, dst, unique, delete)
@classmethod
def create_options(cls) -> PodmanOptions:
"""
Return default creation options, which can be used during node creation.
:return: podman options
"""
return PodmanOptions()
def create_cmd(self, args: str, shell: bool = False) -> str:
"""
Create command used to run commands within the context of a node.
:param args: command arguments
:param shell: True to run shell like, False otherwise
:return: node command
"""
if shell:
args = f"{BASH} -c {shlex.quote(args)}"
return f"{PODMAN} exec {self.name} {args}"
def _unique_name(self, name: str) -> str:
"""
Creates a session/node unique prefixed name for the provided input.
:param name: name to make unique
:return: unique session/node prefixed name
"""
return f"{self.session.id}.{self.id}.{name}"
def alive(self) -> bool:
"""
Check if the node is alive.
:return: True if node is alive, False otherwise
"""
try:
running = self.host_cmd(
f"{PODMAN} inspect -f '{{{{.State.Running}}}}' {self.name}"
)
return json.loads(running)
except CoreCommandError:
return False
def startup(self) -> None:
"""
Create a podman container instance for the specified image.
:return: nothing
"""
with self.lock:
if self.up:
raise CoreError(f"starting node({self.name}) that is already up")
# create node directory
self.makenodedir()
# setup commands for creating bind/volume mounts
binds = ""
for src, dst in self.binds:
binds += f"--mount type=bind,source={src},target={dst} "
volumes = ""
for volume in self.volumes.values():
volumes += (
f"--mount type=volume," f"source={volume.src},target={volume.dst} "
)
# normalize hostname
hostname = self.name.replace("_", "-")
# create container and retrieve the created containers PID
self.host_cmd(
f"{PODMAN} run -td --init --net=none --hostname {hostname} "
f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 "
f"{binds} {volumes} "
f"--privileged {self.image} tail -f /dev/null"
)
# retrieve pid and process environment for use in nsenter commands
self.pid = self.host_cmd(
f"{PODMAN} inspect -f '{{{{.State.Pid}}}}' {self.name}"
)
# setup symlinks for bind and volume mounts within
for src, dst in self.binds:
link_path = self.host_path(Path(dst), True)
self.host_cmd(f"ln -s {src} {link_path}")
for volume in self.volumes.values():
volume.path = self.host_cmd(
f"{PODMAN} volume inspect -f '{{{{.Mountpoint}}}}' {volume.src}"
)
link_path = self.host_path(Path(volume.dst), True)
self.host_cmd(f"ln -s {volume.path} {link_path}")
logger.debug("node(%s) pid: %s", self.name, self.pid)
self.up = True
def shutdown(self) -> None:
"""
Shutdown logic.
:return: nothing
"""
# nothing to do if node is not up
if not self.up:
return
with self.lock:
self.ifaces.clear()
self.host_cmd(f"{PODMAN} rm -f {self.name}")
for volume in self.volumes.values():
if volume.delete:
self.host_cmd(f"{PODMAN} volume rm {volume.src}")
self.up = False
def termcmdstring(self, sh: str = "/bin/sh") -> str:
"""
Create a terminal command string.
:param sh: shell to execute command in
:return: str
"""
terminal = f"{PODMAN} exec -it {self.name} {sh}"
if self.server is None:
return terminal
else:
return f"ssh -X -f {self.server.host} xterm -e {terminal}"
def create_dir(self, dir_path: Path) -> None:
"""
Create a private directory.
:param dir_path: path to create
:return: nothing
"""
logger.debug("creating node dir: %s", dir_path)
self.cmd(f"mkdir -p {dir_path}")
def mount(self, src_path: str, target_path: str) -> None:
"""
Create and mount a directory.
:param src_path: source directory to mount
:param target_path: target directory to create
:return: nothing
:raises CoreCommandError: when a non-zero exit status occurs
"""
logger.debug("mounting source(%s) target(%s)", src_path, target_path)
raise Exception("not supported")
def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
"""
Create a node file with a given mode.
:param file_path: name of file to create
:param contents: contents of file
:param mode: mode for file
:return: nothing
"""
logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode)
temp = NamedTemporaryFile(delete=False)
temp.write(contents.encode())
temp.close()
temp_path = Path(temp.name)
directory = file_path.parent
if str(directory) != ".":
self.cmd(f"mkdir -m {0o755:o} -p {directory}")
if self.server is not None:
self.server.remote_put(temp_path, temp_path)
self.host_cmd(f"{PODMAN} cp {temp_path} {self.name}:{file_path}")
self.cmd(f"chmod {mode:o} {file_path}")
if self.server is not None:
self.host_cmd(f"rm -f {temp_path}")
temp_path.unlink()
def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
"""
Copy a file to a node, following symlinks and preserving metadata.
Change file mode if specified.
:param dst_path: file name to copy file to
:param src_path: file to copy
:param mode: mode to copy to
:return: nothing
"""
logger.info(
"node file copy file(%s) source(%s) mode(%o)", dst_path, src_path, mode or 0
)
self.cmd(f"mkdir -p {dst_path.parent}")
if self.server:
temp = NamedTemporaryFile(delete=False)
temp_path = Path(temp.name)
src_path = temp_path
self.server.remote_put(src_path, temp_path)
self.host_cmd(f"{PODMAN} cp {src_path} {self.name}:{dst_path}")
if mode is not None:
self.cmd(f"chmod {mode:o} {dst_path}")

View file

@ -7,7 +7,7 @@ import logging
import math import math
import secrets import secrets
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List, Set, Tuple from typing import TYPE_CHECKING
from core.config import ConfigBool, ConfigFloat, ConfigInt, Configuration from core.config import ConfigBool, ConfigFloat, ConfigInt, Configuration
from core.emulator.data import LinkData, LinkOptions from core.emulator.data import LinkData, LinkOptions
@ -41,7 +41,7 @@ KEY_LOSS: str = "loss"
def calc_distance( def calc_distance(
point1: Tuple[float, float, float], point2: Tuple[float, float, float] point1: tuple[float, float, float], point2: tuple[float, float, float]
) -> float: ) -> float:
a = point1[0] - point2[0] a = point1[0] - point2[0]
b = point1[1] - point2[1] b = point1[1] - point2[1]
@ -51,7 +51,7 @@ def calc_distance(
return math.hypot(math.hypot(a, b), c) return math.hypot(math.hypot(a, b), c)
def get_key(node1_id: int, node2_id: int) -> Tuple[int, int]: def get_key(node1_id: int, node2_id: int) -> tuple[int, int]:
return (node1_id, node2_id) if node1_id < node2_id else (node2_id, node1_id) return (node1_id, node2_id) if node1_id < node2_id else (node2_id, node1_id)
@ -65,7 +65,7 @@ class WirelessLink:
class WirelessNode(CoreNetworkBase): class WirelessNode(CoreNetworkBase):
options: List[Configuration] = [ options: list[Configuration] = [
ConfigBool( ConfigBool(
id=KEY_ENABLED, default="1" if CONFIG_ENABLED else "0", label="Enabled?" id=KEY_ENABLED, default="1" if CONFIG_ENABLED else "0", label="Enabled?"
), ),
@ -87,7 +87,7 @@ class WirelessNode(CoreNetworkBase):
), ),
ConfigFloat(id=KEY_LOSS, default=str(CONFIG_LOSS), label="Loss Initial"), ConfigFloat(id=KEY_LOSS, default=str(CONFIG_LOSS), label="Loss Initial"),
] ]
devices: Set[str] = set() devices: set[str] = set()
@classmethod @classmethod
def add_device(cls) -> str: def add_device(cls) -> str:
@ -111,8 +111,8 @@ class WirelessNode(CoreNetworkBase):
options: NodeOptions = None, options: NodeOptions = None,
): ):
super().__init__(session, _id, name, server, options) super().__init__(session, _id, name, server, options)
self.bridges: Dict[int, Tuple[CoreInterface, str]] = {} self.bridges: dict[int, tuple[CoreInterface, str]] = {}
self.links: Dict[Tuple[int, int], WirelessLink] = {} self.links: dict[tuple[int, int], WirelessLink] = {}
self.position_enabled: bool = CONFIG_ENABLED self.position_enabled: bool = CONFIG_ENABLED
self.bandwidth: int = CONFIG_BANDWIDTH self.bandwidth: int = CONFIG_BANDWIDTH
self.delay: int = CONFIG_DELAY self.delay: int = CONFIG_DELAY
@ -321,7 +321,7 @@ class WirelessNode(CoreNetworkBase):
def adopt_iface(self, iface: CoreInterface, name: str) -> None: def adopt_iface(self, iface: CoreInterface, name: str) -> None:
raise CoreError(f"{type(self)} does not support adopt interface") raise CoreError(f"{type(self)} does not support adopt interface")
def get_config(self) -> Dict[str, Configuration]: def get_config(self) -> dict[str, Configuration]:
config = {x.id: x for x in copy.copy(self.options)} config = {x.id: x for x in copy.copy(self.options)}
config[KEY_ENABLED].default = "1" if self.position_enabled else "0" config[KEY_ENABLED].default = "1" if self.position_enabled else "0"
config[KEY_RANGE].default = str(self.max_range) config[KEY_RANGE].default = str(self.max_range)
@ -333,7 +333,7 @@ class WirelessNode(CoreNetworkBase):
config[KEY_JITTER].default = str(self.jitter) config[KEY_JITTER].default = str(self.jitter)
return config return config
def set_config(self, config: Dict[str, str]) -> None: def set_config(self, config: dict[str, str]) -> None:
logger.info("wireless config: %s", config) logger.info("wireless config: %s", config)
self.position_enabled = config[KEY_ENABLED] == "1" self.position_enabled = config[KEY_ENABLED] == "1"
self.max_range = float(config[KEY_RANGE]) self.max_range = float(config[KEY_RANGE])

View file

@ -5,7 +5,7 @@ import logging
import sched import sched
from pathlib import Path from pathlib import Path
from threading import Thread from threading import Thread
from typing import IO, Callable, Dict, Optional from typing import IO, Callable, Optional
import grpc import grpc
@ -230,7 +230,7 @@ class CorePlayer:
self.node_streamer: Optional[MoveNodesStreamer] = None self.node_streamer: Optional[MoveNodesStreamer] = None
self.node_streamer_thread: Optional[Thread] = None self.node_streamer_thread: Optional[Thread] = None
self.scheduler: sched.scheduler = sched.scheduler() self.scheduler: sched.scheduler = sched.scheduler()
self.handlers: Dict[PlayerEvents, Callable] = { self.handlers: dict[PlayerEvents, Callable] = {
PlayerEvents.XY: self.handle_xy, PlayerEvents.XY: self.handle_xy,
PlayerEvents.GEO: self.handle_geo, PlayerEvents.GEO: self.handle_geo,
PlayerEvents.CMD: self.handle_cmd, PlayerEvents.CMD: self.handle_cmd,

View file

@ -5,7 +5,7 @@ sdt.py: Scripted Display Tool (SDT3D) helper
import logging import logging
import socket import socket
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type from typing import TYPE_CHECKING, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from core.constants import CORE_CONF_DIR from core.constants import CORE_CONF_DIR
@ -28,9 +28,9 @@ CORE_LAYER: str = "CORE"
NODE_LAYER: str = "CORE::Nodes" NODE_LAYER: str = "CORE::Nodes"
LINK_LAYER: str = "CORE::Links" LINK_LAYER: str = "CORE::Links"
WIRED_LINK_LAYER: str = f"{LINK_LAYER}::wired" WIRED_LINK_LAYER: str = f"{LINK_LAYER}::wired"
CORE_LAYERS: List[str] = [CORE_LAYER, LINK_LAYER, NODE_LAYER, WIRED_LINK_LAYER] CORE_LAYERS: list[str] = [CORE_LAYER, LINK_LAYER, NODE_LAYER, WIRED_LINK_LAYER]
DEFAULT_LINK_COLOR: str = "red" DEFAULT_LINK_COLOR: str = "red"
NODE_TYPES: Dict[Type[NodeBase], str] = { NODE_TYPES: dict[type[NodeBase], str] = {
HubNode: "hub", HubNode: "hub",
SwitchNode: "lanswitch", SwitchNode: "lanswitch",
TunnelNode: "tunnel", TunnelNode: "tunnel",
@ -63,7 +63,7 @@ class Sdt:
# default altitude (in meters) for flyto view # default altitude (in meters) for flyto view
DEFAULT_ALT: int = 2500 DEFAULT_ALT: int = 2500
# TODO: read in user"s nodes.conf here; below are default node types from the GUI # TODO: read in user"s nodes.conf here; below are default node types from the GUI
DEFAULT_SPRITES: Dict[str, str] = [ DEFAULT_SPRITES: dict[str, str] = [
("router", "router.png"), ("router", "router.png"),
("host", "host.png"), ("host", "host.png"),
("PC", "pc.png"), ("PC", "pc.png"),
@ -88,9 +88,9 @@ class Sdt:
self.sock: Optional[socket.socket] = None self.sock: Optional[socket.socket] = None
self.connected: bool = False self.connected: bool = False
self.url: str = self.DEFAULT_SDT_URL self.url: str = self.DEFAULT_SDT_URL
self.address: Optional[Tuple[Optional[str], Optional[int]]] = None self.address: Optional[tuple[Optional[str], Optional[int]]] = None
self.protocol: Optional[str] = None self.protocol: Optional[str] = None
self.network_layers: Set[str] = set() self.network_layers: set[str] = set()
self.session.node_handlers.append(self.handle_node_update) self.session.node_handlers.append(self.handle_node_update)
self.session.link_handlers.append(self.handle_link_update) self.session.link_handlers.append(self.handle_link_update)
@ -138,7 +138,7 @@ class Sdt:
else: else:
# Default to tcp # Default to tcp
self.sock = socket.create_connection(self.address, 5) self.sock = socket.create_connection(self.address, 5)
except IOError: except OSError:
logger.exception("SDT socket connect error") logger.exception("SDT socket connect error")
return False return False
@ -176,7 +176,7 @@ class Sdt:
if self.sock: if self.sock:
try: try:
self.sock.close() self.sock.close()
except IOError: except OSError:
logger.error("error closing socket") logger.error("error closing socket")
finally: finally:
self.sock = None self.sock = None
@ -212,7 +212,7 @@ class Sdt:
logger.debug("sdt cmd: %s", cmd) logger.debug("sdt cmd: %s", cmd)
self.sock.sendall(cmd) self.sock.sendall(cmd)
return True return True
except IOError: except OSError:
logger.exception("SDT connection error") logger.exception("SDT connection error")
self.sock = None self.sock = None
self.connected = False self.connected = False

View file

@ -61,7 +61,7 @@ def cleanup_sessions() -> None:
def cleanup_interfaces() -> None: def cleanup_interfaces() -> None:
print("cleaning up devices") print("cleaning up devices")
output = subprocess.check_output("ip -o -br link show", shell=True) output = subprocess.check_output("ip -br link show", shell=True)
lines = output.decode().strip().split("\n") lines = output.decode().strip().split("\n")
for line in lines: for line in lines:
values = line.split() values = line.split()
@ -73,6 +73,7 @@ def cleanup_interfaces() -> None:
or name.startswith("b.") or name.startswith("b.")
or name.startswith("ctrl") or name.startswith("ctrl")
): ):
name = name.split("@")[0]
result = subprocess.call(f"ip link delete {name}", shell=True) result = subprocess.call(f"ip link delete {name}", shell=True)
if result: if result:
print(f"failed to remove {name}") print(f"failed to remove {name}")

View file

@ -8,7 +8,7 @@ from argparse import (
) )
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional, Tuple from typing import Any, Optional
import grpc import grpc
import netaddr import netaddr
@ -30,7 +30,7 @@ from core.api.grpc.wrappers import (
NODE_TYPES = [x.name for x in NodeType if x != NodeType.PEER_TO_PEER] NODE_TYPES = [x.name for x in NodeType if x != NodeType.PEER_TO_PEER]
def protobuf_to_json(message: Any) -> Dict[str, Any]: def protobuf_to_json(message: Any) -> dict[str, Any]:
return MessageToDict( return MessageToDict(
message, including_default_value_fields=True, preserving_proto_field_name=True message, including_default_value_fields=True, preserving_proto_field_name=True
) )
@ -82,7 +82,7 @@ def ip6_type(value: str) -> IPNetwork:
raise ArgumentTypeError(f"invalid ip6 address: {value}") raise ArgumentTypeError(f"invalid ip6 address: {value}")
def position_type(value: str) -> Tuple[float, float]: def position_type(value: str) -> tuple[float, float]:
error = "invalid position, must be in the format: float,float" error = "invalid position, must be in the format: float,float"
try: try:
values = [float(x) for x in value.split(",")] values = [float(x) for x in value.split(",")]
@ -94,7 +94,7 @@ def position_type(value: str) -> Tuple[float, float]:
return x, y return x, y
def geo_type(value: str) -> Tuple[float, float, float]: def geo_type(value: str) -> tuple[float, float, float]:
error = "invalid geo, must be in the format: float,float,float" error = "invalid geo, must be in the format: float,float,float"
try: try:
values = [float(x) for x in value.split(",")] values = [float(x) for x in value.split(",")]

View file

@ -9,7 +9,6 @@ from argparse import ArgumentDefaultsHelpFormatter
from functools import cmp_to_key from functools import cmp_to_key
from queue import Queue from queue import Queue
from threading import Thread from threading import Thread
from typing import Dict, Tuple
import grpc import grpc
@ -31,7 +30,7 @@ class RouteEnum(enum.Enum):
class SdtClient: class SdtClient:
def __init__(self, address: Tuple[str, int]) -> None: def __init__(self, address: tuple[str, int]) -> None:
self.sock = socket.create_connection(address) self.sock = socket.create_connection(address)
self.links = [] self.links = []
self.send(f'layer "{ROUTE_LAYER}"') self.send(f'layer "{ROUTE_LAYER}"')
@ -85,7 +84,7 @@ class RouterMonitor:
self.sdt = SdtClient((sdt_host, sdt_port)) self.sdt = SdtClient((sdt_host, sdt_port))
self.nodes = self.get_nodes() self.nodes = self.get_nodes()
def get_nodes(self) -> Dict[int, str]: def get_nodes(self) -> dict[int, str]:
with self.core.context_connect(): with self.core.context_connect():
if self.session is None: if self.session is None:
self.session = self.get_session() self.session = self.get_session()
@ -146,7 +145,7 @@ class RouterMonitor:
self.manage_routes() self.manage_routes()
self.route_time = time.monotonic() self.route_time = time.monotonic()
def route_sort(self, x: Tuple[str, int], y: Tuple[str, int]) -> int: def route_sort(self, x: tuple[str, int], y: tuple[str, int]) -> int:
x_node = x[0] x_node = x[0]
y_node = y[0] y_node = y[0]
if x_node == self.src_id: if x_node == self.src_id:

View file

@ -1,7 +1,7 @@
""" """
bird.py: defines routing services provided by the BIRD Internet Routing Daemon. bird.py: defines routing services provided by the BIRD Internet Routing Daemon.
""" """
from typing import Optional, Tuple from typing import Optional
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
@ -14,12 +14,12 @@ class Bird(CoreService):
name: str = "bird" name: str = "bird"
group: str = "BIRD" group: str = "BIRD"
executables: Tuple[str, ...] = ("bird",) executables: tuple[str, ...] = ("bird",)
dirs: Tuple[str, ...] = ("/etc/bird",) dirs: tuple[str, ...] = ("/etc/bird",)
configs: Tuple[str, ...] = ("/etc/bird/bird.conf",) configs: tuple[str, ...] = ("/etc/bird/bird.conf",)
startup: Tuple[str, ...] = ("bird -c %s" % (configs[0]),) startup: tuple[str, ...] = (f"bird -c {configs[0]}",)
shutdown: Tuple[str, ...] = ("killall bird",) shutdown: tuple[str, ...] = ("killall bird",)
validate: Tuple[str, ...] = ("pidof bird",) validate: tuple[str, ...] = ("pidof bird",)
@classmethod @classmethod
def generate_config(cls, node: CoreNode, filename: str) -> str: def generate_config(cls, node: CoreNode, filename: str) -> str:
@ -48,33 +48,30 @@ class Bird(CoreService):
Returns configuration file text. Other services that depend on bird Returns configuration file text. Other services that depend on bird
will have hooks that are invoked here. will have hooks that are invoked here.
""" """
cfg = """\ cfg = f"""\
/* Main configuration file for BIRD. This is ony a template, /* Main configuration file for BIRD. This is ony a template,
* you will *need* to customize it according to your needs * you will *need* to customize it according to your needs
* Beware that only double quotes \'"\' are valid. No singles. */ * Beware that only double quotes \'"\' are valid. No singles. */
log "/var/log/%s.log" all; log "/var/log/{cls.name}.log" all;
#debug protocols all; #debug protocols all;
#debug commands 2; #debug commands 2;
router id %s; # Mandatory for IPv6, may be automatic for IPv4 router id {cls.router_id(node)}; # Mandatory for IPv6, may be automatic for IPv4
protocol kernel { protocol kernel {{
persist; # Don\'t remove routes on BIRD shutdown persist; # Don\'t remove routes on BIRD shutdown
scan time 200; # Scan kernel routing table every 200 seconds scan time 200; # Scan kernel routing table every 200 seconds
export all; export all;
import all; import all;
} }}
protocol device { protocol device {{
scan time 10; # Scan interfaces every 10 seconds scan time 10; # Scan interfaces every 10 seconds
} }}
""" % ( """
cls.name,
cls.router_id(node),
)
# generate protocol specific configurations # generate protocol specific configurations
for s in node.services: for s in node.services:
@ -94,8 +91,8 @@ class BirdService(CoreService):
name: Optional[str] = None name: Optional[str] = None
group: str = "BIRD" group: str = "BIRD"
executables: Tuple[str, ...] = ("bird",) executables: tuple[str, ...] = ("bird",)
dependencies: Tuple[str, ...] = ("bird",) dependencies: tuple[str, ...] = ("bird",)
meta: str = "The config file for this service can be found in the bird service." meta: str = "The config file for this service can be found in the bird service."
@classmethod @classmethod
@ -111,7 +108,7 @@ class BirdService(CoreService):
""" """
cfg = "" cfg = ""
for iface in node.get_ifaces(control=False): for iface in node.get_ifaces(control=False):
cfg += ' interface "%s";\n' % iface.name cfg += f' interface "{iface.name}";\n'
return cfg return cfg

Some files were not shown because too many files have changed in this diff Show more