commit
c37fa33ffe
223 changed files with 6865 additions and 2674 deletions
21
.github/workflows/documentation.yml
vendored
Normal file
21
.github/workflows/documentation.yml
vendored
Normal 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
3
.gitignore
vendored
|
@ -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
|
||||||
|
|
32
CHANGELOG.md
32
CHANGELOG.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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,6 +284,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
|
|
||||||
# session options
|
# session options
|
||||||
for option in request.session.options.values():
|
for option in request.session.options.values():
|
||||||
|
if option.value:
|
||||||
session.options.set(option.name, option.value)
|
session.options.set(option.name, option.value)
|
||||||
session.metadata = dict(request.session.metadata)
|
session.metadata = dict(request.session.metadata)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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]] = {}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
67
daemon/core/emulator/broadcast.py
Normal file
67
daemon/core/emulator/broadcast.py
Normal 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)
|
239
daemon/core/emulator/controlnets.py
Normal file
239
daemon/core/emulator/controlnets.py
Normal 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"
|
||||||
|
)
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
145
daemon/core/emulator/hooks.py
Normal file
145
daemon/core/emulator/hooks.py
Normal 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}"
|
||||||
|
)
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
BIN
daemon/core/gui/data/icons/podman.png
Normal file
BIN
daemon/core/gui/data/icons/podman.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
|
||||||
return
|
|
||||||
self.set_entry(red, green, blue)
|
self.set_entry(red, green, blue)
|
||||||
self.set_scale(red, green, blue)
|
self.set_scale(red, green, blue)
|
||||||
self.display.config(background=hex_code)
|
self.display.config(background=hex_code)
|
||||||
self.set_label(str(red), str(green), str(blue))
|
self.set_label(red, green, 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)
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
271
daemon/core/nodes/podman.py
Normal 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}")
|
|
@ -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])
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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(",")]
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue