Merge branch 'develop' into dependabot/pip/daemon/lxml-4.6.5

This commit is contained in:
bharnden 2022-01-11 12:32:19 -08:00 committed by GitHub
commit fdc009699e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
150 changed files with 5356 additions and 9820 deletions

101
Dockerfile Normal file
View file

@ -0,0 +1,101 @@
# syntax=docker/dockerfile:1
FROM ubuntu:20.04
LABEL Description="CORE Docker Image"
# define variables
ARG DEBIAN_FRONTEND=noninteractive
ARG PREFIX=/usr/local
ARG BRANCH=develop
ARG CORE_TARBALL=core.tar.gz
ARG OSPF_TARBALL=ospf.tar.gz
# install system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
automake \
bash \
ca-certificates \
ethtool \
gawk \
gcc \
g++ \
iproute2 \
iputils-ping \
libc-dev \
libev-dev \
libreadline-dev \
libtool \
libtk-img \
make \
nftables \
python3 \
python3-pip \
python3-tk \
pkg-config \
systemctl \
tk \
wget \
xauth \
xterm \
&& apt-get clean
# install python dependencies
RUN python3 -m pip install \
grpcio==1.27.2 \
grpcio-tools==1.27.2 \
poetry==1.1.7
# retrieve, build, and install core
RUN wget -q -O ${CORE_TARBALL} https://api.github.com/repos/coreemu/core/tarball/${BRANCH} && \
tar xf ${CORE_TARBALL} && \
cd coreemu-core* && \
./bootstrap.sh && \
./configure && \
make -j $(nproc) && \
make install && \
cd daemon && \
python3 -m poetry build -f wheel && \
python3 -m pip install dist/* && \
cp scripts/* ${PREFIX}/bin && \
mkdir /etc/core && \
cp -n data/core.conf /etc/core && \
cp -n data/logging.conf /etc/core && \
mkdir -p ${PREFIX}/share/core && \
cp -r examples ${PREFIX}/share/core && \
echo '\
[Unit]\n\
Description=Common Open Research Emulator Service\n\
After=network.target\n\
\n\
[Service]\n\
Type=simple\n\
ExecStart=/usr/local/bin/core-daemon\n\
TasksMax=infinity\n\
\n\
[Install]\n\
WantedBy=multi-user.target\
' > /lib/systemd/system/core-daemon.service && \
cd ../.. && \
rm ${CORE_TARBALL} && \
rm -rf coreemu-core*
# retrieve, build, and install ospf mdr
RUN wget -q -O ${OSPF_TARBALL} https://github.com/USNavalResearchLaboratory/ospf-mdr/tarball/master && \
tar xf ${OSPF_TARBALL} && \
cd USNavalResearchLaboratory-ospf-mdr* && \
./bootstrap.sh && \
./configure --disable-doc --enable-user=root --enable-group=root \
--with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh \
--localstatedir=/var/run/quagga && \
make -j $(nproc) && \
make install && \
cd .. && \
rm ${OSPF_TARBALL} && \
rm -rf USNavalResearchLaboratory-ospf-mdr*
# retrieve and install emane packages
RUN wget -q https://adjacentlink.com/downloads/emane/emane-1.2.7-release-1.ubuntu-20_04.amd64.tar.gz && \
tar xf emane*.tar.gz && \
cd emane-1.2.7-release-1/debs/ubuntu-20_04/amd64 && \
apt-get install -y ./emane*.deb ./python3-emane_*.deb && \
cd ../../../.. && \
rm emane-1.2.7-release-1.ubuntu-20_04.amd64.tar.gz && \
rm -rf emane-1.2.7-release-1
CMD ["systemctl", "start", "core-daemon"]
# sudo docker run -itd --name core -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix:rw --privileged core

View file

@ -57,7 +57,7 @@ fpm -s dir -t deb -n core-distributed \
-d "procps" \ -d "procps" \
-d "libc6 >= 2.14" \ -d "libc6 >= 2.14" \
-d "bash >= 3.0" \ -d "bash >= 3.0" \
-d "ebtables" \ -d "nftables" \
-d "iproute2" \ -d "iproute2" \
-d "libev4" \ -d "libev4" \
-d "openssh-server" \ -d "openssh-server" \
@ -77,7 +77,7 @@ fpm -s dir -t rpm -n core-distributed \
-d "ethtool" \ -d "ethtool" \
-d "procps-ng" \ -d "procps-ng" \
-d "bash >= 3.0" \ -d "bash >= 3.0" \
-d "ebtables" \ -d "nftables" \
-d "iproute" \ -d "iproute" \
-d "libev" \ -d "libev" \
-d "net-tools" \ -d "net-tools" \
@ -123,7 +123,7 @@ all: change-files
.PHONY: change-files .PHONY: change-files
change-files: change-files:
$(call change-files,gui/core-gui) $(call change-files,gui/core-gui-legacy)
$(call change-files,daemon/core/constants.py) $(call change-files,daemon/core/constants.py)
$(call change-files,netns/setup.py) $(call change-files,netns/setup.py)

View file

@ -2,7 +2,7 @@
CORE: Common Open Research Emulator CORE: Common Open Research Emulator
Copyright (c)2005-2020 the Boeing Company. Copyright (c)2005-2022 the Boeing Company.
See the LICENSE file included in this distribution. See the LICENSE file included in this distribution.
@ -14,6 +14,24 @@ networks to live networks. CORE consists of a GUI for drawing
topologies of lightweight virtual machines, and Python modules for topologies of lightweight virtual machines, and Python modules for
scripting network emulation. scripting network emulation.
## Quick Start
The following should get you up and running on Ubuntu 18+ and CentOS 7+
from a clean install, it will prompt you for sudo password. This would
install CORE into a python3 virtual environment and install
[OSPF MDR](https://github.com/USNavalResearchLaboratory/ospf-mdr) from source.
For more detailed installation see [here](https://coreemu.github.io/core/install.html).
```shell
git clone https://github.com/coreemu/core.git
cd core
./setup.sh
# Ubuntu
inv install
# CentOS
./install.sh -p /usr
```
## Documentation & Support ## Documentation & Support
We are leveraging GitHub hosted documentation and Discord for persistent We are leveraging GitHub hosted documentation and Discord for persistent

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 7.5.2) AC_INIT(core, 8.0.0)
# autoconf and automake initialization # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) AC_CONFIG_SRCDIR([netns/version.h.in])
@ -123,9 +123,9 @@ if test "x$enable_daemon" = "xyes"; then
AC_MSG_ERROR([Could not locate sysctl (from procps package).]) AC_MSG_ERROR([Could not locate sysctl (from procps package).])
fi fi
AC_CHECK_PROG(ebtables_path, ebtables, $as_dir, no, $SEARCHPATH) AC_CHECK_PROG(nftables_path, nft, $as_dir, no, $SEARCHPATH)
if test "x$ebtables_path" = "xno" ; then if test "x$nftables_path" = "xno" ; then
AC_MSG_ERROR([Could not locate ebtables (from ebtables package).]) AC_MSG_ERROR([Could not locate nftables (from nftables package).])
fi fi
AC_CHECK_PROG(ip_path, ip, $as_dir, no, $SEARCHPATH) AC_CHECK_PROG(ip_path, ip, $as_dir, no, $SEARCHPATH)

View file

@ -2,6 +2,3 @@ import logging.config
# setup default null handler # setup default null handler
logging.getLogger(__name__).addHandler(logging.NullHandler()) logging.getLogger(__name__).addHandler(logging.NullHandler())
# disable paramiko logging
logging.getLogger("paramiko").setLevel(logging.WARNING)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,8 @@ from core.emulator.data import (
) )
from core.emulator.session import Session from core.emulator.session import Session
logger = logging.getLogger(__name__)
def handle_node_event(node_data: NodeData) -> core_pb2.Event: def handle_node_event(node_data: NodeData) -> core_pb2.Event:
""" """
@ -199,7 +201,7 @@ class EventStreamer:
elif isinstance(data, FileData): elif isinstance(data, FileData):
event = handle_file_event(data) event = handle_file_event(data)
else: else:
logging.error("unknown event: %s", data) logger.error("unknown event: %s", data)
except Empty: except Empty:
pass pass
if event: if event:

View file

@ -7,10 +7,9 @@ import grpc
from grpc import ServicerContext from grpc import ServicerContext
from core import utils from core import utils
from core.api.grpc import common_pb2, core_pb2 from core.api.grpc import common_pb2, core_pb2, wrappers
from core.api.grpc.common_pb2 import MappedConfig
from core.api.grpc.configservices_pb2 import ConfigServiceConfig from core.api.grpc.configservices_pb2 import ConfigServiceConfig
from core.api.grpc.emane_pb2 import GetEmaneModelConfig from core.api.grpc.emane_pb2 import NodeEmaneConfig
from core.api.grpc.services_pb2 import ( from core.api.grpc.services_pb2 import (
NodeServiceConfig, NodeServiceConfig,
NodeServiceData, NodeServiceData,
@ -28,9 +27,10 @@ from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode from core.nodes.docker import DockerNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode from core.nodes.lxd import LxcNode
from core.nodes.network import WlanNode from core.nodes.network import CtrlNet, PtpNet, WlanNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
logger = logging.getLogger(__name__)
WORKERS = 10 WORKERS = 10
@ -156,7 +156,7 @@ def create_nodes(
start = time.monotonic() start = time.monotonic()
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
total = time.monotonic() - start total = time.monotonic() - start
logging.debug("grpc created nodes time: %s", total) logger.debug("grpc created nodes time: %s", total)
return results, exceptions return results, exceptions
@ -180,7 +180,7 @@ def create_links(
start = time.monotonic() start = time.monotonic()
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
total = time.monotonic() - start total = time.monotonic() - start
logging.debug("grpc created links time: %s", total) logger.debug("grpc created links time: %s", total)
return results, exceptions return results, exceptions
@ -204,7 +204,7 @@ def edit_links(
start = time.monotonic() start = time.monotonic()
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
total = time.monotonic() - start total = time.monotonic() - start
logging.debug("grpc edit links time: %s", total) logger.debug("grpc edit links time: %s", total)
return results, exceptions return results, exceptions
@ -251,12 +251,15 @@ def get_config_options(
return results return results
def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node: def get_node_proto(
session: Session, node: NodeBase, emane_configs: List[NodeEmaneConfig]
) -> core_pb2.Node:
""" """
Convert CORE node to protobuf representation. Convert CORE node to protobuf representation.
:param session: session containing node :param session: session containing node
:param node: node to convert :param node: node to convert
:param emane_configs: emane configs related to node
:return: node proto :return: node proto
""" """
node_type = session.get_node_type(node.__class__) node_type = session.get_node_type(node.__class__)
@ -271,17 +274,53 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
node_dir = None node_dir = None
config_services = [] config_services = []
if isinstance(node, CoreNodeBase): if isinstance(node, CoreNodeBase):
node_dir = node.nodedir node_dir = str(node.directory)
config_services = [x for x in node.config_services] config_services = [x for x in node.config_services]
channel = None channel = None
if isinstance(node, CoreNode): if isinstance(node, CoreNode):
channel = node.ctrlchnlname channel = str(node.ctrlchnlname)
emane_model = None emane_model = None
if isinstance(node, EmaneNet): if isinstance(node, EmaneNet):
emane_model = node.model.name emane_model = node.model.name
image = None image = None
if isinstance(node, (DockerNode, LxcNode)): if isinstance(node, (DockerNode, LxcNode)):
image = node.image image = node.image
# check for wlan config
wlan_config = session.mobility.get_configs(
node.id, config_type=BasicRangeModel.name
)
if wlan_config:
wlan_config = get_config_options(wlan_config, BasicRangeModel)
# check for mobility config
mobility_config = session.mobility.get_configs(
node.id, config_type=Ns2ScriptedMobility.name
)
if mobility_config:
mobility_config = get_config_options(mobility_config, Ns2ScriptedMobility)
# check for service configs
custom_services = session.services.custom_services.get(node.id)
service_configs = {}
if custom_services:
for service in custom_services.values():
service_proto = get_service_configuration(service)
service_configs[service.name] = NodeServiceConfig(
node_id=node.id,
service=service.name,
data=service_proto,
files=service.config_data,
)
# check for config service configs
config_service_configs = {}
if isinstance(node, CoreNode):
for service in node.config_services.values():
if not service.custom_templates and not service.custom_config:
continue
config_service_configs[service.name] = ConfigServiceConfig(
node_id=node.id,
name=service.name,
templates=service.custom_templates,
config=service.custom_config,
)
return core_pb2.Node( return core_pb2.Node(
id=node.id, id=node.id,
name=node.name, name=node.name,
@ -297,6 +336,11 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
dir=node_dir, dir=node_dir,
channel=channel, channel=channel,
canvas=node.canvas, canvas=node.canvas,
wlan_config=wlan_config,
mobility_config=mobility_config,
service_configs=service_configs,
config_service_configs=config_service_configs,
emane_configs=emane_configs,
) )
@ -529,54 +573,20 @@ def get_nem_id(
return nem_id return nem_id
def get_emane_model_configs(session: Session) -> List[GetEmaneModelConfig]: def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]:
configs = [] configs = {}
for _id in session.emane.node_configurations: for _id, model_configs in session.emane.node_configs.items():
if _id == -1:
continue
model_configs = session.emane.node_configurations[_id]
for model_name in model_configs: for model_name in model_configs:
model = session.emane.models[model_name] model_class = session.emane.get_model(model_name)
current_config = session.emane.get_model_config(_id, model_name) current_config = session.emane.get_config(_id, model_name)
config = get_config_options(current_config, model) config = get_config_options(current_config, model_class)
node_id, iface_id = utils.parse_iface_config_id(_id) node_id, iface_id = utils.parse_iface_config_id(_id)
iface_id = iface_id if iface_id is not None else -1 iface_id = iface_id if iface_id is not None else -1
model_config = GetEmaneModelConfig( node_config = NodeEmaneConfig(
node_id=node_id, model=model_name, iface_id=iface_id, config=config model=model_name, iface_id=iface_id, config=config
) )
configs.append(model_config) node_configs = configs.setdefault(node_id, [])
return configs node_configs.append(node_config)
def get_wlan_configs(session: Session) -> Dict[int, MappedConfig]:
configs = {}
for node_id in session.mobility.node_configurations:
model_config = session.mobility.node_configurations[node_id]
if node_id == -1:
continue
for model_name in model_config:
if model_name != BasicRangeModel.name:
continue
current_config = session.mobility.get_model_config(node_id, model_name)
config = get_config_options(current_config, BasicRangeModel)
mapped_config = MappedConfig(config=config)
configs[node_id] = mapped_config
return configs
def get_mobility_configs(session: Session) -> Dict[int, MappedConfig]:
configs = {}
for node_id in session.mobility.node_configurations:
model_config = session.mobility.node_configurations[node_id]
if node_id == -1:
continue
for model_name in model_config:
if model_name != Ns2ScriptedMobility.name:
continue
current_config = session.mobility.get_model_config(node_id, model_name)
config = get_config_options(current_config, Ns2ScriptedMobility)
mapped_config = MappedConfig(config=config)
configs[node_id] = mapped_config
return configs return configs
@ -590,15 +600,6 @@ def get_hooks(session: Session) -> List[core_pb2.Hook]:
return hooks return hooks
def get_emane_models(session: Session) -> List[str]:
emane_models = []
for model in session.emane.models.keys():
if len(model.split("_")) != 2:
continue
emane_models.append(model)
return emane_models
def get_default_services(session: Session) -> List[ServiceDefaults]: def get_default_services(session: Session) -> List[ServiceDefaults]:
default_services = [] default_services = []
for name, services in session.services.default_services.items(): for name, services in session.services.default_services.items():
@ -607,45 +608,6 @@ def get_default_services(session: Session) -> List[ServiceDefaults]:
return default_services return default_services
def get_node_service_configs(session: Session) -> List[NodeServiceConfig]:
configs = []
for node_id, service_configs in session.services.custom_services.items():
for name in service_configs:
service = session.services.get_service(node_id, name)
service_proto = get_service_configuration(service)
config = NodeServiceConfig(
node_id=node_id,
service=name,
data=service_proto,
files=service.config_data,
)
configs.append(config)
return configs
def get_node_config_service_configs(session: Session) -> List[ConfigServiceConfig]:
configs = []
for node in session.nodes.values():
if not isinstance(node, CoreNodeBase):
continue
for name, service in node.config_services.items():
if not service.custom_templates and not service.custom_config:
continue
config_proto = ConfigServiceConfig(
node_id=node.id,
name=name,
templates=service.custom_templates,
config=service.custom_config,
)
configs.append(config_proto)
return configs
def get_emane_config(session: Session) -> Dict[str, common_pb2.ConfigOption]:
current_config = session.emane.get_configs()
return get_config_options(current_config, session.emane.emane_config)
def get_mobility_node( def get_mobility_node(
session: Session, node_id: int, context: ServicerContext session: Session, node_id: int, context: ServicerContext
) -> Union[WlanNode, EmaneNet]: ) -> Union[WlanNode, EmaneNet]:
@ -656,3 +618,88 @@ def get_mobility_node(
return session.get_node(node_id, EmaneNet) return session.get_node(node_id, EmaneNet)
except CoreError: except CoreError:
context.abort(grpc.StatusCode.NOT_FOUND, "node id is not for wlan or emane") context.abort(grpc.StatusCode.NOT_FOUND, "node id is not for wlan or emane")
def convert_session(session: Session) -> wrappers.Session:
links = []
nodes = []
emane_configs = get_emane_model_configs_dict(session)
for _id in session.nodes:
node = session.nodes[_id]
if not isinstance(node, (PtpNet, CtrlNet)):
node_emane_configs = emane_configs.get(node.id, [])
node_proto = get_node_proto(session, node, node_emane_configs)
nodes.append(node_proto)
node_links = get_links(node)
links.extend(node_links)
default_services = get_default_services(session)
x, y, z = session.location.refxyz
lat, lon, alt = session.location.refgeo
location = core_pb2.SessionLocation(
x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=session.location.refscale
)
hooks = get_hooks(session)
session_file = str(session.file_path) if session.file_path else None
options = get_config_options(session.options.get_configs(), session.options)
servers = [
core_pb2.Server(name=x.name, host=x.host)
for x in session.distributed.servers.values()
]
return core_pb2.Session(
id=session.id,
state=session.state.value,
nodes=nodes,
links=links,
dir=str(session.directory),
user=session.user,
default_services=default_services,
location=location,
hooks=hooks,
metadata=session.metadata,
file=session_file,
options=options,
servers=servers,
)
def configure_node(
session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext
) -> None:
for emane_config in node.emane_configs:
_id = utils.iface_config_id(node.id, emane_config.iface_id)
config = {k: v.value for k, v in emane_config.config.items()}
session.emane.set_config(_id, emane_config.model, config)
if node.wlan_config:
config = {k: v.value for k, v in node.wlan_config.items()}
session.mobility.set_model_config(node.id, BasicRangeModel.name, config)
if node.mobility_config:
config = {k: v.value for k, v in node.mobility_config.items()}
session.mobility.set_model_config(node.id, Ns2ScriptedMobility.name, config)
for service_name, service_config in node.service_configs.items():
data = service_config.data
config = ServiceConfig(
node_id=node.id,
service=service_name,
startup=data.startup,
validate=data.validate,
shutdown=data.shutdown,
files=data.configs,
directories=data.dirs,
)
service_configuration(session, config)
for file_name, file_data in service_config.files.items():
session.services.set_service_file(
node.id, service_name, file_name, file_data
)
if node.config_service_configs:
if not isinstance(core_node, CoreNode):
context.abort(
grpc.StatusCode.INVALID_ARGUMENT,
"invalid node type with config service configs",
)
for service_name, service_config in node.config_service_configs.items():
service = core_node.config_services[service_name]
if service_config.config:
service.set_config(service_config.config)
for name, template in service_config.templates.items():
service.set_template(name, template)

File diff suppressed because it is too large Load diff

View file

@ -171,18 +171,32 @@ class ConfigServiceData:
class ConfigServiceDefaults: class ConfigServiceDefaults:
templates: Dict[str, str] templates: Dict[str, str]
config: Dict[str, "ConfigOption"] config: Dict[str, "ConfigOption"]
modes: List[str] modes: Dict[str, Dict[str, str]]
@classmethod @classmethod
def from_proto( def from_proto(
cls, proto: configservices_pb2.GetConfigServicesResponse cls, proto: configservices_pb2.GetConfigServiceDefaultsResponse
) -> "ConfigServiceDefaults": ) -> "ConfigServiceDefaults":
config = ConfigOption.from_dict(proto.config) config = ConfigOption.from_dict(proto.config)
modes = {x.name: dict(x.config) for x in proto.modes}
return ConfigServiceDefaults( return ConfigServiceDefaults(
templates=dict(proto.templates), config=config, modes=list(proto.modes) templates=dict(proto.templates), config=config, modes=modes
) )
@dataclass
class Server:
name: str
host: str
@classmethod
def from_proto(cls, proto: core_pb2.Server) -> "Server":
return Server(name=proto.name, host=proto.host)
def to_proto(self) -> core_pb2.Server:
return core_pb2.Server(name=self.name, host=self.host)
@dataclass @dataclass
class Service: class Service:
group: str group: str
@ -205,16 +219,16 @@ class ServiceDefault:
@dataclass @dataclass
class NodeServiceData: class NodeServiceData:
executables: List[str] executables: List[str] = field(default_factory=list)
dependencies: List[str] dependencies: List[str] = field(default_factory=list)
dirs: List[str] dirs: List[str] = field(default_factory=list)
configs: List[str] configs: List[str] = field(default_factory=list)
startup: List[str] startup: List[str] = field(default_factory=list)
validate: List[str] validate: List[str] = field(default_factory=list)
validation_mode: ServiceValidationMode validation_mode: ServiceValidationMode = ServiceValidationMode.NON_BLOCKING
validation_timer: int validation_timer: int = 5
shutdown: List[str] shutdown: List[str] = field(default_factory=list)
meta: str meta: str = None
@classmethod @classmethod
def from_proto(cls, proto: services_pb2.NodeServiceData) -> "NodeServiceData": def from_proto(cls, proto: services_pb2.NodeServiceData) -> "NodeServiceData":
@ -225,12 +239,43 @@ class NodeServiceData:
configs=proto.configs, configs=proto.configs,
startup=proto.startup, startup=proto.startup,
validate=proto.validate, validate=proto.validate,
validation_mode=proto.validation_mode, validation_mode=ServiceValidationMode(proto.validation_mode),
validation_timer=proto.validation_timer, validation_timer=proto.validation_timer,
shutdown=proto.shutdown, shutdown=proto.shutdown,
meta=proto.meta, meta=proto.meta,
) )
def to_proto(self) -> services_pb2.NodeServiceData:
return services_pb2.NodeServiceData(
executables=self.executables,
dependencies=self.dependencies,
dirs=self.dirs,
configs=self.configs,
startup=self.startup,
validate=self.validate,
validation_mode=self.validation_mode.value,
validation_timer=self.validation_timer,
shutdown=self.shutdown,
meta=self.meta,
)
@dataclass
class NodeServiceConfig:
node_id: int
service: str
data: NodeServiceData
files: Dict[str, str] = field(default_factory=dict)
@classmethod
def from_proto(cls, proto: services_pb2.NodeServiceConfig) -> "NodeServiceConfig":
return NodeServiceConfig(
node_id=proto.node_id,
service=proto.service,
data=NodeServiceData.from_proto(proto.data),
files=dict(proto.files),
)
@dataclass @dataclass
class ServiceConfig: class ServiceConfig:
@ -254,6 +299,19 @@ class ServiceConfig:
) )
@dataclass
class ServiceFileConfig:
node_id: int
service: str
file: str
data: str = field(repr=False)
def to_proto(self) -> services_pb2.ServiceFileConfig:
return services_pb2.ServiceFileConfig(
node_id=self.node_id, service=self.service, file=self.file, data=self.data
)
@dataclass @dataclass
class BridgeThroughput: class BridgeThroughput:
node_id: int node_id: int
@ -364,11 +422,11 @@ class ExceptionEvent:
@dataclass @dataclass
class ConfigOption: class ConfigOption:
label: str
name: str name: str
value: str value: str
type: ConfigOptionType label: str = None
group: str type: ConfigOptionType = None
group: str = None
select: List[str] = None select: List[str] = None
@classmethod @classmethod
@ -386,15 +444,27 @@ class ConfigOption:
@classmethod @classmethod
def from_proto(cls, proto: common_pb2.ConfigOption) -> "ConfigOption": def from_proto(cls, proto: common_pb2.ConfigOption) -> "ConfigOption":
config_type = ConfigOptionType(proto.type) if proto.type is not None else None
return ConfigOption( return ConfigOption(
label=proto.label, label=proto.label,
name=proto.name, name=proto.name,
value=proto.value, value=proto.value,
type=ConfigOptionType(proto.type), type=config_type,
group=proto.group, group=proto.group,
select=proto.select, select=proto.select,
) )
def to_proto(self) -> common_pb2.ConfigOption:
config_type = self.type.value if self.type is not None else None
return common_pb2.ConfigOption(
label=self.label,
name=self.name,
value=self.value,
type=config_type,
select=self.select,
group=self.group,
)
@dataclass @dataclass
class Interface: class Interface:
@ -598,11 +668,12 @@ class EmaneModelConfig:
) )
def to_proto(self) -> emane_pb2.EmaneModelConfig: def to_proto(self) -> emane_pb2.EmaneModelConfig:
config = ConfigOption.to_dict(self.config)
return emane_pb2.EmaneModelConfig( return emane_pb2.EmaneModelConfig(
node_id=self.node_id, node_id=self.node_id,
model=self.model, model=self.model,
iface_id=self.iface_id, iface_id=self.iface_id,
config=self.config, config=config,
) )
@ -635,11 +706,11 @@ class Geo:
@dataclass @dataclass
class Node: class Node:
id: int id: int = None
name: str name: str = None
type: NodeType type: NodeType = NodeType.DEFAULT
model: str = None model: str = None
position: Position = None 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
@ -669,6 +740,23 @@ class Node:
@classmethod @classmethod
def from_proto(cls, proto: core_pb2.Node) -> "Node": def from_proto(cls, proto: core_pb2.Node) -> "Node":
service_configs = {}
service_file_configs = {}
for service, node_config in proto.service_configs.items():
service_configs[service] = NodeServiceData.from_proto(node_config.data)
service_file_configs[service] = dict(node_config.files)
emane_configs = {}
for emane_config in proto.emane_configs:
iface_id = None if emane_config.iface_id == -1 else emane_config.iface_id
model = emane_config.model
key = (model, iface_id)
emane_configs[key] = ConfigOption.from_dict(emane_config.config)
config_service_configs = {}
for service, service_config in proto.config_service_configs.items():
config_service_configs[service] = ConfigServiceData(
templates=dict(service_config.templates),
config=dict(service_config.config),
)
return Node( return Node(
id=proto.id, id=proto.id,
name=proto.name, name=proto.name,
@ -685,9 +773,43 @@ class Node:
dir=proto.dir, dir=proto.dir,
channel=proto.channel, channel=proto.channel,
canvas=proto.canvas, canvas=proto.canvas,
wlan_config=ConfigOption.from_dict(proto.wlan_config),
mobility_config=ConfigOption.from_dict(proto.mobility_config),
service_configs=service_configs,
service_file_configs=service_file_configs,
config_service_configs=config_service_configs,
emane_model_configs=emane_configs,
) )
def to_proto(self) -> core_pb2.Node: def to_proto(self) -> core_pb2.Node:
emane_configs = []
for key, config in self.emane_model_configs.items():
model, iface_id = key
if iface_id is None:
iface_id = -1
config = {k: v.to_proto() for k, v in config.items()}
emane_config = emane_pb2.NodeEmaneConfig(
iface_id=iface_id, model=model, config=config
)
emane_configs.append(emane_config)
service_configs = {}
for service, service_data in self.service_configs.items():
service_configs[service] = services_pb2.NodeServiceConfig(
service=service, data=service_data.to_proto()
)
for service, file_configs in self.service_file_configs.items():
service_config = service_configs.get(service)
if service_config:
service_config.files.update(file_configs)
else:
service_configs[service] = services_pb2.NodeServiceConfig(
service=service, files=file_configs
)
config_service_configs = {}
for service, service_config in self.config_service_configs.items():
config_service_configs[service] = configservices_pb2.ConfigServiceConfig(
templates=service_config.templates, config=service_config.config
)
return core_pb2.Node( return core_pb2.Node(
id=self.id, id=self.id,
name=self.name, name=self.name,
@ -703,24 +825,50 @@ class Node:
dir=self.dir, dir=self.dir,
channel=self.channel, channel=self.channel,
canvas=self.canvas, canvas=self.canvas,
wlan_config={k: v.to_proto() for k, v in self.wlan_config.items()},
mobility_config={k: v.to_proto() for k, v in self.mobility_config.items()},
service_configs=service_configs,
config_service_configs=config_service_configs,
emane_configs=emane_configs,
) )
def set_wlan(self, config: Dict[str, str]) -> None:
for key, value in config.items():
option = ConfigOption(name=key, value=value)
self.wlan_config[key] = option
def set_mobility(self, config: Dict[str, str]) -> None:
for key, value in config.items():
option = ConfigOption(name=key, value=value)
self.mobility_config[key] = option
def set_emane_model(
self, model: str, config: Dict[str, str], iface_id: int = None
) -> None:
key = (model, iface_id)
config_options = self.emane_model_configs.setdefault(key, {})
for key, value in config.items():
option = ConfigOption(name=key, value=value)
config_options[key] = option
@dataclass @dataclass
class Session: class Session:
id: int id: int = None
state: SessionState state: SessionState = SessionState.DEFINITION
nodes: Dict[int, Node] nodes: Dict[int, Node] = field(default_factory=dict)
links: List[Link] links: List[Link] = field(default_factory=list)
dir: str dir: str = None
user: str user: str = None
default_services: Dict[str, Set[str]] default_services: Dict[str, Set[str]] = field(default_factory=dict)
location: SessionLocation location: SessionLocation = SessionLocation(
hooks: Dict[str, Hook] x=0.0, y=0.0, z=0.0, lat=47.57917, lon=-122.13232, alt=2.0, scale=150.0
emane_models: List[str] )
emane_config: Dict[str, ConfigOption] hooks: Dict[str, Hook] = field(default_factory=dict)
metadata: Dict[str, str] metadata: Dict[str, str] = field(default_factory=dict)
file: Path file: Path = None
options: Dict[str, ConfigOption] = field(default_factory=dict)
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":
@ -730,33 +878,9 @@ class Session:
x.node_type: set(x.services) for x in proto.default_services x.node_type: 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}
# update nodes with their current configurations
for model in proto.emane_model_configs:
iface_id = None
if model.iface_id != -1:
iface_id = model.iface_id
node = nodes[model.node_id]
key = (model.model, iface_id)
node.emane_model_configs[key] = ConfigOption.from_dict(model.config)
for node_id, mapped_config in proto.wlan_configs.items():
node = nodes[node_id]
node.wlan_config = ConfigOption.from_dict(mapped_config.config)
for config in proto.service_configs:
service = config.service
node = nodes[config.node_id]
node.service_configs[service] = NodeServiceData.from_proto(config.data)
for file, data in config.files.items():
files = node.service_file_configs.setdefault(service, {})
files[file] = data
for config in proto.config_service_configs:
node = nodes[config.node_id]
node.config_service_configs[config.name] = ConfigServiceData(
templates=dict(config.templates), config=dict(config.config)
)
for node_id, mapped_config in proto.mobility_configs.items():
node = nodes[node_id]
node.mobility_config = ConfigOption.from_dict(mapped_config.config)
file_path = Path(proto.file) if proto.file else None file_path = Path(proto.file) if proto.file else None
options = ConfigOption.from_dict(proto.options)
servers = [Server.from_proto(x) for x in proto.servers]
return Session( return Session(
id=proto.id, id=proto.id,
state=SessionState(proto.state), state=SessionState(proto.state),
@ -767,10 +891,107 @@ class Session:
default_services=default_services, default_services=default_services,
location=SessionLocation.from_proto(proto.location), location=SessionLocation.from_proto(proto.location),
hooks=hooks, hooks=hooks,
emane_models=list(proto.emane_models),
emane_config=ConfigOption.from_dict(proto.emane_config),
metadata=dict(proto.metadata), metadata=dict(proto.metadata),
file=file_path, file=file_path,
options=options,
servers=servers,
)
def to_proto(self) -> core_pb2.Session:
nodes = [x.to_proto() for x in self.nodes.values()]
links = [x.to_proto() for x in self.links]
hooks = [x.to_proto() for x in self.hooks.values()]
options = {k: v.to_proto() for k, v in self.options.items()}
servers = [x.to_proto() for x in self.servers]
default_services = []
for node_type, services in self.default_services.items():
default_service = services_pb2.ServiceDefaults(
node_type=node_type, services=services
)
default_services.append(default_service)
file = str(self.file) if self.file else None
return core_pb2.Session(
id=self.id,
state=self.state.value,
nodes=nodes,
links=links,
dir=self.dir,
user=self.user,
default_services=default_services,
location=self.location.to_proto(),
hooks=hooks,
metadata=self.metadata,
file=file,
options=options,
servers=servers,
)
def add_node(
self,
_id: int,
*,
name: str = None,
_type: NodeType = NodeType.DEFAULT,
model: str = "PC",
position: Position = None,
geo: Geo = None,
emane: str = None,
image: str = None,
server: str = None,
) -> Node:
node = Node(
id=_id,
name=name,
type=_type,
model=model,
position=position,
geo=geo,
emane=emane,
image=image,
server=server,
)
self.nodes[node.id] = node
return node
def add_link(
self,
*,
node1: Node,
node2: Node,
iface1: Interface = None,
iface2: Interface = None,
options: LinkOptions = None,
) -> Link:
link = Link(
node1_id=node1.id,
node2_id=node2.id,
iface1=iface1,
iface2=iface2,
options=options,
)
self.links.append(link)
return link
def set_options(self, config: Dict[str, str]) -> None:
for key, value in config.items():
option = ConfigOption(name=key, value=value)
self.options[key] = option
@dataclass
class CoreConfig:
services: List[Service] = field(default_factory=list)
config_services: List[ConfigService] = field(default_factory=list)
emane_models: List[str] = field(default_factory=list)
@classmethod
def from_proto(cls, proto: core_pb2.GetConfigResponse) -> "CoreConfig":
services = [Service.from_proto(x) for x in proto.services]
config_services = [ConfigService.from_proto(x) for x in proto.config_services]
return CoreConfig(
services=services,
config_services=config_services,
emane_models=list(proto.emane_models),
) )

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,8 @@ from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs
from core.config import ConfigGroup, ConfigurableOptions from core.config import ConfigGroup, ConfigurableOptions
from core.emulator.data import ConfigData, NodeData from core.emulator.data import ConfigData, NodeData
logger = logging.getLogger(__name__)
def convert_node(node_data: NodeData): def convert_node(node_data: NodeData):
""" """
@ -139,9 +141,9 @@ class ConfigShim:
captions = None captions = None
data_types = [] data_types = []
possible_values = [] possible_values = []
logging.debug("configurable: %s", configurable_options) logger.debug("configurable: %s", configurable_options)
logging.debug("configuration options: %s", configurable_options.configurations) logger.debug("configuration options: %s", configurable_options.configurations)
logging.debug("configuration data: %s", config) logger.debug("configuration data: %s", config)
for configuration in configurable_options.configurations(): for configuration in configurable_options.configurations():
if not captions: if not captions:
captions = configuration.label captions = configuration.label

View file

@ -4,6 +4,8 @@ Utilities for working with python struct data.
import logging import logging
logger = logging.getLogger(__name__)
def pack_values(clazz, packers): def pack_values(clazz, packers):
""" """
@ -15,7 +17,7 @@ def pack_values(clazz, packers):
""" """
# iterate through tuples of values to pack # iterate through tuples of values to pack
logging.debug("packing: %s", packers) logger.debug("packing: %s", packers)
data = b"" data = b""
for packer in packers: for packer in packers:
# check if a transformer was provided for valid values # check if a transformer was provided for valid values
@ -37,7 +39,7 @@ def pack_values(clazz, packers):
value = transformer(value) value = transformer(value)
# pack and add to existing data # pack and add to existing data
logging.debug("packing: %s - %s type(%s)", tlv_type, value, type(value)) logger.debug("packing: %s - %s type(%s)", tlv_type, value, type(value))
data += clazz.pack(tlv_type.value, value) data += clazz.pack(tlv_type.value, value)
return data return data

View file

@ -4,73 +4,107 @@ Common support for configurable CORE objects.
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Type, 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
from core.errors import CoreConfigError
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
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"}
@dataclass
class ConfigGroup: class ConfigGroup:
""" """
Defines configuration group tabs used for display by ConfigurationOptions. Defines configuration group tabs used for display by ConfigurationOptions.
""" """
def __init__(self, name: str, start: int, stop: int) -> None: name: str
""" start: int
Creates a ConfigGroup object. stop: int
:param name: configuration group display name
:param start: configurations start index for this group
:param stop: configurations stop index for this group
"""
self.name: str = name
self.start: int = start
self.stop: int = stop
@dataclass
class Configuration: class Configuration:
""" """
Represents a configuration options. Represents a configuration option.
""" """
def __init__( id: str
self, type: ConfigDataTypes
_id: str, label: str = None
_type: ConfigDataTypes, default: str = ""
label: str = None, options: List[str] = field(default_factory=list)
default: str = "",
options: List[str] = None,
) -> None:
"""
Creates a Configuration object.
:param _id: unique name for configuration def __post_init__(self) -> None:
:param _type: configuration data type self.label = self.label if self.label else self.id
:param label: configuration label for display if self.type == ConfigDataTypes.BOOL:
:param default: default value for configuration if self.default and self.default not in _BOOL_OPTIONS:
:param options: list options if this is a configuration with a combobox raise CoreConfigError(
""" f"{self.id} bool value must be one of: {_BOOL_OPTIONS}: "
self.id: str = _id f"{self.default}"
self.type: ConfigDataTypes = _type )
self.default: str = default elif self.type == ConfigDataTypes.FLOAT:
if not options: if self.default:
options = [] try:
self.options: List[str] = options float(self.default)
if not label: except ValueError:
label = _id raise CoreConfigError(
self.label: str = label f"{self.id} is not a valid float: {self.default}"
)
elif self.type != ConfigDataTypes.STRING:
if self.default:
try:
int(self.default)
except ValueError:
raise CoreConfigError(
f"{self.id} is not a valid int: {self.default}"
)
def __str__(self):
return ( @dataclass
f"{self.__class__.__name__}(id={self.id}, type={self.type}, " class ConfigBool(Configuration):
f"default={self.default}, options={self.options})" """
) Represents a boolean configuration option.
"""
type: ConfigDataTypes = ConfigDataTypes.BOOL
@dataclass
class ConfigFloat(Configuration):
"""
Represents a float configuration option.
"""
type: ConfigDataTypes = ConfigDataTypes.FLOAT
@dataclass
class ConfigInt(Configuration):
"""
Represents an integer configuration option.
"""
type: ConfigDataTypes = ConfigDataTypes.INT32
@dataclass
class ConfigString(Configuration):
"""
Represents a string configuration option.
"""
type: ConfigDataTypes = ConfigDataTypes.STRING
class ConfigurableOptions: class ConfigurableOptions:
@ -182,7 +216,7 @@ class ConfigurableManager:
:param config_type: configuration type to store configuration for :param config_type: configuration type to store configuration for
:return: nothing :return: nothing
""" """
logging.debug( logger.debug(
"setting config for node(%s) type(%s): %s", node_id, config_type, config "setting config for node(%s) type(%s): %s", node_id, config_type, config
) )
node_configs = self.node_configurations.setdefault(node_id, OrderedDict()) node_configs = self.node_configurations.setdefault(node_id, OrderedDict())
@ -314,7 +348,7 @@ class ModelManager(ConfigurableManager):
:param config: model configuration, None for default configuration :param config: model configuration, None for default configuration
:return: nothing :return: nothing
""" """
logging.debug( logger.debug(
"setting model(%s) for node(%s): %s", model_class.name, node.id, config "setting model(%s) for node(%s): %s", model_class.name, node.id, config
) )
self.set_model_config(node.id, model_class.name, config) self.set_model_config(node.id, model_class.name, config)
@ -343,5 +377,5 @@ class ModelManager(ConfigurableManager):
model_class = self.models[model_name] model_class = self.models[model_name]
models.append((model_class, config)) models.append((model_class, config))
logging.debug("models for node(%s): %s", node.id, models) logger.debug("models for node(%s): %s", node.id, models)
return models return models

View file

@ -2,9 +2,10 @@ import abc
import enum import enum
import inspect import inspect
import logging import logging
import pathlib
import time import time
from typing import Any, Dict, List from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional
from mako import exceptions from mako import exceptions
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
@ -14,6 +15,7 @@ from core.config import Configuration
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
logger = logging.getLogger(__name__)
TEMPLATES_DIR: str = "templates" TEMPLATES_DIR: str = "templates"
@ -27,6 +29,14 @@ class ConfigServiceBootError(Exception):
pass pass
@dataclass
class ShadowDir:
path: str
src: Optional[str] = None
templates: bool = False
has_node_paths: bool = False
class ConfigService(abc.ABC): class ConfigService(abc.ABC):
""" """
Base class for creating configurable services. Base class for creating configurable services.
@ -38,6 +48,9 @@ class ConfigService(abc.ABC):
# time to wait in seconds for determining if service started successfully # time to wait in seconds for determining if service started successfully
validation_timer: int = 5 validation_timer: int = 5
# directories to shadow and copy files from
shadow_directories: List[ShadowDir] = []
def __init__(self, node: CoreNode) -> None: def __init__(self, node: CoreNode) -> None:
""" """
Create ConfigService instance. Create ConfigService instance.
@ -46,7 +59,7 @@ class ConfigService(abc.ABC):
""" """
self.node: CoreNode = node self.node: CoreNode = node
class_file = inspect.getfile(self.__class__) class_file = inspect.getfile(self.__class__)
templates_path = pathlib.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] = {}
@ -133,7 +146,8 @@ class ConfigService(abc.ABC):
:return: nothing :return: nothing
:raises ConfigServiceBootError: when there is an error starting service :raises ConfigServiceBootError: when there is an error starting service
""" """
logging.info("node(%s) service(%s) starting...", self.node.name, self.name) logger.info("node(%s) service(%s) starting...", self.node.name, self.name)
self.create_shadow_dirs()
self.create_dirs() self.create_dirs()
self.create_files() self.create_files()
wait = self.validation_mode == ConfigServiceMode.BLOCKING wait = self.validation_mode == ConfigServiceMode.BLOCKING
@ -154,7 +168,7 @@ class ConfigService(abc.ABC):
try: try:
self.node.cmd(cmd) self.node.cmd(cmd)
except CoreCommandError: except CoreCommandError:
logging.exception( logger.exception(
f"node({self.node.name}) service({self.name}) " f"node({self.node.name}) service({self.name}) "
f"failed shutdown: {cmd}" f"failed shutdown: {cmd}"
) )
@ -168,6 +182,64 @@ class ConfigService(abc.ABC):
self.stop() self.stop()
self.start() self.start()
def create_shadow_dirs(self) -> None:
"""
Creates a shadow of a host system directory recursively
to be mapped and live within a node.
:return: nothing
:raises CoreError: when there is a failure creating a directory or file
"""
for shadow_dir in self.shadow_directories:
# setup shadow and src paths, using node unique paths when configured
shadow_path = Path(shadow_dir.path)
if shadow_dir.src is None:
src_path = shadow_path
else:
src_path = Path(shadow_dir.src)
if shadow_dir.has_node_paths:
src_path = src_path / self.node.name
# validate shadow and src paths
if not shadow_path.is_absolute():
raise CoreError(f"shadow dir({shadow_path}) is not absolute")
if not src_path.is_absolute():
raise CoreError(f"shadow source dir({src_path}) is not absolute")
if not src_path.is_dir():
raise CoreError(f"shadow source dir({src_path}) does not exist")
# create root of the shadow path within node
logger.info(
"node(%s) creating shadow directory(%s) src(%s) node paths(%s) "
"templates(%s)",
self.node.name,
shadow_path,
src_path,
shadow_dir.has_node_paths,
shadow_dir.templates,
)
self.node.create_dir(shadow_path)
# find all directories and files to create
dir_paths = []
file_paths = []
for path in src_path.rglob("*"):
shadow_src_path = shadow_path / path.relative_to(src_path)
if path.is_dir():
dir_paths.append(shadow_src_path)
else:
file_paths.append((path, shadow_src_path))
# create all directories within node
for path in dir_paths:
self.node.create_dir(path)
# create all files within node, from templates when configured
data = self.data()
templates = TemplateLookup(directories=src_path)
for path, dst_path in file_paths:
if shadow_dir.templates:
template = templates.get_template(path.name)
rendered = self._render(template, data)
self.node.create_file(dst_path, rendered)
else:
self.node.copy_file(path, dst_path)
def create_dirs(self) -> None: def create_dirs(self) -> None:
""" """
Creates directories for service. Creates directories for service.
@ -175,10 +247,12 @@ class ConfigService(abc.ABC):
:return: nothing :return: nothing
:raises CoreError: when there is a failure creating a directory :raises CoreError: when there is a failure creating a directory
""" """
for directory in self.directories: logger.debug("creating config service directories")
for directory in sorted(self.directories):
dir_path = Path(directory)
try: try:
self.node.privatedir(directory) self.node.create_dir(dir_path)
except (CoreCommandError, ValueError): except (CoreCommandError, CoreError):
raise CoreError( raise CoreError(
f"node({self.node.name}) service({self.name}) " f"node({self.node.name}) service({self.name}) "
f"failure to create service directory: {directory}" f"failure to create service directory: {directory}"
@ -219,17 +293,21 @@ class ConfigService(abc.ABC):
:return: mapping of files to templates :return: mapping of files to templates
""" """
templates = {} templates = {}
for name in self.files: for file in self.files:
basename = pathlib.Path(name).name file_path = Path(file)
if name in self.custom_templates: if file_path.is_absolute():
template = self.custom_templates[name] template_path = str(file_path.relative_to("/"))
template = self.clean_text(template)
elif self.templates.has_template(basename):
template = self.templates.get_template(basename).source
else: else:
template = self.get_text_template(name) template_path = str(file_path)
if file in self.custom_templates:
template = self.custom_templates[file]
template = self.clean_text(template) template = self.clean_text(template)
templates[name] = template elif self.templates.has_template(template_path):
template = self.templates.get_template(template_path).source
else:
template = self.get_text_template(file)
template = self.clean_text(template)
templates[file] = template
return templates return templates
def create_files(self) -> None: def create_files(self) -> None:
@ -239,24 +317,20 @@ class ConfigService(abc.ABC):
:return: nothing :return: nothing
""" """
data = self.data() data = self.data()
for name in self.files: for file in sorted(self.files):
basename = pathlib.Path(name).name logger.debug(
if name in self.custom_templates: "node(%s) service(%s) template(%s)", self.node.name, self.name, file
text = self.custom_templates[name]
rendered = self.render_text(text, data)
elif self.templates.has_template(basename):
rendered = self.render_template(basename, data)
else:
text = self.get_text_template(name)
rendered = self.render_text(text, data)
logging.debug(
"node(%s) service(%s) template(%s): \n%s",
self.node.name,
self.name,
name,
rendered,
) )
self.node.nodefile(name, rendered) file_path = Path(file)
if file in self.custom_templates:
text = self.custom_templates[file]
rendered = self.render_text(text, data)
elif self.templates.has_template(file_path.name):
rendered = self.render_template(file_path.name, data)
else:
text = self.get_text_template(file)
rendered = self.render_text(text, data)
self.node.create_file(file_path, rendered)
def run_startup(self, wait: bool) -> None: def run_startup(self, wait: bool) -> None:
""" """
@ -300,7 +374,7 @@ class ConfigService(abc.ABC):
del cmds[index] del cmds[index]
index += 1 index += 1
except CoreCommandError: except CoreCommandError:
logging.debug( logger.debug(
f"node({self.node.name}) service({self.name}) " f"node({self.node.name}) service({self.name}) "
f"validate command failed: {cmd}" f"validate command failed: {cmd}"
) )

View file

@ -1,6 +1,8 @@
import logging import logging
from typing import TYPE_CHECKING, Dict, List, Set from typing import TYPE_CHECKING, Dict, List, Set
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.configservice.base import ConfigService from core.configservice.base import ConfigService
@ -41,7 +43,7 @@ class ConfigServiceDependencies:
for name in self.node_services: for name in self.node_services:
service = self.node_services[name] service = self.node_services[name]
if service.name in self.started: if service.name in self.started:
logging.debug( logger.debug(
"skipping service that will already be started: %s", service.name "skipping service that will already be started: %s", service.name
) )
continue continue
@ -75,7 +77,7 @@ class ConfigServiceDependencies:
:param service: service to check dependencies for :param service: service to check dependencies for
:return: list of config services to start in order :return: list of config services to start in order
""" """
logging.debug("starting service dependency check: %s", service.name) logger.debug("starting service dependency check: %s", service.name)
self._reset() self._reset()
return self._visit(service) return self._visit(service)
@ -86,7 +88,7 @@ class ConfigServiceDependencies:
:param current_service: service being visited :param current_service: service being visited
:return: list of dependent services for a visited service :return: list of dependent services for a visited service
""" """
logging.debug("visiting service(%s): %s", current_service.name, self.path) logger.debug("visiting service(%s): %s", current_service.name, self.path)
self.visited.add(current_service.name) self.visited.add(current_service.name)
self.visiting.add(current_service.name) self.visiting.add(current_service.name)
@ -109,7 +111,7 @@ class ConfigServiceDependencies:
self._visit(service) self._visit(service)
# add service when bottom is found # add service when bottom is found
logging.debug("adding service to startup path: %s", current_service.name) logger.debug("adding service to startup path: %s", current_service.name)
self.started.add(current_service.name) self.started.add(current_service.name)
self.path.append(current_service) self.path.append(current_service)
self.visiting.remove(current_service.name) self.visiting.remove(current_service.name)

View file

@ -1,11 +1,15 @@
import logging import logging
import pathlib import pathlib
import pkgutil
from pathlib import Path
from typing import Dict, List, Type from typing import Dict, List, Type
from core import utils from core import configservices, utils
from core.configservice.base import ConfigService from core.configservice.base import ConfigService
from core.errors import CoreError from core.errors import CoreError
logger = logging.getLogger(__name__)
class ConfigServiceManager: class ConfigServiceManager:
""" """
@ -28,7 +32,7 @@ class ConfigServiceManager:
""" """
service_class = self.services.get(name) service_class = self.services.get(name)
if service_class is None: if service_class is None:
raise CoreError(f"service does not exit {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:
@ -40,7 +44,7 @@ class ConfigServiceManager:
:raises CoreError: when service is a duplicate or has unmet executables :raises CoreError: when service is a duplicate or has unmet executables
""" """
name = service.name name = service.name
logging.debug( logger.debug(
"loading service: class(%s) name(%s)", service.__class__.__name__, name "loading service: class(%s) name(%s)", service.__class__.__name__, name
) )
@ -55,27 +59,46 @@ class ConfigServiceManager:
except CoreError as e: except CoreError as e:
raise CoreError(f"config service({service.name}): {e}") raise CoreError(f"config service({service.name}): {e}")
# make service available # make service available
self.services[name] = service self.services[name] = service
def load(self, path: str) -> List[str]: def load_locals(self) -> List[str]:
""" """
Search path provided for configurable services and add them for being managed. Search and add config service from local core module.
:return: list of errors when loading services
"""
errors = []
for module_info in pkgutil.walk_packages(
configservices.__path__, f"{configservices.__name__}."
):
services = utils.load_module(module_info.name, ConfigService)
for service in services:
try:
self.add(service)
except CoreError as e:
errors.append(service.name)
logger.debug("not loading config service(%s): %s", service.name, e)
return errors
def load(self, path: Path) -> List[str]:
"""
Search path provided for config services and add them for being managed.
:param path: path to search configurable services :param path: path to search configurable services
:return: list errors when loading and adding services :return: list errors when loading services
""" """
path = pathlib.Path(path) path = pathlib.Path(path)
subdirs = [x for x in path.iterdir() if x.is_dir()] subdirs = [x for x in path.iterdir() if x.is_dir()]
subdirs.append(path) subdirs.append(path)
service_errors = [] service_errors = []
for subdir in subdirs: for subdir in subdirs:
logging.debug("loading config services from: %s", subdir) logger.debug("loading config services from: %s", subdir)
services = utils.load_classes(str(subdir), ConfigService) services = utils.load_classes(subdir, ConfigService)
for service in services: for service in services:
try: try:
self.add(service) self.add(service)
except CoreError as e: except CoreError as e:
service_errors.append(service.name) service_errors.append(service.name)
logging.debug("not loading service(%s): %s", service.name, e) logger.debug("not loading service(%s): %s", service.name, e)
return service_errors return service_errors

View file

@ -9,6 +9,7 @@ from core.nodes.base import CoreNodeBase
from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
logger = logging.getLogger(__name__)
GROUP: str = "Quagga" GROUP: str = "Quagga"
QUAGGA_STATE_DIR: str = "/var/run/quagga" QUAGGA_STATE_DIR: str = "/var/run/quagga"
@ -101,9 +102,9 @@ class Zebra(ConfigService):
ip4s = [] ip4s = []
ip6s = [] ip6s = []
for ip4 in iface.ip4s: for ip4 in iface.ip4s:
ip4s.append(str(ip4.ip)) ip4s.append(str(ip4))
for ip6 in iface.ip6s: for ip6 in iface.ip6s:
ip6s.append(str(ip6.ip)) ip6s.append(str(ip6))
ifaces.append((iface, ip4s, ip6s, iface.control)) ifaces.append((iface, ip4s, ip6s, iface.control))
return dict( return dict(
@ -226,12 +227,6 @@ class Ospfv3mdr(Ospfv3):
name: str = "OSPFv3MDR" name: str = "OSPFv3MDR"
def data(self) -> Dict[str, Any]:
for iface in self.node.get_ifaces():
is_wireless = isinstance(iface.net, (WlanNode, EmaneNet))
logging.info("MDR wireless: %s", is_wireless)
return dict()
def quagga_iface_config(self, iface: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
config = super().quagga_iface_config(iface) config = super().quagga_iface_config(iface)
if isinstance(iface.net, (WlanNode, EmaneNet)): if isinstance(iface.net, (WlanNode, EmaneNet)):

View file

@ -1,8 +1,7 @@
from typing import Any, Dict, List from typing import Any, Dict, List
from core.config import Configuration from core.config import ConfigString, Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
from core.emulator.enumerations import ConfigDataTypes
GROUP_NAME: str = "Security" GROUP_NAME: str = "Security"
@ -19,24 +18,9 @@ class VpnClient(ConfigService):
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] = [
Configuration( ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"),
_id="keydir", ConfigString(id="keyname", label="Key Name", default="client1"),
_type=ConfigDataTypes.STRING, ConfigString(id="server", label="Server", default="10.0.2.10"),
label="Key Dir",
default="/etc/core/keys",
),
Configuration(
_id="keyname",
_type=ConfigDataTypes.STRING,
label="Key Name",
default="client1",
),
Configuration(
_id="server",
_type=ConfigDataTypes.STRING,
label="Server",
default="10.0.2.10",
),
] ]
modes: Dict[str, Dict[str, str]] = {} modes: Dict[str, Dict[str, str]] = {}
@ -53,24 +37,9 @@ class VpnServer(ConfigService):
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] = [
Configuration( ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"),
_id="keydir", ConfigString(id="keyname", label="Key Name", default="server"),
_type=ConfigDataTypes.STRING, ConfigString(id="subnet", label="Subnet", default="10.0.200.0"),
label="Key Dir",
default="/etc/core/keys",
),
Configuration(
_id="keyname",
_type=ConfigDataTypes.STRING,
label="Key Name",
default="server",
),
Configuration(
_id="subnet",
_type=ConfigDataTypes.STRING,
label="Subnet",
default="10.0.200.0",
),
] ]
modes: Dict[str, Dict[str, str]] = {} modes: Dict[str, Dict[str, str]] = {}

View file

@ -1,49 +0,0 @@
from typing import Dict, List
from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode
from core.emulator.enumerations import ConfigDataTypes
class SimpleService(ConfigService):
name: str = "Simple"
group: str = "SimpleGroup"
directories: List[str] = ["/etc/quagga", "/usr/local/lib"]
files: List[str] = ["test1.sh", "test2.sh"]
executables: List[str] = []
dependencies: List[str] = []
startup: List[str] = []
validate: List[str] = []
shutdown: List[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
Configuration(
_id="value3",
_type=ConfigDataTypes.STRING,
label="Multiple Choice",
options=["value1", "value2", "value3"],
),
]
modes: Dict[str, Dict[str, str]] = {
"mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
"mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
"mode3": {"value1": "value3", "value2": "0", "value3": "value1"},
}
def get_text_template(self, name: str) -> str:
if name == "test1.sh":
return """
# sample script 1
# node id(${node.id}) name(${node.name})
# config: ${config}
echo hello
"""
elif name == "test2.sh":
return """
# sample script 2
# node id(${node.id}) name(${node.name})
# config: ${config}
echo hello2
"""

View file

@ -149,11 +149,13 @@ class DhcpService(ConfigService):
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:
if ip4.size == 1:
continue
# divide the address space in half # divide the address space in half
index = (ip4.size - 2) / 2 index = (ip4.size - 2) / 2
rangelow = ip4[index] rangelow = ip4[index]
rangehigh = ip4[-2] rangehigh = ip4[-2]
subnets.append((ip4.ip, ip4.netmask, rangelow, rangehigh, str(ip4.ip))) subnets.append((ip4.cidr.ip, ip4.netmask, rangelow, rangehigh, ip4.ip))
return dict(subnets=subnets) return dict(subnets=subnets)

View file

@ -1,3 +1,5 @@
COREDPY_VERSION = "@PACKAGE_VERSION@" from pathlib import Path
CORE_CONF_DIR = "@CORE_CONF_DIR@"
CORE_DATA_DIR = "@CORE_DATA_DIR@" COREDPY_VERSION: str = "@PACKAGE_VERSION@"
CORE_CONF_DIR: Path = Path("@CORE_CONF_DIR@")
CORE_DATA_DIR: Path = Path("@CORE_DATA_DIR@")

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,12 @@
import logging import logging
from pathlib import Path
from typing import Dict, List 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
logger = logging.getLogger(__name__)
manifest = None manifest = None
try: try:
from emane.shell import manifest from emane.shell import manifest
@ -12,7 +15,7 @@ except ImportError:
from emanesh import manifest from emanesh import manifest
except ImportError: except ImportError:
manifest = None manifest = None
logging.debug("compatible emane python bindings not installed") logger.debug("compatible emane python bindings not installed")
def _type_value(config_type: str) -> ConfigDataTypes: def _type_value(config_type: str) -> ConfigDataTypes:
@ -71,9 +74,10 @@ def _get_default(config_type_name: str, config_value: List[str]) -> str:
return config_default return config_default
def parse(manifest_path: str, 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 into ones used by core. Parses a valid emane manifest file and converts the provided configuration values
into ones used by core.
:param manifest_path: absolute manifest file path :param manifest_path: absolute manifest file path
:param defaults: used to override default values for configurations :param defaults: used to override default values for configurations
@ -85,7 +89,7 @@ def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]:
return [] return []
# load configuration file # load configuration file
manifest_file = manifest.Manifest(manifest_path) manifest_file = manifest.Manifest(str(manifest_path))
manifest_configurations = manifest_file.getAllConfiguration() manifest_configurations = manifest_file.getAllConfiguration()
configurations = [] configurations = []
@ -116,8 +120,8 @@ def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]:
config_descriptions = f"{config_descriptions} file" config_descriptions = f"{config_descriptions} file"
configuration = Configuration( configuration = Configuration(
_id=config_name, id=config_name,
_type=config_type_value, type=config_type_value,
default=config_default, default=config_default,
options=possible, options=possible,
label=config_descriptions, label=config_descriptions,

View file

@ -2,19 +2,21 @@
Defines Emane Models used within CORE. Defines Emane Models used within CORE.
""" """
import logging import logging
import os from pathlib import Path
from typing import Dict, List, Optional, Set from typing import Dict, List, Optional, Set
from core.config import ConfigGroup, Configuration from core.config import ConfigBool, ConfigGroup, ConfigString, Configuration
from core.emane import emanemanifest from core.emane import emanemanifest
from core.emane.nodes import EmaneNet
from core.emulator.data import LinkOptions from core.emulator.data import LinkOptions
from core.emulator.enumerations import ConfigDataTypes
from core.errors import CoreError from core.errors import CoreError
from core.location.mobility import WirelessModel from core.location.mobility import WirelessModel
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.xml import emanexml from core.xml import emanexml
logger = logging.getLogger(__name__)
DEFAULT_DEV: str = "ctrl0"
MANIFEST_PATH: str = "share/emane/manifest"
class EmaneModel(WirelessModel): class EmaneModel(WirelessModel):
""" """
@ -23,6 +25,17 @@ class EmaneModel(WirelessModel):
configurable parameters. Helper functions also live here. configurable parameters. Helper functions also live here.
""" """
# default platform configuration settings
platform_controlport: str = "controlportendpoint"
platform_xml: str = "nemmanager.xml"
platform_defaults: Dict[str, str] = {
"eventservicedevice": DEFAULT_DEV,
"eventservicegroup": "224.1.2.8:45703",
"otamanagerdevice": DEFAULT_DEV,
"otamanagergroup": "224.1.2.8:45702",
}
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
@ -41,35 +54,45 @@ class EmaneModel(WirelessModel):
# support for external configurations # support for external configurations
external_config: List[Configuration] = [ external_config: List[Configuration] = [
Configuration("external", ConfigDataTypes.BOOL, default="0"), ConfigBool(id="external", default="0"),
Configuration( ConfigString(id="platformendpoint", default="127.0.0.1:40001"),
"platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001" ConfigString(id="transportendpoint", default="127.0.0.1:50002"),
),
Configuration(
"transportendpoint", ConfigDataTypes.STRING, default="127.0.0.1:50002"
),
] ]
config_ignore: Set[str] = set() config_ignore: Set[str] = set()
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: Path) -> None:
""" """
Called after being loaded within the EmaneManager. Provides configured emane_prefix for Called after being loaded within the EmaneManager. Provides configured
parsing xml files. emane_prefix for parsing xml files.
:param emane_prefix: configured emane prefix path :param emane_prefix: configured emane prefix path
:return: nothing :return: nothing
""" """
manifest_path = "share/emane/manifest" cls._load_platform_config(emane_prefix)
# load mac configuration # load mac configuration
mac_xml_path = os.path.join(emane_prefix, manifest_path, cls.mac_xml) mac_xml_path = emane_prefix / MANIFEST_PATH / cls.mac_xml
cls.mac_config = emanemanifest.parse(mac_xml_path, cls.mac_defaults) cls.mac_config = emanemanifest.parse(mac_xml_path, cls.mac_defaults)
# load phy configuration # load phy configuration
phy_xml_path = os.path.join(emane_prefix, manifest_path, cls.phy_xml) phy_xml_path = emane_prefix / MANIFEST_PATH / cls.phy_xml
cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults) cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults)
@classmethod
def _load_platform_config(cls, emane_prefix: Path) -> None:
platform_xml_path = emane_prefix / MANIFEST_PATH / cls.platform_xml
cls.platform_config = emanemanifest.parse(
platform_xml_path, cls.platform_defaults
)
# remove controlport configuration, since core will set this directly
controlport_index = None
for index, configuration in enumerate(cls.platform_config):
if configuration.id == cls.platform_controlport:
controlport_index = index
break
if controlport_index is not None:
cls.platform_config.pop(controlport_index)
@classmethod @classmethod
def configurations(cls) -> List[Configuration]: def configurations(cls) -> List[Configuration]:
""" """
@ -77,7 +100,9 @@ class EmaneModel(WirelessModel):
:return: all configurations :return: all configurations
""" """
return cls.mac_config + cls.phy_config + cls.external_config return (
cls.platform_config + cls.mac_config + cls.phy_config + cls.external_config
)
@classmethod @classmethod
def config_groups(cls) -> List[ConfigGroup]: def config_groups(cls) -> List[ConfigGroup]:
@ -86,11 +111,13 @@ class EmaneModel(WirelessModel):
:return: list of configuration groups. :return: list of configuration groups.
""" """
mac_len = len(cls.mac_config) platform_len = len(cls.platform_config)
mac_len = len(cls.mac_config) + platform_len
phy_len = len(cls.phy_config) + mac_len phy_len = len(cls.phy_config) + mac_len
config_len = len(cls.configurations()) config_len = len(cls.configurations())
return [ return [
ConfigGroup("MAC Parameters", 1, mac_len), ConfigGroup("Platform Parameters", 1, platform_len),
ConfigGroup("MAC Parameters", platform_len + 1, mac_len),
ConfigGroup("PHY Parameters", mac_len + 1, phy_len), ConfigGroup("PHY Parameters", mac_len + 1, phy_len),
ConfigGroup("External Parameters", phy_len + 1, config_len), ConfigGroup("External Parameters", phy_len + 1, config_len),
] ]
@ -110,13 +137,14 @@ class EmaneModel(WirelessModel):
emanexml.create_phy_xml(self, iface, config) emanexml.create_phy_xml(self, iface, config)
emanexml.create_transport_xml(iface, config) emanexml.create_transport_xml(iface, config)
def post_startup(self) -> None: def post_startup(self, iface: CoreInterface) -> None:
""" """
Logic to execute after the emane manager is finished with startup. Logic to execute after the emane manager is finished with startup.
:param iface: interface for post startup
:return: nothing :return: nothing
""" """
logging.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:
""" """
@ -128,10 +156,9 @@ class EmaneModel(WirelessModel):
:return: nothing :return: nothing
""" """
try: try:
emane_net = self.session.get_node(self.id, EmaneNet) self.session.emane.set_nem_positions(moved_ifaces)
emane_net.setnempositions(moved_ifaces)
except CoreError: except CoreError:
logging.exception("error during update") logger.exception("error during update")
def linkconfig( def linkconfig(
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
@ -144,4 +171,4 @@ class EmaneModel(WirelessModel):
:param iface2: interface two :param iface2: interface two
:return: nothing :return: nothing
""" """
logging.warning("emane model(%s) does not support link config", self.name) logger.warning("emane model(%s) does not support link config", self.name)

View file

@ -6,10 +6,13 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from lxml import etree from lxml import etree
from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData from core.emulator.data import LinkData
from core.emulator.enumerations import LinkTypes, MessageFlags from core.emulator.enumerations import LinkTypes, MessageFlags
from core.nodes.network import CtrlNet from core.nodes.network import CtrlNet
logger = logging.getLogger(__name__)
try: try:
from emane import shell from emane import shell
except ImportError: except ImportError:
@ -17,12 +20,11 @@ except ImportError:
from emanesh import shell from emanesh import shell
except ImportError: except ImportError:
shell = None shell = None
logging.debug("compatible emane python bindings not installed") logger.debug("compatible emane python bindings not installed")
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emane.emanemanager import EmaneManager from core.emane.emanemanager import EmaneManager
DEFAULT_PORT: int = 47_000
MAC_COMPONENT_INDEX: int = 1 MAC_COMPONENT_INDEX: int = 1
EMANE_RFPIPE: str = "rfpipemaclayer" EMANE_RFPIPE: str = "rfpipemaclayer"
EMANE_80211: str = "ieee80211abgmaclayer" EMANE_80211: str = "ieee80211abgmaclayer"
@ -77,10 +79,10 @@ class EmaneLink:
class EmaneClient: class EmaneClient:
def __init__(self, address: str) -> None: def __init__(self, address: str, port: int) -> None:
self.address: str = address self.address: str = address
self.client: shell.ControlPortClient = shell.ControlPortClient( self.client: shell.ControlPortClient = shell.ControlPortClient(
self.address, DEFAULT_PORT self.address, port
) )
self.nems: Dict[int, LossTable] = {} self.nems: Dict[int, LossTable] = {}
self.setup() self.setup()
@ -91,7 +93,7 @@ class EmaneClient:
# get mac config # get mac config
mac_id, _, emane_model = components[MAC_COMPONENT_INDEX] mac_id, _, emane_model = components[MAC_COMPONENT_INDEX]
mac_config = self.client.getConfiguration(mac_id) mac_config = self.client.getConfiguration(mac_id)
logging.debug( logger.debug(
"address(%s) nem(%s) emane(%s)", self.address, nem_id, emane_model "address(%s) nem(%s) emane(%s)", self.address, nem_id, emane_model
) )
@ -101,9 +103,9 @@ class EmaneClient:
elif emane_model == EMANE_RFPIPE: elif emane_model == EMANE_RFPIPE:
loss_table = self.handle_rfpipe(mac_config) loss_table = self.handle_rfpipe(mac_config)
else: else:
logging.warning("unknown emane link model: %s", emane_model) logger.warning("unknown emane link model: %s", emane_model)
continue continue
logging.info("monitoring links nem(%s) model(%s)", nem_id, emane_model) logger.info("monitoring links nem(%s) model(%s)", nem_id, emane_model)
loss_table.mac_id = mac_id loss_table.mac_id = mac_id
self.nems[nem_id] = loss_table self.nems[nem_id] = loss_table
@ -138,12 +140,12 @@ class EmaneClient:
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]
logging.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]
logging.debug("80211 pcr: %s", pcr) logger.debug("80211 pcr: %s", pcr)
tree = etree.parse(pcr) tree = etree.parse(pcr)
root = tree.getroot() root = tree.getroot()
table = root.find("table") table = root.find("table")
@ -159,7 +161,7 @@ class EmaneClient:
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]
logging.debug("rfpipe pcr: %s", pcr) logger.debug("rfpipe pcr: %s", pcr)
tree = etree.parse(pcr) tree = etree.parse(pcr)
root = tree.getroot() root = tree.getroot()
table = root.find("table") table = root.find("table")
@ -187,12 +189,13 @@ class EmaneLinkMonitor:
self.running: bool = False self.running: bool = False
def start(self) -> None: def start(self) -> None:
self.loss_threshold = int(self.emane_manager.get_config("loss_threshold")) options = self.emane_manager.session.options
self.link_interval = int(self.emane_manager.get_config("link_interval")) self.loss_threshold = options.get_config_int("loss_threshold")
self.link_timeout = int(self.emane_manager.get_config("link_timeout")) self.link_interval = options.get_config_int("link_interval")
self.link_timeout = options.get_config_int("link_timeout")
self.initialize() self.initialize()
if not self.clients: if not self.clients:
logging.info("no valid emane models to monitor links") logger.info("no valid emane models to monitor links")
return return
self.scheduler = sched.scheduler() self.scheduler = sched.scheduler()
self.scheduler.enter(0, 0, self.check_links) self.scheduler.enter(0, 0, self.check_links)
@ -202,22 +205,28 @@ class EmaneLinkMonitor:
def initialize(self) -> None: def initialize(self) -> None:
addresses = self.get_addresses() addresses = self.get_addresses()
for address in addresses: for address, port in addresses:
client = EmaneClient(address) client = EmaneClient(address, port)
if client.nems: if client.nems:
self.clients.append(client) self.clients.append(client)
def get_addresses(self) -> List[str]: 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:
control = None
ports = []
for iface in node.get_ifaces(): for iface in node.get_ifaces():
if isinstance(iface.net, CtrlNet): if isinstance(iface.net, CtrlNet):
ip4 = iface.get_ip4() ip4 = iface.get_ip4()
if ip4: if ip4:
address = str(ip4.ip) control = str(ip4.ip)
addresses.append(address) if isinstance(iface.net, EmaneNet):
break port = self.emane_manager.get_nem_port(iface)
ports.append(port)
if control:
for port in ports:
addresses.append((control, port))
return addresses return addresses
def check_links(self) -> None: def check_links(self) -> None:
@ -228,7 +237,7 @@ class EmaneLinkMonitor:
client.check_links(self.links, self.loss_threshold) client.check_links(self.links, self.loss_threshold)
except shell.ControlPortException: except shell.ControlPortException:
if self.running: if self.running:
logging.exception("link monitor error") logger.exception("link monitor error")
# find new links # find new links
current_links = set(self.links.keys()) current_links = set(self.links.keys())

View file

@ -0,0 +1,70 @@
import logging
import pkgutil
from pathlib import Path
from typing import Dict, List, Type
from core import utils
from core.emane import models as emane_models
from core.emane.emanemodel import EmaneModel
from core.errors import CoreError
logger = logging.getLogger(__name__)
class EmaneModelManager:
models: Dict[str, Type[EmaneModel]] = {}
@classmethod
def load_locals(cls, emane_prefix: Path) -> List[str]:
"""
Load local core emane models and make them available.
:param emane_prefix: installed emane prefix
:return: list of errors encountered loading emane models
"""
errors = []
for module_info in pkgutil.walk_packages(
emane_models.__path__, f"{emane_models.__name__}."
):
models = utils.load_module(module_info.name, EmaneModel)
for model in models:
logger.debug("loading emane model: %s", model.name)
try:
model.load(emane_prefix)
cls.models[model.name] = model
except CoreError as e:
errors.append(model.name)
logger.debug("not loading emane model(%s): %s", model.name, e)
return errors
@classmethod
def load(cls, path: Path, emane_prefix: Path) -> List[str]:
"""
Search and load custom emane models and make them available.
:param path: path to search for custom emane models
:param emane_prefix: installed emane prefix
:return: list of errors encountered loading emane models
"""
subdirs = [x for x in path.iterdir() if x.is_dir()]
subdirs.append(path)
errors = []
for subdir in subdirs:
logger.debug("loading emane models from: %s", subdir)
models = utils.load_classes(subdir, EmaneModel)
for model in models:
logger.debug("loading emane model: %s", model.name)
try:
model.load(emane_prefix)
cls.models[model.name] = model
except CoreError as e:
errors.append(model.name)
logger.debug("not loading emane model(%s): %s", model.name, e)
return errors
@classmethod
def get(cls, name: str) -> Type[EmaneModel]:
model = cls.models.get(name)
if model is None:
raise CoreError(f"emame model does not exist {name}")
return model

View file

View file

@ -1,11 +1,11 @@
""" """
EMANE Bypass model for CORE EMANE Bypass model for CORE
""" """
from pathlib import Path
from typing import List, Set from typing import List, Set
from core.config import Configuration from core.config import ConfigBool, Configuration
from core.emane import emanemodel from core.emane import emanemodel
from core.emulator.enumerations import ConfigDataTypes
class EmaneBypassModel(emanemodel.EmaneModel): class EmaneBypassModel(emanemodel.EmaneModel):
@ -17,9 +17,8 @@ class EmaneBypassModel(emanemodel.EmaneModel):
# mac definitions # mac definitions
mac_library: str = "bypassmaclayer" mac_library: str = "bypassmaclayer"
mac_config: List[Configuration] = [ mac_config: List[Configuration] = [
Configuration( ConfigBool(
_id="none", id="none",
_type=ConfigDataTypes.BOOL,
default="0", default="0",
label="There are no parameters for the bypass model.", label="There are no parameters for the bypass model.",
) )
@ -30,6 +29,5 @@ class EmaneBypassModel(emanemodel.EmaneModel):
phy_config: List[Configuration] = [] phy_config: List[Configuration] = []
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: Path) -> None:
# ignore default logic cls._load_platform_config(emane_prefix)
pass

View file

@ -3,7 +3,7 @@ commeffect.py: EMANE CommEffect model for CORE
""" """
import logging import logging
import os from pathlib import Path
from typing import Dict, List from typing import Dict, List
from lxml import etree from lxml import etree
@ -14,6 +14,8 @@ from core.emulator.data import LinkOptions
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.xml import emanexml from core.xml import emanexml
logger = logging.getLogger(__name__)
try: try:
from emane.events.commeffectevent import CommEffectEvent from emane.events.commeffectevent import CommEffectEvent
except ImportError: except ImportError:
@ -21,7 +23,7 @@ except ImportError:
from emanesh.events.commeffectevent import CommEffectEvent from emanesh.events.commeffectevent import CommEffectEvent
except ImportError: except ImportError:
CommEffectEvent = None CommEffectEvent = None
logging.debug("compatible emane python bindings not installed") logger.debug("compatible emane python bindings not installed")
def convert_none(x: float) -> int: def convert_none(x: float) -> int:
@ -48,17 +50,26 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
external_config: List[Configuration] = [] external_config: List[Configuration] = []
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: Path) -> None:
shim_xml_path = os.path.join(emane_prefix, "share/emane/manifest", cls.shim_xml) cls._load_platform_config(emane_prefix)
shim_xml_path = emane_prefix / "share/emane/manifest" / cls.shim_xml
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.config_shim return cls.platform_config + cls.config_shim
@classmethod @classmethod
def config_groups(cls) -> List[ConfigGroup]: def config_groups(cls) -> List[ConfigGroup]:
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))] platform_len = len(cls.platform_config)
return [
ConfigGroup("Platform Parameters", 1, platform_len),
ConfigGroup(
"CommEffect SHIM Parameters",
platform_len + 1,
len(cls.configurations()),
),
]
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None: def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
""" """
@ -111,21 +122,15 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
Generate CommEffect events when a Link Message is received having Generate CommEffect events when a Link Message is received having
link parameters. link parameters.
""" """
service = self.session.emane.service
if service is None:
logging.warning("%s: EMANE event service unavailable", self.name)
return
if iface is None or iface2 is None: if iface is None or iface2 is None:
logging.warning("%s: missing NEM information", self.name) logger.warning("%s: missing NEM information", self.name)
return return
# TODO: batch these into multiple events per transmission # TODO: batch these into multiple events per transmission
# TODO: may want to split out seconds portion of delay and jitter # TODO: may want to split out seconds portion of delay and jitter
event = CommEffectEvent() event = CommEffectEvent()
nem1 = self.session.emane.get_nem_id(iface) nem1 = self.session.emane.get_nem_id(iface)
nem2 = self.session.emane.get_nem_id(iface2) nem2 = self.session.emane.get_nem_id(iface2)
logging.info("sending comm effect event") logger.info("sending comm effect event")
event.append( event.append(
nem1, nem1,
latency=convert_none(options.delay), latency=convert_none(options.delay),
@ -135,4 +140,4 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
unicast=int(convert_none(options.bandwidth)), unicast=int(convert_none(options.bandwidth)),
broadcast=int(convert_none(options.bandwidth)), broadcast=int(convert_none(options.bandwidth)),
) )
service.publish(nem2, event) self.session.emane.publish_event(nem2, event)

View file

@ -1,7 +1,7 @@
""" """
ieee80211abg.py: EMANE IEEE 802.11abg model for CORE ieee80211abg.py: EMANE IEEE 802.11abg model for CORE
""" """
import os from pathlib import Path
from core.emane import emanemodel from core.emane import emanemodel
@ -15,8 +15,8 @@ class EmaneIeee80211abgModel(emanemodel.EmaneModel):
mac_xml: str = "ieee80211abgmaclayer.xml" mac_xml: str = "ieee80211abgmaclayer.xml"
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: Path) -> None:
cls.mac_defaults["pcrcurveuri"] = os.path.join( cls.mac_defaults["pcrcurveuri"] = str(
emane_prefix, "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml" emane_prefix / "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml"
) )
super().load(emane_prefix) super().load(emane_prefix)

View file

@ -1,7 +1,7 @@
""" """
rfpipe.py: EMANE RF-PIPE model for CORE rfpipe.py: EMANE RF-PIPE model for CORE
""" """
import os from pathlib import Path
from core.emane import emanemodel from core.emane import emanemodel
@ -15,8 +15,8 @@ class EmaneRfPipeModel(emanemodel.EmaneModel):
mac_xml: str = "rfpipemaclayer.xml" mac_xml: str = "rfpipemaclayer.xml"
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: Path) -> None:
cls.mac_defaults["pcrcurveuri"] = os.path.join( cls.mac_defaults["pcrcurveuri"] = str(
emane_prefix, "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" emane_prefix / "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
) )
super().load(emane_prefix) super().load(emane_prefix)

View file

@ -0,0 +1,66 @@
"""
tdma.py: EMANE TDMA model bindings for CORE
"""
import logging
from pathlib import Path
from typing import Set
from core import constants, utils
from core.config import ConfigString
from core.emane import emanemodel
from core.emane.nodes import EmaneNet
from core.nodes.interface import CoreInterface
logger = logging.getLogger(__name__)
class EmaneTdmaModel(emanemodel.EmaneModel):
# model name
name: str = "emane_tdma"
# mac configuration
mac_library: str = "tdmaeventschedulerradiomodel"
mac_xml: str = "tdmaeventschedulerradiomodel.xml"
# add custom schedule options and ignore it when writing emane xml
schedule_name: str = "schedule"
default_schedule: Path = (
constants.CORE_DATA_DIR / "examples" / "tdma" / "schedule.xml"
)
config_ignore: Set[str] = {schedule_name}
@classmethod
def load(cls, emane_prefix: Path) -> None:
cls.mac_defaults["pcrcurveuri"] = str(
emane_prefix
/ "share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml"
)
super().load(emane_prefix)
config_item = ConfigString(
id=cls.schedule_name,
default=str(cls.default_schedule),
label="TDMA schedule file (core)",
)
cls.mac_config.insert(0, config_item)
def post_startup(self, iface: CoreInterface) -> None:
# get configured schedule
emane_net = self.session.get_node(self.id, EmaneNet)
config = self.session.emane.get_iface_config(emane_net, iface)
schedule = Path(config[self.schedule_name])
if not schedule.is_file():
logger.error("ignoring invalid tdma schedule: %s", schedule)
return
# initiate tdma schedule
nem_id = self.session.emane.get_nem_id(iface)
if not nem_id:
logger.error("could not find nem for interface")
return
service = self.session.emane.nem_service.get(nem_id)
if service:
device = service.device
logger.info(
"setting up tdma schedule: schedule(%s) device(%s)", schedule, device
)
utils.cmd(f"emaneevent-tdmaschedule -i {device} {schedule}")

View file

@ -4,7 +4,7 @@ share the same MAC+PHY model.
""" """
import logging import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type from typing import TYPE_CHECKING, Dict, List, Optional, Type
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
@ -19,6 +19,8 @@ from core.errors import CoreError
from core.nodes.base import CoreNetworkBase, CoreNode from core.nodes.base import CoreNetworkBase, CoreNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emane.emanemodel import EmaneModel from core.emane.emanemodel import EmaneModel
from core.emulator.session import Session from core.emulator.session import Session
@ -34,7 +36,7 @@ except ImportError:
from emanesh.events import LocationEvent from emanesh.events import LocationEvent
except ImportError: except ImportError:
LocationEvent = None LocationEvent = None
logging.debug("compatible emane python bindings not installed") logger.debug("compatible emane python bindings not installed")
class EmaneNet(CoreNetworkBase): class EmaneNet(CoreNetworkBase):
@ -92,9 +94,7 @@ class EmaneNet(CoreNetworkBase):
def updatemodel(self, config: Dict[str, str]) -> None: def updatemodel(self, config: Dict[str, str]) -> None:
if not self.model: if not self.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})")
logging.info( logger.info("node(%s) updating model(%s): %s", self.id, self.model.name, config)
"node(%s) updating model(%s): %s", self.id, self.model.name, config
)
self.model.update_config(config) self.model.update_config(config)
def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None: def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None:
@ -110,67 +110,6 @@ 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 _nem_position(
self, iface: CoreInterface
) -> Optional[Tuple[int, float, float, float]]:
"""
Creates nem position for emane event for a given interface.
:param iface: interface to get nem emane position for
:return: nem position tuple, None otherwise
"""
nem_id = self.session.emane.get_nem_id(iface)
ifname = iface.localname
if nem_id is None:
logging.info("nemid for %s is unknown", ifname)
return
node = iface.node
x, y, z = node.getposition()
lat, lon, alt = self.session.location.getgeo(x, y, z)
if node.position.alt is not None:
alt = node.position.alt
node.position.set_geo(lon, lat, alt)
# altitude must be an integer or warning is printed
alt = int(round(alt))
return nem_id, lon, lat, alt
def setnemposition(self, iface: CoreInterface) -> None:
"""
Publish a NEM location change event using the EMANE event service.
:param iface: interface to set nem position for
"""
if self.session.emane.service is None:
logging.info("position service not available")
return
position = self._nem_position(iface)
if position:
nemid, lon, lat, alt = position
event = LocationEvent()
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.session.emane.service.publish(0, event)
def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None:
"""
Several NEMs have moved, from e.g. a WaypointMobilityModel
calculation. Generate an EMANE Location Event having several
entries for each interface that has moved.
"""
if len(moved_ifaces) == 0:
return
if self.session.emane.service is None:
logging.info("position service not available")
return
event = LocationEvent()
for iface in moved_ifaces:
position = self._nem_position(iface)
if position:
nemid, lon, lat, alt = position
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.session.emane.service.publish(0, event)
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
links = super().links(flags) links = super().links(flags)
emane_manager = self.session.emane emane_manager = self.session.emane

View file

@ -1,67 +0,0 @@
"""
tdma.py: EMANE TDMA model bindings for CORE
"""
import logging
import os
from typing import Set
from core import constants, utils
from core.config import Configuration
from core.emane import emanemodel
from core.emulator.enumerations import ConfigDataTypes
class EmaneTdmaModel(emanemodel.EmaneModel):
# model name
name: str = "emane_tdma"
# mac configuration
mac_library: str = "tdmaeventschedulerradiomodel"
mac_xml: str = "tdmaeventschedulerradiomodel.xml"
# add custom schedule options and ignore it when writing emane xml
schedule_name: str = "schedule"
default_schedule: str = os.path.join(
constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml"
)
config_ignore: Set[str] = {schedule_name}
@classmethod
def load(cls, emane_prefix: str) -> None:
cls.mac_defaults["pcrcurveuri"] = os.path.join(
emane_prefix,
"share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml",
)
super().load(emane_prefix)
cls.mac_config.insert(
0,
Configuration(
_id=cls.schedule_name,
_type=ConfigDataTypes.STRING,
default=cls.default_schedule,
label="TDMA schedule file (core)",
),
)
def post_startup(self) -> None:
"""
Logic to execute after the emane manager is finished with startup.
:return: nothing
"""
# get configured schedule
config = self.session.emane.get_configs(node_id=self.id, config_type=self.name)
if not config:
return
schedule = config[self.schedule_name]
# get the set event device
event_device = self.session.emane.event_device
# initiate tdma schedule
logging.info(
"setting up tdma schedule: schedule(%s) device(%s)", schedule, event_device
)
args = f"emaneevent-tdmaschedule -i {event_device} {schedule}"
utils.cmd(args)

View file

@ -3,15 +3,21 @@ import logging
import os import os
import signal import signal
import sys import sys
from pathlib import Path
from typing import Dict, List, Type from typing import Dict, List, Type
import core.services import core.services
from core import configservices, utils from core import utils
from core.configservice.manager import ConfigServiceManager from core.configservice.manager import ConfigServiceManager
from core.emane.modelmanager import EmaneModelManager
from core.emulator.session import Session from core.emulator.session import Session
from core.executables import get_requirements from core.executables import get_requirements
from core.services.coreservices import ServiceManager from core.services.coreservices import ServiceManager
logger = logging.getLogger(__name__)
DEFAULT_EMANE_PREFIX: str = "/usr"
def signal_handler(signal_number: int, _) -> None: def signal_handler(signal_number: int, _) -> None:
""" """
@ -21,7 +27,7 @@ def signal_handler(signal_number: int, _) -> None:
:param _: ignored :param _: ignored
:return: nothing :return: nothing
""" """
logging.info("caught signal: %s", signal_number) logger.info("caught signal: %s", signal_number)
sys.exit(signal_number) sys.exit(signal_number)
@ -47,8 +53,7 @@ class CoreEmu:
os.umask(0) os.umask(0)
# configuration # configuration
if config is None: config = config if config else {}
config = {}
self.config: Dict[str, str] = config self.config: Dict[str, str] = config
# session management # session management
@ -56,15 +61,12 @@ class CoreEmu:
# load services # load services
self.service_errors: List[str] = [] self.service_errors: List[str] = []
self.load_services()
# config services
self.service_manager: ConfigServiceManager = ConfigServiceManager() self.service_manager: ConfigServiceManager = ConfigServiceManager()
config_services_path = os.path.abspath(os.path.dirname(configservices.__file__)) self._load_services()
self.service_manager.load(config_services_path)
custom_dir = self.config.get("custom_config_services_dir") # check and load emane
if custom_dir: self.has_emane: bool = False
self.service_manager.load(custom_dir) self._load_emane()
# check executables exist on path # check executables exist on path
self._validate_env() self._validate_env()
@ -83,7 +85,7 @@ class CoreEmu:
for requirement in get_requirements(use_ovs): for requirement in get_requirements(use_ovs):
utils.which(requirement, required=True) utils.which(requirement, required=True)
def load_services(self) -> None: def _load_services(self) -> None:
""" """
Loads default and custom services for use within CORE. Loads default and custom services for use within CORE.
@ -91,15 +93,46 @@ class CoreEmu:
""" """
# load default services # load default services
self.service_errors = core.services.load() self.service_errors = core.services.load()
# load custom services # load custom services
service_paths = self.config.get("custom_services_dir") service_paths = self.config.get("custom_services_dir")
logging.debug("custom service paths: %s", service_paths) logger.debug("custom service paths: %s", service_paths)
if service_paths: if service_paths is not None:
for service_path in service_paths.split(","): for service_path in service_paths.split(","):
service_path = service_path.strip() service_path = Path(service_path.strip())
custom_service_errors = ServiceManager.add_services(service_path) custom_service_errors = ServiceManager.add_services(service_path)
self.service_errors.extend(custom_service_errors) self.service_errors.extend(custom_service_errors)
# load default config services
self.service_manager.load_locals()
# load custom config services
custom_dir = self.config.get("custom_config_services_dir")
if custom_dir is not None:
custom_dir = Path(custom_dir)
self.service_manager.load(custom_dir)
def _load_emane(self) -> None:
"""
Check if emane is installed and load models.
:return: nothing
"""
# check for emane
path = utils.which("emane", required=False)
self.has_emane = path is not None
if not self.has_emane:
logger.info("emane is not installed, emane functionality disabled")
return
# get version
emane_version = utils.cmd("emane --version")
logger.info("using emane: %s", emane_version)
emane_prefix = self.config.get("emane_prefix", DEFAULT_EMANE_PREFIX)
emane_prefix = Path(emane_prefix)
EmaneModelManager.load_locals(emane_prefix)
# load custom models
custom_path = self.config.get("emane_models_dir")
if custom_path is not None:
logger.info("loading custom emane models: %s", custom_path)
custom_path = Path(custom_path)
EmaneModelManager.load(custom_path, emane_prefix)
def shutdown(self) -> None: def shutdown(self) -> None:
""" """
@ -107,7 +140,7 @@ class CoreEmu:
:return: nothing :return: nothing
""" """
logging.info("shutting down all sessions") logger.info("shutting down all sessions")
sessions = self.sessions.copy() sessions = self.sessions.copy()
self.sessions.clear() self.sessions.clear()
for _id in sessions: for _id in sessions:
@ -128,7 +161,7 @@ class CoreEmu:
_id += 1 _id += 1
session = _cls(_id, config=self.config) session = _cls(_id, config=self.config)
session.service_manager = self.service_manager session.service_manager = self.service_manager
logging.info("created session: %s", _id) logger.info("created session: %s", _id)
self.sessions[_id] = session self.sessions[_id] = session
return session return session
@ -139,14 +172,14 @@ class CoreEmu:
:param _id: session id to delete :param _id: session id to delete
:return: True if deleted, False otherwise :return: True if deleted, False otherwise
""" """
logging.info("deleting session: %s", _id) logger.info("deleting session: %s", _id)
session = self.sessions.pop(_id, None) session = self.sessions.pop(_id, None)
result = False result = False
if session: if session:
logging.info("shutting session down: %s", _id) logger.info("shutting session down: %s", _id)
session.data_collect() session.data_collect()
session.shutdown() session.shutdown()
result = True result = True
else: else:
logging.error("session to delete did not exist: %s", _id) logger.error("session to delete did not exist: %s", _id)
return result return result

View file

@ -91,6 +91,7 @@ class NodeOptions:
server: str = None server: str = None
image: str = None image: str = None
emane: str = None emane: str = None
legacy: bool = False
def set_position(self, x: float, y: float) -> None: def set_position(self, x: float, y: float) -> None:
""" """
@ -141,6 +142,7 @@ class InterfaceData:
ip4_mask: int = None ip4_mask: int = None
ip6: str = None ip6: str = None
ip6_mask: int = None ip6_mask: int = None
mtu: int = None
def get_ips(self) -> List[str]: def get_ips(self) -> List[str]:
""" """

View file

@ -6,6 +6,7 @@ import logging
import os import os
import threading import threading
from collections import OrderedDict from collections import OrderedDict
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, Dict, Tuple
@ -19,6 +20,8 @@ from core.executables import get_requirements
from core.nodes.interface import GreTap from core.nodes.interface import GreTap
from core.nodes.network import CoreNetwork, CtrlNet from core.nodes.network import CoreNetwork, CtrlNet
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
@ -61,7 +64,7 @@ class DistributedServer:
replace_env = env is not None replace_env = env is not None
if not wait: if not wait:
cmd += " &" cmd += " &"
logging.debug( logger.debug(
"remote cmd server(%s) cwd(%s) wait(%s): %s", self.host, cwd, wait, cmd "remote cmd server(%s) cwd(%s) wait(%s): %s", self.host, cwd, wait, cmd
) )
try: try:
@ -79,23 +82,23 @@ class DistributedServer:
stdout, stderr = e.streams_for_display() stdout, stderr = e.streams_for_display()
raise CoreCommandError(e.result.exited, cmd, stdout, stderr) raise CoreCommandError(e.result.exited, cmd, stdout, stderr)
def remote_put(self, source: str, destination: str) -> None: def remote_put(self, src_path: Path, dst_path: Path) -> None:
""" """
Push file to remote server. Push file to remote server.
:param source: source file to push :param src_path: source file to push
:param destination: destination file location :param dst_path: destination file location
:return: nothing :return: nothing
""" """
with self.lock: with self.lock:
self.conn.put(source, destination) self.conn.put(str(src_path), str(dst_path))
def remote_put_temp(self, destination: str, data: str) -> None: def remote_put_temp(self, dst_path: Path, data: str) -> None:
""" """
Remote push file contents to a remote server, using a temp file as an Remote push file contents to a remote server, using a temp file as an
intermediate step. intermediate step.
:param destination: file destination for data :param dst_path: file destination for data
:param data: data to store in remote file :param data: data to store in remote file
:return: nothing :return: nothing
""" """
@ -103,7 +106,7 @@ class DistributedServer:
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
temp.write(data.encode("utf-8")) temp.write(data.encode("utf-8"))
temp.close() temp.close()
self.conn.put(temp.name, destination) self.conn.put(temp.name, str(dst_path))
os.unlink(temp.name) os.unlink(temp.name)
@ -144,7 +147,7 @@ class DistributedController:
f"command({requirement})" f"command({requirement})"
) )
self.servers[name] = server self.servers[name] = server
cmd = f"mkdir -p {self.session.session_dir}" cmd = f"mkdir -p {self.session.directory}"
server.remote_cmd(cmd) server.remote_cmd(cmd)
def execute(self, func: Callable[[DistributedServer], None]) -> None: def execute(self, func: Callable[[DistributedServer], None]) -> None:
@ -170,13 +173,11 @@ class DistributedController:
tunnels = self.tunnels[key] tunnels = self.tunnels[key]
for tunnel in tunnels: for tunnel in tunnels:
tunnel.shutdown() tunnel.shutdown()
# remove all remote session directories # remove all remote session directories
for name in self.servers: for name in self.servers:
server = self.servers[name] server = self.servers[name]
cmd = f"rm -rf {self.session.session_dir}" cmd = f"rm -rf {self.session.directory}"
server.remote_cmd(cmd) server.remote_cmd(cmd)
# clear tunnels # clear tunnels
self.tunnels.clear() self.tunnels.clear()
@ -186,6 +187,7 @@ class DistributedController:
:return: nothing :return: nothing
""" """
mtu = self.session.options.get_config_int("mtu")
for node_id in self.session.nodes: for node_id in self.session.nodes:
node = self.session.nodes[node_id] node = self.session.nodes[node_id]
if not isinstance(node, CoreNetwork): if not isinstance(node, CoreNetwork):
@ -194,17 +196,18 @@ class DistributedController:
continue continue
for name in self.servers: for name in self.servers:
server = self.servers[name] server = self.servers[name]
self.create_gre_tunnel(node, server) self.create_gre_tunnel(node, server, mtu, True)
def create_gre_tunnel( def create_gre_tunnel(
self, node: CoreNetwork, server: DistributedServer 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.
:param node: node to create gre tunnel for :param node: node to create gre tunnel for
:param server: server to create :param server: server to create tunnel for
tunnel for :param mtu: mtu for gre taps
:param start: True to start gre taps, False otherwise
:return: local and remote gre taps created for tunnel :return: local and remote gre taps created for tunnel
""" """
host = server.host host = server.host
@ -212,23 +215,20 @@ class DistributedController:
tunnel = self.tunnels.get(key) tunnel = self.tunnels.get(key)
if tunnel is not None: if tunnel is not None:
return tunnel return tunnel
# local to server # local to server
logging.info( logger.info("local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key)
"local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key local_tap = GreTap(self.session, host, key=key, mtu=mtu)
) if start:
local_tap = GreTap(session=self.session, remoteip=host, key=key) local_tap.startup()
local_tap.net_client.set_iface_master(node.brname, local_tap.localname) local_tap.net_client.set_iface_master(node.brname, local_tap.localname)
# server to local # server to local
logging.info( logger.info(
"remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key "remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key
) )
remote_tap = GreTap( remote_tap = GreTap(self.session, self.address, key=key, server=server, mtu=mtu)
session=self.session, remoteip=self.address, key=key, server=server if start:
) remote_tap.startup()
remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname) remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
# save tunnels for shutdown # save tunnels for shutdown
tunnel = (local_tap, remote_tap) tunnel = (local_tap, remote_tap)
self.tunnels[key] = tunnel self.tunnels[key] = tunnel
@ -244,7 +244,7 @@ class DistributedController:
:param node2_id: node two id :param node2_id: node two id
:return: tunnel key for the node pair :return: tunnel key for the node pair
""" """
logging.debug("creating tunnel key for: %s, %s", node1_id, node2_id) logger.debug("creating tunnel key for: %s, %s", node1_id, node2_id)
key = ( key = (
(self.session.id << 16) (self.session.id << 16)
^ utils.hashkey(node1_id) ^ utils.hashkey(node1_id)

View file

@ -4,6 +4,7 @@ that manages a CORE session.
""" """
import logging import logging
import math
import os import os
import pwd import pwd
import shutil import shutil
@ -45,7 +46,7 @@ from core.location.geo import GeoLocation
from core.location.mobility import BasicRangeModel, MobilityManager from core.location.mobility import BasicRangeModel, MobilityManager
from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode from core.nodes.docker import DockerNode
from core.nodes.interface import CoreInterface from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.lxd import LxcNode from core.nodes.lxd import LxcNode
from core.nodes.network import ( from core.nodes.network import (
CtrlNet, CtrlNet,
@ -62,6 +63,8 @@ from core.services.coreservices import CoreServices
from core.xml import corexml, corexmldeployment from core.xml import corexml, corexmldeployment
from core.xml.corexml import CoreXmlReader, CoreXmlWriter from core.xml.corexml import CoreXmlReader, CoreXmlWriter
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,
@ -103,13 +106,13 @@ class Session:
self.id: int = _id self.id: int = _id
# define and create session directory when desired # define and create session directory when desired
self.session_dir: str = os.path.join(tempfile.gettempdir(), f"pycore.{self.id}") self.directory: Path = Path(tempfile.gettempdir()) / f"pycore.{self.id}"
if mkdir: if mkdir:
os.mkdir(self.session_dir) self.directory.mkdir()
self.name: Optional[str] = None self.name: Optional[str] = None
self.file_name: Optional[str] = None self.file_path: Optional[Path] = None
self.thumbnail: Optional[str] = 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] = {}
@ -197,7 +200,7 @@ class Session:
:raises core.CoreError: when objects to link is less than 2, or no common :raises core.CoreError: when objects to link is less than 2, or no common
networks are found networks are found
""" """
logging.info( logger.info(
"handling wireless linking node1(%s) node2(%s): %s", "handling wireless linking node1(%s) node2(%s): %s",
node1.name, node1.name,
node2.name, node2.name,
@ -208,7 +211,7 @@ class Session:
raise CoreError("no common network found for wireless link/unlink") raise CoreError("no common network found for wireless link/unlink")
for common_network, iface1, iface2 in common_networks: for common_network, iface1, iface2 in common_networks:
if not isinstance(common_network, (WlanNode, EmaneNet)): if not isinstance(common_network, (WlanNode, EmaneNet)):
logging.info( logger.info(
"skipping common network that is not wireless/emane: %s", "skipping common network that is not wireless/emane: %s",
common_network, common_network,
) )
@ -250,7 +253,12 @@ class Session:
node2 = self.get_node(node2_id, NodeBase) node2 = self.get_node(node2_id, NodeBase)
iface1 = None iface1 = None
iface2 = None iface2 = None
# set mtu
mtu = self.options.get_config_int("mtu") or DEFAULT_MTU
if iface1_data:
iface1_data.mtu = mtu
if iface2_data:
iface2_data.mtu = mtu
# wireless link # wireless link
if link_type == LinkTypes.WIRELESS: if link_type == LinkTypes.WIRELESS:
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
@ -263,7 +271,7 @@ class Session:
else: else:
# peer to peer link # peer to peer link
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
logging.info("linking ptp: %s - %s", node1.name, node2.name) logger.info("linking ptp: %s - %s", node1.name, node2.name)
start = self.state.should_start() start = self.state.should_start()
ptp = self.create_node(PtpNet, start) ptp = self.create_node(PtpNet, start)
iface1 = node1.new_iface(ptp, iface1_data) iface1 = node1.new_iface(ptp, iface1_data)
@ -286,7 +294,7 @@ class Session:
elif isinstance(node1, CoreNetworkBase) and isinstance( elif isinstance(node1, CoreNetworkBase) and isinstance(
node2, CoreNetworkBase node2, CoreNetworkBase
): ):
logging.info( logger.info(
"linking network to network: %s - %s", node1.name, node2.name "linking network to network: %s - %s", node1.name, node2.name
) )
iface1 = node1.linknet(node2) iface1 = node1.linknet(node2)
@ -303,10 +311,10 @@ class Session:
# configure tunnel nodes # configure tunnel nodes
key = options.key key = options.key
if isinstance(node1, TunnelNode): if isinstance(node1, TunnelNode):
logging.info("setting tunnel key for: %s", node1.name) logger.info("setting tunnel key for: %s", node1.name)
node1.setkey(key, iface1_data) node1.setkey(key, iface1_data)
if isinstance(node2, TunnelNode): if isinstance(node2, TunnelNode):
logging.info("setting tunnel key for: %s", node2.name) logger.info("setting tunnel key for: %s", node2.name)
node2.setkey(key, iface2_data) node2.setkey(key, iface2_data)
self.sdt.add_link(node1_id, node2_id) self.sdt.add_link(node1_id, node2_id)
return iface1, iface2 return iface1, iface2
@ -332,7 +340,7 @@ class Session:
""" """
node1 = self.get_node(node1_id, NodeBase) node1 = self.get_node(node1_id, NodeBase)
node2 = self.get_node(node2_id, NodeBase) node2 = self.get_node(node2_id, NodeBase)
logging.info( logger.info(
"deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)", "deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)",
link_type.name, link_type.name,
node1.name, node1.name,
@ -409,7 +417,7 @@ class Session:
options = LinkOptions() options = LinkOptions()
node1 = self.get_node(node1_id, NodeBase) node1 = self.get_node(node1_id, NodeBase)
node2 = self.get_node(node2_id, NodeBase) node2 = self.get_node(node2_id, NodeBase)
logging.info( logger.info(
"update link(%s) node(%s):interface(%s) node(%s):interface(%s)", "update link(%s) node(%s):interface(%s) node(%s):interface(%s)",
link_type.name, link_type.name,
node1.name, node1.name,
@ -525,7 +533,7 @@ class Session:
raise CoreError(f"invalid distributed server: {options.server}") raise CoreError(f"invalid distributed server: {options.server}")
# create node # create node
logging.info( logger.info(
"creating node(%s) id(%s) name(%s) start(%s)", "creating node(%s) id(%s) name(%s) start(%s)",
_class.__name__, _class.__name__,
_id, _id,
@ -542,30 +550,40 @@ class Session:
node.canvas = options.canvas node.canvas = options.canvas
# set node position and broadcast it # set node position and broadcast it
self.set_node_position(node, options) has_geo = all(i is not None for i in [options.lon, options.lat, options.alt])
if has_geo:
self.set_node_geo(node, options.lon, options.lat, options.alt)
else:
self.set_node_pos(node, options.x, options.y)
# add services to needed nodes # add services to needed nodes
if isinstance(node, (CoreNode, PhysicalNode)): if isinstance(node, (CoreNode, PhysicalNode)):
node.type = options.model node.type = options.model
logging.debug("set node type: %s", node.type) if options.legacy or options.services:
self.services.add_services(node, node.type, options.services) logger.debug("set node type: %s", node.type)
self.services.add_services(node, node.type, options.services)
# add config services # add config services
logging.info("setting node config services: %s", options.config_services) config_services = options.config_services
for name in options.config_services: if not options.legacy and not config_services and not node.services:
config_services = self.services.default_services.get(node.type, [])
logger.info("setting node config services: %s", config_services)
for name in config_services:
service_class = self.service_manager.get_service(name) service_class = self.service_manager.get_service(name)
node.add_config_service(service_class) node.add_config_service(service_class)
# set network mtu, if configured
mtu = self.options.get_config_int("mtu")
if isinstance(node, CoreNetworkBase) and mtu > 0:
node.mtu = mtu
# ensure default emane configuration # ensure default emane configuration
if isinstance(node, EmaneNet) and options.emane: if isinstance(node, EmaneNet) and options.emane:
model = self.emane.models.get(options.emane) model_class = self.emane.get_model(options.emane)
if not model: node.model = model_class(self, node.id)
raise CoreError(
f"node({node.name}) emane model({options.emane}) does not exist"
)
node.model = model(self, node.id)
if self.state == EventTypes.RUNTIME_STATE: if self.state == EventTypes.RUNTIME_STATE:
self.emane.add_node(node) self.emane.add_node(node)
# set default wlan config if needed # set default wlan config if needed
if isinstance(node, WlanNode): if isinstance(node, WlanNode):
self.mobility.set_model_config(_id, BasicRangeModel.name) self.mobility.set_model_config(_id, BasicRangeModel.name)
@ -575,51 +593,26 @@ class Session:
if self.state == EventTypes.RUNTIME_STATE and is_boot_node: if self.state == EventTypes.RUNTIME_STATE and is_boot_node:
self.write_nodes() self.write_nodes()
self.add_remove_control_iface(node, remove=False) self.add_remove_control_iface(node, remove=False)
self.services.boot_services(node) self.boot_node(node)
self.sdt.add_node(node) self.sdt.add_node(node)
return node return node
def edit_node(self, node_id: int, options: NodeOptions) -> None: def set_node_pos(self, node: NodeBase, x: float, y: float) -> None:
""" node.setposition(x, y, None)
Edit node information. self.sdt.edit_node(
node, node.position.lon, node.position.lat, node.position.alt
)
:param node_id: id of node to update def set_node_geo(self, node: NodeBase, lon: float, lat: float, alt: float) -> None:
:param options: data to update node with x, y, _ = self.location.getxyz(lat, lon, alt)
:return: nothing if math.isinf(x) or math.isinf(y):
:raises core.CoreError: when node to update does not exist raise CoreError(
""" f"invalid geo for current reference/scale: {lon},{lat},{alt}"
node = self.get_node(node_id, NodeBase) )
node.icon = options.icon node.setposition(x, y, None)
self.set_node_position(node, options) node.position.set_geo(lon, lat, alt)
self.sdt.edit_node(node, options.lon, options.lat, options.alt) self.sdt.edit_node(node, lon, lat, alt)
def set_node_position(self, node: NodeBase, options: NodeOptions) -> None:
"""
Set position for a node, use lat/lon/alt if needed.
:param node: node to set position for
:param options: data for node
:return: nothing
"""
# extract location values
x = options.x
y = options.y
lat = options.lat
lon = options.lon
alt = options.alt
# check if we need to generate position from lat/lon/alt
has_empty_position = all(i is None for i in [x, y])
has_lat_lon_alt = all(i is not None for i in [lat, lon, alt])
using_lat_lon_alt = has_empty_position and has_lat_lon_alt
if using_lat_lon_alt:
x, y, _ = self.location.getxyz(lat, lon, alt)
node.setposition(x, y, None)
node.position.set_geo(lon, lat, alt)
self.broadcast_node(node)
elif not has_empty_position:
node.setposition(x, y, None)
def start_mobility(self, node_ids: List[int] = None) -> None: def start_mobility(self, node_ids: List[int] = None) -> None:
""" """
@ -638,45 +631,42 @@ class Session:
:return: True if active, False otherwise :return: True if active, False otherwise
""" """
result = self.state in {EventTypes.RUNTIME_STATE, EventTypes.DATACOLLECT_STATE} result = self.state in {EventTypes.RUNTIME_STATE, EventTypes.DATACOLLECT_STATE}
logging.info("session(%s) checking if active: %s", self.id, result) logger.info("session(%s) checking if active: %s", self.id, result)
return result return result
def open_xml(self, file_name: str, start: bool = False) -> None: def open_xml(self, file_path: Path, start: bool = False) -> None:
""" """
Import a session from the EmulationScript XML format. Import a session from the EmulationScript XML format.
:param file_name: xml file to load session from :param file_path: xml file to load session from
:param start: instantiate session if true, false otherwise :param start: instantiate session if true, false otherwise
:return: nothing :return: nothing
""" """
logging.info("opening xml: %s", file_name) logger.info("opening xml: %s", file_path)
# clear out existing session # clear out existing session
self.clear() self.clear()
# set state and read xml # set state and read xml
state = EventTypes.CONFIGURATION_STATE if start else EventTypes.DEFINITION_STATE state = EventTypes.CONFIGURATION_STATE if start else EventTypes.DEFINITION_STATE
self.set_state(state) self.set_state(state)
self.name = os.path.basename(file_name) self.name = file_path.name
self.file_name = file_name self.file_path = file_path
CoreXmlReader(self).read(file_name) CoreXmlReader(self).read(file_path)
# start session if needed # start session if needed
if start: if start:
self.set_state(EventTypes.INSTANTIATION_STATE) self.set_state(EventTypes.INSTANTIATION_STATE)
self.instantiate() self.instantiate()
def save_xml(self, file_name: str) -> None: def save_xml(self, file_path: Path) -> None:
""" """
Export a session to the EmulationScript XML format. Export a session to the EmulationScript XML format.
:param file_name: file name to write session xml to :param file_path: file name to write session xml to
:return: nothing :return: nothing
""" """
CoreXmlWriter(self).write(file_name) CoreXmlWriter(self).write(file_path)
def add_hook( def add_hook(
self, state: EventTypes, file_name: str, data: str, source_name: str = None self, state: EventTypes, file_name: str, data: str, src_name: str = None
) -> None: ) -> None:
""" """
Store a hook from a received file message. Store a hook from a received file message.
@ -684,11 +674,11 @@ class Session:
:param state: when to run hook :param state: when to run hook
:param file_name: file name for hook :param file_name: file name for hook
:param data: hook data :param data: hook data
:param source_name: source name :param src_name: source name
:return: nothing :return: nothing
""" """
logging.info( logger.info(
"setting state hook: %s - %s source(%s)", state, file_name, source_name "setting state hook: %s - %s source(%s)", state, file_name, src_name
) )
hook = file_name, data hook = file_name, data
state_hooks = self.hooks.setdefault(state, []) state_hooks = self.hooks.setdefault(state, [])
@ -696,26 +686,26 @@ class Session:
# immediately run a hook if it is in the current state # immediately run a hook if it is in the current state
if self.state == state: if self.state == state:
logging.info("immediately running new state hook") logger.info("immediately running new state hook")
self.run_hook(hook) self.run_hook(hook)
def add_node_file( def add_node_file(
self, node_id: int, source_name: str, file_name: str, data: str self, node_id: int, src_path: Path, file_path: Path, data: str
) -> None: ) -> None:
""" """
Add a file to a node. Add a file to a node.
:param node_id: node to add file to :param node_id: node to add file to
:param source_name: source file name :param src_path: source file path
:param file_name: file name to add :param file_path: file path to add
:param data: file data :param data: file data
:return: nothing :return: nothing
""" """
node = self.get_node(node_id, CoreNodeBase) node = self.get_node(node_id, CoreNode)
if source_name is not None: if src_path is not None:
node.addfile(source_name, file_name) node.addfile(src_path, file_path)
elif data is not None: elif data is not None:
node.nodefile(file_name, data) node.create_file(file_path, data)
def clear(self) -> None: def clear(self) -> None:
""" """
@ -769,9 +759,9 @@ class Session:
Shutdown all session nodes and remove the session directory. Shutdown all session nodes and remove the session directory.
""" """
if self.state == EventTypes.SHUTDOWN_STATE: if self.state == EventTypes.SHUTDOWN_STATE:
logging.info("session(%s) state(%s) already shutdown", self.id, self.state) logger.info("session(%s) state(%s) already shutdown", self.id, self.state)
else: else:
logging.info("session(%s) state(%s) shutting down", self.id, self.state) logger.info("session(%s) state(%s) shutting down", self.id, self.state)
self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True) self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True)
# clear out current core session # clear out current core session
self.clear() self.clear()
@ -780,7 +770,7 @@ class Session:
# remove this sessions working directory # remove this sessions working directory
preserve = self.options.get_config("preservedir") == "1" preserve = self.options.get_config("preservedir") == "1"
if not preserve: if not preserve:
shutil.rmtree(self.session_dir, ignore_errors=True) shutil.rmtree(self.directory, ignore_errors=True)
def broadcast_event(self, event_data: EventData) -> None: def broadcast_event(self, event_data: EventData) -> None:
""" """
@ -864,7 +854,7 @@ class Session:
return return
self.state = state self.state = state
self.state_time = time.monotonic() self.state_time = time.monotonic()
logging.info("changing session(%s) to state %s", self.id, state.name) logger.info("changing session(%s) to state %s", self.id, state.name)
self.write_state(state) self.write_state(state)
self.run_hooks(state) self.run_hooks(state)
self.run_state_hooks(state) self.run_state_hooks(state)
@ -879,12 +869,12 @@ class Session:
:param state: state to write to file :param state: state to write to file
:return: nothing :return: nothing
""" """
state_file = os.path.join(self.session_dir, "state") state_file = self.directory / "state"
try: try:
with open(state_file, "w") as f: with state_file.open("w") as f:
f.write(f"{state.value} {state.name}\n") f.write(f"{state.value} {state.name}\n")
except IOError: except IOError:
logging.exception("error writing state file: %s", state.name) logger.exception("error writing state file: %s", state.name)
def run_hooks(self, state: EventTypes) -> None: def run_hooks(self, state: EventTypes) -> None:
""" """
@ -906,24 +896,24 @@ class Session:
:return: nothing :return: nothing
""" """
file_name, data = hook file_name, data = hook
logging.info("running hook %s", file_name) logger.info("running hook %s", file_name)
file_path = os.path.join(self.session_dir, file_name) file_path = self.directory / file_name
log_path = os.path.join(self.session_dir, f"{file_name}.log") log_path = self.directory / f"{file_name}.log"
try: try:
with open(file_path, "w") as f: with file_path.open("w") as f:
f.write(data) f.write(data)
with open(log_path, "w") as f: with log_path.open("w") as f:
args = ["/bin/sh", file_name] args = ["/bin/sh", file_name]
subprocess.check_call( subprocess.check_call(
args, args,
stdout=f, stdout=f,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
close_fds=True, close_fds=True,
cwd=self.session_dir, cwd=self.directory,
env=self.get_environment(), env=self.get_environment(),
) )
except (IOError, subprocess.CalledProcessError): except (IOError, subprocess.CalledProcessError):
logging.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:
""" """
@ -940,7 +930,7 @@ class Session:
hook(state) hook(state)
except Exception: except Exception:
message = f"exception occurred when running {state.name} state hook: {hook}" message = f"exception occurred when running {state.name} state hook: {hook}"
logging.exception(message) logger.exception(message)
self.exception(ExceptionLevels.ERROR, "Session.run_state_hooks", message) self.exception(ExceptionLevels.ERROR, "Session.run_state_hooks", message)
def add_state_hook( def add_state_hook(
@ -983,10 +973,10 @@ class Session:
""" """
self.emane.poststartup() self.emane.poststartup()
# create session deployed xml # create session deployed xml
xml_file_name = os.path.join(self.session_dir, "session-deployed.xml")
xml_writer = corexml.CoreXmlWriter(self) xml_writer = corexml.CoreXmlWriter(self)
corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario) corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario)
xml_writer.write(xml_file_name) xml_file_path = self.directory / "session-deployed.xml"
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]:
""" """
@ -1001,9 +991,9 @@ class Session:
env["CORE_PYTHON"] = sys.executable env["CORE_PYTHON"] = sys.executable
env["SESSION"] = str(self.id) env["SESSION"] = str(self.id)
env["SESSION_SHORT"] = self.short_session_id() env["SESSION_SHORT"] = self.short_session_id()
env["SESSION_DIR"] = self.session_dir env["SESSION_DIR"] = str(self.directory)
env["SESSION_NAME"] = str(self.name) env["SESSION_NAME"] = str(self.name)
env["SESSION_FILENAME"] = str(self.file_name) env["SESSION_FILENAME"] = str(self.file_path)
env["SESSION_USER"] = str(self.user) env["SESSION_USER"] = str(self.user)
if state: if state:
env["SESSION_STATE"] = str(self.state) env["SESSION_STATE"] = str(self.state)
@ -1011,8 +1001,8 @@ class Session:
# /etc/core/environment # /etc/core/environment
# /home/user/.core/environment # /home/user/.core/environment
# /tmp/pycore.<session id>/environment # /tmp/pycore.<session id>/environment
core_env_path = Path(constants.CORE_CONF_DIR) / "environment" core_env_path = constants.CORE_CONF_DIR / "environment"
session_env_path = Path(self.session_dir) / "environment" session_env_path = self.directory / "environment"
if self.user: if self.user:
user_home_path = Path(f"~{self.user}").expanduser() user_home_path = Path(f"~{self.user}").expanduser()
user_env1 = user_home_path / ".core" / "environment" user_env1 = user_home_path / ".core" / "environment"
@ -1025,23 +1015,23 @@ class Session:
try: try:
utils.load_config(path, env) utils.load_config(path, env)
except IOError: except IOError:
logging.exception("error reading environment file: %s", path) logger.exception("error reading environment file: %s", path)
return env return env
def set_thumbnail(self, thumb_file: str) -> None: def set_thumbnail(self, thumb_file: Path) -> None:
""" """
Set the thumbnail filename. Move files from /tmp to session dir. Set the thumbnail filename. Move files from /tmp to session dir.
:param thumb_file: tumbnail file to set for session :param thumb_file: tumbnail file to set for session
:return: nothing :return: nothing
""" """
if not os.path.exists(thumb_file): if not thumb_file.is_file():
logging.error("thumbnail file to set does not exist: %s", thumb_file) logger.error("thumbnail file to set does not exist: %s", thumb_file)
self.thumbnail = None self.thumbnail = None
return return
destination_file = os.path.join(self.session_dir, os.path.basename(thumb_file)) dst_path = self.directory / thumb_file.name
shutil.copy(thumb_file, destination_file) shutil.copy(thumb_file, dst_path)
self.thumbnail = destination_file self.thumbnail = dst_path
def set_user(self, user: str) -> None: def set_user(self, user: str) -> None:
""" """
@ -1054,10 +1044,10 @@ class Session:
if user: if user:
try: try:
uid = pwd.getpwnam(user).pw_uid uid = pwd.getpwnam(user).pw_uid
gid = os.stat(self.session_dir).st_gid gid = self.directory.stat().st_gid
os.chown(self.session_dir, uid, gid) os.chown(self.directory, uid, gid)
except IOError: except IOError:
logging.exception("failed to set permission on %s", self.session_dir) logger.exception("failed to set permission on %s", self.directory)
self.user = user self.user = user
def create_node( def create_node(
@ -1114,7 +1104,7 @@ class Session:
with self.nodes_lock: with self.nodes_lock:
if _id in self.nodes: if _id in self.nodes:
node = self.nodes.pop(_id) node = self.nodes.pop(_id)
logging.info("deleted node(%s)", node.name) logger.info("deleted node(%s)", node.name)
if node: if node:
node.shutdown() node.shutdown()
self.sdt.delete_node(_id) self.sdt.delete_node(_id)
@ -1140,14 +1130,14 @@ class Session:
Write nodes to a 'nodes' file in the session dir. Write nodes to a 'nodes' file in the session dir.
The 'nodes' file lists: number, name, api-type, class-type The 'nodes' file lists: number, name, api-type, class-type
""" """
file_path = os.path.join(self.session_dir, "nodes") file_path = self.directory / "nodes"
try: try:
with self.nodes_lock: with self.nodes_lock:
with open(file_path, "w") as f: with file_path.open("w") as f:
for _id, node in self.nodes.items(): for _id, node in self.nodes.items():
f.write(f"{_id} {node.name} {node.apitype} {type(node)}\n") f.write(f"{_id} {node.name} {node.apitype} {type(node)}\n")
except IOError: except IOError:
logging.exception("error writing nodes file") logger.exception("error writing nodes file")
def exception( def exception(
self, level: ExceptionLevels, source: str, text: str, node_id: int = None self, level: ExceptionLevels, source: str, text: str, node_id: int = None
@ -1238,13 +1228,13 @@ class Session:
""" """
# this is called from instantiate() after receiving an event message # this is called from instantiate() after receiving an event message
# for the instantiation state # for the instantiation state
logging.debug( logger.debug(
"session(%s) checking if not in runtime state, current state: %s", "session(%s) checking if not in runtime state, current state: %s",
self.id, self.id,
self.state.name, self.state.name,
) )
if self.state == EventTypes.RUNTIME_STATE: if self.state == EventTypes.RUNTIME_STATE:
logging.info("valid runtime state found, returning") logger.info("valid runtime state found, returning")
return return
# start event loop and set to runtime # start event loop and set to runtime
self.event_loop.run() self.event_loop.run()
@ -1258,25 +1248,23 @@ class Session:
:return: nothing :return: nothing
""" """
if self.state.already_collected(): if self.state.already_collected():
logging.info( logger.info(
"session(%s) state(%s) already data collected", self.id, self.state "session(%s) state(%s) already data collected", self.id, self.state
) )
return return
logging.info("session(%s) state(%s) data collection", self.id, self.state) logger.info("session(%s) state(%s) data collection", self.id, self.state)
self.set_state(EventTypes.DATACOLLECT_STATE, send_event=True) self.set_state(EventTypes.DATACOLLECT_STATE, send_event=True)
# stop event loop # stop event loop
self.event_loop.stop() self.event_loop.stop()
# stop node services # stop mobility and node services
with self.nodes_lock: with self.nodes_lock:
funcs = [] funcs = []
for node_id in self.nodes: for node in self.nodes.values():
node = self.nodes[node_id] if isinstance(node, CoreNodeBase) and node.up:
if not isinstance(node, CoreNodeBase) or not node.up: args = (node,)
continue funcs.append((self.services.stop_services, args, {}))
args = (node,)
funcs.append((self.services.stop_services, args, {}))
utils.threadpool(funcs) utils.threadpool(funcs)
# shutdown emane # shutdown emane
@ -1307,7 +1295,7 @@ class Session:
:param node: node to boot :param node: node to boot
:return: nothing :return: nothing
""" """
logging.info("booting node(%s): %s", node.name, [x.name for x in node.services]) logger.info("booting node(%s): %s", node.name, [x.name for x in node.services])
self.services.boot_services(node) self.services.boot_services(node)
node.start_config_services() node.start_config_services()
@ -1328,7 +1316,7 @@ class Session:
funcs.append((self.boot_node, (node,), {})) funcs.append((self.boot_node, (node,), {}))
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
total = time.monotonic() - start total = time.monotonic() - start
logging.debug("boot run time: %s", total) logger.debug("boot run time: %s", total)
if not exceptions: if not exceptions:
self.update_control_iface_hosts() self.update_control_iface_hosts()
return exceptions return exceptions
@ -1356,7 +1344,7 @@ class Session:
""" """
d0 = self.options.get_config("controlnetif0") d0 = self.options.get_config("controlnetif0")
if d0: if d0:
logging.error("controlnet0 cannot be assigned with a host interface") logger.error("controlnet0 cannot be assigned with a host interface")
d1 = self.options.get_config("controlnetif1") d1 = self.options.get_config("controlnetif1")
d2 = self.options.get_config("controlnetif2") d2 = self.options.get_config("controlnetif2")
d3 = self.options.get_config("controlnetif3") d3 = self.options.get_config("controlnetif3")
@ -1401,7 +1389,7 @@ class Session:
:param conf_required: flag to check if conf is required :param conf_required: flag to check if conf is required
:return: control net node :return: control net node
""" """
logging.debug( logger.debug(
"add/remove control net: index(%s) remove(%s) conf_required(%s)", "add/remove control net: index(%s) remove(%s) conf_required(%s)",
net_index, net_index,
remove, remove,
@ -1415,7 +1403,7 @@ class Session:
return None return None
else: else:
prefix_spec = CtrlNet.DEFAULT_PREFIX_LIST[net_index] prefix_spec = CtrlNet.DEFAULT_PREFIX_LIST[net_index]
logging.debug("prefix spec: %s", prefix_spec) logger.debug("prefix spec: %s", prefix_spec)
server_iface = self.get_control_net_server_ifaces()[net_index] server_iface = self.get_control_net_server_ifaces()[net_index]
# return any existing controlnet bridge # return any existing controlnet bridge
@ -1438,7 +1426,7 @@ class Session:
if net_index == 0: if net_index == 0:
updown_script = self.options.get_config("controlnet_updown_script") updown_script = self.options.get_config("controlnet_updown_script")
if not updown_script: if not updown_script:
logging.debug("controlnet updown script not configured") logger.debug("controlnet updown script not configured")
prefixes = prefix_spec.split() prefixes = prefix_spec.split()
if len(prefixes) > 1: if len(prefixes) > 1:
@ -1452,7 +1440,7 @@ class Session:
else: else:
prefix = prefixes[0] prefix = prefixes[0]
logging.info( logger.info(
"controlnet(%s) prefix(%s) updown(%s) serverintf(%s)", "controlnet(%s) prefix(%s) updown(%s) serverintf(%s)",
_id, _id,
prefix, prefix,
@ -1508,6 +1496,7 @@ class Session:
mac=utils.random_mac(), mac=utils.random_mac(),
ip4=ip4, ip4=ip4,
ip4_mask=ip4_mask, ip4_mask=ip4_mask,
mtu=DEFAULT_MTU,
) )
iface = node.new_iface(control_net, iface_data) iface = node.new_iface(control_net, iface_data)
iface.control = True iface.control = True
@ -1515,7 +1504,7 @@ class Session:
msg = f"Control interface not added to node {node.id}. " msg = f"Control interface not added to node {node.id}. "
msg += f"Invalid control network prefix ({control_net.prefix}). " msg += f"Invalid control network prefix ({control_net.prefix}). "
msg += "A longer prefix length may be required for this many nodes." msg += "A longer prefix length may be required for this many nodes."
logging.exception(msg) logger.exception(msg)
def update_control_iface_hosts( def update_control_iface_hosts(
self, net_index: int = 0, remove: bool = False self, net_index: int = 0, remove: bool = False
@ -1533,12 +1522,12 @@ class Session:
try: try:
control_net = self.get_control_net(net_index) control_net = self.get_control_net(net_index)
except CoreError: except CoreError:
logging.exception("error retrieving control net node") logger.exception("error retrieving control net node")
return return
header = f"CORE session {self.id} host entries" header = f"CORE session {self.id} host entries"
if remove: if remove:
logging.info("Removing /etc/hosts file entries.") logger.info("Removing /etc/hosts file entries.")
utils.file_demunge("/etc/hosts", header) utils.file_demunge("/etc/hosts", header)
return return
@ -1548,7 +1537,7 @@ class Session:
for ip in iface.ips(): for ip in iface.ips():
entries.append(f"{ip.ip} {name}") entries.append(f"{ip.ip} {name}")
logging.info("Adding %d /etc/hosts file entries.", len(entries)) logger.info("Adding %d /etc/hosts file entries.", len(entries))
utils.file_munge("/etc/hosts", header, "\n".join(entries) + "\n") utils.file_munge("/etc/hosts", header, "\n".join(entries) + "\n")
def runtime(self) -> float: def runtime(self) -> float:
@ -1577,7 +1566,7 @@ class Session:
current_time = self.runtime() current_time = self.runtime()
if current_time > 0: if current_time > 0:
if event_time <= current_time: if event_time <= current_time:
logging.warning( logger.warning(
"could not schedule past event for time %s (run time is now %s)", "could not schedule past event for time %s (run time is now %s)",
event_time, event_time,
current_time, current_time,
@ -1589,7 +1578,7 @@ class Session:
) )
if not name: if not name:
name = "" name = ""
logging.info( logger.info(
"scheduled event %s at time %s data=%s", "scheduled event %s at time %s data=%s",
name, name,
event_time + current_time, event_time + current_time,
@ -1608,12 +1597,12 @@ class Session:
:return: nothing :return: nothing
""" """
if data is None: if data is None:
logging.warning("no data for event node(%s) name(%s)", node_id, name) logger.warning("no data for event node(%s) name(%s)", node_id, name)
return return
now = self.runtime() now = self.runtime()
if not name: if not name:
name = "" name = ""
logging.info("running event %s at time %s cmd=%s", name, now, data) logger.info("running event %s at time %s cmd=%s", name, now, data)
if not node_id: if not node_id:
utils.mute_detach(data) utils.mute_detach(data)
else: else:

View file

@ -1,7 +1,14 @@
from typing import Any, List from typing import Any, List
from core.config import ConfigurableManager, ConfigurableOptions, Configuration from core.config import (
from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs ConfigBool,
ConfigInt,
ConfigString,
ConfigurableManager,
ConfigurableOptions,
Configuration,
)
from core.emulator.enumerations import RegisterTlvs
from core.plugins.sdt import Sdt from core.plugins.sdt import Sdt
@ -12,53 +19,28 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
name: str = "session" name: str = "session"
options: List[Configuration] = [ options: List[Configuration] = [
Configuration( ConfigString(id="controlnet", label="Control Network"),
_id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network" ConfigString(id="controlnet0", label="Control Network 0"),
ConfigString(id="controlnet1", label="Control Network 1"),
ConfigString(id="controlnet2", label="Control Network 2"),
ConfigString(id="controlnet3", label="Control Network 3"),
ConfigString(id="controlnet_updown_script", label="Control Network Script"),
ConfigBool(id="enablerj45", default="1", label="Enable RJ45s"),
ConfigBool(id="preservedir", default="0", label="Preserve session dir"),
ConfigBool(id="enablesdt", default="0", label="Enable SDT3D output"),
ConfigString(id="sdturl", default=Sdt.DEFAULT_SDT_URL, label="SDT3D URL"),
ConfigBool(id="ovs", default="0", label="Enable OVS"),
ConfigInt(id="platform_id_start", default="1", label="EMANE Platform ID Start"),
ConfigInt(id="nem_id_start", default="1", label="EMANE NEM ID Start"),
ConfigBool(id="link_enabled", default="1", label="EMANE Links?"),
ConfigInt(
id="loss_threshold", default="30", label="EMANE Link Loss Threshold (%)"
), ),
Configuration( ConfigInt(
_id="controlnet0", _type=ConfigDataTypes.STRING, label="Control Network 0" id="link_interval", default="1", label="EMANE Link Check Interval (sec)"
),
Configuration(
_id="controlnet1", _type=ConfigDataTypes.STRING, label="Control Network 1"
),
Configuration(
_id="controlnet2", _type=ConfigDataTypes.STRING, label="Control Network 2"
),
Configuration(
_id="controlnet3", _type=ConfigDataTypes.STRING, label="Control Network 3"
),
Configuration(
_id="controlnet_updown_script",
_type=ConfigDataTypes.STRING,
label="Control Network Script",
),
Configuration(
_id="enablerj45",
_type=ConfigDataTypes.BOOL,
default="1",
label="Enable RJ45s",
),
Configuration(
_id="preservedir",
_type=ConfigDataTypes.BOOL,
default="0",
label="Preserve session dir",
),
Configuration(
_id="enablesdt",
_type=ConfigDataTypes.BOOL,
default="0",
label="Enable SDT3D output",
),
Configuration(
_id="sdturl",
_type=ConfigDataTypes.STRING,
default=Sdt.DEFAULT_SDT_URL,
label="SDT3D URL",
),
Configuration(
_id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS"
), ),
ConfigInt(id="link_timeout", default="4", label="EMANE Link Timeout (sec)"),
ConfigInt(id="mtu", default="0", label="MTU for All Devices"),
] ]
config_type: RegisterTlvs = RegisterTlvs.UTILITY config_type: RegisterTlvs = RegisterTlvs.UTILITY
@ -112,3 +94,13 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
if value is not None: if value is not None:
value = int(value) value = int(value)
return value return value
def config_reset(self, node_id: int = None) -> None:
"""
Clear prior configuration files and reset to default values.
:param node_id: node id to store configuration for
:return: nothing
"""
super().config_reset(node_id)
self.set_configs(self.default_values())

View file

@ -46,3 +46,11 @@ class CoreServiceBootError(Exception):
""" """
pass pass
class CoreConfigError(Exception):
"""
Used when there is an error defining a configurable option.
"""
pass

View file

@ -7,15 +7,15 @@ SYSCTL: str = "sysctl"
IP: str = "ip" IP: str = "ip"
ETHTOOL: str = "ethtool" ETHTOOL: str = "ethtool"
TC: str = "tc" TC: str = "tc"
EBTABLES: str = "ebtables"
MOUNT: str = "mount" MOUNT: str = "mount"
UMOUNT: str = "umount" UMOUNT: str = "umount"
OVS_VSCTL: str = "ovs-vsctl" OVS_VSCTL: str = "ovs-vsctl"
TEST: str = "test" TEST: str = "test"
NFTABLES: str = "nft"
COMMON_REQUIREMENTS: List[str] = [ COMMON_REQUIREMENTS: List[str] = [
BASH, BASH,
EBTABLES, NFTABLES,
ETHTOOL, ETHTOOL,
IP, IP,
MOUNT, MOUNT,

View file

@ -22,6 +22,7 @@ from core.gui.statusbar import StatusBar
from core.gui.themes import PADY from core.gui.themes import PADY
from core.gui.toolbar import Toolbar from core.gui.toolbar import Toolbar
logger = logging.getLogger(__name__)
WIDTH: int = 1000 WIDTH: int = 1000
HEIGHT: int = 800 HEIGHT: int = 800
@ -171,7 +172,7 @@ class Application(ttk.Frame):
def show_grpc_exception( def show_grpc_exception(
self, message: str, e: grpc.RpcError, blocking: bool = False self, message: str, e: grpc.RpcError, blocking: bool = False
) -> None: ) -> None:
logging.exception("app grpc exception", exc_info=e) logger.exception("app grpc exception", exc_info=e)
dialog = ErrorDialog(self, "GRPC Exception", message, e.details()) dialog = ErrorDialog(self, "GRPC Exception", message, e.details())
if blocking: if blocking:
dialog.show() dialog.show()
@ -179,7 +180,7 @@ class Application(ttk.Frame):
self.after(0, lambda: dialog.show()) self.after(0, lambda: dialog.show())
def show_exception(self, message: str, e: Exception) -> None: def show_exception(self, message: str, e: Exception) -> None:
logging.exception("app exception", exc_info=e) logger.exception("app exception", exc_info=e)
self.after( self.after(
0, lambda: ErrorDialog(self, "App Exception", message, str(e)).show() 0, lambda: ErrorDialog(self, "App Exception", message, str(e)).show()
) )

View file

@ -120,25 +120,18 @@ 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__( def __init__(self, **kwargs) -> None:
self, self.__setstate__(kwargs)
ip4: str = None,
ip6: str = None, def __setstate__(self, kwargs):
ip4s: List[str] = None, self.ip4s: List[str] = kwargs.get(
ip6s: List[str] = None, "ip4s", ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
) -> None: )
if ip4s is None: self.ip4: str = kwargs.get("ip4", self.ip4s[0])
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"] self.ip6s: List[str] = kwargs.get("ip6s", ["2001::", "2002::", "a::"])
self.ip4s: List[str] = ip4s self.ip6: str = kwargs.get("ip6", self.ip6s[0])
if ip6s is None: self.enable_ip4: bool = kwargs.get("enable_ip4", True)
ip6s = ["2001::", "2002::", "a::"] self.enable_ip6: bool = kwargs.get("enable_ip6", True)
self.ip6s: List[str] = ip6s
if ip4 is None:
ip4 = self.ip4s[0]
self.ip4: str = ip4
if ip6 is None:
ip6 = self.ip6s[0]
self.ip6: str = ip6
class GuiConfig(yaml.YAMLObject): class GuiConfig(yaml.YAMLObject):
@ -223,7 +216,7 @@ def check_directory() -> None:
def read() -> GuiConfig: def read() -> GuiConfig:
with CONFIG_PATH.open("r") as f: with CONFIG_PATH.open("r") as f:
return yaml.load(f, Loader=yaml.SafeLoader) return yaml.safe_load(f)
def save(config: GuiConfig) -> None: def save(config: GuiConfig) -> None:

View file

@ -6,23 +6,18 @@ import json
import logging import logging
import os import os
import tkinter as tk import tkinter as tk
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, Dict, Iterable, List, Optional, Set, Tuple
import grpc import grpc
from core.api.grpc import ( from core.api.grpc import client, configservices_pb2, core_pb2
client,
configservices_pb2,
core_pb2,
emane_pb2,
mobility_pb2,
services_pb2,
wlan_pb2,
)
from core.api.grpc.wrappers import ( from core.api.grpc.wrappers import (
ConfigOption, ConfigOption,
ConfigService, ConfigService,
EmaneModelConfig,
Event,
ExceptionEvent, ExceptionEvent,
Link, Link,
LinkEvent, LinkEvent,
@ -33,6 +28,9 @@ from core.api.grpc.wrappers import (
NodeServiceData, NodeServiceData,
NodeType, NodeType,
Position, Position,
Server,
ServiceConfig,
ServiceFileConfig,
Session, Session,
SessionLocation, SessionLocation,
SessionState, SessionState,
@ -45,10 +43,11 @@ from core.gui.dialogs.mobilityplayer import MobilityPlayer
from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.sessions import SessionsDialog
from core.gui.graph.edges import CanvasEdge from core.gui.graph.edges import CanvasEdge
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import Shape
from core.gui.interface import InterfaceManager from core.gui.interface import InterfaceManager
from core.gui.nodeutils import NodeDraw from core.gui.nodeutils import NodeDraw
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -77,6 +76,7 @@ class CoreClient:
self.config_services: Dict[str, ConfigService] = {} self.config_services: Dict[str, ConfigService] = {}
# loaded configuration data # loaded configuration data
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] = {}
@ -98,8 +98,7 @@ class CoreClient:
@property @property
def client(self) -> client.CoreGrpcClient: def client(self) -> client.CoreGrpcClient:
if self.session: if self.session:
response = self._client.check_session(self.session.id) if not self._client.check_session(self.session.id):
if not response.result:
throughputs_enabled = self.handling_throughputs is not None throughputs_enabled = self.handling_throughputs is not None
self.cancel_throughputs() self.cancel_throughputs()
self.cancel_events() self.cancel_events()
@ -150,22 +149,20 @@ class CoreClient:
for observer in self.app.guiconfig.observers: for observer in self.app.guiconfig.observers:
self.custom_observers[observer.name] = observer self.custom_observers[observer.name] = observer
def handle_events(self, event: core_pb2.Event) -> None: def handle_events(self, event: Event) -> None:
if not self.session or event.source == GUI_SOURCE: if not self.session or event.source == GUI_SOURCE:
return return
if event.session_id != self.session.id: if event.session_id != self.session.id:
logging.warning( logger.warning(
"ignoring event session(%s) current(%s)", "ignoring event session(%s) current(%s)",
event.session_id, event.session_id,
self.session.id, self.session.id,
) )
return return
if event.link_event:
if event.HasField("link_event"): self.app.after(0, self.handle_link_event, event.link_event)
link_event = LinkEvent.from_proto(event.link_event) elif event.session_event:
self.app.after(0, self.handle_link_event, link_event) logger.info("session event: %s", event)
elif event.HasField("session_event"):
logging.info("session event: %s", event)
session_event = event.session_event session_event = event.session_event
if session_event.event <= SessionState.SHUTDOWN.value: if session_event.event <= SessionState.SHUTDOWN.value:
self.session.state = SessionState(session_event.event) self.session.state = SessionState(session_event.event)
@ -180,24 +177,22 @@ class CoreClient:
else: else:
dialog.set_pause() dialog.set_pause()
else: else:
logging.warning("unknown session event: %s", session_event) logger.warning("unknown session event: %s", session_event)
elif event.HasField("node_event"): elif event.node_event:
node_event = NodeEvent.from_proto(event.node_event) self.app.after(0, self.handle_node_event, event.node_event)
self.app.after(0, self.handle_node_event, node_event) elif event.config_event:
elif event.HasField("config_event"): logger.info("config event: %s", event)
logging.info("config event: %s", event) elif event.exception_event:
elif event.HasField("exception_event"): self.handle_exception_event(event.exception_event)
event = ExceptionEvent.from_proto(event.session_id, event.exception_event)
self.handle_exception_event(event)
else: else:
logging.info("unhandled event: %s", event) logger.info("unhandled event: %s", event)
def handle_link_event(self, event: LinkEvent) -> None: def handle_link_event(self, event: LinkEvent) -> None:
logging.debug("Link event: %s", event) logger.debug("Link event: %s", event)
node1_id = event.link.node1_id node1_id = event.link.node1_id
node2_id = event.link.node2_id node2_id = event.link.node2_id
if node1_id == node2_id: if node1_id == node2_id:
logging.warning("ignoring links with loops: %s", event) logger.warning("ignoring links with loops: %s", event)
return return
canvas_node1 = self.canvas_nodes[node1_id] canvas_node1 = self.canvas_nodes[node1_id]
canvas_node2 = self.canvas_nodes[node2_id] canvas_node2 = self.canvas_nodes[node2_id]
@ -215,7 +210,7 @@ class CoreClient:
canvas_node1, canvas_node2, event.link canvas_node1, canvas_node2, event.link
) )
else: else:
logging.warning("unknown link event: %s", event) logger.warning("unknown link event: %s", event)
else: else:
if event.message_type == MessageType.ADD: if event.message_type == MessageType.ADD:
self.app.manager.add_wired_edge(canvas_node1, canvas_node2, event.link) self.app.manager.add_wired_edge(canvas_node1, canvas_node2, event.link)
@ -224,10 +219,10 @@ class CoreClient:
elif event.message_type == MessageType.NONE: elif event.message_type == MessageType.NONE:
self.app.manager.update_wired_edge(event.link) self.app.manager.update_wired_edge(event.link)
else: else:
logging.warning("unknown link event: %s", event) logger.warning("unknown link event: %s", event)
def handle_node_event(self, event: NodeEvent) -> None: def handle_node_event(self, event: NodeEvent) -> None:
logging.debug("node event: %s", event) logger.debug("node event: %s", event)
node = event.node node = event.node
if event.message_type == MessageType.NONE: if event.message_type == MessageType.NONE:
canvas_node = self.canvas_nodes[node.id] canvas_node = self.canvas_nodes[node.id]
@ -238,15 +233,13 @@ class CoreClient:
canvas_node.update_icon(node.icon) canvas_node.update_icon(node.icon)
elif event.message_type == MessageType.DELETE: elif event.message_type == MessageType.DELETE:
canvas_node = self.canvas_nodes[node.id] canvas_node = self.canvas_nodes[node.id]
canvas_node.canvas.clear_selection() canvas_node.canvas_delete()
canvas_node.canvas.select_object(canvas_node.id)
canvas_node.canvas.delete_selected_objects()
elif event.message_type == MessageType.ADD: elif event.message_type == MessageType.ADD:
if node.id in self.session.nodes: if node.id in self.session.nodes:
logging.error("core node already exists: %s", node) logger.error("core node already exists: %s", node)
self.app.manager.add_core_node(node) self.app.manager.add_core_node(node)
else: else:
logging.warning("unknown node event: %s", event) logger.warning("unknown node event: %s", event)
def enable_throughputs(self) -> None: def enable_throughputs(self) -> None:
self.handling_throughputs = self.client.throughputs( self.handling_throughputs = self.client.throughputs(
@ -278,34 +271,35 @@ class CoreClient:
CPU_USAGE_DELAY, self.handle_cpu_event CPU_USAGE_DELAY, self.handle_cpu_event
) )
def handle_throughputs(self, event: core_pb2.ThroughputsEvent) -> None: def handle_throughputs(self, event: ThroughputsEvent) -> None:
event = ThroughputsEvent.from_proto(event)
if event.session_id != self.session.id: if event.session_id != self.session.id:
logging.warning( logger.warning(
"ignoring throughput event session(%s) current(%s)", "ignoring throughput event session(%s) current(%s)",
event.session_id, event.session_id,
self.session.id, self.session.id,
) )
return return
logging.debug("handling throughputs event: %s", event) logger.debug("handling throughputs event: %s", event)
self.app.after(0, self.app.manager.set_throughputs, event) self.app.after(0, self.app.manager.set_throughputs, event)
def handle_cpu_event(self, event: core_pb2.CpuUsageEvent) -> None: def handle_cpu_event(self, event: core_pb2.CpuUsageEvent) -> None:
self.app.after(0, self.app.statusbar.set_cpu, event.usage) self.app.after(0, self.app.statusbar.set_cpu, event.usage)
def handle_exception_event(self, event: ExceptionEvent) -> None: def handle_exception_event(self, event: ExceptionEvent) -> None:
logging.info("exception event: %s", event) logger.info("exception event: %s", event)
self.app.statusbar.add_alert(event) self.app.statusbar.add_alert(event)
def update_session_title(self) -> None:
title_file = self.session.file.name if self.session.file else ""
self.master.title(f"CORE Session({self.session.id}) {title_file}")
def join_session(self, session_id: int) -> None: def join_session(self, session_id: int) -> None:
logging.info("joining session(%s)", session_id) logger.info("joining session(%s)", session_id)
self.reset() self.reset()
try: try:
response = self.client.get_session(session_id) self.session = self.client.get_session(session_id)
self.session = Session.from_proto(response.session) self.session.user = self.user
self.client.set_session_user(self.session.id, self.user) self.update_session_title()
title_file = self.session.file.name if self.session.file else ""
self.master.title(f"CORE Session({self.session.id}) {title_file}")
self.handling_events = self.client.events( self.handling_events = self.client.events(
self.session.id, self.handle_events self.session.id, self.handle_events
) )
@ -320,53 +314,14 @@ class CoreClient:
def is_runtime(self) -> bool: def is_runtime(self) -> bool:
return self.session and self.session.state == SessionState.RUNTIME return self.session and self.session.state == SessionState.RUNTIME
def parse_metadata(self) -> None:
# canvas setting
config = self.session.metadata
canvas_config = config.get("canvas")
logging.debug("canvas metadata: %s", canvas_config)
if canvas_config:
canvas_config = json.loads(canvas_config)
self.app.manager.parse_metadata(canvas_config)
# load saved shapes
shapes_config = config.get("shapes")
if shapes_config:
shapes_config = json.loads(shapes_config)
for shape_config in shapes_config:
logging.debug("loading shape: %s", shape_config)
Shape.from_metadata(self.app, shape_config)
# load edges config
edges_config = config.get("edges")
if edges_config:
edges_config = json.loads(edges_config)
logging.info("edges config: %s", edges_config)
for edge_config in edges_config:
edge = self.links[edge_config["token"]]
edge.width = edge_config["width"]
edge.color = edge_config["color"]
edge.redraw()
# read hidden nodes
hidden = config.get("hidden")
if hidden:
hidden = json.loads(hidden)
for _id in hidden:
canvas_node = self.canvas_nodes.get(_id)
if canvas_node:
canvas_node.hide()
else:
logging.warning("invalid node to hide: %s", _id)
def create_new_session(self) -> None: def create_new_session(self) -> None:
""" """
Create a new session Create a new session
""" """
try: try:
response = self.client.create_session() session = self.client.create_session()
logging.info("created session: %s", response) logger.info("created session: %s", session.id)
self.join_session(response.session_id) self.join_session(session.id)
location_config = self.app.guiconfig.location location_config = self.app.guiconfig.location
self.session.location = SessionLocation( self.session.location = SessionLocation(
x=location_config.x, x=location_config.x,
@ -387,7 +342,7 @@ class CoreClient:
session_id = self.session.id session_id = self.session.id
try: try:
response = self.client.delete_session(session_id) response = self.client.delete_session(session_id)
logging.info("deleted session(%s), Result: %s", session_id, response) logger.info("deleted session(%s), Result: %s", session_id, response)
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Delete Session Error", e) self.app.show_grpc_exception("Delete Session Error", e)
@ -397,23 +352,21 @@ class CoreClient:
""" """
try: try:
self.client.connect() self.client.connect()
# get all available services # get current core configurations services/config services
response = self.client.get_services() core_config = self.client.get_config()
for service in response.services: self.emane_models = sorted(core_config.emane_models)
for service in core_config.services:
group_services = self.services.setdefault(service.group, set()) group_services = self.services.setdefault(service.group, set())
group_services.add(service.name) group_services.add(service.name)
# get config service informations for service in core_config.config_services:
response = self.client.get_config_services() self.config_services[service.name] = service
for service in response.services:
self.config_services[service.name] = ConfigService.from_proto(service)
group_services = self.config_services_groups.setdefault( group_services = self.config_services_groups.setdefault(
service.group, set() service.group, set()
) )
group_services.add(service.name) group_services.add(service.name)
# join provided session, create new session, or show dialog to select an # join provided session, create new session, or show dialog to select an
# existing session # existing session
response = self.client.get_sessions() sessions = self.client.get_sessions()
sessions = response.sessions
if session_id: if session_id:
session_ids = set(x.id for x in sessions) session_ids = set(x.id for x in sessions)
if session_id not in session_ids: if session_id not in session_ids:
@ -432,71 +385,50 @@ class CoreClient:
dialog = SessionsDialog(self.app, True) dialog = SessionsDialog(self.app, True)
dialog.show() dialog.show()
except grpc.RpcError as e: except grpc.RpcError as e:
logging.exception("core setup error") logger.exception("core setup error")
self.app.show_grpc_exception("Setup Error", e, blocking=True) self.app.show_grpc_exception("Setup Error", e, blocking=True)
self.app.close() self.app.close()
def edit_node(self, core_node: Node) -> None: def edit_node(self, core_node: Node) -> None:
try: try:
position = core_node.position.to_proto() self.client.move_node(
self.client.edit_node( self.session.id, core_node.id, core_node.position, source=GUI_SOURCE
self.session.id, core_node.id, position, source=GUI_SOURCE
) )
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 send_servers(self) -> None: def get_links(self, definition: bool = False) -> List[Link]:
for server in self.servers.values(): if not definition:
self.client.add_session_server(self.session.id, server.name, server.address) self.ifaces_manager.set_macs([x.link for x in self.links.values()])
def start_session(self) -> Tuple[bool, List[str]]:
self.ifaces_manager.set_macs([x.link for x in self.links.values()])
nodes = [x.to_proto() for x in self.session.nodes.values()]
links = [] links = []
asymmetric_links = []
for edge in self.links.values(): for edge in self.links.values():
link = edge.link link = edge.link
if link.iface1 and not link.iface1.mac: if not definition:
link.iface1.mac = self.ifaces_manager.next_mac() if link.iface1 and not link.iface1.mac:
if link.iface2 and not link.iface2.mac: link.iface1.mac = self.ifaces_manager.next_mac()
link.iface2.mac = self.ifaces_manager.next_mac() if link.iface2 and not link.iface2.mac:
links.append(link.to_proto()) link.iface2.mac = self.ifaces_manager.next_mac()
links.append(link)
if edge.asymmetric_link: if edge.asymmetric_link:
asymmetric_links.append(edge.asymmetric_link.to_proto()) links.append(edge.asymmetric_link)
wlan_configs = self.get_wlan_configs_proto() return links
mobility_configs = self.get_mobility_configs_proto()
emane_model_configs = self.get_emane_model_configs_proto() def start_session(self, definition: bool = False) -> Tuple[bool, List[str]]:
hooks = [x.to_proto() for x in self.session.hooks.values()] self.session.links = self.get_links(definition)
service_configs = self.get_service_configs_proto() self.session.metadata = self.get_metadata()
file_configs = self.get_service_file_configs_proto() self.session.servers.clear()
config_service_configs = self.get_config_service_configs_proto() for server in self.servers.values():
emane_config = to_dict(self.session.emane_config) self.session.servers.append(Server(name=server.name, host=server.address))
result = False result = False
exceptions = [] exceptions = []
try: try:
self.send_servers() result, exceptions = self.client.start_session(self.session, definition)
response = self.client.start_session( logger.info(
"start session(%s) definition(%s), result: %s",
self.session.id, self.session.id,
nodes, definition,
links, result,
self.session.location.to_proto(),
hooks,
emane_config,
emane_model_configs,
wlan_configs,
mobility_configs,
service_configs,
file_configs,
asymmetric_links,
config_service_configs,
) )
logging.info(
"start session(%s), result: %s", self.session.id, response.result
)
if response.result:
self.set_metadata()
result = response.result
exceptions = response.exceptions
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Start Session Error", e) self.app.show_grpc_exception("Start Session Error", e)
return result, exceptions return result, exceptions
@ -506,9 +438,8 @@ class CoreClient:
session_id = self.session.id session_id = self.session.id
result = False result = False
try: try:
response = self.client.stop_session(session_id) result = self.client.stop_session(session_id)
logging.info("stopped session(%s), result: %s", session_id, response) logger.info("stopped session(%s), result: %s", session_id, result)
result = response.result
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Stop Session Error", e) self.app.show_grpc_exception("Stop Session Error", e)
return result return result
@ -522,7 +453,7 @@ class CoreClient:
self.mobility_players[node.id] = mobility_player self.mobility_players[node.id] = mobility_player
mobility_player.show() mobility_player.show()
def set_metadata(self) -> None: 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)
@ -548,11 +479,9 @@ class CoreClient:
hidden = json.dumps(hidden) hidden = json.dumps(hidden)
# save metadata # save metadata
metadata = dict( return dict(
canvas=canvas_config, shapes=shapes, edges=edges_config, hidden=hidden canvas=canvas_config, shapes=shapes, edges=edges_config, hidden=hidden
) )
response = self.client.set_session_metadata(self.session.id, metadata)
logging.debug("set session metadata %s, result: %s", metadata, response)
def launch_terminal(self, node_id: int) -> None: def launch_terminal(self, node_id: int) -> None:
try: try:
@ -564,9 +493,9 @@ class CoreClient:
parent=self.app, parent=self.app,
) )
return return
response = self.client.get_node_terminal(self.session.id, node_id) node_term = self.client.get_node_terminal(self.session.id, node_id)
cmd = f"{terminal} {response.terminal} &" cmd = f"{terminal} {node_term} &"
logging.info("launching terminal %s", cmd) logger.info("launching terminal %s", cmd)
os.system(cmd) os.system(cmd)
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Node Terminal Error", e) self.app.show_grpc_exception("Node Terminal Error", e)
@ -574,189 +503,82 @@ class CoreClient:
def get_xml_dir(self) -> str: def get_xml_dir(self) -> str:
return str(self.session.file.parent) if self.session.file else str(XMLS_PATH) return str(self.session.file.parent) if self.session.file else str(XMLS_PATH)
def save_xml(self, file_path: str = None) -> None: def save_xml(self, file_path: Path = None) -> bool:
""" """
Save core session as to an xml file Save core session as to an xml file
""" """
if not file_path and not self.session.file: if not file_path and not self.session.file:
logging.error("trying to save xml for session with no file") logger.error("trying to save xml for session with no file")
return return False
if not file_path: if not file_path:
file_path = str(self.session.file) file_path = self.session.file
result = False
try: try:
if not self.is_runtime(): if not self.is_runtime():
logging.debug("Send session data to the daemon") logger.debug("sending session data to the daemon")
self.send_data() result, exceptions = self.start_session(definition=True)
response = self.client.save_xml(self.session.id, file_path) if not result:
logging.info("saved xml file %s, result: %s", file_path, response) message = "\n".join(exceptions)
self.app.show_exception_data(
"Session Definition Exception",
"Failed to define session",
message,
)
self.client.save_xml(self.session.id, str(file_path))
if self.session.file != file_path:
self.session.file = file_path
self.update_session_title()
logger.info("saved xml file %s", file_path)
result = True
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Save XML Error", e) self.app.show_grpc_exception("Save XML Error", e)
return result
def open_xml(self, file_path: str) -> None: def open_xml(self, file_path: Path) -> None:
""" """
Open core xml Open core xml
""" """
try: try:
response = self._client.open_xml(file_path) result, session_id = self._client.open_xml(file_path)
logging.info("open xml file %s, response: %s", file_path, response) logger.info(
self.join_session(response.session_id) "open xml file %s, result(%s) session(%s)",
file_path,
result,
session_id,
)
self.join_session(session_id)
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Open XML Error", e) self.app.show_grpc_exception("Open XML Error", e)
def get_node_service(self, node_id: int, service_name: str) -> NodeServiceData: def get_node_service(self, node_id: int, service_name: str) -> NodeServiceData:
response = self.client.get_node_service(self.session.id, node_id, service_name) node_service = self.client.get_node_service(
logging.debug( self.session.id, node_id, service_name
"get node(%s) %s service, response: %s", node_id, service_name, response
) )
return NodeServiceData.from_proto(response.service) logger.debug(
"get node(%s) service(%s): %s", node_id, service_name, node_service
def set_node_service(
self,
node_id: int,
service_name: str,
dirs: List[str],
files: List[str],
startups: List[str],
validations: List[str],
shutdowns: List[str],
) -> NodeServiceData:
response = self.client.set_node_service(
self.session.id,
node_id,
service_name,
directories=dirs,
files=files,
startup=startups,
validate=validations,
shutdown=shutdowns,
) )
logging.info( return node_service
"Set %s service for node(%s), files: %s, Startup: %s, "
"Validation: %s, Shutdown: %s, Result: %s",
service_name,
node_id,
files,
startups,
validations,
shutdowns,
response,
)
response = self.client.get_node_service(self.session.id, node_id, service_name)
return NodeServiceData.from_proto(response.service)
def get_node_service_file( def get_node_service_file(
self, node_id: int, service_name: str, file_name: str self, node_id: int, service_name: str, file_name: str
) -> str: ) -> str:
response = self.client.get_node_service_file( data = self.client.get_node_service_file(
self.session.id, node_id, service_name, file_name self.session.id, node_id, service_name, file_name
) )
logging.debug( logger.debug(
"get service file for node(%s), service: %s, file: %s, result: %s", "get service file for node(%s), service: %s, file: %s, data: %s",
node_id,
service_name,
file_name,
response,
)
return response.data
def set_node_service_file(
self, node_id: int, service_name: str, file_name: str, data: str
) -> None:
response = self.client.set_node_service_file(
self.session.id, node_id, service_name, file_name, data
)
logging.info(
"set node(%s) service file, service: %s, file: %s, data: %s, result: %s",
node_id, node_id,
service_name, service_name,
file_name, file_name,
data, data,
response,
) )
return data
def create_nodes_and_links(self) -> None:
"""
create nodes and links that have not been created yet
"""
self.client.set_session_state(self.session.id, SessionState.DEFINITION.value)
for node in self.session.nodes.values():
response = self.client.add_node(
self.session.id, node.to_proto(), source=GUI_SOURCE
)
logging.debug("created node: %s", response)
asymmetric_links = []
for edge in self.links.values():
self.add_link(edge.link)
if edge.asymmetric_link:
asymmetric_links.append(edge.asymmetric_link)
for link in asymmetric_links:
self.add_link(link)
def send_data(self) -> None:
"""
Send to daemon all session info, but don't start the session
"""
self.send_servers()
self.create_nodes_and_links()
for config_proto in self.get_wlan_configs_proto():
self.client.set_wlan_config(
self.session.id, config_proto.node_id, config_proto.config
)
for config_proto in self.get_mobility_configs_proto():
self.client.set_mobility_config(
self.session.id, config_proto.node_id, config_proto.config
)
for config_proto in self.get_service_configs_proto():
self.client.set_node_service(
self.session.id,
config_proto.node_id,
config_proto.service,
config_proto.files,
config_proto.directories,
config_proto.startup,
config_proto.validate,
config_proto.shutdown,
)
for config_proto in self.get_service_file_configs_proto():
self.client.set_node_service_file(
self.session.id,
config_proto.node_id,
config_proto.service,
config_proto.file,
config_proto.data,
)
for hook in self.session.hooks.values():
self.client.add_hook(
self.session.id, hook.state.value, hook.file, hook.data
)
for config_proto in self.get_emane_model_configs_proto():
self.client.set_emane_model_config(
self.session.id,
config_proto.node_id,
config_proto.model,
config_proto.config,
config_proto.iface_id,
)
config = to_dict(self.session.emane_config)
self.client.set_emane_config(self.session.id, config)
location = self.session.location
self.client.set_session_location(
self.session.id,
location.x,
location.y,
location.z,
location.lat,
location.lon,
location.alt,
location.scale,
)
self.set_metadata()
def close(self) -> None: def close(self) -> None:
""" """
Clean ups when done using grpc Clean ups when done using grpc
""" """
logging.debug("close grpc") logger.debug("close grpc")
self.client.close() self.client.close()
def next_node_id(self) -> int: def next_node_id(self) -> int:
@ -783,11 +605,11 @@ class CoreClient:
image = "ubuntu:latest" image = "ubuntu:latest"
emane = None emane = None
if node_type == NodeType.EMANE: if node_type == NodeType.EMANE:
if not self.session.emane_models: if not self.emane_models:
dialog = EmaneInstallDialog(self.app) dialog = EmaneInstallDialog(self.app)
dialog.show() dialog.show()
return return
emane = self.session.emane_models[0] emane = self.emane_models[0]
name = f"emane{node_id}" name = f"emane{node_id}"
elif node_type == NodeType.WIRELESS_LAN: elif node_type == NodeType.WIRELESS_LAN:
name = f"wlan{node_id}" name = f"wlan{node_id}"
@ -806,13 +628,13 @@ class CoreClient:
) )
if nutils.is_custom(node): if nutils.is_custom(node):
services = nutils.get_custom_services(self.app.guiconfig, model) services = nutils.get_custom_services(self.app.guiconfig, model)
node.services = set(services) node.config_services = set(services)
# assign default services to CORE node # assign default services to CORE node
else: else:
services = self.session.default_services.get(model) services = self.session.default_services.get(model)
if services: if services:
node.services = services.copy() node.config_services = set(services)
logging.info( logger.info(
"add node(%s) to session(%s), coordinates(%s, %s)", "add node(%s) to session(%s), coordinates(%s, %s)",
node.name, node.name,
self.session.id, self.session.id,
@ -850,7 +672,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_proto(self) -> List[wlan_pb2.WlanConfig]: 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:
@ -858,11 +680,10 @@ class CoreClient:
if not node.wlan_config: if not node.wlan_config:
continue continue
config = ConfigOption.to_dict(node.wlan_config) config = ConfigOption.to_dict(node.wlan_config)
wlan_config = wlan_pb2.WlanConfig(node_id=node.id, config=config) configs.append((node.id, config))
configs.append(wlan_config)
return configs return configs
def get_mobility_configs_proto(self) -> List[mobility_pb2.MobilityConfig]: 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):
@ -870,27 +691,24 @@ class CoreClient:
if not node.mobility_config: if not node.mobility_config:
continue continue
config = ConfigOption.to_dict(node.mobility_config) config = ConfigOption.to_dict(node.mobility_config)
mobility_config = mobility_pb2.MobilityConfig( configs.append((node.id, config))
node_id=node.id, config=config
)
configs.append(mobility_config)
return configs return configs
def get_emane_model_configs_proto(self) -> List[emane_pb2.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():
model, iface_id = key model, iface_id = key
config = ConfigOption.to_dict(config) # config = ConfigOption.to_dict(config)
if iface_id is None: if iface_id is None:
iface_id = -1 iface_id = -1
config_proto = emane_pb2.EmaneModelConfig( config = EmaneModelConfig(
node_id=node.id, iface_id=iface_id, model=model, config=config node_id=node.id, model=model, iface_id=iface_id, config=config
) )
configs.append(config_proto) configs.append(config)
return configs return configs
def get_service_configs_proto(self) -> List[services_pb2.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):
@ -898,19 +716,19 @@ class CoreClient:
if not node.service_configs: if not node.service_configs:
continue continue
for name, config in node.service_configs.items(): for name, config in node.service_configs.items():
config_proto = services_pb2.ServiceConfig( config = ServiceConfig(
node_id=node.id, node_id=node.id,
service=name, service=name,
directories=config.dirs,
files=config.configs, files=config.configs,
directories=config.dirs,
startup=config.startup, startup=config.startup,
validate=config.validate, validate=config.validate,
shutdown=config.shutdown, shutdown=config.shutdown,
) )
configs.append(config_proto) configs.append(config)
return configs return configs
def get_service_file_configs_proto(self) -> List[services_pb2.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):
@ -919,10 +737,8 @@ class CoreClient:
continue continue
for service, file_configs in node.service_file_configs.items(): for service, file_configs in node.service_file_configs.items():
for file, data in file_configs.items(): for file, data in file_configs.items():
config_proto = services_pb2.ServiceFileConfig( config = ServiceFileConfig(node.id, service, file, data)
node_id=node.id, service=service, file=file, data=data configs.append(config)
)
configs.append(config_proto)
return configs return configs
def get_config_service_configs_proto( def get_config_service_configs_proto(
@ -945,39 +761,37 @@ class CoreClient:
return config_service_protos return config_service_protos
def run(self, node_id: int) -> str: def run(self, node_id: int) -> str:
logging.info("running node(%s) cmd: %s", node_id, self.observer) logger.info("running node(%s) cmd: %s", node_id, self.observer)
return self.client.node_command(self.session.id, node_id, self.observer).output _, output = self.client.node_command(self.session.id, node_id, self.observer)
return output
def get_wlan_config(self, node_id: int) -> Dict[str, ConfigOption]: def get_wlan_config(self, node_id: int) -> Dict[str, ConfigOption]:
response = self.client.get_wlan_config(self.session.id, node_id) config = self.client.get_wlan_config(self.session.id, node_id)
config = response.config logger.debug(
logging.debug(
"get wlan configuration from node %s, result configuration: %s", "get wlan configuration from node %s, result configuration: %s",
node_id, node_id,
config, config,
) )
return ConfigOption.from_dict(config) return config
def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]: def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]:
response = self.client.get_mobility_config(self.session.id, node_id) config = self.client.get_mobility_config(self.session.id, node_id)
config = response.config logger.debug(
logging.debug(
"get mobility config from node %s, result configuration: %s", "get mobility config from node %s, result configuration: %s",
node_id, node_id,
config, config,
) )
return ConfigOption.from_dict(config) return config
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
response = self.client.get_emane_model_config( config = self.client.get_emane_model_config(
self.session.id, node_id, model, iface_id self.session.id, node_id, model, iface_id
) )
config = response.config logger.debug(
logging.debug(
"get emane model config: node id: %s, EMANE model: %s, " "get emane model config: node id: %s, EMANE model: %s, "
"interface: %s, config: %s", "interface: %s, config: %s",
node_id, node_id,
@ -985,42 +799,21 @@ class CoreClient:
iface_id, iface_id,
config, config,
) )
return ConfigOption.from_dict(config) return config
def execute_script(self, script) -> None: def execute_script(self, script: str, options: str) -> None:
response = self.client.execute_script(script) session_id = self.client.execute_script(script, options)
logging.info("execute python script %s", response) logger.info("execute python script %s", session_id)
if response.session_id != -1: if session_id != -1:
self.join_session(response.session_id) self.join_session(session_id)
def add_link(self, link: Link) -> None: def add_link(self, link: Link) -> None:
iface1 = link.iface1.to_proto() if link.iface1 else None result, _, _ = self.client.add_link(self.session.id, link, source=GUI_SOURCE)
iface2 = link.iface2.to_proto() if link.iface2 else None logger.debug("added link: %s", result)
options = link.options.to_proto() if link.options else None if not result:
response = self.client.add_link( logger.error("error adding link: %s", link)
self.session.id,
link.node1_id,
link.node2_id,
iface1,
iface2,
options,
source=GUI_SOURCE,
)
logging.debug("added link: %s", response)
if not response.result:
logging.error("error adding link: %s", link)
def edit_link(self, link: Link) -> None: def edit_link(self, link: Link) -> None:
iface1_id = link.iface1.id if link.iface1 else None result = self.client.edit_link(self.session.id, link, source=GUI_SOURCE)
iface2_id = link.iface2.id if link.iface2 else None if not result:
response = self.client.edit_link( logger.error("error editing link: %s", link)
self.session.id,
link.node1_id,
link.node2_id,
link.options.to_proto(),
iface1_id,
iface2_id,
source=GUI_SOURCE,
)
if not response.result:
logging.error("error editing link: %s", link)

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,7 @@ class SizeAndScaleDialog(Dialog):
super().__init__(app, "Canvas Size and Scale") super().__init__(app, "Canvas Size and Scale")
self.manager: CanvasManager = self.app.manager self.manager: CanvasManager = self.app.manager
self.section_font: font.Font = font.Font(weight=font.BOLD) self.section_font: font.Font = font.Font(weight=font.BOLD)
width, height = self.manager.current_dimensions width, height = self.manager.current().current_dimensions
self.pixel_width: tk.IntVar = tk.IntVar(value=width) self.pixel_width: tk.IntVar = tk.IntVar(value=width)
self.pixel_height: tk.IntVar = tk.IntVar(value=height) self.pixel_height: tk.IntVar = tk.IntVar(value=height)
location = self.app.core.session.location location = self.app.core.session.location
@ -189,7 +189,7 @@ class SizeAndScaleDialog(Dialog):
def click_apply(self) -> None: def click_apply(self) -> None:
width, height = self.pixel_width.get(), self.pixel_height.get() width, height = self.pixel_width.get(), self.pixel_height.get()
self.manager.redraw_canvases((width, height)) self.manager.redraw_canvas((width, height))
location = self.app.core.session.location location = self.app.core.session.location
location.x = self.x.get() location.x = self.x.get()
location.y = self.y.get() location.y = self.y.get()

View file

@ -13,6 +13,8 @@ from core.gui.graph.graph import CanvasGraph
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import image_chooser from core.gui.widgets import image_chooser
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -167,5 +169,5 @@ class CanvasWallpaperDialog(Dialog):
try: try:
self.canvas.set_wallpaper(filename) self.canvas.set_wallpaper(filename)
except FileNotFoundError: except FileNotFoundError:
logging.error("invalid background: %s", filename) logger.error("invalid background: %s", filename)
self.destroy() self.destroy()

View file

@ -18,6 +18,8 @@ from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
@ -73,7 +75,7 @@ class ConfigServiceConfigDialog(Dialog):
def load(self) -> None: def load(self) -> None:
try: try:
self.core.create_nodes_and_links() self.core.start_session(definition=True)
service = self.core.config_services[self.service_name] service = self.core.config_services[self.service_name]
self.dependencies = service.dependencies[:] self.dependencies = service.dependencies[:]
self.executables = service.executables[:] self.executables = service.executables[:]
@ -86,18 +88,18 @@ class ConfigServiceConfigDialog(Dialog):
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)
response = self.core.client.get_config_service_defaults(self.service_name) defaults = self.core.client.get_config_service_defaults(self.service_name)
self.original_service_files = response.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(x.name for x in response.modes) self.modes = sorted(defaults.modes)
self.mode_configs = {x.name: x.config for x in response.modes} self.mode_configs = defaults.modes
self.config = ConfigOption.from_dict(response.config) self.config = ConfigOption.from_dict(defaults.config)
self.default_config = {x.name: x.value for x in self.config.values()} self.default_config = {x.name: x.value for x in self.config.values()}
service_config = self.node.config_service_configs.get(self.service_name) service_config = self.node.config_service_configs.get(self.service_name)
if service_config: if service_config:
for key, value in service_config.config.items(): for key, value in service_config.config.items():
self.config[key].value = value self.config[key].value = value
logging.info("default config: %s", self.default_config) logger.info("default config: %s", self.default_config)
for file, data in service_config.templates.items(): for file, data in service_config.templates.items():
self.modified_files.add(file) self.modified_files.add(file)
self.temp_service_files[file] = data self.temp_service_files[file] = data
@ -181,7 +183,7 @@ class ConfigServiceConfigDialog(Dialog):
self.modes_combobox.bind("<<ComboboxSelected>>", self.handle_mode_changed) self.modes_combobox.bind("<<ComboboxSelected>>", self.handle_mode_changed)
self.modes_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY) self.modes_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
logging.info("config service config: %s", self.config) logger.info("config service config: %s", self.config)
self.config_frame = ConfigFrame(tab, self.app, self.config) self.config_frame = ConfigFrame(tab, self.app, self.config)
self.config_frame.draw_config() self.config_frame.draw_config()
self.config_frame.grid(sticky=tk.NSEW, pady=PADY) self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
@ -308,9 +310,9 @@ class ConfigServiceConfigDialog(Dialog):
current_listbox.itemconfig(current_listbox.curselection()[0], bg="") current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
self.destroy() self.destroy()
return return
service_config = self.node.config_service_configs.get(self.service_name) service_config = self.node.config_service_configs.setdefault(
if not service_config: self.service_name, ConfigServiceData()
service_config = ConfigServiceData() )
if self.config_frame: if self.config_frame:
self.config_frame.parse_config() self.config_frame.parse_config()
service_config.config = {x.name: x.value for x in self.config.values()} service_config.config = {x.name: x.value for x in self.config.values()}
@ -328,7 +330,7 @@ class ConfigServiceConfigDialog(Dialog):
def handle_mode_changed(self, event: tk.Event) -> None: def handle_mode_changed(self, event: tk.Event) -> None:
mode = self.modes_combobox.get() mode = self.modes_combobox.get()
config = self.mode_configs[mode] config = self.mode_configs[mode]
logging.info("mode config: %s", config) logger.info("mode config: %s", config)
self.config_frame.set_values(config) self.config_frame.set_values(config)
def update_template_file_data(self, event: tk.Event) -> None: def update_template_file_data(self, event: tk.Event) -> None:
@ -350,7 +352,7 @@ class ConfigServiceConfigDialog(Dialog):
def click_defaults(self) -> None: def click_defaults(self) -> None:
self.node.config_service_configs.pop(self.service_name, None) self.node.config_service_configs.pop(self.service_name, None)
logging.info( logger.info(
"cleared config service config: %s", self.node.config_service_configs "cleared config service config: %s", self.node.config_service_configs
) )
self.temp_service_files = dict(self.original_service_files) self.temp_service_files = dict(self.original_service_files)
@ -358,7 +360,7 @@ class ConfigServiceConfigDialog(Dialog):
self.template_text.text.delete(1.0, "end") self.template_text.text.delete(1.0, "end")
self.template_text.text.insert("end", self.temp_service_files[filename]) self.template_text.text.insert("end", self.temp_service_files[filename])
if self.config_frame: if self.config_frame:
logging.info("resetting defaults: %s", self.default_config) logger.info("resetting defaults: %s", self.default_config)
self.config_frame.set_values(self.default_config) self.config_frame.set_values(self.default_config)
def click_copy(self) -> None: def click_copy(self) -> None:

View file

@ -13,6 +13,8 @@ from core.gui.nodeutils import NodeDraw
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -209,7 +211,7 @@ class CustomNodesDialog(Dialog):
name, node_draw.image_file, list(node_draw.services) name, node_draw.image_file, list(node_draw.services)
) )
self.app.guiconfig.nodes.append(custom_node) self.app.guiconfig.nodes.append(custom_node)
logging.info("saving custom nodes: %s", self.app.guiconfig.nodes) logger.info("saving custom nodes: %s", self.app.guiconfig.nodes)
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()
@ -219,7 +221,7 @@ class CustomNodesDialog(Dialog):
image_file = str(Path(self.image_file).absolute()) image_file = str(Path(self.image_file).absolute())
custom_node = CustomNode(name, image_file, list(self.services)) custom_node = CustomNode(name, image_file, list(self.services))
node_draw = NodeDraw.from_custom(custom_node) node_draw = NodeDraw.from_custom(custom_node)
logging.info( logger.info(
"created new custom node (%s), image file (%s), services: (%s)", "created new custom node (%s), image file (%s), services: (%s)",
name, name,
image_file, image_file,
@ -239,7 +241,7 @@ class CustomNodesDialog(Dialog):
node_draw.image_file = str(Path(self.image_file).absolute()) node_draw.image_file = str(Path(self.image_file).absolute())
node_draw.image = self.image node_draw.image = self.image
node_draw.services = set(self.services) node_draw.services = set(self.services)
logging.debug( logger.debug(
"edit custom node (%s), image: (%s), services (%s)", "edit custom node (%s), image: (%s), services (%s)",
node_draw.model, node_draw.model,
node_draw.image_file, node_draw.image_file,

View file

@ -19,40 +19,6 @@ if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
class GlobalEmaneDialog(Dialog):
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
super().__init__(app, "EMANE Configuration", master=master)
self.config_frame: Optional[ConfigFrame] = None
self.enabled: bool = not self.app.core.is_runtime()
self.draw()
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
session = self.app.core.session
self.config_frame = ConfigFrame(
self.top, self.app, session.emane_config, self.enabled
)
self.config_frame.draw_config()
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
self.draw_buttons()
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky=tk.EW)
def click_apply(self) -> None:
self.config_frame.parse_config()
self.destroy()
class EmaneModelDialog(Dialog): class EmaneModelDialog(Dialog):
def __init__( def __init__(
self, self,
@ -115,7 +81,7 @@ class EmaneConfigDialog(Dialog):
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.session.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]
self.emane_model: tk.StringVar = tk.StringVar(value=model) self.emane_model: tk.StringVar = tk.StringVar(value=model)
@ -179,9 +145,7 @@ class EmaneConfigDialog(Dialog):
def draw_emane_buttons(self) -> None: def draw_emane_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky=tk.EW, pady=PADY) frame.grid(sticky=tk.EW, pady=PADY)
for i in range(2): frame.columnconfigure(0, weight=1)
frame.columnconfigure(i, weight=1)
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE) image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
self.emane_model_button = ttk.Button( self.emane_model_button = ttk.Button(
frame, frame,
@ -191,18 +155,7 @@ class EmaneConfigDialog(Dialog):
command=self.click_model_config, command=self.click_model_config,
) )
self.emane_model_button.image = image self.emane_model_button.image = image
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky=tk.EW) self.emane_model_button.grid(padx=PADX, sticky=tk.EW)
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
button = ttk.Button(
frame,
text="EMANE options",
image=image,
compound=tk.RIGHT,
command=self.click_emane_config,
)
button.image = image
button.grid(row=0, column=1, sticky=tk.EW)
def draw_apply_and_cancel(self) -> None: def draw_apply_and_cancel(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
@ -215,10 +168,6 @@ class EmaneConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky=tk.EW) button.grid(row=0, column=1, sticky=tk.EW)
def click_emane_config(self) -> None:
dialog = GlobalEmaneDialog(self, self.app)
dialog.show()
def click_model_config(self) -> None: def click_model_config(self) -> None:
""" """
draw emane model configuration draw emane model configuration

View file

@ -7,6 +7,8 @@ from core.gui.appconfig import SCRIPT_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX from core.gui.themes import FRAME_PAD, PADX
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -83,6 +85,6 @@ class ExecutePythonDialog(Dialog):
def script_execute(self) -> None: def script_execute(self) -> None:
file = self.file_entry.get() file = self.file_entry.get()
options = self.option_entry.get() options = self.option_entry.get()
logging.info("Execute %s with options %s", file, options) logger.info("Execute %s with options %s", file, options)
self.app.core.execute_script(file) self.app.core.execute_script(file, options)
self.destroy() self.destroy()

View file

@ -6,6 +6,8 @@ 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 FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -139,8 +141,8 @@ class FindDialog(Dialog):
_x, _y, _, _ = canvas_node.canvas.bbox(canvas_node.id) _x, _y, _, _ = canvas_node.canvas.bbox(canvas_node.id)
oid = canvas_node.canvas.find_withtag("rectangle") oid = canvas_node.canvas.find_withtag("rectangle")
x0, y0, x1, y1 = canvas_node.canvas.bbox(oid[0]) x0, y0, x1, y1 = canvas_node.canvas.bbox(oid[0])
logging.debug("Dist to most left: %s", abs(x0 - _x)) logger.debug("Dist to most left: %s", abs(x0 - _x))
logging.debug("White canvas width: %s", abs(x0 - x1)) logger.debug("White canvas width: %s", abs(x0 - x1))
# calculate the node's location # calculate the node's location
# (as fractions of white canvas's width and height) # (as fractions of white canvas's width and height)

View file

@ -23,6 +23,8 @@ class IpConfigDialog(Dialog):
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
self.ip6_listbox: Optional[ListboxScroll] = None self.ip6_listbox: Optional[ListboxScroll] = None
self.enable_ip4 = tk.BooleanVar(value=self.app.guiconfig.ips.enable_ip4)
self.enable_ip6 = tk.BooleanVar(value=self.app.guiconfig.ips.enable_ip6)
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:
@ -36,10 +38,19 @@ class IpConfigDialog(Dialog):
frame.rowconfigure(0, weight=1) frame.rowconfigure(0, weight=1)
frame.grid(sticky=tk.NSEW, pady=PADY) frame.grid(sticky=tk.NSEW, pady=PADY)
ip4_checkbox = ttk.Checkbutton(
frame, text="Enable IP4?", variable=self.enable_ip4
)
ip4_checkbox.grid(row=0, column=0, sticky=tk.EW)
ip6_checkbox = ttk.Checkbutton(
frame, text="Enable IP6?", variable=self.enable_ip6
)
ip6_checkbox.grid(row=0, column=1, sticky=tk.EW)
ip4_frame = ttk.LabelFrame(frame, text="IPv4", padding=FRAME_PAD) ip4_frame = ttk.LabelFrame(frame, text="IPv4", padding=FRAME_PAD)
ip4_frame.columnconfigure(0, weight=1) ip4_frame.columnconfigure(0, weight=1)
ip4_frame.rowconfigure(0, weight=1) ip4_frame.rowconfigure(1, weight=1)
ip4_frame.grid(row=0, column=0, stick="nsew") ip4_frame.grid(row=1, column=0, stick=tk.NSEW)
self.ip4_listbox = ListboxScroll(ip4_frame) self.ip4_listbox = ListboxScroll(ip4_frame)
self.ip4_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip4) self.ip4_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip4)
self.ip4_listbox.grid(sticky=tk.NSEW, pady=PADY) self.ip4_listbox.grid(sticky=tk.NSEW, pady=PADY)
@ -63,7 +74,7 @@ class IpConfigDialog(Dialog):
ip6_frame = ttk.LabelFrame(frame, text="IPv6", padding=FRAME_PAD) ip6_frame = ttk.LabelFrame(frame, text="IPv6", padding=FRAME_PAD)
ip6_frame.columnconfigure(0, weight=1) ip6_frame.columnconfigure(0, weight=1)
ip6_frame.rowconfigure(0, weight=1) ip6_frame.rowconfigure(0, weight=1)
ip6_frame.grid(row=0, column=1, stick="nsew") ip6_frame.grid(row=1, column=1, stick=tk.NSEW)
self.ip6_listbox = ListboxScroll(ip6_frame) self.ip6_listbox = ListboxScroll(ip6_frame)
self.ip6_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip6) self.ip6_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip6)
self.ip6_listbox.grid(sticky=tk.NSEW, pady=PADY) self.ip6_listbox.grid(sticky=tk.NSEW, pady=PADY)
@ -86,7 +97,7 @@ class IpConfigDialog(Dialog):
# draw buttons # draw buttons
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(stick="ew") frame.grid(stick=tk.EW)
for i in range(2): for i in range(2):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save) button = ttk.Button(frame, text="Save", command=self.click_save)
@ -142,10 +153,18 @@ class IpConfigDialog(Dialog):
ip6 = self.ip6_listbox.listbox.get(index) ip6 = self.ip6_listbox.listbox.get(index)
ip6s.append(ip6) ip6s.append(ip6)
ip_config = self.app.guiconfig.ips ip_config = self.app.guiconfig.ips
ip_config.ip4 = self.ip4 ip_changed = False
ip_config.ip6 = self.ip6 if ip_config.ip4 != self.ip4:
ip_config.ip4 = self.ip4
ip_changed = True
if ip_config.ip6 != self.ip6:
ip_config.ip6 = self.ip6
ip_changed = True
ip_config.ip4s = ip4s ip_config.ip4s = ip4s
ip_config.ip6s = ip6s ip_config.ip6s = ip6s
self.app.core.ifaces_manager.update_ips(self.ip4, self.ip6) ip_config.enable_ip4 = self.enable_ip4.get()
ip_config.enable_ip6 = self.enable_ip6.get()
if ip_changed:
self.app.core.ifaces_manager.update_ips(self.ip4, self.ip6)
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()

View file

@ -134,7 +134,7 @@ class MobilityPlayerDialog(Dialog):
session_id = self.app.core.session.id session_id = self.app.core.session.id
try: try:
self.app.core.client.mobility_action( self.app.core.client.mobility_action(
session_id, self.node.id, MobilityAction.START.value session_id, self.node.id, MobilityAction.START
) )
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Mobility Error", e) self.app.show_grpc_exception("Mobility Error", e)
@ -144,7 +144,7 @@ class MobilityPlayerDialog(Dialog):
session_id = self.app.core.session.id session_id = self.app.core.session.id
try: try:
self.app.core.client.mobility_action( self.app.core.client.mobility_action(
session_id, self.node.id, MobilityAction.PAUSE.value session_id, self.node.id, MobilityAction.PAUSE
) )
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Mobility Error", e) self.app.show_grpc_exception("Mobility Error", e)
@ -154,7 +154,7 @@ class MobilityPlayerDialog(Dialog):
session_id = self.app.core.session.id session_id = self.app.core.session.id
try: try:
self.app.core.client.mobility_action( self.app.core.client.mobility_action(
session_id, self.node.id, MobilityAction.STOP.value session_id, self.node.id, MobilityAction.STOP
) )
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Mobility Error", e) self.app.show_grpc_exception("Mobility Error", e)

View file

@ -17,6 +17,8 @@ from core.gui.dialogs.emaneconfig import EmaneModelDialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import ListboxScroll, image_chooser from core.gui.widgets import ListboxScroll, image_chooser
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
@ -260,17 +262,17 @@ class NodeConfigDialog(Dialog):
row += 1 row += 1
if nutils.is_rj45(self.node): if nutils.is_rj45(self.node):
response = self.app.core.client.get_ifaces() ifaces = self.app.core.client.get_ifaces()
logging.debug("host machine available interfaces: %s", response) logger.debug("host machine available interfaces: %s", ifaces)
ifaces = ListboxScroll(frame) ifaces_scroll = ListboxScroll(frame)
ifaces.listbox.config(state=state) ifaces_scroll.listbox.config(state=state)
ifaces.grid( ifaces_scroll.grid(
row=row, column=0, columnspan=2, sticky=tk.EW, padx=PADX, pady=PADY row=row, column=0, columnspan=2, sticky=tk.EW, padx=PADX, pady=PADY
) )
for inf in sorted(response.ifaces[:]): for inf in sorted(ifaces):
ifaces.listbox.insert(tk.END, inf) ifaces_scroll.listbox.insert(tk.END, inf)
row += 1 row += 1
ifaces.listbox.bind("<<ListboxSelect>>", self.iface_select) ifaces_scroll.listbox.bind("<<ListboxSelect>>", self.iface_select)
# interfaces # interfaces
if self.canvas_node.ifaces: if self.canvas_node.ifaces:
@ -296,10 +298,9 @@ class NodeConfigDialog(Dialog):
emane_node = self.canvas_node.has_emane_link(iface.id) emane_node = self.canvas_node.has_emane_link(iface.id)
if emane_node: if emane_node:
emane_model = emane_node.emane.split("_")[1] emane_model = emane_node.emane.split("_")[1]
command = partial(self.click_emane_config, emane_model, iface.id)
button = ttk.Button( button = ttk.Button(
tab, tab, text=f"Configure EMANE {emane_model}", command=command
text=f"Configure EMANE {emane_model}",
command=lambda: self.click_emane_config(emane_model, iface.id),
) )
button.grid(row=row, sticky=tk.EW, columnspan=3, pady=PADY) button.grid(row=row, sticky=tk.EW, columnspan=3, pady=PADY)
row += 1 row += 1
@ -365,6 +366,7 @@ class NodeConfigDialog(Dialog):
button.grid(row=0, column=1, sticky=tk.EW) button.grid(row=0, column=1, sticky=tk.EW)
def click_emane_config(self, emane_model: str, iface_id: int) -> None: def click_emane_config(self, emane_model: str, iface_id: int) -> None:
logger.info("configuring emane: %s - %s", emane_model, iface_id)
dialog = EmaneModelDialog(self, self.app, self.node, emane_model, iface_id) dialog = EmaneModelDialog(self, self.app, self.node, emane_model, iface_id)
dialog.show() dialog.show()

View file

@ -12,6 +12,8 @@ from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll from core.gui.widgets import CheckboxList, ListboxScroll
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -29,6 +31,7 @@ class NodeConfigServiceDialog(Dialog):
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.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:
@ -100,6 +103,7 @@ class NodeConfigServiceDialog(Dialog):
self.current_services.add(name) self.current_services.add(name)
elif not var.get() and name in self.current_services: elif not var.get() and name in self.current_services:
self.current_services.remove(name) self.current_services.remove(name)
self.node.config_service_configs.pop(name, None)
self.draw_current_services() self.draw_current_services()
self.node.config_services = self.current_services.copy() self.node.config_services = self.current_services.copy()
@ -131,7 +135,7 @@ class NodeConfigServiceDialog(Dialog):
def click_save(self) -> None: def click_save(self) -> None:
self.node.config_services = self.current_services.copy() self.node.config_services = self.current_services.copy()
logging.info("saved node config services: %s", self.node.config_services) logger.info("saved node config services: %s", self.node.config_services)
self.destroy() self.destroy()
def click_cancel(self) -> None: def click_cancel(self) -> None:
@ -144,6 +148,7 @@ class NodeConfigServiceDialog(Dialog):
service = self.current.listbox.get(cur[0]) service = self.current.listbox.get(cur[0])
self.current.listbox.delete(cur[0]) self.current.listbox.delete(cur[0])
self.current_services.remove(service) self.current_services.remove(service)
self.node.config_service_configs.pop(service, None)
for checkbutton in self.services.frame.winfo_children(): for checkbutton in self.services.frame.winfo_children():
if checkbutton["text"] == service: if checkbutton["text"] == service:
checkbutton.invoke() checkbutton.invoke()

View file

@ -17,7 +17,7 @@ if TYPE_CHECKING:
class NodeServiceDialog(Dialog): class NodeServiceDialog(Dialog):
def __init__(self, app: "Application", node: Node) -> None: def __init__(self, app: "Application", node: Node) -> None:
title = f"{node.name} Services" title = f"{node.name} Services (Deprecated)"
super().__init__(app, title) super().__init__(app, title)
self.node: Node = node self.node: Node = node
self.groups: Optional[ListboxScroll] = None self.groups: Optional[ListboxScroll] = None
@ -25,6 +25,7 @@ class NodeServiceDialog(Dialog):
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.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:
@ -77,7 +78,7 @@ class NodeServiceDialog(Dialog):
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Remove", command=self.click_remove) button = ttk.Button(frame, text="Remove", command=self.click_remove)
button.grid(row=0, column=2, sticky=tk.EW, padx=PADX) button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
button.grid(row=0, column=3, sticky=tk.EW) button.grid(row=0, column=3, sticky=tk.EW)
# trigger group change # trigger group change
@ -98,6 +99,8 @@ class NodeServiceDialog(Dialog):
self.current_services.add(name) self.current_services.add(name)
elif not var.get() and name in self.current_services: elif not var.get() and name in self.current_services:
self.current_services.remove(name) self.current_services.remove(name)
self.node.service_configs.pop(name, None)
self.node.service_file_configs.pop(name, None)
self.current.listbox.delete(0, tk.END) self.current.listbox.delete(0, tk.END)
for name in sorted(self.current_services): for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name) self.current.listbox.insert(tk.END, name)
@ -125,6 +128,9 @@ class NodeServiceDialog(Dialog):
"Service Configuration", "Select a service to configure", parent=self "Service Configuration", "Select a service to configure", parent=self
) )
def click_cancel(self) -> None:
self.destroy()
def click_save(self) -> None: def click_save(self) -> None:
self.node.services = self.current_services.copy() self.node.services = self.current_services.copy()
self.destroy() self.destroy()
@ -135,6 +141,8 @@ class NodeServiceDialog(Dialog):
service = self.current.listbox.get(cur[0]) service = self.current.listbox.get(cur[0])
self.current.listbox.delete(cur[0]) self.current.listbox.delete(cur[0])
self.current_services.remove(service) self.current_services.remove(service)
self.node.service_configs.pop(service, None)
self.node.service_file_configs.pop(service, None)
for checkbutton in self.services.frame.winfo_children(): for checkbutton in self.services.frame.winfo_children():
if checkbutton["text"] == service: if checkbutton["text"] == service:
checkbutton.invoke() checkbutton.invoke()

View file

@ -9,6 +9,8 @@ from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts
from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -102,7 +104,7 @@ class PreferencesDialog(Dialog):
def theme_change(self, event: tk.Event) -> None: def theme_change(self, event: tk.Event) -> None:
theme = self.theme.get() theme = self.theme.get()
logging.info("changing theme: %s", theme) logger.info("changing theme: %s", theme)
self.app.style.theme_use(theme) self.app.style.theme_use(theme)
def click_save(self) -> None: def click_save(self) -> None:

View file

@ -106,10 +106,8 @@ class RunToolDialog(Dialog):
for selection in self.node_list.listbox.curselection(): for selection in self.node_list.listbox.curselection():
node_name = self.node_list.listbox.get(selection) node_name = self.node_list.listbox.get(selection)
node_id = self.executable_nodes[node_name] node_id = self.executable_nodes[node_name]
response = self.app.core.client.node_command( _, output = self.app.core.client.node_command(
self.app.core.session.id, node_id, command self.app.core.session.id, node_id, command
) )
self.result.text.insert( self.result.text.insert(tk.END, f"> {node_name} > {command}:\n{output}\n")
tk.END, f"> {node_name} > {command}:\n{response.output}\n"
)
self.result.text.config(state=tk.DISABLED) self.result.text.config(state=tk.DISABLED)

View file

@ -1,7 +1,7 @@
import logging import logging
import os
import tkinter as tk import tkinter as tk
from tkinter import filedialog, ttk from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
import grpc import grpc
@ -15,6 +15,8 @@ from core.gui.images import ImageEnum
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll from core.gui.widgets import CodeText, ListboxScroll
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
@ -26,7 +28,7 @@ class ServiceConfigDialog(Dialog):
def __init__( def __init__(
self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node
) -> None: ) -> None:
title = f"{service_name} Service" title = f"{service_name} Service (Deprecated)"
super().__init__(app, title, master=master) super().__init__(app, title, master=master)
self.core: "CoreClient" = app.core self.core: "CoreClient" = app.core
self.node: Node = node self.node: Node = node
@ -76,7 +78,7 @@ class ServiceConfigDialog(Dialog):
def load(self) -> None: def load(self) -> None:
try: try:
self.app.core.create_nodes_and_links() self.core.start_session(definition=True)
default_config = self.app.core.get_node_service( default_config = self.app.core.get_node_service(
self.node.id, self.service_name self.node.id, self.service_name
) )
@ -388,7 +390,7 @@ class ServiceConfigDialog(Dialog):
1.0, "end" 1.0, "end"
) )
else: else:
logging.debug("file already existed") logger.debug("file already existed")
def delete_filename(self) -> None: def delete_filename(self) -> None:
cbb = self.filename_combobox cbb = self.filename_combobox
@ -447,36 +449,31 @@ class ServiceConfigDialog(Dialog):
self.current_service_color("") self.current_service_color("")
self.destroy() self.destroy()
return return
files = set(self.filenames)
try: if (
if ( self.is_custom_command()
self.is_custom_command() or self.has_new_files()
or self.has_new_files() or self.is_custom_directory()
or self.is_custom_directory() ):
): startup, validate, shutdown = self.get_commands()
startup, validate, shutdown = self.get_commands() files = set(self.filename_combobox["values"])
config = self.core.set_node_service( service_data = NodeServiceData(
self.node.id, configs=list(files),
self.service_name, dirs=self.temp_directories,
dirs=self.temp_directories, startup=startup,
files=list(self.filename_combobox["values"]), validate=validate,
startups=startup, shutdown=shutdown,
validations=validate, )
shutdowns=shutdown, logger.info("setting service data: %s", service_data)
) self.node.service_configs[self.service_name] = service_data
self.node.service_configs[self.service_name] = config for file in self.modified_files:
for file in self.modified_files: if file not in files:
file_configs = self.node.service_file_configs.setdefault( continue
self.service_name, {} file_configs = self.node.service_file_configs.setdefault(
) self.service_name, {}
file_configs[file] = self.temp_service_files[file] )
# TODO: check if this is really needed file_configs[file] = self.temp_service_files[file]
self.app.core.set_node_service_file( self.current_service_color("green")
self.node.id, self.service_name, file, self.temp_service_files[file]
)
self.current_service_color("green")
except grpc.RpcError as e:
self.app.show_grpc_exception("Save Service Config Error", e)
self.destroy() self.destroy()
def display_service_file_data(self, event: tk.Event) -> None: def display_service_file_data(self, event: tk.Event) -> None:
@ -579,11 +576,13 @@ class ServiceConfigDialog(Dialog):
self.directory_entry.insert("end", d) self.directory_entry.insert("end", d)
def add_directory(self) -> None: def add_directory(self) -> None:
d = self.directory_entry.get() directory = Path(self.directory_entry.get())
if os.path.isdir(d): if directory.is_absolute():
if d not in self.temp_directories: if str(directory) not in self.temp_directories:
self.dir_list.listbox.insert("end", d) self.dir_list.listbox.insert("end", directory)
self.temp_directories.append(d) self.temp_directories.append(str(directory))
else:
messagebox.showerror("Add Directory", "Path must be absolute!", parent=self)
def remove_directory(self) -> None: def remove_directory(self) -> None:
d = self.directory_entry.get() d = self.directory_entry.get()
@ -594,7 +593,7 @@ class ServiceConfigDialog(Dialog):
i = dirs.index(d) i = dirs.index(d)
self.dir_list.listbox.delete(i) self.dir_list.listbox.delete(i)
except ValueError: except ValueError:
logging.debug("directory is not in the list") logger.debug("directory is not in the list")
self.directory_entry.delete(0, "end") self.directory_entry.delete(0, "end")
def directory_select(self, event) -> None: def directory_select(self, event) -> None:

View file

@ -1,15 +1,14 @@
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, Optional from typing import TYPE_CHECKING, Optional
import grpc
from core.api.grpc.wrappers import ConfigOption
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
from core.gui.widgets import ConfigFrame from core.gui.widgets import ConfigFrame
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -19,25 +18,15 @@ class SessionOptionsDialog(Dialog):
super().__init__(app, "Session Options") super().__init__(app, "Session Options")
self.config_frame: Optional[ConfigFrame] = None self.config_frame: Optional[ConfigFrame] = None
self.has_error: bool = False self.has_error: bool = False
self.config: Dict[str, ConfigOption] = self.get_config()
self.enabled: bool = not self.app.core.is_runtime() self.enabled: bool = not self.app.core.is_runtime()
if not self.has_error: if not self.has_error:
self.draw() self.draw()
def get_config(self) -> Dict[str, ConfigOption]:
try:
session_id = self.app.core.session.id
response = self.app.core.client.get_session_options(session_id)
return ConfigOption.from_dict(response.config)
except grpc.RpcError as e:
self.app.show_grpc_exception("Get Session Options Error", e)
self.has_error = True
self.destroy()
def draw(self) -> None: def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled) options = self.app.core.session.options
self.config_frame = ConfigFrame(self.top, self.app, options, self.enabled)
self.config_frame.draw_config() self.config_frame.draw_config()
self.config_frame.grid(sticky=tk.NSEW, pady=PADY) self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
@ -53,10 +42,6 @@ class SessionOptionsDialog(Dialog):
def save(self) -> None: def save(self) -> None:
config = self.config_frame.parse_config() config = self.config_frame.parse_config()
try: for key, value in config.items():
session_id = self.app.core.session.id self.app.core.session.options[key].value = value
response = self.app.core.client.set_session_options(session_id, config)
logging.info("saved session config: %s", response)
except grpc.RpcError as e:
self.app.show_grpc_exception("Set Session Options Error", e)
self.destroy() self.destroy()

View file

@ -12,6 +12,8 @@ from core.gui.images import ImageEnum
from core.gui.task import ProgressTask from core.gui.task import ProgressTask
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -30,10 +32,9 @@ class SessionsDialog(Dialog):
def get_sessions(self) -> List[SessionSummary]: def get_sessions(self) -> List[SessionSummary]:
try: try:
response = self.app.core.client.get_sessions() sessions = self.app.core.client.get_sessions()
logging.info("sessions: %s", response) logger.info("sessions: %s", sessions)
sessions = sorted(response.sessions, key=lambda x: x.id) return sorted(sessions, key=lambda x: x.id)
return [SessionSummary.from_proto(x) for x in sessions]
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Get Sessions Error", e) self.app.show_grpc_exception("Get Sessions Error", e)
self.destroy() self.destroy()
@ -176,7 +177,7 @@ class SessionsDialog(Dialog):
self.selected_id = None self.selected_id = None
self.delete_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED)
self.connect_button.config(state=tk.DISABLED) self.connect_button.config(state=tk.DISABLED)
logging.debug("selected session: %s", self.selected_session) logger.debug("selected session: %s", self.selected_session)
def click_connect(self) -> None: def click_connect(self) -> None:
if not self.selected_session: if not self.selected_session:
@ -200,7 +201,7 @@ class SessionsDialog(Dialog):
def click_delete(self) -> None: def click_delete(self) -> None:
if not self.selected_session: if not self.selected_session:
return return
logging.info("click delete session: %s", self.selected_session) logger.info("click delete session: %s", self.selected_session)
self.tree.delete(self.selected_id) self.tree.delete(self.selected_id)
self.app.core.delete_session(self.selected_session) self.app.core.delete_session(self.selected_session)
session_id = None session_id = None

View file

@ -11,6 +11,8 @@ from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.utils import bandwidth_text, delay_jitter_text from core.gui.utils import bandwidth_text, delay_jitter_text
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
@ -393,7 +395,7 @@ class Edge:
self.dst.canvas.coords(self.dst_label2, *dst_pos) self.dst.canvas.coords(self.dst_label2, *dst_pos)
def delete(self) -> None: def delete(self) -> None:
logging.debug("deleting canvas edge, id: %s", self.id) logger.debug("deleting canvas edge, id: %s", self.id)
self.src.canvas.delete(self.id) self.src.canvas.delete(self.id)
self.src.canvas.delete(self.src_label) self.src.canvas.delete(self.src_label)
self.src.canvas.delete(self.dst_label) self.src.canvas.delete(self.dst_label)
@ -488,7 +490,7 @@ class CanvasWirelessEdge(Edge):
token: str, token: str,
link: Link, link: Link,
) -> None: ) -> None:
logging.debug("drawing wireless link from node %s to node %s", src, dst) logger.debug("drawing wireless link from node %s to node %s", src, dst)
super().__init__(app, src, dst) super().__init__(app, src, dst)
self.src.wireless_edges.add(self) self.src.wireless_edges.add(self)
self.dst.wireless_edges.add(self) self.dst.wireless_edges.add(self)
@ -622,7 +624,7 @@ class CanvasEdge(Edge):
self.draw_link_options() self.draw_link_options()
def complete(self, dst: "CanvasNode", link: Link = None) -> None: def complete(self, dst: "CanvasNode", link: Link = None) -> None:
logging.debug( logger.debug(
"completing wired link from node(%s) to node(%s)", "completing wired link from node(%s) to node(%s)",
self.src.core_node.name, self.src.core_node.name,
dst.core_node.name, dst.core_node.name,

View file

@ -18,6 +18,8 @@ from core.gui.graph.node import CanvasNode, ShadowNode
from core.gui.graph.shape import Shape from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.manager import CanvasManager from core.gui.graph.manager import CanvasManager
@ -101,10 +103,15 @@ class CanvasGraph(tk.Canvas):
""" """
Bind any mouse events or hot keys to the matching action Bind any mouse events or hot keys to the matching action
""" """
self.bind("<Control-c>", self.copy_selected)
self.bind("<Control-v>", self.paste_selected)
self.bind("<Control-x>", self.cut_selected)
self.bind("<Control-d>", self.delete_selected)
self.bind("<Control-h>", self.hide_selected)
self.bind("<ButtonPress-1>", self.click_press) self.bind("<ButtonPress-1>", self.click_press)
self.bind("<ButtonRelease-1>", self.click_release) self.bind("<ButtonRelease-1>", self.click_release)
self.bind("<B1-Motion>", self.click_motion) self.bind("<B1-Motion>", self.click_motion)
self.bind("<Delete>", self.press_delete) self.bind("<Delete>", self.delete_selected)
self.bind("<Control-1>", self.ctrl_click) self.bind("<Control-1>", self.ctrl_click)
self.bind("<Double-Button-1>", self.double_click) self.bind("<Double-Button-1>", self.double_click)
self.bind("<MouseWheel>", self.zoom) self.bind("<MouseWheel>", self.zoom)
@ -184,7 +191,7 @@ class CanvasGraph(tk.Canvas):
""" """
Draw a node or finish drawing an edge according to the current graph mode Draw a node or finish drawing an edge according to the current graph mode
""" """
logging.debug("click release") logger.debug("click release")
x, y = self.canvas_xy(event) x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y): if not self.inside_canvas(x, y):
return return
@ -210,7 +217,7 @@ class CanvasGraph(tk.Canvas):
else: else:
self.focus_set() self.focus_set()
self.selected = self.get_selected(event) self.selected = self.get_selected(event)
logging.debug( logger.debug(
"click release selected(%s) mode(%s)", self.selected, self.manager.mode "click release selected(%s) mode(%s)", self.selected, self.manager.mode
) )
if self.manager.mode == GraphMode.EDGE: if self.manager.mode == GraphMode.EDGE:
@ -228,7 +235,7 @@ class CanvasGraph(tk.Canvas):
edge = self.drawing_edge edge = self.drawing_edge
self.drawing_edge = None self.drawing_edge = None
# edge dst must be a node # edge dst must be a node
logging.debug("current selected: %s", self.selected) logger.debug("current selected: %s", self.selected)
dst_node = self.nodes.get(self.selected) dst_node = self.nodes.get(self.selected)
if not dst_node: if not dst_node:
edge.delete() edge.delete()
@ -275,7 +282,7 @@ class CanvasGraph(tk.Canvas):
if select_id is not None: if select_id is not None:
self.move(select_id, x_offset, y_offset) self.move(select_id, x_offset, y_offset)
def delete_selected_objects(self) -> None: def delete_selected_objects(self, _event: tk.Event = None) -> None:
edges = set() edges = set()
nodes = [] nodes = []
for object_id in self.selection: for object_id in self.selection:
@ -305,7 +312,7 @@ class CanvasGraph(tk.Canvas):
self.selection.clear() self.selection.clear()
self.core.deleted_canvas_nodes(nodes) self.core.deleted_canvas_nodes(nodes)
def hide_selected_objects(self) -> None: def hide_selected(self, _event: tk.Event = None) -> None:
for object_id in self.selection: for object_id in self.selection:
# delete selection box # delete selection box
selection_id = self.selection[object_id] selection_id = self.selection[object_id]
@ -331,8 +338,8 @@ class CanvasGraph(tk.Canvas):
self.offset[0] * factor + event.x * (1 - factor), self.offset[0] * factor + event.x * (1 - factor),
self.offset[1] * factor + event.y * (1 - factor), self.offset[1] * factor + event.y * (1 - factor),
) )
logging.debug("ratio: %s", self.ratio) logger.debug("ratio: %s", self.ratio)
logging.debug("offset: %s", self.offset) logger.debug("offset: %s", self.offset)
self.app.statusbar.set_zoom(self.ratio) self.app.statusbar.set_zoom(self.ratio)
if self.wallpaper: if self.wallpaper:
self.redraw_wallpaper() self.redraw_wallpaper()
@ -347,10 +354,10 @@ class CanvasGraph(tk.Canvas):
self.cursor = x, y self.cursor = x, y
selected = self.get_selected(event) selected = self.get_selected(event)
logging.debug("click press(%s): %s", self.cursor, selected) logger.debug("click press(%s): %s", self.cursor, selected)
x_check = self.cursor[0] - self.offset[0] x_check = self.cursor[0] - self.offset[0]
y_check = self.cursor[1] - self.offset[1] y_check = self.cursor[1] - self.offset[1]
logging.debug("click press offset(%s, %s)", x_check, y_check) logger.debug("click press offset(%s, %s)", x_check, y_check)
is_node = selected in self.nodes is_node = selected in self.nodes
if self.manager.mode == GraphMode.EDGE and is_node: if self.manager.mode == GraphMode.EDGE and is_node:
node = self.nodes[selected] node = self.nodes[selected]
@ -387,7 +394,7 @@ class CanvasGraph(tk.Canvas):
node = self.nodes[selected] node = self.nodes[selected]
self.select_object(node.id) self.select_object(node.id)
self.selected = selected self.selected = selected
logging.debug( logger.debug(
"selected node(%s), coords: (%s, %s)", "selected node(%s), coords: (%s, %s)",
node.core_node.name, node.core_node.name,
node.core_node.position.x, node.core_node.position.x,
@ -397,7 +404,7 @@ class CanvasGraph(tk.Canvas):
shadow_node = self.shadow_nodes[selected] shadow_node = self.shadow_nodes[selected]
self.select_object(shadow_node.id) self.select_object(shadow_node.id)
self.selected = selected self.selected = selected
logging.debug( logger.debug(
"selected shadow node(%s), coords: (%s, %s)", "selected shadow node(%s), coords: (%s, %s)",
shadow_node.node.core_node.name, shadow_node.node.core_node.name,
shadow_node.node.core_node.position.x, shadow_node.node.core_node.position.x,
@ -418,7 +425,7 @@ class CanvasGraph(tk.Canvas):
self.cursor = x, y self.cursor = x, y
# handle multiple selections # handle multiple selections
logging.debug("control left click: %s", event) logger.debug("control left click: %s", event)
selected = self.get_selected(event) selected = self.get_selected(event)
if ( if (
selected not in self.selection selected not in self.selection
@ -485,17 +492,6 @@ class CanvasGraph(tk.Canvas):
if self.select_box and self.manager.mode == GraphMode.SELECT: if self.select_box and self.manager.mode == GraphMode.SELECT:
self.select_box.shape_motion(x, y) self.select_box.shape_motion(x, y)
def press_delete(self, _event: tk.Event) -> None:
"""
delete selected nodes and any data that relates to it
"""
logging.debug("press delete key")
if not self.app.core.is_runtime():
self.delete_selected_objects()
self.app.default_info()
else:
logging.debug("node deletion is disabled during runtime state")
def double_click(self, event: tk.Event) -> None: def double_click(self, event: tk.Event) -> None:
selected = self.get_selected(event) selected = self.get_selected(event)
if selected is not None and selected in self.shapes: if selected is not None and selected in self.shapes:
@ -606,10 +602,10 @@ class CanvasGraph(tk.Canvas):
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:
logging.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
logging.debug("resetting scaling: %s %s", self.ratio, self.offset) logger.debug("resetting scaling: %s %s", self.ratio, self.offset)
factor = 1 / self.ratio factor = 1 / self.ratio
self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor) self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor)
self.move(tk.ALL, -self.offset[0], -self.offset[1]) self.move(tk.ALL, -self.offset[0], -self.offset[1])
@ -628,11 +624,11 @@ class CanvasGraph(tk.Canvas):
def redraw_wallpaper(self) -> None: def redraw_wallpaper(self) -> None:
if self.adjust_to_dim.get(): if self.adjust_to_dim.get():
logging.debug("drawing wallpaper to canvas dimensions") logger.debug("drawing wallpaper to canvas dimensions")
self.resize_to_wallpaper() self.resize_to_wallpaper()
else: else:
option = ScaleOption(self.scale_option.get()) option = ScaleOption(self.scale_option.get())
logging.debug("drawing canvas using scaling option: %s", option) logger.debug("drawing canvas using scaling option: %s", option)
if option == ScaleOption.UPPER_LEFT: if option == ScaleOption.UPPER_LEFT:
self.wallpaper_upper_left() self.wallpaper_upper_left()
elif option == ScaleOption.CENTERED: elif option == ScaleOption.CENTERED:
@ -640,7 +636,7 @@ class CanvasGraph(tk.Canvas):
elif option == ScaleOption.SCALED: elif option == ScaleOption.SCALED:
self.wallpaper_scaled() self.wallpaper_scaled()
elif option == ScaleOption.TILED: elif option == ScaleOption.TILED:
logging.warning("tiled background not implemented yet") logger.warning("tiled background not implemented yet")
self.organize() self.organize()
def organize(self) -> None: def organize(self) -> None:
@ -648,7 +644,7 @@ class CanvasGraph(tk.Canvas):
self.tag_raise(tag) self.tag_raise(tag)
def set_wallpaper(self, filename: Optional[str]) -> None: def set_wallpaper(self, filename: Optional[str]) -> None:
logging.info("setting canvas(%s) background: %s", self.id, filename) logger.info("setting canvas(%s) background: %s", self.id, filename)
if filename: if filename:
img = Image.open(filename) img = Image.open(filename)
self.wallpaper = img self.wallpaper = img
@ -671,20 +667,38 @@ class CanvasGraph(tk.Canvas):
edge.complete(dst) edge.complete(dst)
return edge return edge
def copy(self) -> None: def copy_selected(self, _event: tk.Event = None) -> None:
if self.core.is_runtime(): if self.core.is_runtime():
logging.debug("copy is disabled during runtime state") logger.debug("copy is disabled during runtime state")
return return
if self.selection: if self.selection:
logging.debug("to copy nodes: %s", self.selection) logger.debug("to copy nodes: %s", self.selection)
self.to_copy.clear() self.to_copy.clear()
for node_id in self.selection.keys(): for node_id in self.selection.keys():
canvas_node = self.nodes[node_id] canvas_node = self.nodes[node_id]
self.to_copy.append(canvas_node) self.to_copy.append(canvas_node)
def paste(self) -> None: def cut_selected(self, _event: tk.Event = None) -> None:
if self.core.is_runtime(): if self.core.is_runtime():
logging.debug("paste is disabled during runtime state") logger.debug("cut is disabled during runtime state")
return
self.copy_selected()
self.delete_selected()
def delete_selected(self, _event: tk.Event = None) -> None:
"""
delete selected nodes and any data that relates to it
"""
logger.debug("press delete key")
if self.core.is_runtime():
logger.debug("node deletion is disabled during runtime state")
return
self.delete_selected_objects()
self.app.default_info()
def paste_selected(self, _event: tk.Event = None) -> None:
if self.core.is_runtime():
logger.debug("paste is disabled during runtime state")
return return
# maps original node canvas id to copy node canvas id # maps original node canvas id to copy node canvas id
copy_map = {} copy_map = {}
@ -813,6 +827,7 @@ class CanvasGraph(tk.Canvas):
wallpaper=wallpaper_path, wallpaper=wallpaper_path,
wallpaper_style=self.scale_option.get(), wallpaper_style=self.scale_option.get(),
fit_image=self.adjust_to_dim.get(), fit_image=self.adjust_to_dim.get(),
dimensions=self.current_dimensions,
) )
def parse_metadata(self, config: Dict[str, Any]) -> None: def parse_metadata(self, config: Dict[str, Any]) -> None:
@ -820,12 +835,15 @@ class CanvasGraph(tk.Canvas):
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)
self.scale_option.set(wallpaper_style) self.scale_option.set(wallpaper_style)
dimensions = config.get("dimensions")
if dimensions:
self.redraw_canvas(dimensions)
wallpaper = config.get("wallpaper") wallpaper = config.get("wallpaper")
if wallpaper: if wallpaper:
wallpaper = Path(wallpaper) wallpaper = Path(wallpaper)
if not wallpaper.is_file(): if not wallpaper.is_file():
wallpaper = appconfig.BACKGROUNDS_PATH.joinpath(wallpaper) wallpaper = appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)
logging.info("canvas(%s), wallpaper: %s", self.id, wallpaper) logger.info("canvas(%s), wallpaper: %s", self.id, wallpaper)
if wallpaper.is_file(): if wallpaper.is_file():
self.set_wallpaper(str(wallpaper)) self.set_wallpaper(str(wallpaper))
else: else:

View file

@ -1,3 +1,4 @@
import json
import logging import logging
import tkinter as tk import tkinter as tk
from copy import deepcopy from copy import deepcopy
@ -16,9 +17,12 @@ from core.gui.graph.edges import (
from core.gui.graph.enums import GraphMode from core.gui.graph.enums import GraphMode
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType from core.gui.graph.shapeutils import ShapeType
from core.gui.nodeutils import NodeDraw from core.gui.nodeutils import NodeDraw
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
@ -85,7 +89,6 @@ class CanvasManager:
self.app.guiconfig.preferences.width, self.app.guiconfig.preferences.width,
self.app.guiconfig.preferences.height, self.app.guiconfig.preferences.height,
) )
self.current_dimensions: Tuple[int, int] = self.default_dimensions
self.show_node_labels: ShowVar = ShowNodeLabels( self.show_node_labels: ShowVar = ShowNodeLabels(
self, tags.NODE_LABEL, value=True self, tags.NODE_LABEL, value=True
) )
@ -166,7 +169,7 @@ class CanvasManager:
canvas_id = self._next_id() canvas_id = self._next_id()
self.notebook.add(tab, text=f"Canvas {canvas_id}") self.notebook.add(tab, text=f"Canvas {canvas_id}")
unique_id = self.notebook.tabs()[-1] unique_id = self.notebook.tabs()[-1]
logging.info("creating canvas(%s)", canvas_id) logger.info("creating canvas(%s)", canvas_id)
self.canvas_ids[unique_id] = canvas_id self.canvas_ids[unique_id] = canvas_id
self.unique_ids[canvas_id] = unique_id self.unique_ids[canvas_id] = unique_id
@ -205,7 +208,7 @@ class CanvasManager:
edge.delete() edge.delete()
def join(self, session: Session) -> None: def join(self, session: Session) -> None:
# clear out all canvas # clear out all canvases
for canvas_id in self.notebook.tabs(): for canvas_id in self.notebook.tabs():
self.notebook.forget(canvas_id) self.notebook.forget(canvas_id)
self.canvases.clear() self.canvases.clear()
@ -213,7 +216,7 @@ class CanvasManager:
self.unique_ids.clear() self.unique_ids.clear()
self.edges.clear() self.edges.clear()
self.wireless_edges.clear() self.wireless_edges.clear()
logging.info("cleared canvases") logger.info("cleared canvases")
# reset settings # reset settings
self.show_node_labels.set(True) self.show_node_labels.set(True)
@ -232,6 +235,10 @@ class CanvasManager:
self.draw_session(session) self.draw_session(session)
def draw_session(self, session: Session) -> None: def draw_session(self, session: Session) -> None:
# draw canvas configurations and shapes
self.parse_metadata_canvas(session.metadata)
self.parse_metadata_shapes(session.metadata)
# create session nodes # create session nodes
for core_node in session.nodes.values(): for core_node in session.nodes.values():
# add node, avoiding ignored nodes # add node, avoiding ignored nodes
@ -254,50 +261,92 @@ class CanvasManager:
else: else:
self.add_wired_edge(node1, node2, link) self.add_wired_edge(node1, node2, link)
# parse metadata and organize canvases # organize canvas order
self.core.parse_metadata()
for canvas in self.canvases.values(): for canvas in self.canvases.values():
canvas.organize() canvas.organize()
# parse metada for edge configs and hidden nodes
self.parse_metadata_edges(session.metadata)
self.parse_metadata_hidden(session.metadata)
# create a default canvas if none were created prior # create a default canvas if none were created prior
if not self.canvases: if not self.canvases:
self.add_canvas() self.add_canvas()
def redraw_canvases(self, dimensions: Tuple[int, int]) -> None: def redraw_canvas(self, dimensions: Tuple[int, int]) -> None:
for canvas in self.canvases.values(): 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( return dict(gridlines=self.show_grid.get(), canvases=canvases)
gridlines=self.app.manager.show_grid.get(),
dimensions=self.app.manager.current_dimensions,
canvases=canvases,
)
def parse_metadata(self, config: Dict[str, Any]) -> None: def parse_metadata_canvas(self, metadata: Dict[str, Any]) -> None:
# canvas setting
canvas_config = metadata.get("canvas")
logger.debug("canvas metadata: %s", canvas_config)
if not canvas_config:
return
canvas_config = json.loads(canvas_config)
# get configured dimensions and gridlines option # get configured dimensions and gridlines option
dimensions = self.default_dimensions gridlines = canvas_config.get("gridlines", True)
dimensions = config.get("dimensions", dimensions)
gridlines = config.get("gridlines", True)
self.show_grid.set(gridlines) self.show_grid.set(gridlines)
self.redraw_canvases(dimensions)
# get background configurations # get background configurations
for canvas_config in config.get("canvases", []): for canvas_config in canvas_config.get("canvases", []):
canvas_id = canvas_config.get("id") canvas_id = canvas_config.get("id")
if canvas_id is None: if canvas_id is None:
logging.error("canvas config id not provided") logger.error("canvas config id not provided")
continue continue
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:
# load saved shapes
shapes_config = metadata.get("shapes")
if not shapes_config:
return
shapes_config = json.loads(shapes_config)
for shape_config in shapes_config:
logger.debug("loading shape: %s", shape_config)
Shape.from_metadata(self.app, shape_config)
def parse_metadata_edges(self, metadata: Dict[str, Any]) -> None:
# load edges config
edges_config = metadata.get("edges")
if not edges_config:
return
edges_config = json.loads(edges_config)
logger.info("edges config: %s", edges_config)
for edge_config in edges_config:
edge_token = edge_config["token"]
edge = self.core.links.get(edge_token)
if edge:
edge.width = edge_config["width"]
edge.color = edge_config["color"]
edge.redraw()
else:
logger.warning("invalid edge token to configure: %s", edge_token)
def parse_metadata_hidden(self, metadata: Dict[str, Any]) -> None:
# read hidden nodes
hidden_config = metadata.get("hidden")
if not hidden_config:
return
hidden_config = json.loads(hidden_config)
for node_id in hidden_config:
canvas_node = self.core.canvas_nodes.get(node_id)
if canvas_node:
canvas_node.hide()
else:
logger.warning("invalid node to hide: %s", node_id)
def add_core_node(self, core_node: Node) -> None: def add_core_node(self, core_node: Node) -> None:
# get canvas tab for node # get canvas tab for node
canvas_id = core_node.canvas if core_node.canvas > 0 else 1 canvas_id = core_node.canvas if core_node.canvas > 0 else 1
logging.info("adding core node canvas(%s): %s", core_node.name, canvas_id) logger.info("adding core node canvas(%s): %s", core_node.name, canvas_id)
canvas = self.get(canvas_id) canvas = self.get(canvas_id)
image = nutils.get_icon(core_node, self.app) image = nutils.get_icon(core_node, self.app)
x = core_node.position.x x = core_node.position.x
@ -354,7 +403,7 @@ class CanvasManager:
network_id = link.network_id if link.network_id else None network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id) token = create_wireless_token(src.id, dst.id, network_id)
if token in self.wireless_edges: if token in self.wireless_edges:
logging.warning("ignoring link that already exists: %s", link) logger.warning("ignoring link that already exists: %s", link)
return return
edge = CanvasWirelessEdge(self.app, src, dst, network_id, token, link) edge = CanvasWirelessEdge(self.app, src, dst, network_id, token, link)
self.wireless_edges[token] = edge self.wireless_edges[token] = edge

View file

@ -24,6 +24,8 @@ from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.tooltip import CanvasTooltip from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum from core.gui.images import ImageEnum
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
@ -87,7 +89,7 @@ class CanvasNode:
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info) self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
def delete(self) -> None: def delete(self) -> None:
logging.debug("Delete canvas node for %s", self.core_node) logger.debug("Delete canvas node for %s", self.core_node)
self.canvas.delete(self.id) self.canvas.delete(self.id)
self.canvas.delete(self.text_id) self.canvas.delete(self.text_id)
self.delete_antennas() self.delete_antennas()
@ -110,7 +112,7 @@ class CanvasNode:
""" """
delete one antenna delete one antenna
""" """
logging.debug("Delete an antenna on %s", self.core_node.name) logger.debug("Delete an antenna on %s", self.core_node.name)
if self.antennas: if self.antennas:
antenna_id = self.antennas.pop() antenna_id = self.antennas.pop()
self.canvas.delete(antenna_id) self.canvas.delete(antenna_id)
@ -120,7 +122,7 @@ class CanvasNode:
""" """
delete all antennas delete all antennas
""" """
logging.debug("Remove all antennas for %s", self.core_node.name) logger.debug("Remove all antennas for %s", self.core_node.name)
for antenna_id in self.antennas: for antenna_id in self.antennas:
self.canvas.delete(antenna_id) self.canvas.delete(antenna_id)
self.antennas.clear() self.antennas.clear()
@ -253,10 +255,12 @@ class CanvasNode:
else: else:
self.context.add_command(label="Configure", command=self.show_config) self.context.add_command(label="Configure", command=self.show_config)
if nutils.is_container(self.core_node): if nutils.is_container(self.core_node):
self.context.add_command(label="Services", command=self.show_services)
self.context.add_command( self.context.add_command(
label="Config Services", command=self.show_config_services label="Config Services", command=self.show_config_services
) )
self.context.add_command(
label="Services (Deprecated)", command=self.show_services
)
if is_emane: if is_emane:
self.context.add_command( self.context.add_command(
label="EMANE Config", command=self.show_emane_config label="EMANE Config", command=self.show_emane_config
@ -334,7 +338,7 @@ class CanvasNode:
def canvas_copy(self) -> None: def canvas_copy(self) -> None:
self.canvas.clear_selection() self.canvas.clear_selection()
self.canvas.select_object(self.id) self.canvas.select_object(self.id)
self.canvas.copy() self.canvas.copy_selected()
def show_config(self) -> None: def show_config(self) -> None:
dialog = NodeConfigDialog(self.app, self) dialog = NodeConfigDialog(self.app, self)
@ -400,7 +404,7 @@ class CanvasNode:
def update_icon(self, icon_path: str) -> None: def update_icon(self, icon_path: str) -> None:
if not Path(icon_path).exists(): if not Path(icon_path).exists():
logging.error(f"node icon does not exist: {icon_path}") logger.error(f"node icon does not exist: {icon_path}")
return return
self.core_node.icon = icon_path self.core_node.icon = icon_path
self.image = images.from_file(icon_path, width=images.NODE_SIZE) self.image = images.from_file(icon_path, width=images.NODE_SIZE)
@ -459,10 +463,10 @@ class CanvasNode:
def _service_action(self, service: str, action: ServiceAction) -> None: def _service_action(self, service: str, action: ServiceAction) -> None:
session_id = self.app.core.session.id session_id = self.app.core.session.id
try: try:
response = self.app.core.client.service_action( result = self.app.core.client.service_action(
session_id, self.core_node.id, service, action session_id, self.core_node.id, service, action
) )
if not response.result: if not result:
self.app.show_error("Service Action Error", "Action Failed!") self.app.show_error("Service Action Error", "Action Failed!")
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Service Error", e) self.app.show_grpc_exception("Service Error", e)

View file

@ -5,6 +5,8 @@ from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.shapeutils import ShapeType from core.gui.graph.shapeutils import ShapeType
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
@ -92,7 +94,7 @@ class Shape:
shape = Shape(app, canvas, shape_type, *coords, data=data) shape = Shape(app, canvas, shape_type, *coords, data=data)
canvas.shapes[shape.id] = shape canvas.shapes[shape.id] = shape
except ValueError: except ValueError:
logging.exception("unknown shape: %s", shape_type) logger.exception("unknown shape: %s", shape_type)
def draw(self) -> None: def draw(self) -> None:
if self.created: if self.created:
@ -139,7 +141,7 @@ class Shape:
state=self.app.manager.show_annotations.state(), state=self.app.manager.show_annotations.state(),
) )
else: else:
logging.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]]:
@ -192,7 +194,7 @@ class Shape:
self.canvas.move(self.text_id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset)
def delete(self) -> None: def delete(self) -> None:
logging.debug("Delete shape, id(%s)", self.id) logger.debug("Delete shape, id(%s)", self.id)
self.canvas.delete(self.id) self.canvas.delete(self.id)
self.canvas.delete(self.text_id) self.canvas.delete(self.text_id)

View file

@ -9,6 +9,8 @@ from core.gui import nodeutils as nutils
from core.gui.graph.edges import CanvasEdge from core.gui.graph.edges import CanvasEdge
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -158,11 +160,18 @@ class InterfaceManager:
index += 1 index += 1
return index return index
def get_ips(self, node: Node) -> [str, str]: def get_ips(self, node: Node) -> [Optional[str], Optional[str]]:
enable_ip4 = self.app.guiconfig.ips.enable_ip4
enable_ip6 = self.app.guiconfig.ips.enable_ip6
ip4, ip6 = None, None
if not enable_ip4 and not enable_ip6:
return ip4, ip6
index = self.next_index(node) index = self.next_index(node)
ip4 = self.current_subnets.ip4[index] if enable_ip4:
ip6 = self.current_subnets.ip6[index] ip4 = str(self.current_subnets.ip4[index])
return str(ip4), str(ip6) if enable_ip6:
ip6 = str(self.current_subnets.ip6[index])
return ip4, ip6
def get_subnets(self, iface: Interface) -> Subnets: def get_subnets(self, iface: Interface) -> Subnets:
ip4_subnet = self.ip4_subnets ip4_subnet = self.ip4_subnets
@ -196,12 +205,12 @@ class InterfaceManager:
else: else:
self.current_subnets = self.next_subnets() self.current_subnets = self.next_subnets()
else: else:
logging.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]:
logging.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
if not visited: if not visited:
visited = set() visited = set()
@ -220,7 +229,7 @@ class InterfaceManager:
else: else:
subnets = self.find_subnets(check_node, visited) subnets = self.find_subnets(check_node, visited)
if subnets: if subnets:
logging.info("found subnets: %s", subnets) logger.info("found subnets: %s", subnets)
break break
return subnets return subnets
@ -244,7 +253,7 @@ class InterfaceManager:
iface1=src_iface, iface1=src_iface,
iface2=dst_iface, iface2=dst_iface,
) )
logging.info("added link between %s and %s", src_node.name, dst_node.name) logger.info("added link between %s and %s", src_node.name, dst_node.name)
return link return link
def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface: def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface:
@ -266,5 +275,5 @@ class InterfaceManager:
ip6=ip6, ip6=ip6,
ip6_mask=ip6_mask, ip6_mask=ip6_mask,
) )
logging.info("create node(%s) interface(%s)", node.name, iface) logger.info("create node(%s) interface(%s)", node.name, iface)
return iface return iface

View file

@ -1,8 +1,8 @@
import logging import logging
import os
import tkinter as tk import tkinter as tk
import webbrowser import webbrowser
from functools import partial from functools import partial
from pathlib import Path
from tkinter import filedialog, messagebox from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
@ -27,6 +27,8 @@ from core.gui.graph.manager import CanvasManager
from core.gui.observers import ObserversMenu from core.gui.observers import ObserversMenu
from core.gui.task import ProgressTask from core.gui.task import ProgressTask
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -76,7 +78,7 @@ class Menubar(tk.Menu):
self.app.bind_all("<Control-n>", lambda e: self.click_new()) self.app.bind_all("<Control-n>", lambda e: self.click_new())
menu.add_command(label="Save", accelerator="Ctrl+S", command=self.click_save) menu.add_command(label="Save", accelerator="Ctrl+S", command=self.click_save)
self.app.bind_all("<Control-s>", self.click_save) self.app.bind_all("<Control-s>", self.click_save)
menu.add_command(label="Save As...", command=self.click_save_xml) menu.add_command(label="Save As...", command=self.click_save_as)
menu.add_command( menu.add_command(
label="Open...", command=self.click_open_xml, accelerator="Ctrl+O" label="Open...", command=self.click_open_xml, accelerator="Ctrl+O"
) )
@ -84,7 +86,7 @@ class Menubar(tk.Menu):
self.recent_menu = tk.Menu(menu) self.recent_menu = tk.Menu(menu)
for i in self.app.guiconfig.recentfiles: for i in self.app.guiconfig.recentfiles:
self.recent_menu.add_command( self.recent_menu.add_command(
label=i, command=partial(self.open_recent_files, i) label=i, command=partial(self.open_recent_files, Path(i))
) )
menu.add_cascade(label="Recent Files", menu=self.recent_menu) menu.add_cascade(label="Recent Files", menu=self.recent_menu)
menu.add_separator() menu.add_separator()
@ -120,11 +122,6 @@ class Menubar(tk.Menu):
) )
menu.add_command(label="Hide", accelerator="Ctrl+H", command=self.click_hide) menu.add_command(label="Hide", accelerator="Ctrl+H", command=self.click_hide)
self.add_cascade(label="Edit", menu=menu) self.add_cascade(label="Edit", menu=menu)
self.app.master.bind_all("<Control-x>", self.click_cut)
self.app.master.bind_all("<Control-c>", self.click_copy)
self.app.master.bind_all("<Control-v>", self.click_paste)
self.app.master.bind_all("<Control-d>", self.click_delete)
self.app.master.bind_all("<Control-h>", self.click_hide)
self.edit_menu = menu self.edit_menu = menu
def draw_canvas_menu(self) -> None: def draw_canvas_menu(self) -> None:
@ -272,27 +269,28 @@ class Menubar(tk.Menu):
menu.add_command(label="About", command=self.click_about) menu.add_command(label="About", command=self.click_about)
self.add_cascade(label="Help", menu=menu) self.add_cascade(label="Help", menu=menu)
def open_recent_files(self, filename: str) -> None: def open_recent_files(self, file_path: Path) -> None:
if os.path.isfile(filename): if file_path.is_file():
logging.debug("Open recent file %s", filename) logger.debug("Open recent file %s", file_path)
self.open_xml_task(filename) self.open_xml_task(file_path)
else: else:
logging.warning("File does not exist %s", filename) logger.warning("File does not exist %s", file_path)
def update_recent_files(self) -> None: def update_recent_files(self) -> None:
self.recent_menu.delete(0, tk.END) self.recent_menu.delete(0, tk.END)
for i in self.app.guiconfig.recentfiles: for i in self.app.guiconfig.recentfiles:
self.recent_menu.add_command( self.recent_menu.add_command(
label=i, command=partial(self.open_recent_files, i) label=i, command=partial(self.open_recent_files, Path(i))
) )
def click_save(self, _event=None) -> None: def click_save(self, _event: tk.Event = None) -> None:
if self.core.session.file: if self.core.session.file:
self.core.save_xml() if self.core.save_xml():
self.add_recent_file_to_gui_config(self.core.session.file)
else: else:
self.click_save_xml() self.click_save_as()
def click_save_xml(self, _event: tk.Event = None) -> None: def click_save_as(self, _event: tk.Event = None) -> None:
init_dir = self.core.get_xml_dir() init_dir = self.core.get_xml_dir()
file_path = filedialog.asksaveasfilename( file_path = filedialog.asksaveasfilename(
initialdir=init_dir, initialdir=init_dir,
@ -301,8 +299,9 @@ class Menubar(tk.Menu):
defaultextension=".xml", defaultextension=".xml",
) )
if file_path: if file_path:
self.add_recent_file_to_gui_config(file_path) file_path = Path(file_path)
self.core.save_xml(file_path) if self.core.save_xml(file_path):
self.add_recent_file_to_gui_config(file_path)
def click_open_xml(self, _event: tk.Event = None) -> None: def click_open_xml(self, _event: tk.Event = None) -> None:
init_dir = self.core.get_xml_dir() init_dir = self.core.get_xml_dir()
@ -312,9 +311,10 @@ class Menubar(tk.Menu):
filetypes=(("XML Files", "*.xml"), ("All Files", "*")), filetypes=(("XML Files", "*.xml"), ("All Files", "*")),
) )
if file_path: if file_path:
file_path = Path(file_path)
self.open_xml_task(file_path) self.open_xml_task(file_path)
def open_xml_task(self, file_path: str) -> None: def open_xml_task(self, file_path: Path) -> None:
self.add_recent_file_to_gui_config(file_path) self.add_recent_file_to_gui_config(file_path)
self.prompt_save_running_session() self.prompt_save_running_session()
task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(file_path,)) task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(file_path,))
@ -324,21 +324,14 @@ class Menubar(tk.Menu):
dialog = ExecutePythonDialog(self.app) dialog = ExecutePythonDialog(self.app)
dialog.show() dialog.show()
def add_recent_file_to_gui_config(self, file_path) -> None: def add_recent_file_to_gui_config(self, file_path: Path) -> None:
recent_files = self.app.guiconfig.recentfiles recent_files = self.app.guiconfig.recentfiles
num_files = len(recent_files) file_path = str(file_path)
if num_files == 0: if file_path in recent_files:
recent_files.insert(0, file_path) recent_files.remove(file_path)
elif 0 < num_files <= MAX_FILES: recent_files.insert(0, file_path)
if file_path in recent_files: if len(recent_files) > MAX_FILES:
recent_files.remove(file_path) recent_files.pop()
recent_files.insert(0, file_path)
else:
if num_files == MAX_FILES:
recent_files.pop()
recent_files.insert(0, file_path)
else:
logging.error("unexpected number of recent files")
self.app.save_config() self.app.save_config()
self.app.menubar.update_recent_files() self.app.menubar.update_recent_files()
@ -411,47 +404,47 @@ class Menubar(tk.Menu):
def click_copy(self, _event: tk.Event = None) -> None: def click_copy(self, _event: tk.Event = None) -> None:
canvas = self.manager.current() canvas = self.manager.current()
canvas.copy() canvas.copy_selected()
def click_paste(self, _event: tk.Event = None) -> None: def click_paste(self, event: tk.Event = None) -> None:
canvas = self.manager.current() canvas = self.manager.current()
canvas.paste() canvas.paste_selected(event)
def click_delete(self, _event: tk.Event = None) -> None: def click_delete(self, event: tk.Event = None) -> None:
canvas = self.manager.current() canvas = self.manager.current()
canvas.delete_selected_objects() canvas.delete_selected(event)
def click_hide(self, _event: tk.Event = None) -> None: def click_hide(self, event: tk.Event = None) -> None:
canvas = self.manager.current() canvas = self.manager.current()
canvas.hide_selected_objects() canvas.hide_selected(event)
def click_cut(self, _event: tk.Event = None) -> None: def click_cut(self, event: tk.Event = None) -> None:
canvas = self.manager.current() canvas = self.manager.current()
canvas.copy() canvas.copy_selected(event)
canvas.delete_selected_objects() canvas.delete_selected(event)
def click_show_hidden(self, _event: tk.Event = None) -> None: def click_show_hidden(self, _event: tk.Event = None) -> None:
for canvas in self.manager.all(): for canvas in self.manager.all():
canvas.show_hidden() canvas.show_hidden()
def click_session_options(self) -> None: def click_session_options(self) -> None:
logging.debug("Click options") logger.debug("Click options")
dialog = SessionOptionsDialog(self.app) dialog = SessionOptionsDialog(self.app)
if not dialog.has_error: if not dialog.has_error:
dialog.show() dialog.show()
def click_sessions(self) -> None: def click_sessions(self) -> None:
logging.debug("Click change sessions") logger.debug("Click change sessions")
dialog = SessionsDialog(self.app) dialog = SessionsDialog(self.app)
dialog.show() dialog.show()
def click_hooks(self) -> None: def click_hooks(self) -> None:
logging.debug("Click hooks") logger.debug("Click hooks")
dialog = HooksDialog(self.app) dialog = HooksDialog(self.app)
dialog.show() dialog.show()
def click_servers(self) -> None: def click_servers(self) -> None:
logging.debug("Click emulation servers") logger.debug("Click emulation servers")
dialog = ServersDialog(self.app) dialog = ServersDialog(self.app)
dialog.show() dialog.show()
@ -460,11 +453,11 @@ class Menubar(tk.Menu):
dialog.show() dialog.show()
def click_autogrid(self) -> None: def click_autogrid(self) -> None:
width, height = self.manager.current_dimensions width, height = self.manager.current().current_dimensions
padding = (images.NODE_SIZE / 2) + 10 padding = (images.NODE_SIZE / 2) + 10
layout_size = padding + images.NODE_SIZE layout_size = padding + images.NODE_SIZE
col_count = width // layout_size col_count = width // layout_size
logging.info( logger.info(
"auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count "auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count
) )
canvas = self.manager.current() canvas = self.manager.current()

View file

@ -8,6 +8,8 @@ from core.gui import images
from core.gui.appconfig import CustomNode, GuiConfig from core.gui.appconfig import CustomNode, GuiConfig
from core.gui.images import ImageEnum from core.gui.images import ImageEnum
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -29,10 +31,9 @@ ANTENNA_ICON: Optional[PhotoImage] = None
def setup() -> None: def setup() -> None:
global ANTENNA_ICON global ANTENNA_ICON
nodes = [ nodes = [
(ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"),
(ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"),
(ImageEnum.PC, NodeType.DEFAULT, "PC", "PC"), (ImageEnum.PC, NodeType.DEFAULT, "PC", "PC"),
(ImageEnum.MDR, NodeType.DEFAULT, "MDR", "mdr"), (ImageEnum.MDR, NodeType.DEFAULT, "MDR", "mdr"),
(ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"),
(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),
@ -118,11 +119,11 @@ def get_icon(node: Node, app: "Application") -> PhotoImage:
try: try:
image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale) image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale)
except OSError: except OSError:
logging.error("invalid icon: %s", node.icon) logger.error("invalid icon: %s", node.icon)
# custom node # custom node
elif is_custom(node): elif is_custom(node):
image_file = _get_custom_file(app.guiconfig, node.model) image_file = _get_custom_file(app.guiconfig, node.model)
logging.info("custom node file: %s", image_file) logger.info("custom node file: %s", image_file)
if image_file: if image_file:
image = images.from_file(image_file, width=images.NODE_SIZE, scale=scale) image = images.from_file(image_file, width=images.NODE_SIZE, scale=scale)
# built in node # built in node

View file

@ -4,6 +4,8 @@ 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, Tuple
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -43,7 +45,7 @@ class ProgressTask:
if self.callback: if self.callback:
self.app.after(0, self.callback, *values) self.app.after(0, self.callback, *values)
except Exception as e: except Exception as e:
logging.exception("progress task exception") logger.exception("progress task exception")
self.app.show_exception("Task Error", e) self.app.show_exception("Task Error", e)
finally: finally:
self.app.after(0, self.complete) self.app.after(0, self.complete)

View file

@ -20,6 +20,8 @@ from core.gui.task import ProgressTask
from core.gui.themes import Styles from core.gui.themes import Styles
from core.gui.tooltip import Tooltip from core.gui.tooltip import Tooltip
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -304,7 +306,6 @@ class Toolbar(ttk.Frame):
def start_callback(self, result: bool, exceptions: List[str]) -> None: def start_callback(self, result: bool, exceptions: List[str]) -> None:
if result: if result:
self.set_runtime() self.set_runtime()
self.app.core.set_metadata()
self.app.core.show_mobility_players() self.app.core.show_mobility_players()
else: else:
enable_buttons(self.design_frame, enabled=True) enable_buttons(self.design_frame, enabled=True)
@ -338,7 +339,7 @@ class Toolbar(ttk.Frame):
type_enum: NodeTypeEnum, type_enum: NodeTypeEnum,
image: PhotoImage, image: PhotoImage,
) -> None: ) -> None:
logging.debug("update button(%s): %s", button, node_draw) logger.debug("update button(%s): %s", button, node_draw)
button.configure(image=image) button.configure(image=image)
button.image = image button.image = image
self.app.manager.node_draw = node_draw self.app.manager.node_draw = node_draw
@ -399,7 +400,7 @@ class Toolbar(ttk.Frame):
""" """
redraw buttons on the toolbar, send node and link messages to grpc server redraw buttons on the toolbar, send node and link messages to grpc server
""" """
logging.info("clicked stop button") logger.info("clicked stop button")
self.app.menubar.set_state(is_runtime=False) self.app.menubar.set_state(is_runtime=False)
self.app.core.close_mobility_players() self.app.core.close_mobility_players()
enable_buttons(self.runtime_frame, enabled=False) enable_buttons(self.runtime_frame, enabled=False)
@ -415,7 +416,7 @@ class Toolbar(ttk.Frame):
def update_annotation( def update_annotation(
self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage
) -> None: ) -> None:
logging.debug("clicked annotation") logger.debug("clicked annotation")
self.annotation_button.configure(image=image) self.annotation_button.configure(image=image)
self.annotation_button.image = image self.annotation_button.image = image
self.app.manager.annotation_type = shape_type self.app.manager.annotation_type = shape_type
@ -433,7 +434,7 @@ class Toolbar(ttk.Frame):
self.marker_frame.grid() self.marker_frame.grid()
def click_run_button(self) -> None: def click_run_button(self) -> None:
logging.debug("Click on RUN button") logger.debug("Click on RUN button")
dialog = RunToolDialog(self.app) dialog = RunToolDialog(self.app)
dialog.show() dialog.show()

View file

@ -10,6 +10,8 @@ from core.gui import appconfig, themes, validation
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -161,7 +163,7 @@ class ConfigFrame(ttk.Notebook):
) )
entry.grid(row=index, column=1, sticky=tk.EW) entry.grid(row=index, column=1, sticky=tk.EW)
else: else:
logging.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]:

View file

@ -10,6 +10,7 @@ from pyproj import Transformer
from core.emulator.enumerations import RegisterTlvs from core.emulator.enumerations import RegisterTlvs
logger = logging.getLogger(__name__)
SCALE_FACTOR: float = 100.0 SCALE_FACTOR: float = 100.0
CRS_WGS84: int = 4326 CRS_WGS84: int = 4326
CRS_PROJ: int = 3857 CRS_PROJ: int = 3857
@ -92,7 +93,7 @@ class GeoLocation:
:param alt: altitude value :param alt: altitude value
:return: x,y,z representation of provided values :return: x,y,z representation of provided values
""" """
logging.debug("input lon,lat,alt(%s, %s, %s)", lon, lat, alt) logger.debug("input lon,lat,alt(%s, %s, %s)", lon, lat, alt)
px, py = self.to_pixels.transform(lon, lat) px, py = self.to_pixels.transform(lon, lat)
px -= self.refproj[0] px -= self.refproj[0]
py -= self.refproj[1] py -= self.refproj[1]
@ -100,7 +101,7 @@ class GeoLocation:
x = self.meters2pixels(px) + self.refxyz[0] x = self.meters2pixels(px) + self.refxyz[0]
y = -(self.meters2pixels(py) + self.refxyz[1]) y = -(self.meters2pixels(py) + self.refxyz[1])
z = self.meters2pixels(pz) + self.refxyz[2] z = self.meters2pixels(pz) + self.refxyz[2]
logging.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]:
@ -112,7 +113,7 @@ class GeoLocation:
:param z: z value :param z: z value
:return: lat,lon,alt representation of provided values :return: lat,lon,alt representation of provided values
""" """
logging.debug("input x,y(%s, %s)", x, y) logger.debug("input x,y(%s, %s)", x, y)
x -= self.refxyz[0] x -= self.refxyz[0]
y = -(y - self.refxyz[1]) y = -(y - self.refxyz[1])
if z is None: if z is None:
@ -123,5 +124,5 @@ class GeoLocation:
py = self.refproj[1] + self.pixels2meters(y) py = self.refproj[1] + self.pixels2meters(y)
lon, lat = self.to_geo.transform(px, py) lon, lat = self.to_geo.transform(px, py)
alt = self.refgeo[2] + self.pixels2meters(z) alt = self.refgeo[2] + self.pixels2meters(z)
logging.debug("result lon,lat,alt(%s, %s, %s)", lon, lat, alt) logger.debug("result lon,lat,alt(%s, %s, %s)", lon, lat, alt)
return lat, lon, alt return lat, lon, alt

View file

@ -12,22 +12,27 @@ from pathlib import Path
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union
from core import utils from core import utils
from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager from core.config import (
ConfigBool,
ConfigFloat,
ConfigGroup,
ConfigInt,
ConfigString,
ConfigurableOptions,
Configuration,
ModelManager,
)
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import EventData, LinkData, LinkOptions from core.emulator.data import EventData, LinkData, LinkOptions
from core.emulator.enumerations import ( from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags, RegisterTlvs
ConfigDataTypes,
EventTypes,
LinkTypes,
MessageFlags,
RegisterTlvs,
)
from core.errors import CoreError from core.errors import CoreError
from core.executables import BASH from core.executables import BASH
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
@ -42,6 +47,43 @@ 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]:
"""
Convenience function to get config values as int.
:param current: current config value to use when one is not provided
:param config: config to get values from
:param name: name of config value to get
:return: current config value when not provided, new value otherwise
"""
value = get_config_float(current, config, name)
if value is not None:
value = int(value)
return value
def get_config_float(
current: Union[int, float], config: Dict[str, str], name: str
) -> Optional[float]:
"""
Convenience function to get config values as float.
:param current: current config value to use when one is not provided
:param config: config to get values from
:param name: name of config value to get
:return: current config value when not provided, new value otherwise
"""
value = config.get(name)
if value is not None:
if value == "":
value = None
else:
value = float(value)
else:
value = current
return value
class MobilityManager(ModelManager): class MobilityManager(ModelManager):
""" """
Member of session class for handling configuration data for mobility and Member of session class for handling configuration data for mobility and
@ -81,7 +123,7 @@ class MobilityManager(ModelManager):
if node_ids is None: if node_ids is None:
node_ids = self.nodes() node_ids = self.nodes()
for node_id in node_ids: for node_id in node_ids:
logging.debug( logger.debug(
"node(%s) mobility startup: %s", node_id, self.get_all_configs(node_id) "node(%s) mobility startup: %s", node_id, self.get_all_configs(node_id)
) )
try: try:
@ -95,8 +137,8 @@ class MobilityManager(ModelManager):
if node.mobility: if node.mobility:
self.session.event_loop.add_event(0.0, node.mobility.startup) self.session.event_loop.add_event(0.0, node.mobility.startup)
except CoreError: except CoreError:
logging.exception("mobility startup error") logger.exception("mobility startup error")
logging.warning( logger.warning(
"skipping mobility configuration for unknown node: %s", node_id "skipping mobility configuration for unknown node: %s", node_id
) )
@ -114,7 +156,7 @@ class MobilityManager(ModelManager):
try: try:
node = get_mobility_node(self.session, node_id) node = get_mobility_node(self.session, node_id)
except CoreError: except CoreError:
logging.exception( logger.exception(
"ignoring event for model(%s), unknown node(%s)", name, node_id "ignoring event for model(%s), unknown node(%s)", name, node_id
) )
return return
@ -124,17 +166,17 @@ class MobilityManager(ModelManager):
for model in models: for model in models:
cls = self.models.get(model) cls = self.models.get(model)
if not cls: if not cls:
logging.warning("ignoring event for unknown model '%s'", model) logger.warning("ignoring event for unknown model '%s'", model)
continue continue
if cls.config_type in [RegisterTlvs.WIRELESS, RegisterTlvs.MOBILITY]: if cls.config_type in [RegisterTlvs.WIRELESS, RegisterTlvs.MOBILITY]:
model = node.mobility model = node.mobility
else: else:
continue continue
if model is None: if model is None:
logging.warning("ignoring event, %s has no model", node.name) logger.warning("ignoring event, %s has no model", node.name)
continue continue
if cls.name != model.name: if cls.name != model.name:
logging.warning( logger.warning(
"ignoring event for %s wrong model %s,%s", "ignoring event for %s wrong model %s,%s",
node.name, node.name,
cls.name, cls.name,
@ -235,39 +277,12 @@ class BasicRangeModel(WirelessModel):
name: str = "basic_range" name: str = "basic_range"
options: List[Configuration] = [ options: List[Configuration] = [
Configuration( ConfigInt(id="range", default="275", label="wireless range (pixels)"),
_id="range", ConfigInt(id="bandwidth", default="54000000", label="bandwidth (bps)"),
_type=ConfigDataTypes.UINT32, ConfigInt(id="jitter", default="0", label="transmission jitter (usec)"),
default="275", ConfigInt(id="delay", default="5000", label="transmission delay (usec)"),
label="wireless range (pixels)", ConfigFloat(id="error", default="0.0", label="loss (%)"),
), ConfigBool(id="promiscuous", default="0", label="promiscuous mode"),
Configuration(
_id="bandwidth",
_type=ConfigDataTypes.UINT64,
default="54000000",
label="bandwidth (bps)",
),
Configuration(
_id="jitter",
_type=ConfigDataTypes.UINT64,
default="0",
label="transmission jitter (usec)",
),
Configuration(
_id="delay",
_type=ConfigDataTypes.UINT64,
default="5000",
label="transmission delay (usec)",
),
Configuration(
_id="error", _type=ConfigDataTypes.STRING, default="0", label="loss (%)"
),
Configuration(
_id="promiscuous",
_type=ConfigDataTypes.BOOL,
default="0",
label="promiscuous mode",
),
] ]
@classmethod @classmethod
@ -293,25 +308,6 @@ class BasicRangeModel(WirelessModel):
self.jitter: Optional[int] = None self.jitter: Optional[int] = None
self.promiscuous: bool = False self.promiscuous: bool = False
def _get_config(self, current_value: int, config: Dict[str, str], name: str) -> int:
"""
Convenience for updating value to use from a provided configuration.
:param current_value: current config value to use when one is not provided
:param config: config to get values from
:param name: name of config value to get
:return: current config value when not provided, new value otherwise
"""
value = config.get(name)
if value is not None:
if value == "":
value = None
else:
value = int(float(value))
else:
value = current_value
return value
def setlinkparams(self) -> None: def setlinkparams(self) -> None:
""" """
Apply link parameters to all interfaces. This is invoked from Apply link parameters to all interfaces. This is invoked from
@ -406,20 +402,20 @@ class BasicRangeModel(WirelessModel):
a = min(iface, iface2) a = min(iface, iface2)
b = max(iface, iface2) b = max(iface, iface2)
with self.wlan._linked_lock: with self.wlan.linked_lock:
linked = self.wlan.linked(a, b) linked = self.wlan.is_linked(a, b)
if d > self.range: if d > self.range:
if linked: if linked:
logging.debug("was linked, unlinking") logger.debug("was linked, unlinking")
self.wlan.unlink(a, b) self.wlan.unlink(a, b)
self.sendlinkmsg(a, b, unlink=True) self.sendlinkmsg(a, b, unlink=True)
else: else:
if not linked: if not linked:
logging.debug("was not linked, linking") logger.debug("was not linked, linking")
self.wlan.link(a, b) self.wlan.link(a, b)
self.sendlinkmsg(a, b) self.sendlinkmsg(a, b)
except KeyError: except KeyError:
logging.exception("error getting interfaces during calclinkS") logger.exception("error getting interfaces during calclink")
@staticmethod @staticmethod
def calcdistance( def calcdistance(
@ -446,15 +442,15 @@ class BasicRangeModel(WirelessModel):
:param config: values to update configuration :param config: values to update configuration
:return: nothing :return: nothing
""" """
self.range = self._get_config(self.range, config, "range") self.range = get_config_int(self.range, config, "range")
if self.range is None: if self.range is None:
self.range = 0 self.range = 0
logging.debug("wlan %s set range to %s", self.wlan.name, self.range) logger.debug("wlan %s set range to %s", self.wlan.name, self.range)
self.bw = self._get_config(self.bw, config, "bandwidth") self.bw = get_config_int(self.bw, config, "bandwidth")
self.delay = self._get_config(self.delay, config, "delay") self.delay = get_config_int(self.delay, config, "delay")
self.loss = self._get_config(self.loss, config, "error") self.loss = get_config_float(self.loss, config, "error")
self.jitter = self._get_config(self.jitter, config, "jitter") self.jitter = get_config_int(self.jitter, config, "jitter")
promiscuous = config["promiscuous"] == "1" promiscuous = config.get("promiscuous", "0") == "1"
if self.promiscuous and not promiscuous: if self.promiscuous and not promiscuous:
self.wlan.net_client.set_mac_learning(self.wlan.brname, LEARNING_ENABLED) self.wlan.net_client.set_mac_learning(self.wlan.brname, LEARNING_ENABLED)
elif not self.promiscuous and promiscuous: elif not self.promiscuous and promiscuous:
@ -506,10 +502,10 @@ class BasicRangeModel(WirelessModel):
:return: all link data :return: all link data
""" """
all_links = [] all_links = []
with self.wlan._linked_lock: with self.wlan.linked_lock:
for a in self.wlan._linked: for a in self.wlan.linked:
for b in self.wlan._linked[a]: for b in self.wlan.linked[a]:
if self.wlan._linked[a][b]: if self.wlan.linked[a][b]:
all_links.append(self.create_link_data(a, b, flags)) all_links.append(self.create_link_data(a, b, flags))
return all_links return all_links
@ -867,43 +863,14 @@ class Ns2ScriptedMobility(WayPointMobility):
name: str = "ns2script" name: str = "ns2script"
options: List[Configuration] = [ options: List[Configuration] = [
Configuration( ConfigString(id="file", label="mobility script file"),
_id="file", _type=ConfigDataTypes.STRING, label="mobility script file" ConfigInt(id="refresh_ms", default="50", label="refresh time (ms)"),
), ConfigBool(id="loop", default="1", label="loop"),
Configuration( ConfigString(id="autostart", label="auto-start seconds (0.0 for runtime)"),
_id="refresh_ms", ConfigString(id="map", label="node mapping (optional, e.g. 0:1,1:2,2:3)"),
_type=ConfigDataTypes.UINT32, ConfigString(id="script_start", label="script file to run upon start"),
default="50", ConfigString(id="script_pause", label="script file to run upon pause"),
label="refresh time (ms)", ConfigString(id="script_stop", label="script file to run upon stop"),
),
Configuration(
_id="loop", _type=ConfigDataTypes.BOOL, default="1", label="loop"
),
Configuration(
_id="autostart",
_type=ConfigDataTypes.STRING,
label="auto-start seconds (0.0 for runtime)",
),
Configuration(
_id="map",
_type=ConfigDataTypes.STRING,
label="node mapping (optional, e.g. 0:1,1:2,2:3)",
),
Configuration(
_id="script_start",
_type=ConfigDataTypes.STRING,
label="script file to run upon start",
),
Configuration(
_id="script_pause",
_type=ConfigDataTypes.STRING,
label="script file to run upon pause",
),
Configuration(
_id="script_stop",
_type=ConfigDataTypes.STRING,
label="script file to run upon stop",
),
] ]
@classmethod @classmethod
@ -920,7 +887,7 @@ class Ns2ScriptedMobility(WayPointMobility):
:param _id: object id :param _id: object id
""" """
super().__init__(session, _id) super().__init__(session, _id)
self.file: Optional[str] = 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
@ -928,8 +895,8 @@ class Ns2ScriptedMobility(WayPointMobility):
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 = config["file"] self.file = Path(config["file"])
logging.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",
self.id, self.id,
self.file, self.file,
@ -953,15 +920,15 @@ class Ns2ScriptedMobility(WayPointMobility):
:return: nothing :return: nothing
""" """
filename = self.findfile(self.file) file_path = self.findfile(self.file)
try: try:
f = open(filename, "r") f = file_path.open("r")
except IOError: except IOError:
logging.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
) )
return return
logging.info("reading ns-2 script file: %s", filename) logger.info("reading ns-2 script file: %s", file_path)
ln = 0 ln = 0
ix = iy = iz = None ix = iy = iz = None
inodenum = None inodenum = None
@ -977,13 +944,13 @@ class Ns2ScriptedMobility(WayPointMobility):
# waypoints: # waypoints:
# $ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0" # $ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0"
parts = line.split() parts = line.split()
time = float(parts[2]) line_time = float(parts[2])
nodenum = parts[3][1 + parts[3].index("(") : parts[3].index(")")] nodenum = parts[3][1 + parts[3].index("(") : parts[3].index(")")]
x = float(parts[5]) x = float(parts[5])
y = float(parts[6]) y = float(parts[6])
z = None z = None
speed = float(parts[7].strip('"')) speed = float(parts[7].strip('"'))
self.addwaypoint(time, self.map(nodenum), x, y, z, speed) self.addwaypoint(line_time, self.map(nodenum), x, y, z, speed)
elif line[:7] == "$node_(": elif line[:7] == "$node_(":
# initial position (time=0, speed=0): # initial position (time=0, speed=0):
# $node_(6) set X_ 780.0 # $node_(6) set X_ 780.0
@ -1004,38 +971,38 @@ class Ns2ScriptedMobility(WayPointMobility):
else: else:
raise ValueError raise ValueError
except ValueError: except ValueError:
logging.exception( logger.exception(
"skipping line %d of file %s '%s'", ln, self.file, line "skipping line %d of file %s '%s'", ln, self.file, line
) )
continue continue
if ix is not None and iy is not None: if ix is not None and iy is not None:
self.addinitial(self.map(inodenum), ix, iy, iz) self.addinitial(self.map(inodenum), ix, iy, iz)
def findfile(self, file_name: str) -> str: def findfile(self, file_path: Path) -> Path:
""" """
Locate a script file. If the specified file doesn't exist, look in the Locate a script file. If the specified file doesn't exist, look in the
same directory as the scenario file, or in gui directories. same directory as the scenario file, or in gui directories.
:param file_name: file name to find :param file_path: file name to find
:return: absolute path to the file :return: absolute path to the file
:raises CoreError: when file is not found :raises CoreError: when file is not found
""" """
file_path = Path(file_name).expanduser() file_path = file_path.expanduser()
if file_path.exists(): if file_path.exists():
return str(file_path) return file_path
if self.session.file_name: if self.session.file_path:
file_path = Path(self.session.file_name).parent / file_name session_file_path = self.session.file_path.parent / file_path
if file_path.exists(): if session_file_path.exists():
return str(file_path) return session_file_path
if self.session.user: if self.session.user:
user_path = Path(f"~{self.session.user}").expanduser() user_path = Path(f"~{self.session.user}").expanduser()
file_path = user_path / ".core" / "configs" / file_name configs_path = user_path / ".core" / "configs" / file_path
if file_path.exists(): if configs_path.exists():
return str(file_path) return configs_path
file_path = user_path / ".coregui" / "mobility" / file_name mobility_path = user_path / ".coregui" / "mobility" / file_path
if file_path.exists(): if mobility_path.exists():
return str(file_path) return mobility_path
raise CoreError(f"invalid file: {file_name}") raise CoreError(f"invalid file: {file_path}")
def parsemap(self, mapstr: str) -> None: def parsemap(self, mapstr: str) -> None:
""" """
@ -1047,7 +1014,6 @@ class Ns2ScriptedMobility(WayPointMobility):
self.nodemap = {} self.nodemap = {}
if mapstr.strip() == "": if mapstr.strip() == "":
return return
for pair in mapstr.split(","): for pair in mapstr.split(","):
parts = pair.split(":") parts = pair.split(":")
try: try:
@ -1055,7 +1021,7 @@ class Ns2ScriptedMobility(WayPointMobility):
raise ValueError raise ValueError
self.nodemap[int(parts[0])] = int(parts[1]) self.nodemap[int(parts[0])] = int(parts[1])
except ValueError: except ValueError:
logging.exception("ns-2 mobility node map error") logger.exception("ns-2 mobility node map error")
def map(self, nodenum: str) -> int: def map(self, nodenum: str) -> int:
""" """
@ -1077,19 +1043,19 @@ class Ns2ScriptedMobility(WayPointMobility):
:return: nothing :return: nothing
""" """
if self.autostart == "": if self.autostart == "":
logging.info("not auto-starting ns-2 script for %s", self.net.name) logger.info("not auto-starting ns-2 script for %s", self.net.name)
return return
try: try:
t = float(self.autostart) t = float(self.autostart)
except ValueError: except ValueError:
logging.exception( logger.exception(
"Invalid auto-start seconds specified '%s' for %s", "Invalid auto-start seconds specified '%s' for %s",
self.autostart, self.autostart,
self.net.name, self.net.name,
) )
return return
self.movenodesinitial() self.movenodesinitial()
logging.info("scheduling ns-2 script for %s autostart at %s", self.net.name, t) logger.info("scheduling ns-2 script for %s autostart at %s", self.net.name, t)
self.state = self.STATE_RUNNING self.state = self.STATE_RUNNING
self.session.event_loop.add_event(t, self.run) self.session.event_loop.add_event(t, self.run)
@ -1099,7 +1065,7 @@ class Ns2ScriptedMobility(WayPointMobility):
:return: nothing :return: nothing
""" """
logging.info("starting script: %s", self.file) logger.info("starting script: %s", self.file)
laststate = self.state laststate = self.state
super().start() super().start()
if laststate == self.STATE_PAUSED: if laststate == self.STATE_PAUSED:
@ -1120,7 +1086,7 @@ class Ns2ScriptedMobility(WayPointMobility):
:return: nothing :return: nothing
""" """
logging.info("pausing script: %s", self.file) logger.info("pausing script: %s", self.file)
super().pause() super().pause()
self.statescript("pause") self.statescript("pause")
@ -1132,7 +1098,7 @@ class Ns2ScriptedMobility(WayPointMobility):
position position
:return: nothing :return: nothing
""" """
logging.info("stopping script: %s", self.file) logger.info("stopping script: %s", self.file)
super().stop(move_initial=move_initial) super().stop(move_initial=move_initial)
self.statescript("stop") self.statescript("stop")
@ -1152,8 +1118,7 @@ class Ns2ScriptedMobility(WayPointMobility):
filename = self.script_stop filename = self.script_stop
if filename is None or filename == "": if filename is None or filename == "":
return return
filename = Path(filename)
filename = self.findfile(filename) filename = self.findfile(filename)
args = f"{BASH} {filename} {typestr}" args = f"{BASH} {filename} {typestr}"
utils.cmd( utils.cmd(args, cwd=self.session.directory, env=self.session.get_environment())
args, cwd=self.session.session_dir, env=self.session.get_environment()
)

View file

@ -3,9 +3,9 @@ Defines the base logic for nodes used within core.
""" """
import abc import abc
import logging import logging
import os
import shutil import shutil
import threading import threading
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, Dict, List, Optional, Tuple, Type, Union
@ -18,9 +18,11 @@ from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import MOUNT, TEST, VNODED from core.executables import MOUNT, TEST, VNODED
from core.nodes.client import VnodeClient from core.nodes.client import VnodeClient
from core.nodes.interface import CoreInterface, TunTap, Veth from core.nodes.interface import DEFAULT_MTU, CoreInterface, TunTap, Veth
from core.nodes.netclient import LinuxNetClient, get_net_client from core.nodes.netclient import LinuxNetClient, get_net_client
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.session import Session from core.emulator.session import Session
@ -30,6 +32,8 @@ if TYPE_CHECKING:
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")]
class NodeBase(abc.ABC): class NodeBase(abc.ABC):
""" """
@ -97,7 +101,7 @@ class NodeBase(abc.ABC):
self, self,
args: str, args: str,
env: Dict[str, str] = None, env: Dict[str, str] = None,
cwd: str = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
) -> str: ) -> str:
@ -221,7 +225,7 @@ class CoreNodeBase(NodeBase):
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
self.config_services: Dict[str, "ConfigService"] = {} self.config_services: Dict[str, "ConfigService"] = {}
self.nodedir: Optional[str] = None self.directory: Optional[Path] = None
self.tmpnodedir: bool = False self.tmpnodedir: bool = False
@abc.abstractmethod @abc.abstractmethod
@ -233,11 +237,21 @@ class CoreNodeBase(NodeBase):
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: def create_dir(self, dir_path: Path) -> None:
"""
Create a node private directory.
:param dir_path: path to create
:return: nothing
"""
raise NotImplementedError
@abc.abstractmethod
def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
""" """
Create a node file with a given mode. Create a node file with a given mode.
:param filename: name of file to create :param file_path: name of file to create
:param contents: contents of file :param contents: contents of file
:param mode: mode for file :param mode: mode for file
:return: nothing :return: nothing
@ -245,12 +259,25 @@ class CoreNodeBase(NodeBase):
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def addfile(self, srcname: str, filename: str) -> None: def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
"""
Copy source file to node host destination, updating the file mode when
provided.
:param src_path: source file to copy
:param dst_path: node host destination
:param mode: file mode
:return: nothing
"""
raise NotImplementedError
@abc.abstractmethod
def addfile(self, src_path: Path, file_path: Path) -> None:
""" """
Add a file. Add a file.
:param srcname: source file name :param src_path: source file path
:param filename: file name to add :param file_path: file name to add
:return: nothing :return: nothing
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
@ -302,6 +329,21 @@ class CoreNodeBase(NodeBase):
""" """
raise NotImplementedError raise NotImplementedError
def host_path(self, path: Path, is_dir: bool = False) -> Path:
"""
Return the name of a node"s file on the host filesystem.
:param path: path to translate to host path
:param is_dir: True if path is a directory path, False otherwise
:return: path to file
"""
if is_dir:
directory = str(path).strip("/").replace("/", ".")
return self.directory / directory
else:
directory = str(path.parent).strip("/").replace("/", ".")
return self.directory / directory / path.name
def add_config_service(self, service_class: "ConfigServiceType") -> None: def add_config_service(self, service_class: "ConfigServiceType") -> None:
""" """
Adds a configuration service to the node. Adds a configuration service to the node.
@ -345,9 +387,9 @@ class CoreNodeBase(NodeBase):
:return: nothing :return: nothing
""" """
if self.nodedir is None: if self.directory is None:
self.nodedir = os.path.join(self.session.session_dir, self.name + ".conf") self.directory = self.session.directory / f"{self.name}.conf"
self.host_cmd(f"mkdir -p {self.nodedir}") self.host_cmd(f"mkdir -p {self.directory}")
self.tmpnodedir = True self.tmpnodedir = True
else: else:
self.tmpnodedir = False self.tmpnodedir = False
@ -362,7 +404,7 @@ class CoreNodeBase(NodeBase):
if preserve: if preserve:
return return
if self.tmpnodedir: if self.tmpnodedir:
self.host_cmd(f"rm -rf {self.nodedir}") self.host_cmd(f"rm -rf {self.directory}")
def add_iface(self, iface: CoreInterface, iface_id: int) -> None: def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
""" """
@ -387,7 +429,7 @@ class CoreNodeBase(NodeBase):
if iface_id not in self.ifaces: if iface_id not in self.ifaces:
raise CoreError(f"node({self.name}) interface({iface_id}) does not exist") raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
iface = self.ifaces.pop(iface_id) iface = self.ifaces.pop(iface_id)
logging.info("node(%s) removing interface(%s)", self.name, iface.name) logger.info("node(%s) removing interface(%s)", self.name, iface.name)
iface.detachnet() iface.detachnet()
iface.shutdown() iface.shutdown()
@ -458,7 +500,7 @@ class CoreNode(CoreNodeBase):
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
nodedir: str = None, directory: Path = None,
server: "DistributedServer" = None, server: "DistributedServer" = None,
) -> None: ) -> None:
""" """
@ -467,19 +509,17 @@ class CoreNode(CoreNodeBase):
:param session: core session instance :param session: core session instance
:param _id: object id :param _id: object id
:param name: object name :param name: object name
:param nodedir: node directory :param directory: node directory
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
self.nodedir: Optional[str] = nodedir self.directory: Optional[Path] = directory
self.ctrlchnlname: str = os.path.abspath( self.ctrlchnlname: Path = self.session.directory / self.name
os.path.join(self.session.session_dir, self.name)
)
self.client: Optional[VnodeClient] = None self.client: Optional[VnodeClient] = None
self.pid: Optional[int] = None self.pid: Optional[int] = None
self.lock: RLock = RLock() self.lock: RLock = RLock()
self._mounts: List[Tuple[str, str]] = [] 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()
) )
@ -524,33 +564,33 @@ class CoreNode(CoreNodeBase):
f"{VNODED} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log " f"{VNODED} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log "
f"-p {self.ctrlchnlname}.pid" f"-p {self.ctrlchnlname}.pid"
) )
if self.nodedir: if self.directory:
vnoded += f" -C {self.nodedir}" vnoded += f" -C {self.directory}"
env = self.session.get_environment(state=False) env = self.session.get_environment(state=False)
env["NODE_NUMBER"] = str(self.id) env["NODE_NUMBER"] = str(self.id)
env["NODE_NAME"] = str(self.name) env["NODE_NAME"] = str(self.name)
output = self.host_cmd(vnoded, env=env) output = self.host_cmd(vnoded, env=env)
self.pid = int(output) self.pid = int(output)
logging.debug("node(%s) pid: %s", self.name, self.pid) logger.debug("node(%s) pid: %s", self.name, self.pid)
# create vnode client # create vnode client
self.client = VnodeClient(self.name, self.ctrlchnlname) self.client = VnodeClient(self.name, self.ctrlchnlname)
# bring up the loopback interface # bring up the loopback interface
logging.debug("bringing up loopback interface") logger.debug("bringing up loopback interface")
self.node_net_client.device_up("lo") self.node_net_client.device_up("lo")
# set hostname for node # set hostname for node
logging.debug("setting hostname: %s", self.name) logger.debug("setting hostname: %s", self.name)
self.node_net_client.set_hostname(self.name) self.node_net_client.set_hostname(self.name)
# mark node as up # mark node as up
self.up = True self.up = True
# create private directories # create private directories
self.privatedir("/var/run") for dir_path in PRIVATE_DIRS:
self.privatedir("/var/log") self.create_dir(dir_path)
def shutdown(self) -> None: def shutdown(self) -> None:
""" """
@ -561,35 +601,30 @@ class CoreNode(CoreNodeBase):
# nothing to do if node is not up # nothing to do if node is not up
if not self.up: if not self.up:
return return
with self.lock: with self.lock:
try: try:
# unmount all targets (NOTE: non-persistent mount namespaces are # unmount all targets (NOTE: non-persistent mount namespaces are
# removed by the kernel when last referencing process is killed) # removed by the kernel when last referencing process is killed)
self._mounts = [] self._mounts = []
# shutdown all interfaces # shutdown all interfaces
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.shutdown() iface.shutdown()
# kill node process if present # kill node process if present
try: try:
self.host_cmd(f"kill -9 {self.pid}") self.host_cmd(f"kill -9 {self.pid}")
except CoreCommandError: except CoreCommandError:
logging.exception("error killing process") logger.exception("error killing process")
# remove node directory if present # remove node directory if present
try: try:
self.host_cmd(f"rm -rf {self.ctrlchnlname}") self.host_cmd(f"rm -rf {self.ctrlchnlname}")
except CoreCommandError: except CoreCommandError:
logging.exception("error removing node directory") logger.exception("error removing node directory")
# clear interface data, close client, and mark self and not up # clear interface data, close client, and mark self and not up
self.ifaces.clear() self.ifaces.clear()
self.client.close() self.client.close()
self.up = False self.up = False
except OSError: except OSError:
logging.exception("error during shutdown") logger.exception("error during shutdown")
finally: finally:
self.rmnodedir() self.rmnodedir()
@ -636,35 +671,37 @@ class CoreNode(CoreNodeBase):
else: else:
return f"ssh -X -f {self.server.host} xterm -e {terminal}" return f"ssh -X -f {self.server.host} xterm -e {terminal}"
def privatedir(self, path: str) -> None: def create_dir(self, dir_path: Path) -> None:
""" """
Create a private directory. Create a node private directory.
:param path: path to create :param dir_path: path to create
:return: nothing :return: nothing
""" """
if path[0] != "/": if not dir_path.is_absolute():
raise ValueError(f"path not fully qualified: {path}") raise CoreError(f"private directory path not fully qualified: {dir_path}")
hostpath = os.path.join( logger.debug("node(%s) creating private directory: %s", self.name, dir_path)
self.nodedir, os.path.normpath(path).strip("/").replace("/", ".") parent_path = self._find_parent_path(dir_path)
) if parent_path:
self.host_cmd(f"mkdir -p {hostpath}") self.host_cmd(f"mkdir -p {parent_path}")
self.mount(hostpath, path) else:
host_path = self.host_path(dir_path, is_dir=True)
self.host_cmd(f"mkdir -p {host_path}")
self.mount(host_path, dir_path)
def mount(self, source: str, target: str) -> None: def mount(self, src_path: Path, target_path: Path) -> None:
""" """
Create and mount a directory. Create and mount a directory.
:param source: source directory to mount :param src_path: source directory to mount
:param target: target directory to create :param target_path: target directory to create
:return: nothing :return: nothing
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
source = os.path.abspath(source) logger.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path)
logging.debug("node(%s) mounting: %s at %s", self.name, source, target) self.cmd(f"mkdir -p {target_path}")
self.cmd(f"mkdir -p {target}") self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}")
self.cmd(f"{MOUNT} -n --bind {source} {target}") self._mounts.append((src_path, target_path))
self._mounts.append((source, target))
def next_iface_id(self) -> int: def next_iface_id(self) -> int:
""" """
@ -675,64 +712,28 @@ class CoreNode(CoreNodeBase):
with self.lock: with self.lock:
return super().next_iface_id() return super().next_iface_id()
def newveth(self, iface_id: int = None, ifname: str = None) -> int: def newveth(self, iface_id: int = None, ifname: str = None, mtu: int = None) -> int:
""" """
Create a new interface. Create a new interface.
:param iface_id: id for the new interface :param iface_id: id for the new interface
:param ifname: name for the new interface :param ifname: name for the new interface
:param mtu: mtu for interface
:return: nothing :return: nothing
""" """
with self.lock: with self.lock:
if iface_id is None: mtu = mtu if mtu is not None else DEFAULT_MTU
iface_id = self.next_iface_id() iface_id = iface_id if iface_id is not None else self.next_iface_id()
ifname = ifname if ifname is not None else f"eth{iface_id}"
if ifname is None:
ifname = f"eth{iface_id}"
sessionid = self.session.short_session_id() sessionid = self.session.short_session_id()
try: try:
suffix = f"{self.id:x}.{iface_id}.{sessionid}" suffix = f"{self.id:x}.{iface_id}.{sessionid}"
except TypeError: except TypeError:
suffix = f"{self.id}.{iface_id}.{sessionid}" suffix = f"{self.id}.{iface_id}.{sessionid}"
localname = f"veth{suffix}" localname = f"veth{suffix}"
if len(localname) >= 16: name = f"{localname}p"
raise ValueError(f"interface local name ({localname}) too long") veth = Veth(self.session, name, localname, mtu, self.server, self)
veth.adopt_node(iface_id, ifname, self.up)
name = localname + "p"
if len(name) >= 16:
raise ValueError(f"interface name ({name}) too long")
veth = Veth(
self.session, self, name, localname, start=self.up, server=self.server
)
if self.up:
self.net_client.device_ns(veth.name, str(self.pid))
self.node_net_client.device_name(veth.name, ifname)
self.node_net_client.checksums_off(ifname)
veth.name = ifname
if self.up:
flow_id = self.node_net_client.get_ifindex(veth.name)
veth.flow_id = int(flow_id)
logging.debug("interface flow index: %s - %s", veth.name, veth.flow_id)
mac = self.node_net_client.get_mac(veth.name)
logging.debug("interface mac: %s - %s", veth.name, mac)
veth.set_mac(mac)
try:
# add network interface to the node. If unsuccessful, destroy the
# network interface and raise exception.
self.add_iface(veth, iface_id)
except ValueError as e:
veth.shutdown()
del veth
raise e
return iface_id return iface_id
def newtuntap(self, iface_id: int = None, ifname: str = None) -> int: def newtuntap(self, iface_id: int = None, ifname: str = None) -> int:
@ -744,24 +745,19 @@ class CoreNode(CoreNodeBase):
:return: interface index :return: interface index
""" """
with self.lock: with self.lock:
if iface_id is None: iface_id = iface_id if iface_id is not None else self.next_iface_id()
iface_id = self.next_iface_id() ifname = ifname if ifname is not None else f"eth{iface_id}"
if ifname is None:
ifname = f"eth{iface_id}"
sessionid = self.session.short_session_id() sessionid = self.session.short_session_id()
localname = f"tap{self.id}.{iface_id}.{sessionid}" localname = f"tap{self.id}.{iface_id}.{sessionid}"
name = ifname name = ifname
tuntap = TunTap(self.session, self, name, localname, start=self.up) tuntap = TunTap(self.session, name, localname, node=self)
if self.up:
tuntap.startup()
try: try:
self.add_iface(tuntap, iface_id) self.add_iface(tuntap, iface_id)
except ValueError as e: except CoreError as e:
tuntap.shutdown() tuntap.shutdown()
del tuntap
raise e raise e
return iface_id return iface_id
def set_mac(self, iface_id: int, mac: str) -> None: def set_mac(self, iface_id: int, mac: str) -> None:
@ -842,7 +838,7 @@ class CoreNode(CoreNodeBase):
raise CoreError( raise CoreError(
f"node({self.name}) already has interface({iface_id})" f"node({self.name}) already has interface({iface_id})"
) )
iface_id = self.newveth(iface_id, iface_data.name) iface_id = self.newveth(iface_id, iface_data.name, iface_data.mtu)
self.attachnet(iface_id, net) self.attachnet(iface_id, net)
if iface_data.mac: if iface_data.mac:
self.set_mac(iface_id, iface_data.mac) self.set_mac(iface_id, iface_data.mac)
@ -851,86 +847,99 @@ class CoreNode(CoreNodeBase):
self.ifup(iface_id) self.ifup(iface_id)
return self.get_iface(iface_id) return self.get_iface(iface_id)
def addfile(self, srcname: str, filename: str) -> None: def addfile(self, src_path: Path, file_path: Path) -> None:
""" """
Add a file. Add a file.
:param srcname: source file name :param src_path: source file path
:param filename: file name to add :param file_path: file name to add
:return: nothing :return: nothing
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
logging.info("adding file from %s to %s", srcname, filename) logger.info("adding file from %s to %s", src_path, file_path)
directory = os.path.dirname(filename) directory = file_path.parent
if self.server is None: if self.server is None:
self.client.check_cmd(f"mkdir -p {directory}") self.client.check_cmd(f"mkdir -p {directory}")
self.client.check_cmd(f"mv {srcname} {filename}") self.client.check_cmd(f"mv {src_path} {file_path}")
self.client.check_cmd("sync") self.client.check_cmd("sync")
else: else:
self.host_cmd(f"mkdir -p {directory}") self.host_cmd(f"mkdir -p {directory}")
self.server.remote_put(srcname, filename) self.server.remote_put(src_path, file_path)
def hostfilename(self, filename: str) -> str: def _find_parent_path(self, path: Path) -> Optional[Path]:
""" """
Return the name of a node"s file on the host filesystem. Check if there is an existing mounted parent directory created for this node.
:param filename: host file name :param path: existing parent path to use
:return: path to file :return: exist parent path if exists, None otherwise
""" """
dirname, basename = os.path.split(filename) logger.debug("looking for existing parent: %s", path)
if not basename: existing_path = None
raise ValueError(f"no basename for filename: {filename}") for parent in path.parents:
if dirname and dirname[0] == "/": node_path = self.host_path(parent, is_dir=True)
dirname = dirname[1:] if node_path == self.directory:
dirname = dirname.replace("/", ".") break
dirname = os.path.join(self.nodedir, dirname) if self.path_exists(str(node_path)):
return os.path.join(dirname, basename) relative_path = path.relative_to(parent)
existing_path = node_path / relative_path
break
return existing_path
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
""" """
Create a node file with a given mode. Create file within a node at the given path, using contents and mode.
:param filename: name of file to create :param file_path: desired path for file
:param contents: contents of file :param contents: contents of file
:param mode: mode for file :param mode: mode to create file with
:return: nothing :return: nothing
""" """
hostfilename = self.hostfilename(filename) logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode)
dirname, _basename = os.path.split(hostfilename) host_path = self._find_parent_path(file_path)
if self.server is None: if host_path:
if not os.path.isdir(dirname): self.host_cmd(f"mkdir -p {host_path.parent}")
os.makedirs(dirname, mode=0o755)
with open(hostfilename, "w") as open_file:
open_file.write(contents)
os.chmod(open_file.name, mode)
else: else:
self.host_cmd(f"mkdir -m {0o755:o} -p {dirname}") host_path = self.host_path(file_path)
self.server.remote_put_temp(hostfilename, contents) directory = host_path.parent
self.host_cmd(f"chmod {mode:o} {hostfilename}") if self.server is None:
logging.debug( if not directory.exists():
"node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode directory.mkdir(parents=True, mode=0o755)
with host_path.open("w") as f:
f.write(contents)
host_path.chmod(mode)
else:
self.host_cmd(f"mkdir -m {0o755:o} -p {directory}")
self.server.remote_put_temp(host_path, contents)
self.host_cmd(f"chmod {mode:o} {host_path}")
def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
"""
Copy source file to node host destination, updating the file mode when
provided.
:param src_path: source file to copy
:param dst_path: node host destination
:param mode: file mode
:return: nothing
"""
logger.debug(
"node(%s) copying file src(%s) to dst(%s) mode(%o)",
self.name,
src_path,
dst_path,
mode or 0,
) )
host_path = self._find_parent_path(dst_path)
def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: if host_path:
""" self.host_cmd(f"mkdir -p {host_path.parent}")
Copy a file to a node, following symlinks and preserving metadata.
Change file mode if specified.
:param filename: file name to copy file to
:param srcfilename: file to copy
:param mode: mode to copy to
:return: nothing
"""
hostfilename = self.hostfilename(filename)
if self.server is None:
shutil.copy2(srcfilename, hostfilename)
else: else:
self.server.remote_put(srcfilename, hostfilename) host_path = self.host_path(dst_path)
if self.server is None:
shutil.copy2(src_path, host_path)
else:
self.server.remote_put(src_path, host_path)
if mode is not None: if mode is not None:
self.host_cmd(f"chmod {mode:o} {hostfilename}") self.host_cmd(f"chmod {mode:o} {host_path}")
logging.info(
"node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode
)
class CoreNetworkBase(NodeBase): class CoreNetworkBase(NodeBase):
@ -951,16 +960,17 @@ class CoreNetworkBase(NodeBase):
""" """
Create a CoreNetworkBase instance. Create a CoreNetworkBase instance.
:param session: CORE session object :param session: session object
:param _id: object id :param _id: object id
:param name: object name :param name: object name
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
self.brname = None self.mtu: int = DEFAULT_MTU
self._linked = {} self.brname: Optional[str] = None
self._linked_lock = threading.Lock() self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {}
self.linked_lock: threading.Lock = threading.Lock()
@abc.abstractmethod @abc.abstractmethod
def startup(self) -> None: def startup(self) -> None:
@ -1029,8 +1039,8 @@ class CoreNetworkBase(NodeBase):
i = self.next_iface_id() i = self.next_iface_id()
self.ifaces[i] = iface self.ifaces[i] = iface
iface.net_id = i iface.net_id = i
with self._linked_lock: with self.linked_lock:
self._linked[iface] = {} self.linked[iface] = {}
def detach(self, iface: CoreInterface) -> None: def detach(self, iface: CoreInterface) -> None:
""" """
@ -1041,8 +1051,8 @@ class CoreNetworkBase(NodeBase):
""" """
del self.ifaces[iface.net_id] del self.ifaces[iface.net_id]
iface.net_id = None iface.net_id = None
with self._linked_lock: with self.linked_lock:
del self._linked[iface] del self.linked[iface]
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
""" """

View file

@ -3,6 +3,7 @@ client.py: implementation of the VnodeClient class for issuing commands
over a control channel to the vnoded process running in a network namespace. over a control channel to the vnoded process running in a network namespace.
The control channel can be accessed via calls using the vcmd shell. The control channel can be accessed via calls using the vcmd shell.
""" """
from pathlib import Path
from core import utils from core import utils
from core.executables import BASH, VCMD from core.executables import BASH, VCMD
@ -13,7 +14,7 @@ class VnodeClient:
Provides client functionality for interacting with a virtual node. Provides client functionality for interacting with a virtual node.
""" """
def __init__(self, name: str, ctrlchnlname: str) -> None: def __init__(self, name: str, ctrlchnlname: Path) -> None:
""" """
Create a VnodeClient instance. Create a VnodeClient instance.
@ -21,7 +22,7 @@ class VnodeClient:
:param ctrlchnlname: control channel name :param ctrlchnlname: control channel name
""" """
self.name: str = name self.name: str = name
self.ctrlchnlname: str = ctrlchnlname self.ctrlchnlname: Path = ctrlchnlname
def _verify_connection(self) -> None: def _verify_connection(self) -> None:
""" """

View file

@ -1,6 +1,6 @@
import json import json
import logging import logging
import os from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Callable, Dict, Optional from typing import TYPE_CHECKING, Callable, Dict, Optional
@ -11,6 +11,8 @@ from core.errors import CoreCommandError
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
from core.nodes.netclient import LinuxNetClient, get_net_client from core.nodes.netclient import LinuxNetClient, get_net_client
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
@ -50,7 +52,7 @@ class DockerClient:
self.run(f"docker rm -f {self.name}") self.run(f"docker rm -f {self.name}")
def check_cmd(self, cmd: str, wait: bool = True, shell: bool = False) -> str: def check_cmd(self, cmd: str, wait: bool = True, shell: bool = False) -> str:
logging.info("docker cmd output: %s", cmd) logger.info("docker cmd output: %s", cmd)
return utils.cmd(f"docker exec {self.name} {cmd}", wait=wait, shell=shell) return utils.cmd(f"docker exec {self.name} {cmd}", wait=wait, shell=shell)
def create_ns_cmd(self, cmd: str) -> str: def create_ns_cmd(self, cmd: str) -> str:
@ -60,11 +62,11 @@ class DockerClient:
args = f"docker inspect -f '{{{{.State.Pid}}}}' {self.name}" args = f"docker inspect -f '{{{{.State.Pid}}}}' {self.name}"
output = self.run(args) output = self.run(args)
self.pid = output self.pid = output
logging.debug("node(%s) pid: %s", self.name, self.pid) logger.debug("node(%s) pid: %s", self.name, self.pid)
return output return output
def copy_file(self, source: str, destination: str) -> str: def copy_file(self, src_path: Path, dst_path: Path) -> str:
args = f"docker cp {source} {self.name}:{destination}" args = f"docker cp {src_path} {self.name}:{dst_path}"
return self.run(args) return self.run(args)
@ -76,7 +78,7 @@ class DockerNode(CoreNode):
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
nodedir: str = None, directory: str = None,
server: DistributedServer = None, server: DistributedServer = None,
image: str = None, image: str = None,
) -> None: ) -> None:
@ -86,7 +88,7 @@ class DockerNode(CoreNode):
:param session: core session instance :param session: core session instance
:param _id: object id :param _id: object id
:param name: object name :param name: object name
:param nodedir: node directory :param directory: node directory
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param image: image to start container with :param image: image to start container with
@ -94,7 +96,7 @@ class DockerNode(CoreNode):
if image is None: if image is None:
image = "ubuntu" image = "ubuntu"
self.image: str = image self.image: str = image
super().__init__(session, _id, name, nodedir, server) super().__init__(session, _id, name, directory, server)
def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient:
""" """
@ -162,77 +164,73 @@ class DockerNode(CoreNode):
""" """
return f"docker exec -it {self.name} bash" return f"docker exec -it {self.name} bash"
def privatedir(self, path: str) -> None: def create_dir(self, dir_path: Path) -> None:
""" """
Create a private directory. Create a private directory.
:param path: path to create :param dir_path: path to create
:return: nothing :return: nothing
""" """
logging.debug("creating node dir: %s", path) logger.debug("creating node dir: %s", dir_path)
args = f"mkdir -p {path}" args = f"mkdir -p {dir_path}"
self.cmd(args) self.cmd(args)
def mount(self, source: str, target: str) -> None: def mount(self, src_path: str, target_path: str) -> None:
""" """
Create and mount a directory. Create and mount a directory.
:param source: source directory to mount :param src_path: source directory to mount
:param target: target directory to create :param target_path: target directory to create
:return: nothing :return: nothing
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
logging.debug("mounting source(%s) target(%s)", source, target) logger.debug("mounting source(%s) target(%s)", src_path, target_path)
raise Exception("not supported") raise Exception("not supported")
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
""" """
Create a node file with a given mode. Create a node file with a given mode.
:param filename: name of file to create :param file_path: name of file to create
:param contents: contents of file :param contents: contents of file
:param mode: mode for file :param mode: mode for file
:return: nothing :return: nothing
""" """
logging.debug("nodefile filename(%s) mode(%s)", filename, mode) logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode)
directory = os.path.dirname(filename)
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
temp.write(contents.encode("utf-8")) temp.write(contents.encode("utf-8"))
temp.close() temp.close()
temp_path = Path(temp.name)
if directory: directory = file_path.name
if str(directory) != ".":
self.cmd(f"mkdir -m {0o755:o} -p {directory}") self.cmd(f"mkdir -m {0o755:o} -p {directory}")
if self.server is not None: if self.server is not None:
self.server.remote_put(temp.name, temp.name) self.server.remote_put(temp_path, temp_path)
self.client.copy_file(temp.name, filename) self.client.copy_file(temp_path, file_path)
self.cmd(f"chmod {mode:o} {filename}") self.cmd(f"chmod {mode:o} {file_path}")
if self.server is not None: if self.server is not None:
self.host_cmd(f"rm -f {temp.name}") self.host_cmd(f"rm -f {temp_path}")
os.unlink(temp.name) temp_path.unlink()
logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode)
def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: 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. Copy a file to a node, following symlinks and preserving metadata.
Change file mode if specified. Change file mode if specified.
:param filename: file name to copy file to :param dst_path: file name to copy file to
:param srcfilename: file to copy :param src_path: file to copy
:param mode: mode to copy to :param mode: mode to copy to
:return: nothing :return: nothing
""" """
logging.info( logger.info(
"node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode "node file copy file(%s) source(%s) mode(%o)", dst_path, src_path, mode or 0
) )
directory = os.path.dirname(filename) self.cmd(f"mkdir -p {dst_path.parent}")
self.cmd(f"mkdir -p {directory}") if self.server:
if self.server is None:
source = srcfilename
else:
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
source = temp.name temp_path = Path(temp.name)
self.server.remote_put(source, temp.name) src_path = temp_path
self.server.remote_put(src_path, temp_path)
self.client.copy_file(source, filename) self.client.copy_file(src_path, dst_path)
self.cmd(f"chmod {mode:o} {filename}") if mode is not None:
self.cmd(f"chmod {mode:o} {dst_path}")

View file

@ -4,6 +4,7 @@ virtual ethernet classes that implement the interfaces available under Linux.
import logging import logging
import time import time
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
import netaddr import netaddr
@ -14,6 +15,8 @@ from core.emulator.enumerations import TransportType
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.netclient import LinuxNetClient, get_net_client from core.nodes.netclient import LinuxNetClient, get_net_client
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.session import Session from core.emulator.session import Session
@ -30,25 +33,28 @@ class CoreInterface:
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
node: "CoreNode",
name: str, name: str,
localname: str, localname: str,
mtu: int, mtu: int = DEFAULT_MTU,
server: "DistributedServer" = None, server: "DistributedServer" = None,
node: "CoreNode" = None,
) -> None: ) -> None:
""" """
Creates a CoreInterface instance. Creates a CoreInterface instance.
:param session: core session instance :param session: core session instance
:param node: node for interface
:param name: interface name :param name: interface name
:param localname: interface local name :param localname: interface local name
:param mtu: mtu value :param mtu: mtu value
:param server: remote server node :param server: remote server node will run on, default is None for localhost
will run on, default is None for localhost :param node: node for interface
""" """
if len(name) >= 16:
raise CoreError(f"interface name ({name}) too long, max 16")
if len(localname) >= 16:
raise CoreError(f"interface local name ({localname}) too long, max 16")
self.session: "Session" = session self.session: "Session" = session
self.node: "CoreNode" = node self.node: Optional["CoreNode"] = node
self.name: str = name self.name: str = name
self.localname: str = localname self.localname: str = localname
self.up: bool = False self.up: bool = False
@ -79,7 +85,7 @@ class CoreInterface:
self, self,
args: str, args: str,
env: Dict[str, str] = None, env: Dict[str, str] = None,
cwd: str = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
) -> str: ) -> str:
@ -125,7 +131,6 @@ class CoreInterface:
if self.net: if self.net:
self.detachnet() self.detachnet()
self.net = None self.net = None
net.attach(self) net.attach(self)
self.net = net self.net = net
@ -273,14 +278,12 @@ class CoreInterface:
:return: True if parameter changed, False otherwise :return: True if parameter changed, False otherwise
""" """
# treat None and 0 as unchanged values # treat None and 0 as unchanged values
logging.debug("setting param: %s - %s", key, value) logger.debug("setting param: %s - %s", key, value)
if value is None or value < 0: if value is None or value < 0:
return False return False
current_value = self._params.get(key) current_value = self._params.get(key)
if current_value is not None and current_value == value: if current_value is not None and current_value == value:
return False return False
self._params[key] = value self._params[key] = value
return True return True
@ -339,33 +342,32 @@ class Veth(CoreInterface):
Provides virtual ethernet functionality for core nodes. Provides virtual ethernet functionality for core nodes.
""" """
def __init__( def adopt_node(self, iface_id: int, name: str, start: bool) -> None:
self,
session: "Session",
node: "CoreNode",
name: str,
localname: str,
mtu: int = DEFAULT_MTU,
server: "DistributedServer" = None,
start: bool = True,
) -> None:
""" """
Creates a VEth instance. Adopt this interface to the provided node, configuring and associating
with the node as needed.
:param session: core session instance :param iface_id: interface id for node
:param node: related core node :param name: name of interface fo rnode
:param name: interface name :param start: True to start interface, False otherwise
:param localname: interface local name :return: nothing
:param mtu: interface mtu
:param server: remote server node
will run on, default is None for localhost
:param start: start flag
:raises CoreCommandError: when there is a command exception
""" """
# note that net arg is ignored
super().__init__(session, node, name, localname, mtu, server)
if start: if start:
self.startup() self.startup()
self.net_client.device_ns(self.name, str(self.node.pid))
self.node.node_net_client.checksums_off(self.name)
self.flow_id = self.node.node_net_client.get_ifindex(self.name)
logger.debug("interface flow index: %s - %s", self.name, self.flow_id)
mac = self.node.node_net_client.get_mac(self.name)
logger.debug("interface mac: %s - %s", self.name, mac)
self.set_mac(mac)
self.node.node_net_client.device_name(self.name, name)
self.name = name
try:
self.node.add_iface(self, iface_id)
except CoreError as e:
self.shutdown()
raise e
def startup(self) -> None: def startup(self) -> None:
""" """
@ -375,6 +377,9 @@ class Veth(CoreInterface):
:raises CoreCommandError: when there is a command exception :raises CoreCommandError: when there is a command exception
""" """
self.net_client.create_veth(self.localname, self.name) self.net_client.create_veth(self.localname, self.name)
if self.mtu > 0:
self.net_client.set_mtu(self.name, self.mtu)
self.net_client.set_mtu(self.localname, self.mtu)
self.net_client.device_up(self.localname) self.net_client.device_up(self.localname)
self.up = True self.up = True
@ -404,32 +409,6 @@ class TunTap(CoreInterface):
TUN/TAP virtual device in TAP mode TUN/TAP virtual device in TAP mode
""" """
def __init__(
self,
session: "Session",
node: "CoreNode",
name: str,
localname: str,
mtu: int = DEFAULT_MTU,
server: "DistributedServer" = None,
start: bool = True,
) -> None:
"""
Create a TunTap instance.
:param session: core session instance
:param node: related core node
:param name: interface name
:param localname: local interface name
:param mtu: interface mtu
:param server: remote server node
will run on, default is None for localhost
:param start: start flag
"""
super().__init__(session, node, name, localname, mtu, server)
if start:
self.startup()
def startup(self) -> None: def startup(self) -> None:
""" """
Startup logic for a tunnel tap. Startup logic for a tunnel tap.
@ -452,12 +431,10 @@ class TunTap(CoreInterface):
""" """
if not self.up: if not self.up:
return return
try: try:
self.node.node_net_client.device_flush(self.name) self.node.node_net_client.device_flush(self.name)
except CoreCommandError: except CoreCommandError:
logging.exception("error shutting down tunnel tap") logger.exception("error shutting down tunnel tap")
self.up = False self.up = False
def waitfor( def waitfor(
@ -481,14 +458,14 @@ class TunTap(CoreInterface):
msg = f"attempt {i} failed with nonzero exit status {r}" msg = f"attempt {i} failed with nonzero exit status {r}"
if i < attempts + 1: if i < attempts + 1:
msg += ", retrying..." msg += ", retrying..."
logging.info(msg) logger.info(msg)
time.sleep(delay) time.sleep(delay)
delay += delay delay += delay
if delay > maxretrydelay: if delay > maxretrydelay:
delay = maxretrydelay delay = maxretrydelay
else: else:
msg += ", giving up" msg += ", giving up"
logging.info(msg) logger.info(msg)
return result return result
@ -499,7 +476,7 @@ class TunTap(CoreInterface):
:return: wait for device local response :return: wait for device local response
""" """
logging.debug("waiting for device local: %s", self.localname) logger.debug("waiting for device local: %s", self.localname)
def localdevexists(): def localdevexists():
try: try:
@ -516,7 +493,7 @@ class TunTap(CoreInterface):
:return: nothing :return: nothing
""" """
logging.debug("waiting for device node: %s", self.name) logger.debug("waiting for device node: %s", self.name)
def nodedevexists(): def nodedevexists():
try: try:
@ -578,47 +555,55 @@ class GreTap(CoreInterface):
def __init__( def __init__(
self, self,
session: "Session",
remoteip: str,
key: int = None,
node: "CoreNode" = None, node: "CoreNode" = None,
name: str = None, mtu: int = DEFAULT_MTU,
session: "Session" = None,
mtu: int = 1458,
remoteip: str = None,
_id: int = None, _id: int = None,
localip: str = None, localip: str = None,
ttl: int = 255, ttl: int = 255,
key: int = None,
start: bool = True,
server: "DistributedServer" = None, server: "DistributedServer" = None,
) -> None: ) -> None:
""" """
Creates a GreTap instance. Creates a GreTap instance.
:param node: related core node
:param name: interface name
:param session: core session instance :param session: core session instance
:param mtu: interface mtu
:param remoteip: remote address :param remoteip: remote address
:param key: gre tap key
:param node: related core node
:param mtu: interface mtu
:param _id: object id :param _id: object id
:param localip: local address :param localip: local address
:param ttl: ttl value :param ttl: ttl value
:param key: gre tap key
:param start: start flag
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:raises CoreCommandError: when there is a command exception :raises CoreCommandError: when there is a command exception
""" """
if _id is None: if _id is None:
_id = ((id(self) >> 16) ^ (id(self) & 0xFFFF)) & 0xFFFF _id = ((id(self) >> 16) ^ (id(self) & 0xFFFF)) & 0xFFFF
self.id = _id self.id: int = _id
sessionid = session.short_session_id() sessionid = session.short_session_id()
localname = f"gt.{self.id}.{sessionid}" localname = f"gt.{self.id}.{sessionid}"
super().__init__(session, node, name, localname, mtu, server) name = f"{localname}p"
self.transport_type = TransportType.RAW super().__init__(session, name, localname, mtu, server, node)
if not start: self.transport_type: TransportType = TransportType.RAW
return self.remote_ip: str = remoteip
if remoteip is None: self.ttl: int = ttl
raise CoreError("missing remote IP required for GRE TAP device") self.key: Optional[int] = key
self.net_client.create_gretap(self.localname, remoteip, localip, ttl, key) self.local_ip: Optional[str] = localip
def startup(self) -> None:
"""
Startup logic for a GreTap.
:return: nothing
"""
self.net_client.create_gretap(
self.localname, self.remote_ip, self.local_ip, self.ttl, self.key
)
if self.mtu > 0:
self.net_client.set_mtu(self.localname, self.mtu)
self.net_client.device_up(self.localname) self.net_client.device_up(self.localname)
self.up = True self.up = True
@ -633,5 +618,5 @@ class GreTap(CoreInterface):
self.net_client.device_down(self.localname) self.net_client.device_down(self.localname)
self.net_client.delete_device(self.localname) self.net_client.delete_device(self.localname)
except CoreCommandError: except CoreCommandError:
logging.exception("error during shutdown") logger.exception("error during shutdown")
self.localname = None self.localname = None

View file

@ -1,7 +1,7 @@
import json import json
import logging import logging
import os
import time import time
from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Callable, Dict, Optional from typing import TYPE_CHECKING, Callable, Dict, Optional
@ -12,6 +12,8 @@ from core.errors import CoreCommandError
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
@ -57,11 +59,10 @@ class LxdClient:
args = self.create_cmd(cmd) args = self.create_cmd(cmd)
return utils.cmd(args, wait=wait, shell=shell) return utils.cmd(args, wait=wait, shell=shell)
def copy_file(self, source: str, destination: str) -> None: def copy_file(self, src_path: Path, dst_path: Path) -> None:
if destination[0] != "/": if not str(dst_path).startswith("/"):
destination = os.path.join("/root/", destination) dst_path = Path("/root/") / dst_path
args = f"lxc file push {src_path} {self.name}/{dst_path}"
args = f"lxc file push {source} {self.name}/{destination}"
self.run(args) self.run(args)
@ -73,7 +74,7 @@ class LxcNode(CoreNode):
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
nodedir: str = None, directory: str = None,
server: DistributedServer = None, server: DistributedServer = None,
image: str = None, image: str = None,
) -> None: ) -> None:
@ -83,7 +84,7 @@ class LxcNode(CoreNode):
:param session: core session instance :param session: core session instance
:param _id: object id :param _id: object id
:param name: object name :param name: object name
:param nodedir: node directory :param directory: node directory
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param image: image to start container with :param image: image to start container with
@ -91,7 +92,7 @@ class LxcNode(CoreNode):
if image is None: if image is None:
image = "ubuntu" image = "ubuntu"
self.image: str = image self.image: str = image
super().__init__(session, _id, name, nodedir, server) super().__init__(session, _id, name, directory, server)
def alive(self) -> bool: def alive(self) -> bool:
""" """
@ -139,81 +140,77 @@ class LxcNode(CoreNode):
""" """
return f"lxc exec {self.name} -- {sh}" return f"lxc exec {self.name} -- {sh}"
def privatedir(self, path: str) -> None: def create_dir(self, dir_path: Path) -> None:
""" """
Create a private directory. Create a private directory.
:param path: path to create :param dir_path: path to create
:return: nothing :return: nothing
""" """
logging.info("creating node dir: %s", path) logger.info("creating node dir: %s", dir_path)
args = f"mkdir -p {path}" args = f"mkdir -p {dir_path}"
self.cmd(args) self.cmd(args)
def mount(self, source: str, target: str) -> None: def mount(self, src_path: Path, target_path: Path) -> None:
""" """
Create and mount a directory. Create and mount a directory.
:param source: source directory to mount :param src_path: source directory to mount
:param target: target directory to create :param target_path: target directory to create
:return: nothing :return: nothing
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
logging.debug("mounting source(%s) target(%s)", source, target) logger.debug("mounting source(%s) target(%s)", src_path, target_path)
raise Exception("not supported") raise Exception("not supported")
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
""" """
Create a node file with a given mode. Create a node file with a given mode.
:param filename: name of file to create :param file_path: name of file to create
:param contents: contents of file :param contents: contents of file
:param mode: mode for file :param mode: mode for file
:return: nothing :return: nothing
""" """
logging.debug("nodefile filename(%s) mode(%s)", filename, mode) logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode)
directory = os.path.dirname(filename)
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
temp.write(contents.encode("utf-8")) temp.write(contents.encode("utf-8"))
temp.close() temp.close()
temp_path = Path(temp.name)
if directory: directory = file_path.parent
if str(directory) != ".":
self.cmd(f"mkdir -m {0o755:o} -p {directory}") self.cmd(f"mkdir -m {0o755:o} -p {directory}")
if self.server is not None: if self.server is not None:
self.server.remote_put(temp.name, temp.name) self.server.remote_put(temp_path, temp_path)
self.client.copy_file(temp.name, filename) self.client.copy_file(temp_path, file_path)
self.cmd(f"chmod {mode:o} {filename}") self.cmd(f"chmod {mode:o} {file_path}")
if self.server is not None: if self.server is not None:
self.host_cmd(f"rm -f {temp.name}") self.host_cmd(f"rm -f {temp_path}")
os.unlink(temp.name) temp_path.unlink()
logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode) logger.debug("node(%s) added file: %s; mode: 0%o", self.name, file_path, mode)
def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: 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. Copy a file to a node, following symlinks and preserving metadata.
Change file mode if specified. Change file mode if specified.
:param filename: file name to copy file to :param dst_path: file name to copy file to
:param srcfilename: file to copy :param src_path: file to copy
:param mode: mode to copy to :param mode: mode to copy to
:return: nothing :return: nothing
""" """
logging.info( logger.info(
"node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode "node file copy file(%s) source(%s) mode(%o)", dst_path, src_path, mode or 0
) )
directory = os.path.dirname(filename) self.cmd(f"mkdir -p {dst_path.parent}")
self.cmd(f"mkdir -p {directory}") if self.server:
if self.server is None:
source = srcfilename
else:
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
source = temp.name temp_path = Path(temp.name)
self.server.remote_put(source, temp.name) src_path = temp_path
self.server.remote_put(src_path, temp_path)
self.client.copy_file(source, filename) self.client.copy_file(src_path, dst_path)
self.cmd(f"chmod {mode:o} {filename}") if mode is not None:
self.cmd(f"chmod {mode:o} {dst_path}")
def add_iface(self, iface: CoreInterface, iface_id: int) -> None: def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
super().add_iface(iface, iface_id) super().add_iface(iface, iface_id)

View file

@ -38,7 +38,7 @@ class LinuxNetClient:
:param device: device to add route to :param device: device to add route to
:return: nothing :return: nothing
""" """
self.run(f"{IP} route add {route} dev {device}") self.run(f"{IP} route replace {route} dev {device}")
def device_up(self, device: str) -> None: def device_up(self, device: str) -> None:
""" """
@ -95,14 +95,14 @@ class LinuxNetClient:
""" """
return self.run(f"cat /sys/class/net/{device}/address") return self.run(f"cat /sys/class/net/{device}/address")
def get_ifindex(self, device: str) -> str: def get_ifindex(self, device: str) -> int:
""" """
Retrieve ifindex for a given device. Retrieve ifindex for a given device.
:param device: device to get ifindex for :param device: device to get ifindex for
:return: ifindex :return: ifindex
""" """
return self.run(f"cat /sys/class/net/{device}/ifindex") return int(self.run(f"cat /sys/class/net/{device}/ifindex"))
def device_ns(self, device: str, namespace: str) -> None: def device_ns(self, device: str, namespace: str) -> None:
""" """
@ -296,6 +296,16 @@ class LinuxNetClient:
""" """
self.run(f"{IP} link set {name} type bridge ageing_time {value}") self.run(f"{IP} link set {name} type bridge ageing_time {value}")
def set_mtu(self, name: str, value: int) -> None:
"""
Sets the mtu value for a device.
:param name: name of device to set value for
:param value: mtu value to set
:return: nothing
"""
self.run(f"{IP} link set {name} mtu {value}")
class OvsNetClient(LinuxNetClient): class OvsNetClient(LinuxNetClient):
""" """
@ -361,14 +371,15 @@ class OvsNetClient(LinuxNetClient):
return True return True
return False return False
def disable_mac_learning(self, name: str) -> None: def set_mac_learning(self, name: str, value: int) -> None:
""" """
Disable mac learning for a OVS bridge. Set mac learning for an OVS bridge.
:param name: bridge name :param name: bridge name
:param value: ageing time value
:return: nothing :return: nothing
""" """
self.run(f"{OVS_VSCTL} set bridge {name} other_config:mac-aging-time=0") self.run(f"{OVS_VSCTL} set bridge {name} other_config:mac-aging-time={value}")
def get_net_client(use_ovs: bool, run: Callable[..., str]) -> LinuxNetClient: def get_net_client(use_ovs: bool, run: Callable[..., str]) -> LinuxNetClient:

View file

@ -6,7 +6,10 @@ import logging
import math import math
import threading import threading
import time import time
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type from collections import OrderedDict
from pathlib import Path
from queue import Queue
from typing import TYPE_CHECKING, Dict, List, Optional, Type
import netaddr import netaddr
@ -20,11 +23,13 @@ from core.emulator.enumerations import (
RegisterTlvs, RegisterTlvs,
) )
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import EBTABLES, TC from core.executables import NFTABLES, TC
from core.nodes.base import CoreNetworkBase from core.nodes.base import CoreNetworkBase
from core.nodes.interface import CoreInterface, GreTap, Veth from core.nodes.interface import CoreInterface, GreTap, Veth
from core.nodes.netclient import get_net_client from core.nodes.netclient import get_net_client
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.session import Session from core.emulator.session import Session
@ -33,224 +38,194 @@ if TYPE_CHECKING:
WirelessModelType = Type[WirelessModel] WirelessModelType = Type[WirelessModel]
LEARNING_DISABLED: int = 0 LEARNING_DISABLED: int = 0
ebtables_lock: threading.Lock = threading.Lock()
class EbtablesQueue: class SetQueue(Queue):
""" """
Helper class for queuing up ebtables commands into rate-limited Set backed queue to avoid duplicate submissions.
"""
def _init(self, maxsize):
self.queue: OrderedDict = OrderedDict()
def _put(self, item):
self.queue[item] = None
def _get(self):
key, _ = self.queue.popitem(last=False)
return key
class NftablesQueue:
"""
Helper class for queuing up nftables commands into rate-limited
atomic commits. This improves performance and reliability when there are atomic commits. This improves performance and reliability when there are
many WLAN link updates. many WLAN link updates.
""" """
# update rate is every 300ms # update rate is every 300ms
rate: float = 0.3 rate: float = 0.3
# ebtables atomic_file: str = "/tmp/pycore.nftables.atomic"
atomic_file: str = "/tmp/pycore.ebtables.atomic" chain: str = "forward"
def __init__(self) -> None: def __init__(self) -> None:
""" """
Initialize the helper class, but don't start the update thread Initialize the helper class, but don't start the update thread
until a WLAN is instantiated. until a WLAN is instantiated.
""" """
self.doupdateloop: bool = False self.running: bool = False
self.updatethread: Optional[threading.Thread] = None self.run_thread: Optional[threading.Thread] = None
# this lock protects cmds and updates lists # this lock protects cmds and updates lists
self.updatelock: threading.Lock = threading.Lock() self.lock: threading.Lock = threading.Lock()
# list of pending ebtables 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: List["CoreNetwork"] = [] self.updates: SetQueue = SetQueue()
# timestamps of last WLAN update; this keeps track of WLANs that are # timestamps of last WLAN update; this keeps track of WLANs that are
# using this queue # using this queue
self.last_update_time: Dict["CoreNetwork", float] = {} self.last_update_time: Dict["CoreNetwork", float] = {}
def startupdateloop(self, wlan: "CoreNetwork") -> None: def start(self, net: "CoreNetwork") -> None:
""" """
Kick off the update loop; only needs to be invoked once. Start thread to listen for updates for the provided network.
:param net: network to start checking updates
:return: nothing :return: nothing
""" """
with self.updatelock: with self.lock:
self.last_update_time[wlan] = time.monotonic() self.last_update_time[net] = time.monotonic()
if self.doupdateloop: if self.running:
return return
self.doupdateloop = True self.running = True
self.updatethread = threading.Thread(target=self.updateloop, daemon=True) self.run_thread = threading.Thread(target=self.run, daemon=True)
self.updatethread.start() self.run_thread.start()
def stopupdateloop(self, wlan: "CoreNetwork") -> None: def stop(self, net: "CoreNetwork") -> None:
""" """
Kill the update loop thread if there are no more WLANs using it. Stop updates for network, when no networks remain, stop update thread.
:param net: network to stop watching updates
:return: nothing
"""
with self.lock:
self.last_update_time.pop(net, None)
if self.last_update_time:
return
self.running = False
if self.run_thread:
self.updates.put(None)
self.run_thread.join()
self.run_thread = None
def last_update(self, net: "CoreNetwork") -> float:
"""
Return the time elapsed since this network was last updated.
:param net: network node
:return: elapsed time
"""
now = time.monotonic()
last_update = self.last_update_time.setdefault(net, now)
return now - last_update
def run(self) -> None:
"""
Thread target that looks for networks needing update, and
rate limits the amount of nftables activity. Only one userspace program
should use nftables at any given time, or results can be unpredictable.
:return: nothing :return: nothing
""" """
with self.updatelock: while self.running:
try: net = self.updates.get()
del self.last_update_time[wlan] if net is None:
except KeyError: break
logging.exception( if not net.up:
"error deleting last update time for wlan, ignored before: %s", wlan self.last_update_time[net] = time.monotonic()
) elif self.last_update(net) > self.rate:
if len(self.last_update_time) > 0: with self.lock:
self.build_cmds(net)
self.commit(net)
self.last_update_time[net] = time.monotonic()
def commit(self, net: "CoreNetwork") -> None:
"""
Commit changes to nftables for the provided network.
:param net: network to commit nftables changes
:return: nothing
"""
if not self.cmds:
return return
self.doupdateloop = False # write out nft commands to file
if self.updatethread: for cmd in self.cmds:
self.updatethread.join() net.host_cmd(f"echo {cmd} >> {self.atomic_file}", shell=True)
self.updatethread = None # read file as atomic change
net.host_cmd(f"{NFTABLES} -f {self.atomic_file}")
# remove file
net.host_cmd(f"rm -f {self.atomic_file}")
self.cmds.clear()
def ebatomiccmd(self, cmd: str) -> str: def update(self, net: "CoreNetwork") -> None:
""" """
Helper for building ebtables atomic file command list. Flag this network has an update, so the nftables chain will be rebuilt.
:param net: wlan network
:param cmd: ebtable command
:return: ebtable atomic command
"""
return f"{EBTABLES} --atomic-file {self.atomic_file} {cmd}"
def lastupdate(self, wlan: "CoreNetwork") -> float:
"""
Return the time elapsed since this WLAN was last updated.
:param wlan: wlan entity
:return: elpased time
"""
try:
elapsed = time.monotonic() - self.last_update_time[wlan]
except KeyError:
self.last_update_time[wlan] = time.monotonic()
elapsed = 0.0
return elapsed
def updated(self, wlan: "CoreNetwork") -> None:
"""
Keep track of when this WLAN was last updated.
:param wlan: wlan entity
:return: nothing :return: nothing
""" """
self.last_update_time[wlan] = time.monotonic() self.updates.put(net)
self.updates.remove(wlan)
def updateloop(self) -> None: def delete_table(self, net: "CoreNetwork") -> None:
""" """
Thread target that looks for WLANs needing update, and Delete nftable bridge rule table.
rate limits the amount of ebtables activity. Only one userspace program
should use ebtables at any given time, or results can be unpredictable.
:param net: network to delete table for
:return: nothing :return: nothing
""" """
while self.doupdateloop: with self.lock:
with self.updatelock: net.host_cmd(f"{NFTABLES} delete table bridge {net.brname}")
for wlan in self.updates:
# Check if wlan is from a previously closed session. Because of the
# rate limiting scheme employed here, this may happen if a new session
# is started soon after closing a previous session.
# TODO: if these are WlanNodes, this will never throw an exception
try:
wlan.session
except Exception:
# Just mark as updated to remove from self.updates.
self.updated(wlan)
continue
if self.lastupdate(wlan) > self.rate: def build_cmds(self, net: "CoreNetwork") -> None:
self.buildcmds(wlan)
self.ebcommit(wlan)
self.updated(wlan)
time.sleep(self.rate)
def ebcommit(self, wlan: "CoreNetwork") -> None:
""" """
Perform ebtables atomic commit using commands built in the self.cmds list. Inspect linked nodes for a network, and rebuild the nftables chain commands.
:param net: network to build commands for
:return: nothing :return: nothing
""" """
# save kernel ebtables snapshot to a file with net.linked_lock:
args = self.ebatomiccmd("--atomic-save") if net.has_nftables_chain:
wlan.host_cmd(args) self.cmds.append(f"flush table bridge {net.brname}")
# modify the table file using queued ebtables commands
for c in self.cmds:
args = self.ebatomiccmd(c)
wlan.host_cmd(args)
self.cmds = []
# commit the table file to the kernel
args = self.ebatomiccmd("--atomic-commit")
wlan.host_cmd(args)
try:
wlan.host_cmd(f"rm -f {self.atomic_file}")
except CoreCommandError:
logging.exception("error removing atomic file: %s", self.atomic_file)
def ebchange(self, wlan: "CoreNetwork") -> None:
"""
Flag a change to the given WLAN's _linked dict, so the ebtables
chain will be rebuilt at the next interval.
:return: nothing
"""
with self.updatelock:
if wlan not in self.updates:
self.updates.append(wlan)
def buildcmds(self, wlan: "CoreNetwork") -> None:
"""
Inspect a _linked dict from a wlan, and rebuild the ebtables chain for that WLAN.
:return: nothing
"""
with wlan._linked_lock:
if wlan.has_ebtables_chain:
# flush the chain
self.cmds.append(f"-F {wlan.brname}")
else: else:
wlan.has_ebtables_chain = True net.has_nftables_chain = True
self.cmds.extend( policy = net.policy.value.lower()
[ self.cmds.append(f"add table bridge {net.brname}")
f"-N {wlan.brname} -P {wlan.policy.value}", self.cmds.append(
f"-A FORWARD --logical-in {wlan.brname} -j {wlan.brname}", f"add chain bridge {net.brname} {self.chain} {{type filter hook "
] f"forward priority 0\\; policy {policy}\\;}}"
) )
# add default rule to accept all traffic not for this bridge
self.cmds.append(
f"add rule bridge {net.brname} {self.chain} "
f"ibriport != {net.brname} accept"
)
# rebuild the chain # rebuild the chain
for iface1, v in wlan._linked.items(): for iface1, v in net.linked.items():
for oface2, linked in v.items(): for iface2, linked in v.items():
if wlan.policy == NetworkPolicy.DROP and linked: policy = None
self.cmds.extend( if net.policy == NetworkPolicy.DROP and linked:
[ policy = "accept"
f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j ACCEPT", elif net.policy == NetworkPolicy.ACCEPT and not linked:
f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j ACCEPT", policy = "drop"
] if policy:
self.cmds.append(
f"add rule bridge {net.brname} {self.chain} "
f"iif {iface1.localname} oif {iface2.localname} "
f"{policy}"
) )
elif wlan.policy == NetworkPolicy.ACCEPT and not linked: self.cmds.append(
self.cmds.extend( f"add rule bridge {net.brname} {self.chain} "
[ f"oif {iface1.localname} iif {iface2.localname} "
f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j DROP", f"{policy}"
f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j DROP",
]
) )
# a global object because all WLANs share the same queue # a global object because all networks share the same queue
# cannot have multiple threads invoking the ebtables commnd # cannot have multiple threads invoking the nftables commnd
ebq: EbtablesQueue = EbtablesQueue() nft_queue: NftablesQueue = NftablesQueue()
def ebtablescmds(call: Callable[..., str], cmds: List[str]) -> None:
"""
Run ebtable commands.
:param call: function to call commands
:param cmds: commands to call
:return: nothing
"""
with ebtables_lock:
for args in cmds:
call(args)
class CoreNetwork(CoreNetworkBase): class CoreNetwork(CoreNetworkBase):
@ -282,17 +257,17 @@ class CoreNetwork(CoreNetworkBase):
if name is None: if name is None:
name = str(self.id) name = str(self.id)
if policy is not None: if policy is not None:
self.policy = policy self.policy: NetworkPolicy = policy
self.name: Optional[str] = name self.name: Optional[str] = name
sessionid = self.session.short_session_id() sessionid = self.session.short_session_id()
self.brname: str = f"b.{self.id}.{sessionid}" self.brname: str = f"b.{self.id}.{sessionid}"
self.has_ebtables_chain: bool = False self.has_nftables_chain: bool = False
def host_cmd( def host_cmd(
self, self,
args: str, args: str,
env: Dict[str, str] = None, env: Dict[str, str] = None,
cwd: str = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
) -> str: ) -> str:
@ -308,22 +283,24 @@ class CoreNetwork(CoreNetworkBase):
:return: combined stdout and stderr :return: combined stdout and stderr
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
logging.debug("network node(%s) cmd", self.name) logger.debug("network node(%s) cmd", self.name)
output = utils.cmd(args, env, cwd, wait, shell) output = utils.cmd(args, env, cwd, wait, shell)
self.session.distributed.execute(lambda x: x.remote_cmd(args, env, cwd, wait)) self.session.distributed.execute(lambda x: x.remote_cmd(args, env, cwd, wait))
return output return output
def startup(self) -> None: def startup(self) -> None:
""" """
Linux bridge starup logic. Linux bridge startup logic.
:return: nothing :return: nothing
:raises CoreCommandError: when there is a command exception :raises CoreCommandError: when there is a command exception
""" """
self.net_client.create_bridge(self.brname) self.net_client.create_bridge(self.brname)
self.has_ebtables_chain = False if self.mtu > 0:
self.net_client.set_mtu(self.brname, self.mtu)
self.has_nftables_chain = False
self.up = True self.up = True
ebq.startupdateloop(self) nft_queue.start(self)
def shutdown(self) -> None: def shutdown(self) -> None:
""" """
@ -333,27 +310,18 @@ class CoreNetwork(CoreNetworkBase):
""" """
if not self.up: if not self.up:
return return
nft_queue.stop(self)
ebq.stopupdateloop(self)
try: try:
self.net_client.delete_bridge(self.brname) self.net_client.delete_bridge(self.brname)
if self.has_ebtables_chain: if self.has_nftables_chain:
cmds = [ nft_queue.delete_table(self)
f"{EBTABLES} -D FORWARD --logical-in {self.brname} -j {self.brname}",
f"{EBTABLES} -X {self.brname}",
]
ebtablescmds(self.host_cmd, cmds)
except CoreCommandError: except CoreCommandError:
logging.exception("error during shutdown") logging.exception("error during shutdown")
# removes veth pairs used for bridge-to-bridge connections # removes veth pairs used for bridge-to-bridge connections
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.shutdown() iface.shutdown()
self.ifaces.clear() self.ifaces.clear()
self._linked.clear() self.linked.clear()
del self.session
self.up = False self.up = False
def attach(self, iface: CoreInterface) -> None: def attach(self, iface: CoreInterface) -> None:
@ -378,7 +346,7 @@ class CoreNetwork(CoreNetworkBase):
iface.net_client.delete_iface(self.brname, iface.localname) iface.net_client.delete_iface(self.brname, iface.localname)
super().detach(iface) super().detach(iface)
def linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool: def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool:
""" """
Determine if the provided network interfaces are linked. Determine if the provided network interfaces are linked.
@ -389,12 +357,10 @@ class CoreNetwork(CoreNetworkBase):
# check if the network interfaces are attached to this network # check if the network interfaces are attached to this network
if self.ifaces[iface1.net_id] != iface1: if self.ifaces[iface1.net_id] != iface1:
raise ValueError(f"inconsistency for interface {iface1.name}") raise ValueError(f"inconsistency for interface {iface1.name}")
if self.ifaces[iface2.net_id] != iface2: if self.ifaces[iface2.net_id] != iface2:
raise ValueError(f"inconsistency for interface {iface2.name}") raise ValueError(f"inconsistency for interface {iface2.name}")
try: try:
linked = self._linked[iface1][iface2] linked = self.linked[iface1][iface2]
except KeyError: except KeyError:
if self.policy == NetworkPolicy.ACCEPT: if self.policy == NetworkPolicy.ACCEPT:
linked = True linked = True
@ -402,41 +368,37 @@ class CoreNetwork(CoreNetworkBase):
linked = False linked = False
else: else:
raise Exception(f"unknown policy: {self.policy.value}") raise Exception(f"unknown policy: {self.policy.value}")
self._linked[iface1][iface2] = linked self.linked[iface1][iface2] = linked
return linked return linked
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
""" """
Unlink two interfaces, resulting in adding or removing ebtables Unlink two interfaces, resulting in adding or removing filtering rules.
:param iface1: interface one
:param iface2: interface two
:return: nothing
"""
with self.linked_lock:
if not self.is_linked(iface1, iface2):
return
self.linked[iface1][iface2] = False
nft_queue.update(self)
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
"""
Link two interfaces together, resulting in adding or removing
filtering rules. filtering rules.
:param iface1: interface one :param iface1: interface one
:param iface2: interface two :param iface2: interface two
:return: nothing :return: nothing
""" """
with self._linked_lock: with self.linked_lock:
if not self.linked(iface1, iface2): if self.is_linked(iface1, iface2):
return return
self._linked[iface1][iface2] = False self.linked[iface1][iface2] = True
nft_queue.update(self)
ebq.ebchange(self)
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
"""
Link two interfaces together, resulting in adding or removing
ebtables filtering rules.
:param iface1: interface one
:param iface2: interface two
:return: nothing
"""
with self._linked_lock:
if self.linked(iface1, iface2):
return
self._linked[iface1][iface2] = True
ebq.ebchange(self)
def linkconfig( def linkconfig(
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
@ -522,28 +484,22 @@ class CoreNetwork(CoreNetworkBase):
_id = f"{self.id:x}" _id = f"{self.id:x}"
except TypeError: except TypeError:
_id = str(self.id) _id = str(self.id)
try: try:
net_id = f"{net.id:x}" net_id = f"{net.id:x}"
except TypeError: except TypeError:
net_id = str(net.id) net_id = str(net.id)
localname = f"veth{_id}.{net_id}.{sessionid}" localname = f"veth{_id}.{net_id}.{sessionid}"
if len(localname) >= 16:
raise ValueError(f"interface local name {localname} too long")
name = f"veth{net_id}.{_id}.{sessionid}" name = f"veth{net_id}.{_id}.{sessionid}"
if len(name) >= 16: iface = Veth(self.session, name, localname)
raise ValueError(f"interface name {name} too long") if self.up:
iface.startup()
iface = Veth(self.session, None, name, localname, start=self.up)
self.attach(iface) self.attach(iface)
if net.up and net.brname: if net.up and net.brname:
iface.net_client.set_iface_master(net.brname, iface.name) iface.net_client.set_iface_master(net.brname, iface.name)
i = net.next_iface_id() i = net.next_iface_id()
net.ifaces[i] = iface net.ifaces[i] = iface
with net._linked_lock: with net.linked_lock:
net._linked[iface] = {} net.linked[iface] = {}
iface.net = self iface.net = self
iface.othernet = net iface.othernet = net
return iface return iface
@ -616,14 +572,15 @@ class GreTapBridge(CoreNetwork):
self.localip: Optional[str] = localip self.localip: Optional[str] = localip
self.ttl: int = ttl self.ttl: int = ttl
self.gretap: Optional[GreTap] = None self.gretap: Optional[GreTap] = None
if remoteip is not None: if self.remoteip is not None:
self.gretap = GreTap( self.gretap = GreTap(
session,
remoteip,
key=self.grekey,
node=self, node=self,
session=session,
remoteip=remoteip,
localip=localip, localip=localip,
ttl=ttl, ttl=ttl,
key=self.grekey, mtu=self.mtu,
) )
def startup(self) -> None: def startup(self) -> None:
@ -634,6 +591,7 @@ class GreTapBridge(CoreNetwork):
""" """
super().startup() super().startup()
if self.gretap: if self.gretap:
self.gretap.startup()
self.attach(self.gretap) self.attach(self.gretap)
def shutdown(self) -> None: def shutdown(self) -> None:
@ -659,18 +617,20 @@ class GreTapBridge(CoreNetwork):
:return: nothing :return: nothing
""" """
if self.gretap: if self.gretap:
raise ValueError(f"gretap already exists for {self.name}") raise CoreError(f"gretap already exists for {self.name}")
remoteip = ips[0].split("/")[0] remoteip = ips[0].split("/")[0]
localip = None localip = None
if len(ips) > 1: if len(ips) > 1:
localip = ips[1].split("/")[0] localip = ips[1].split("/")[0]
self.gretap = GreTap( self.gretap = GreTap(
session=self.session, self.session,
remoteip=remoteip, remoteip,
key=self.grekey,
localip=localip, localip=localip,
ttl=self.ttl, ttl=self.ttl,
key=self.grekey, mtu=self.mtu,
) )
self.startup()
self.attach(self.gretap) self.attach(self.gretap)
def setkey(self, key: int, iface_data: InterfaceData) -> None: def setkey(self, key: int, iface_data: InterfaceData) -> None:
@ -769,7 +729,7 @@ class CtrlNet(CoreNetwork):
raise CoreError(f"old bridges exist for node: {self.id}") raise CoreError(f"old bridges exist for node: {self.id}")
super().startup() super().startup()
logging.info("added control network bridge: %s %s", self.brname, self.prefix) logger.info("added control network bridge: %s %s", self.brname, self.prefix)
if self.hostid and self.assign_address: if self.hostid and self.assign_address:
self.add_addresses(self.hostid) self.add_addresses(self.hostid)
@ -777,7 +737,7 @@ class CtrlNet(CoreNetwork):
self.add_addresses(-2) self.add_addresses(-2)
if self.updown_script: if self.updown_script:
logging.info( logger.info(
"interface %s updown script (%s startup) called", "interface %s updown script (%s startup) called",
self.brname, self.brname,
self.updown_script, self.updown_script,
@ -797,7 +757,7 @@ class CtrlNet(CoreNetwork):
try: try:
self.net_client.delete_iface(self.brname, self.serverintf) self.net_client.delete_iface(self.brname, self.serverintf)
except CoreCommandError: except CoreCommandError:
logging.exception( logger.exception(
"error deleting server interface %s from bridge %s", "error deleting server interface %s from bridge %s",
self.serverintf, self.serverintf,
self.brname, self.brname,
@ -805,14 +765,14 @@ class CtrlNet(CoreNetwork):
if self.updown_script is not None: if self.updown_script is not None:
try: try:
logging.info( logger.info(
"interface %s updown script (%s shutdown) called", "interface %s updown script (%s shutdown) called",
self.brname, self.brname,
self.updown_script, self.updown_script,
) )
self.host_cmd(f"{self.updown_script} {self.brname} shutdown") self.host_cmd(f"{self.updown_script} {self.brname} shutdown")
except CoreCommandError: except CoreCommandError:
logging.exception("error issuing shutdown script shutdown") logger.exception("error issuing shutdown script shutdown")
super().shutdown() super().shutdown()
@ -990,7 +950,7 @@ class WlanNode(CoreNetwork):
:return: nothing :return: nothing
""" """
super().startup() super().startup()
ebq.ebchange(self) nft_queue.update(self)
def attach(self, iface: CoreInterface) -> None: def attach(self, iface: CoreInterface) -> None:
""" """
@ -1012,7 +972,7 @@ class WlanNode(CoreNetwork):
:param config: configuration for model being set :param config: configuration for model being set
:return: nothing :return: nothing
""" """
logging.debug("node(%s) setting model: %s", self.name, model.name) logger.debug("node(%s) setting model: %s", self.name, model.name)
if model.config_type == RegisterTlvs.WIRELESS: if model.config_type == RegisterTlvs.WIRELESS:
self.model = model(session=self.session, _id=self.id) self.model = model(session=self.session, _id=self.id)
for iface in self.get_ifaces(): for iface in self.get_ifaces():
@ -1031,7 +991,7 @@ class WlanNode(CoreNetwork):
def updatemodel(self, config: Dict[str, str]) -> None: def updatemodel(self, config: Dict[str, str]) -> None:
if not self.model: if not self.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})")
logging.debug( logger.debug(
"node(%s) updating model(%s): %s", self.id, self.model.name, config "node(%s) updating model(%s): %s", self.id, self.model.name, config
) )
self.model.update_config(config) self.model.update_config(config)

View file

@ -3,9 +3,9 @@ PhysicalNode class for including real systems in the emulated network.
""" """
import logging import logging
import os
import threading import threading
from typing import IO, TYPE_CHECKING, List, Optional, Tuple from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Tuple
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
@ -14,7 +14,9 @@ from core.errors import CoreCommandError, CoreError
from core.executables import MOUNT, TEST, UMOUNT from core.executables import MOUNT, TEST, UMOUNT
from core.nodes.base import CoreNetworkBase, CoreNodeBase from core.nodes.base import CoreNetworkBase, CoreNodeBase
from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.network import CoreNetwork, GreTap from core.nodes.network import CoreNetwork
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
@ -26,15 +28,15 @@ class PhysicalNode(CoreNodeBase):
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
nodedir: str = None, directory: Path = None,
server: DistributedServer = None, server: DistributedServer = None,
) -> None: ) -> None:
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
if not self.server: if not self.server:
raise CoreError("physical nodes must be assigned to a remote server") raise CoreError("physical nodes must be assigned to a remote server")
self.nodedir: Optional[str] = nodedir self.directory: Optional[Path] = directory
self.lock: threading.RLock = threading.RLock() self.lock: threading.RLock = threading.RLock()
self._mounts: List[Tuple[str, str]] = [] self._mounts: List[Tuple[Path, Path]] = []
def startup(self) -> None: def startup(self) -> None:
with self.lock: with self.lock:
@ -44,15 +46,12 @@ class PhysicalNode(CoreNodeBase):
def shutdown(self) -> None: def shutdown(self) -> None:
if not self.up: if not self.up:
return return
with self.lock: with self.lock:
while self._mounts: while self._mounts:
_source, target = self._mounts.pop(-1) _, target_path = self._mounts.pop(-1)
self.umount(target) self.umount(target_path)
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.shutdown() iface.shutdown()
self.rmnodedir() self.rmnodedir()
def path_exists(self, path: str) -> bool: def path_exists(self, path: str) -> bool:
@ -166,7 +165,7 @@ class PhysicalNode(CoreNodeBase):
def new_iface( def new_iface(
self, net: CoreNetworkBase, iface_data: InterfaceData self, net: CoreNetworkBase, iface_data: InterfaceData
) -> CoreInterface: ) -> CoreInterface:
logging.info("creating interface") logger.info("creating interface")
ips = iface_data.get_ips() ips = iface_data.get_ips()
iface_id = iface_data.id iface_id = iface_data.id
if iface_id is None: if iface_id is None:
@ -174,67 +173,46 @@ class PhysicalNode(CoreNodeBase):
name = iface_data.name name = iface_data.name
if name is None: if name is None:
name = f"gt{iface_id}" name = f"gt{iface_id}"
if self.up: _, remote_tap = self.session.distributed.create_gre_tunnel(
# this is reached when this node is linked to a network node net, self.server, iface_data.mtu, self.up
# tunnel to net not built yet, so build it now and adopt it
_, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server)
self.adopt_iface(remote_tap, iface_id, iface_data.mac, ips)
return remote_tap
else:
# this is reached when configuring services (self.up=False)
iface = GreTap(node=self, name=name, session=self.session, start=False)
self.adopt_iface(iface, iface_id, iface_data.mac, ips)
return iface
def privatedir(self, path: str) -> None:
if path[0] != "/":
raise ValueError(f"path not fully qualified: {path}")
hostpath = os.path.join(
self.nodedir, os.path.normpath(path).strip("/").replace("/", ".")
) )
os.mkdir(hostpath) self.adopt_iface(remote_tap, iface_id, iface_data.mac, ips)
self.mount(hostpath, path) return remote_tap
def mount(self, source: str, target: str) -> None: def privatedir(self, dir_path: Path) -> None:
source = os.path.abspath(source) if not str(dir_path).startswith("/"):
logging.info("mounting %s at %s", source, target) raise CoreError(f"private directory path not fully qualified: {dir_path}")
os.makedirs(target) host_path = self.host_path(dir_path, is_dir=True)
self.host_cmd(f"{MOUNT} --bind {source} {target}", cwd=self.nodedir) self.host_cmd(f"mkdir -p {host_path}")
self._mounts.append((source, target)) self.mount(host_path, dir_path)
def umount(self, target: str) -> None: def mount(self, src_path: Path, target_path: Path) -> None:
logging.info("unmounting '%s'", target) logger.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path)
self.cmd(f"mkdir -p {target_path}")
self.host_cmd(f"{MOUNT} --bind {src_path} {target_path}", cwd=self.directory)
self._mounts.append((src_path, target_path))
def umount(self, target_path: Path) -> None:
logger.info("unmounting '%s'", target_path)
try: try:
self.host_cmd(f"{UMOUNT} -l {target}", cwd=self.nodedir) self.host_cmd(f"{UMOUNT} -l {target_path}", cwd=self.directory)
except CoreCommandError: except CoreCommandError:
logging.exception("unmounting failed for %s", target) logger.exception("unmounting failed for %s", target_path)
def opennodefile(self, filename: str, mode: str = "w") -> IO: def nodefile(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
dirname, basename = os.path.split(filename) host_path = self.host_path(file_path)
if not basename: directory = host_path.parent
raise ValueError("no basename for filename: " + filename) if not directory.is_dir():
directory.mkdir(parents=True, mode=0o755)
if dirname and dirname[0] == "/": with host_path.open("w") as f:
dirname = dirname[1:]
dirname = dirname.replace("/", ".")
dirname = os.path.join(self.nodedir, dirname)
if not os.path.isdir(dirname):
os.makedirs(dirname, mode=0o755)
hostfilename = os.path.join(dirname, basename)
return open(hostfilename, mode)
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None:
with self.opennodefile(filename, "w") as f:
f.write(contents) f.write(contents)
os.chmod(f.name, mode) host_path.chmod(mode)
logging.info("created nodefile: '%s'; mode: 0%o", f.name, mode) logger.info("created nodefile: '%s'; mode: 0%o", host_path, mode)
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
return self.host_cmd(args, wait=wait) return self.host_cmd(args, wait=wait)
def addfile(self, srcname: str, filename: str) -> None: def addfile(self, src_path: str, file_path: str) -> None:
raise CoreError("physical node does not support addfile") raise CoreError("physical node does not support addfile")
@ -433,7 +411,7 @@ class Rj45Node(CoreNodeBase):
if items[1][:4] == "fe80": if items[1][:4] == "fe80":
continue continue
self.old_addrs.append((items[1], None)) self.old_addrs.append((items[1], None))
logging.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up) logger.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up)
def restorestate(self) -> None: def restorestate(self) -> None:
""" """
@ -443,7 +421,7 @@ class Rj45Node(CoreNodeBase):
:raises CoreCommandError: when there is a command exception :raises CoreCommandError: when there is a command exception
""" """
localname = self.iface.localname localname = self.iface.localname
logging.info("restoring rj45 state: %s", localname) logger.info("restoring rj45 state: %s", localname)
for addr in self.old_addrs: for addr in self.old_addrs:
self.net_client.create_address(localname, addr[0], addr[1]) self.net_client.create_address(localname, addr[0], addr[1])
if self.old_up: if self.old_up:
@ -464,10 +442,10 @@ class Rj45Node(CoreNodeBase):
def termcmdstring(self, sh: str) -> str: def termcmdstring(self, sh: str) -> str:
raise CoreError("rj45 does not support terminal commands") raise CoreError("rj45 does not support terminal commands")
def addfile(self, srcname: str, filename: str) -> None: def addfile(self, src_path: str, file_path: str) -> None:
raise CoreError("rj45 does not support addfile") raise CoreError("rj45 does not support addfile")
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: def nodefile(self, file_path: str, contents: str, mode: int = 0o644) -> None:
raise CoreError("rj45 does not support nodefile") raise CoreError("rj45 does not support nodefile")
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:

View file

@ -15,6 +15,8 @@ from core.errors import CoreError
from core.nodes.base import CoreNetworkBase, NodeBase from core.nodes.base import CoreNetworkBase, NodeBase
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
@ -109,7 +111,7 @@ class Sdt:
return False return False
self.seturl() self.seturl()
logging.info("connecting to SDT at %s://%s", self.protocol, self.address) logger.info("connecting to SDT at %s://%s", self.protocol, self.address)
if self.sock is None: if self.sock is None:
try: try:
if self.protocol.lower() == "udp": if self.protocol.lower() == "udp":
@ -119,7 +121,7 @@ class Sdt:
# 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 IOError:
logging.exception("SDT socket connect error") logger.exception("SDT socket connect error")
return False return False
if not self.initialize(): if not self.initialize():
@ -157,7 +159,7 @@ class Sdt:
try: try:
self.sock.close() self.sock.close()
except IOError: except IOError:
logging.error("error closing socket") logger.error("error closing socket")
finally: finally:
self.sock = None self.sock = None
@ -191,11 +193,11 @@ class Sdt:
try: try:
cmd = f"{cmdstr}\n".encode() cmd = f"{cmdstr}\n".encode()
logging.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 IOError:
logging.exception("SDT connection error") logger.exception("SDT connection error")
self.sock = None self.sock = None
self.connected = False self.connected = False
return False return False
@ -250,7 +252,7 @@ class Sdt:
:param node: node to add :param node: node to add
:return: nothing :return: nothing
""" """
logging.debug("sdt add node: %s - %s", node.id, node.name) logger.debug("sdt add node: %s - %s", node.id, node.name)
if not self.connect(): if not self.connect():
return return
pos = self.get_node_position(node) pos = self.get_node_position(node)
@ -262,8 +264,8 @@ class Sdt:
icon = node.icon icon = node.icon
if icon: if icon:
node_type = node.name node_type = node.name
icon = icon.replace("$CORE_DATA_DIR", CORE_DATA_DIR) icon = icon.replace("$CORE_DATA_DIR", str(CORE_DATA_DIR))
icon = icon.replace("$CORE_CONF_DIR", CORE_CONF_DIR) icon = icon.replace("$CORE_CONF_DIR", str(CORE_CONF_DIR))
self.cmd(f"sprite {node_type} image {icon}") self.cmd(f"sprite {node_type} image {icon}")
self.cmd( self.cmd(
f'node {node.id} nodeLayer "{NODE_LAYER}" ' f'node {node.id} nodeLayer "{NODE_LAYER}" '
@ -280,7 +282,7 @@ class Sdt:
:param alt: node altitude :param alt: node altitude
:return: nothing :return: nothing
""" """
logging.debug("sdt update node: %s - %s", node.id, node.name) logger.debug("sdt update node: %s - %s", node.id, node.name)
if not self.connect(): if not self.connect():
return return
@ -300,7 +302,7 @@ class Sdt:
:param node_id: node id to delete :param node_id: node id to delete
:return: nothing :return: nothing
""" """
logging.debug("sdt delete node: %s", node_id) logger.debug("sdt delete node: %s", node_id)
if not self.connect(): if not self.connect():
return return
self.cmd(f"delete node,{node_id}") self.cmd(f"delete node,{node_id}")
@ -315,7 +317,7 @@ class Sdt:
if not self.connect(): if not self.connect():
return return
node = node_data.node node = node_data.node
logging.debug("sdt handle node update: %s - %s", node.id, node.name) logger.debug("sdt handle node update: %s - %s", node.id, node.name)
if node_data.message_type == MessageFlags.DELETE: if node_data.message_type == MessageFlags.DELETE:
self.cmd(f"delete node,{node.id}") self.cmd(f"delete node,{node.id}")
else: else:
@ -356,7 +358,7 @@ class Sdt:
:param label: label for link :param label: label for link
:return: nothing :return: nothing
""" """
logging.debug("sdt add link: %s, %s, %s", node1_id, node2_id, network_id) logger.debug("sdt add link: %s, %s, %s", node1_id, node2_id, network_id)
if not self.connect(): if not self.connect():
return return
if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id):
@ -396,7 +398,7 @@ class Sdt:
:param network_id: network link is associated with, None otherwise :param network_id: network link is associated with, None otherwise
:return: nothing :return: nothing
""" """
logging.debug("sdt delete link: %s, %s, %s", node1_id, node2_id, network_id) logger.debug("sdt delete link: %s, %s, %s", node1_id, node2_id, network_id)
if not self.connect(): if not self.connect():
return return
if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id):
@ -416,7 +418,7 @@ class Sdt:
:param label: label to update :param label: label to update
:return: nothing :return: nothing
""" """
logging.debug("sdt edit link: %s, %s, %s", node1_id, node2_id, network_id) logger.debug("sdt edit link: %s, %s, %s", node1_id, node2_id, network_id)
if not self.connect(): if not self.connect():
return return
if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id):

View file

@ -4,11 +4,11 @@ Services
Services available to nodes can be put in this directory. Everything listed in Services available to nodes can be put in this directory. Everything listed in
__all__ is automatically loaded by the main core module. __all__ is automatically loaded by the main core module.
""" """
import os from pathlib import Path
from core.services.coreservices import ServiceManager from core.services.coreservices import ServiceManager
_PATH = os.path.abspath(os.path.dirname(__file__)) _PATH: Path = Path(__file__).resolve().parent
def load(): def load():

View file

@ -10,6 +10,7 @@ services.
import enum import enum
import logging import logging
import time import time
from pathlib import Path
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Dict, Dict,
@ -33,6 +34,8 @@ from core.errors import (
) )
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
@ -160,7 +163,7 @@ class ServiceShim:
cls.setvalue(service, key, values[cls.keys.index(key)]) cls.setvalue(service, key, values[cls.keys.index(key)])
except IndexError: except IndexError:
# old config does not need to have new keys # old config does not need to have new keys
logging.exception("error indexing into key") logger.exception("error indexing into key")
@classmethod @classmethod
def setvalue(cls, service: "CoreService", key: str, value: str) -> None: def setvalue(cls, service: "CoreService", key: str, value: str) -> None:
@ -227,7 +230,7 @@ class ServiceManager:
:raises ValueError: when service cannot be loaded :raises ValueError: when service cannot be loaded
""" """
name = service.name name = service.name
logging.debug("loading service: class(%s) name(%s)", service.__name__, name) logger.debug("loading service: class(%s) name(%s)", service.__name__, name)
# avoid duplicate services # avoid duplicate services
if name in cls.services: if name in cls.services:
@ -244,7 +247,7 @@ class ServiceManager:
try: try:
service.on_load() service.on_load()
except Exception as e: except Exception as e:
logging.exception("error during service(%s) on load", service.name) logger.exception("error during service(%s) on load", service.name)
raise ValueError(e) raise ValueError(e)
# make service available # make service available
@ -264,7 +267,7 @@ class ServiceManager:
return service return service
@classmethod @classmethod
def add_services(cls, path: str) -> List[str]: def add_services(cls, path: Path) -> List[str]:
""" """
Method for retrieving all CoreServices from a given path. Method for retrieving all CoreServices from a given path.
@ -276,12 +279,11 @@ class ServiceManager:
for service in services: for service in services:
if not service.name: if not service.name:
continue continue
try: try:
cls.add(service) cls.add(service)
except (CoreError, ValueError) as e: except (CoreError, ValueError) as e:
service_errors.append(service.name) service_errors.append(service.name)
logging.debug("not loading service(%s): %s", service.name, e) logger.debug("not loading service(%s): %s", service.name, e)
return service_errors return service_errors
@ -329,14 +331,14 @@ class CoreServices:
:param node_type: node type to get default services for :param node_type: node type to get default services for
:return: default services :return: default services
""" """
logging.debug("getting default services for type: %s", node_type) logger.debug("getting default services for type: %s", node_type)
results = [] results = []
defaults = self.default_services.get(node_type, []) defaults = self.default_services.get(node_type, [])
for name in defaults: for name in defaults:
logging.debug("checking for service with service manager: %s", name) logger.debug("checking for service with service manager: %s", name)
service = ServiceManager.get(name) service = ServiceManager.get(name)
if not service: if not service:
logging.warning("default service %s is unknown", name) logger.warning("default service %s is unknown", name)
else: else:
results.append(service) results.append(service)
return results return results
@ -369,7 +371,7 @@ class CoreServices:
:param service_name: name of service to set :param service_name: name of service to set
:return: nothing :return: nothing
""" """
logging.debug("setting custom service(%s) for node: %s", service_name, node_id) logger.debug("setting custom service(%s) for node: %s", service_name, node_id)
service = self.get_service(node_id, service_name) service = self.get_service(node_id, service_name)
if not service: if not service:
service_class = ServiceManager.get(service_name) service_class = ServiceManager.get(service_name)
@ -391,15 +393,15 @@ class CoreServices:
:return: nothing :return: nothing
""" """
if not services: if not services:
logging.info( logger.info(
"using default services for node(%s) type(%s)", node.name, node_type "using default services for node(%s) type(%s)", node.name, node_type
) )
services = self.default_services.get(node_type, []) services = self.default_services.get(node_type, [])
logging.info("setting services for node(%s): %s", node.name, services) logger.info("setting services for node(%s): %s", node.name, services)
for service_name in services: for service_name in services:
service = self.get_service(node.id, service_name, default_service=True) service = self.get_service(node.id, service_name, default_service=True)
if not service: if not service:
logging.warning( logger.warning(
"unknown service(%s) for node(%s)", service_name, node.name "unknown service(%s) for node(%s)", service_name, node.name
) )
continue continue
@ -457,7 +459,7 @@ class CoreServices:
raise CoreServiceBootError(*exceptions) raise CoreServiceBootError(*exceptions)
def _boot_service_path(self, node: CoreNode, boot_path: List["CoreServiceType"]): def _boot_service_path(self, node: CoreNode, boot_path: List["CoreServiceType"]):
logging.info( logger.info(
"booting node(%s) services: %s", "booting node(%s) services: %s",
node.name, node.name,
" -> ".join([x.name for x in boot_path]), " -> ".join([x.name for x in boot_path]),
@ -467,7 +469,7 @@ class CoreServices:
try: try:
self.boot_service(node, service) self.boot_service(node, service)
except Exception as e: except Exception as e:
logging.exception("exception booting service: %s", service.name) logger.exception("exception booting service: %s", service.name)
raise CoreServiceBootError(e) raise CoreServiceBootError(e)
def boot_service(self, node: CoreNode, service: "CoreServiceType") -> None: def boot_service(self, node: CoreNode, service: "CoreServiceType") -> None:
@ -479,7 +481,7 @@ class CoreServices:
:param service: service to start :param service: service to start
:return: nothing :return: nothing
""" """
logging.info( logger.info(
"starting node(%s) service(%s) validation(%s)", "starting node(%s) service(%s) validation(%s)",
node.name, node.name,
service.name, service.name,
@ -488,10 +490,11 @@ class CoreServices:
# create service directories # create service directories
for directory in service.dirs: for directory in service.dirs:
dir_path = Path(directory)
try: try:
node.privatedir(directory) node.create_dir(dir_path)
except (CoreCommandError, ValueError) as e: except (CoreCommandError, CoreError) as e:
logging.warning( logger.warning(
"error mounting private dir '%s' for service '%s': %s", "error mounting private dir '%s' for service '%s': %s",
directory, directory,
service.name, service.name,
@ -534,14 +537,14 @@ class CoreServices:
"node(%s) service(%s) failed validation" % (node.name, service.name) "node(%s) service(%s) failed validation" % (node.name, service.name)
) )
def copy_service_file(self, node: CoreNode, filename: str, cfg: str) -> bool: def copy_service_file(self, node: CoreNode, file_path: Path, cfg: str) -> bool:
""" """
Given a configured service filename and config, determine if the Given a configured service filename and config, determine if the
config references an existing file that should be copied. config references an existing file that should be copied.
Returns True for local files, False for generated. Returns True for local files, False for generated.
:param node: node to copy service for :param node: node to copy service for
:param filename: file name for a configured service :param file_path: file name for a configured service
:param cfg: configuration string :param cfg: configuration string
:return: True if successful, False otherwise :return: True if successful, False otherwise
""" """
@ -550,7 +553,7 @@ class CoreServices:
src = src.split("\n")[0] src = src.split("\n")[0]
src = utils.expand_corepath(src, node.session, node) src = utils.expand_corepath(src, node.session, node)
# TODO: glob here # TODO: glob here
node.nodefilecopy(filename, src, mode=0o644) node.copy_file(src, file_path, mode=0o644)
return True return True
return False return False
@ -562,21 +565,21 @@ class CoreServices:
:param service: service to validate :param service: service to validate
:return: service validation status :return: service validation status
""" """
logging.debug("validating node(%s) service(%s)", node.name, service.name) logger.debug("validating node(%s) service(%s)", node.name, service.name)
cmds = service.validate cmds = service.validate
if not service.custom: if not service.custom:
cmds = service.get_validate(node) cmds = service.get_validate(node)
status = 0 status = 0
for cmd in cmds: for cmd in cmds:
logging.debug("validating service(%s) using: %s", service.name, cmd) logger.debug("validating service(%s) using: %s", service.name, cmd)
try: try:
node.cmd(cmd) node.cmd(cmd)
except CoreCommandError as e: except CoreCommandError as e:
logging.debug( logger.debug(
"node(%s) service(%s) validate failed", node.name, service.name "node(%s) service(%s) validate failed", node.name, service.name
) )
logging.debug("cmd(%s): %s", e.cmd, e.output) logger.debug("cmd(%s): %s", e.cmd, e.output)
status = -1 status = -1
break break
@ -611,7 +614,7 @@ class CoreServices:
f"error stopping service {service.name}: {e.stderr}", f"error stopping service {service.name}: {e.stderr}",
node.id, node.id,
) )
logging.exception("error running stop command %s", args) logger.exception("error running stop command %s", args)
status = -1 status = -1
return status return status
@ -679,13 +682,13 @@ class CoreServices:
# retrieve custom service # retrieve custom service
service = self.get_service(node_id, service_name) service = self.get_service(node_id, service_name)
if service is None: if service is None:
logging.warning("received file name for unknown service: %s", service_name) logger.warning("received file name for unknown service: %s", service_name)
return return
# validate file being set is valid # validate file being set is valid
config_files = service.configs config_files = service.configs
if file_name not in config_files: if file_name not in config_files:
logging.warning( logger.warning(
"received unknown file(%s) for service(%s)", file_name, service_name "received unknown file(%s) for service(%s)", file_name, service_name
) )
return return
@ -713,7 +716,7 @@ class CoreServices:
try: try:
node.cmd(cmd, wait) node.cmd(cmd, wait)
except CoreCommandError: except CoreCommandError:
logging.exception("error starting command") logger.exception("error starting command")
status = -1 status = -1
return status return status
@ -729,27 +732,25 @@ class CoreServices:
config_files = service.configs config_files = service.configs
if not service.custom: if not service.custom:
config_files = service.get_configs(node) config_files = service.get_configs(node)
for file_name in config_files: for file_name in config_files:
logging.debug( file_path = Path(file_name)
logger.debug(
"generating service config custom(%s): %s", service.custom, file_name "generating service config custom(%s): %s", service.custom, file_name
) )
if service.custom: if service.custom:
cfg = service.config_data.get(file_name) cfg = service.config_data.get(file_name)
if cfg is None: if cfg is None:
cfg = service.generate_config(node, file_name) cfg = service.generate_config(node, file_name)
# cfg may have a file:/// url for copying from a file # cfg may have a file:/// url for copying from a file
try: try:
if self.copy_service_file(node, file_name, cfg): if self.copy_service_file(node, file_path, cfg):
continue continue
except IOError: except IOError:
logging.exception("error copying service file: %s", file_name) logger.exception("error copying service file: %s", file_name)
continue continue
else: else:
cfg = service.generate_config(node, file_name) cfg = service.generate_config(node, file_name)
node.create_file(file_path, cfg)
node.nodefile(file_name, cfg)
def service_reconfigure(self, node: CoreNode, service: "CoreService") -> None: def service_reconfigure(self, node: CoreNode, service: "CoreService") -> None:
""" """
@ -762,17 +763,15 @@ class CoreServices:
config_files = service.configs config_files = service.configs
if not service.custom: if not service.custom:
config_files = service.get_configs(node) config_files = service.get_configs(node)
for file_name in config_files: for file_name in config_files:
file_path = Path(file_name)
if file_name[:7] == "file:///": if file_name[:7] == "file:///":
# TODO: implement this # TODO: implement this
raise NotImplementedError raise NotImplementedError
cfg = service.config_data.get(file_name) cfg = service.config_data.get(file_name)
if cfg is None: if cfg is None:
cfg = service.generate_config(node, file_name) cfg = service.generate_config(node, file_name)
node.create_file(file_path, cfg)
node.nodefile(file_name, cfg)
class CoreService: class CoreService:

View file

@ -11,6 +11,8 @@ from core.nodes.base import CoreNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
logger = logging.getLogger(__name__)
class VPNClient(CoreService): class VPNClient(CoreService):
name: str = "VPNClient" name: str = "VPNClient"
@ -33,7 +35,7 @@ class VPNClient(CoreService):
with open(fname, "r") as f: with open(fname, "r") as f:
cfg += f.read() cfg += f.read()
except IOError: except IOError:
logging.exception( logger.exception(
"error opening VPN client configuration template (%s)", fname "error opening VPN client configuration template (%s)", fname
) )
return cfg return cfg
@ -61,7 +63,7 @@ class VPNServer(CoreService):
with open(fname, "r") as f: with open(fname, "r") as f:
cfg += f.read() cfg += f.read()
except IOError: except IOError:
logging.exception( logger.exception(
"Error opening VPN server configuration template (%s)", fname "Error opening VPN server configuration template (%s)", fname
) )
return cfg return cfg
@ -89,7 +91,7 @@ class IPsec(CoreService):
with open(fname, "r") as f: with open(fname, "r") as f:
cfg += f.read() cfg += f.read()
except IOError: except IOError:
logging.exception("Error opening IPsec configuration template (%s)", fname) logger.exception("Error opening IPsec configuration template (%s)", fname)
return cfg return cfg
@ -112,7 +114,7 @@ class Firewall(CoreService):
with open(fname, "r") as f: with open(fname, "r") as f:
cfg += f.read() cfg += f.read()
except IOError: except IOError:
logging.exception( logger.exception(
"Error opening Firewall configuration template (%s)", fname "Error opening Firewall configuration template (%s)", fname
) )
return cfg return cfg

View file

@ -236,7 +236,7 @@ max-lease-time 7200;
ddns-update-style none; ddns-update-style none;
""" """
for iface in node.get_ifaces(control=False): for iface in node.get_ifaces(control=False):
cfg += "\n".join(map(cls.subnetentry, iface.ips())) cfg += "\n".join(map(cls.subnetentry, iface.ip4s))
cfg += "\n" cfg += "\n"
return cfg return cfg
@ -246,15 +246,13 @@ ddns-update-style none;
Generate a subnet declaration block given an IPv4 prefix string Generate a subnet declaration block given an IPv4 prefix string
for inclusion in the dhcpd3 config file. for inclusion in the dhcpd3 config file.
""" """
address = str(ip.ip) if ip.size == 1:
if netaddr.valid_ipv6(address):
return "" return ""
else: # divide the address space in half
# divide the address space in half index = (ip.size - 2) / 2
index = (ip.size - 2) / 2 rangelow = ip[index]
rangelow = ip[index] rangehigh = ip[-2]
rangehigh = ip[-2] return """
return """
subnet %s netmask %s { subnet %s netmask %s {
pool { pool {
range %s %s; range %s %s;
@ -263,12 +261,12 @@ subnet %s netmask %s {
} }
} }
""" % ( """ % (
ip.ip, ip.cidr.ip,
ip.netmask, ip.netmask,
rangelow, rangelow,
rangehigh, rangehigh,
address, ip.ip,
) )
class DhcpClientService(UtilService): class DhcpClientService(UtilService):

View file

@ -15,6 +15,7 @@ import random
import shlex import shlex
import shutil import shutil
import sys import sys
import threading
from pathlib import Path from pathlib import Path
from subprocess import PIPE, STDOUT, Popen from subprocess import PIPE, STDOUT, Popen
from typing import ( from typing import (
@ -36,7 +37,10 @@ import netaddr
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.coreemu import CoreEmu
from core.emulator.session import Session from core.emulator.session import Session
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
T = TypeVar("T") T = TypeVar("T")
@ -45,12 +49,29 @@ DEVNULL = open(os.devnull, "wb")
IFACE_CONFIG_FACTOR: int = 1000 IFACE_CONFIG_FACTOR: int = 1000
def execute_script(coreemu: "CoreEmu", file_path: Path, args: str) -> None:
"""
Provides utility function to execute a python script in context of the
provide coreemu instance.
:param coreemu: coreemu to provide to script
:param file_path: python script to execute
:param args: args to provide script
:return: nothing
"""
sys.argv = shlex.split(args)
thread = threading.Thread(
target=execute_file, args=(file_path, {"coreemu": coreemu}), daemon=True
)
thread.start()
thread.join()
def execute_file( def execute_file(
path: str, exec_globals: Dict[str, str] = None, exec_locals: Dict[str, str] = None path: Path, exec_globals: Dict[str, str] = None, exec_locals: Dict[str, str] = None
) -> None: ) -> None:
""" """
Provides an alternative way to run execfile to be compatible for Provides a way to execute a file.
both python2/3.
:param path: path of file to execute :param path: path of file to execute
:param exec_globals: globals values to pass to execution :param exec_globals: globals values to pass to execution
@ -59,10 +80,10 @@ def execute_file(
""" """
if exec_globals is None: if exec_globals is None:
exec_globals = {} exec_globals = {}
exec_globals.update({"__file__": path, "__name__": "__main__"}) exec_globals.update({"__file__": str(path), "__name__": "__main__"})
with open(path, "rb") as f: with path.open("rb") as f:
data = compile(f.read(), path, "exec") data = compile(f.read(), path, "exec")
exec(data, exec_globals, exec_locals) exec(data, exec_globals, exec_locals)
def hashkey(value: Union[str, int]) -> int: def hashkey(value: Union[str, int]) -> int:
@ -92,24 +113,19 @@ def _detach_init() -> None:
os.setsid() os.setsid()
def _valid_module(path: str, file_name: str) -> bool: def _valid_module(path: Path) -> bool:
""" """
Check if file is a valid python module. Check if file is a valid python module.
:param path: path to file :param path: path to file
:param file_name: file name to check
:return: True if a valid python module file, False otherwise :return: True if a valid python module file, False otherwise
""" """
file_path = os.path.join(path, file_name) if not path.is_file():
if not os.path.isfile(file_path):
return False return False
if path.name.startswith("_"):
if file_name.startswith("_"):
return False return False
if not path.suffix == ".py":
if not file_name.endswith(".py"):
return False return False
return True return True
@ -124,13 +140,10 @@ def _is_class(module: Any, member: Type, clazz: Type) -> bool:
""" """
if not inspect.isclass(member): if not inspect.isclass(member):
return False return False
if not issubclass(member, clazz): if not issubclass(member, clazz):
return False return False
if member.__module__ != module.__name__: if member.__module__ != module.__name__:
return False return False
return True return True
@ -196,7 +209,7 @@ def mute_detach(args: str, **kwargs: Dict[str, Any]) -> int:
def cmd( def cmd(
args: str, args: str,
env: Dict[str, str] = None, env: Dict[str, str] = None,
cwd: str = None, cwd: Path = None,
wait: bool = True, wait: bool = True,
shell: bool = False, shell: bool = False,
) -> str: ) -> str:
@ -213,7 +226,7 @@ def cmd(
:raises CoreCommandError: when there is a non-zero exit status or the file to :raises CoreCommandError: when there is a non-zero exit status or the file to
execute is not found execute is not found
""" """
logging.debug("command cwd(%s) wait(%s): %s", cwd, wait, args) logger.debug("command cwd(%s) wait(%s): %s", cwd, wait, args)
if shell is False: if shell is False:
args = shlex.split(args) args = shlex.split(args)
try: try:
@ -230,7 +243,7 @@ def cmd(
else: else:
return "" return ""
except OSError as e: except OSError as e:
logging.error("cmd error: %s", e.strerror) logger.error("cmd error: %s", e.strerror)
raise CoreCommandError(1, args, "", e.strerror) raise CoreCommandError(1, args, "", e.strerror)
@ -282,7 +295,7 @@ def file_demunge(pathname: str, header: str) -> None:
def expand_corepath( def expand_corepath(
pathname: str, session: "Session" = None, node: "CoreNode" = None pathname: str, session: "Session" = None, node: "CoreNode" = None
) -> str: ) -> Path:
""" """
Expand a file path given session information. Expand a file path given session information.
@ -294,14 +307,12 @@ def expand_corepath(
if session is not None: if session is not None:
pathname = pathname.replace("~", f"/home/{session.user}") pathname = pathname.replace("~", f"/home/{session.user}")
pathname = pathname.replace("%SESSION%", str(session.id)) pathname = pathname.replace("%SESSION%", str(session.id))
pathname = pathname.replace("%SESSION_DIR%", session.session_dir) pathname = pathname.replace("%SESSION_DIR%", str(session.directory))
pathname = pathname.replace("%SESSION_USER%", session.user) pathname = pathname.replace("%SESSION_USER%", session.user)
if node is not None: if node is not None:
pathname = pathname.replace("%NODE%", str(node.id)) pathname = pathname.replace("%NODE%", str(node.id))
pathname = pathname.replace("%NODENAME%", node.name) pathname = pathname.replace("%NODENAME%", node.name)
return Path(pathname)
return pathname
def sysctl_devname(devname: str) -> Optional[str]: def sysctl_devname(devname: str) -> Optional[str]:
@ -334,10 +345,25 @@ def load_config(file_path: Path, d: Dict[str, str]) -> None:
key, value = line.split("=", 1) key, value = line.split("=", 1)
d[key] = value.strip() d[key] = value.strip()
except ValueError: except ValueError:
logging.exception("error reading file to dict: %s", file_path) logger.exception("error reading file to dict: %s", file_path)
def load_classes(path: str, clazz: Generic[T]) -> T: def load_module(import_statement: str, clazz: Generic[T]) -> List[T]:
classes = []
try:
module = importlib.import_module(import_statement)
members = inspect.getmembers(module, lambda x: _is_class(module, x, clazz))
for member in members:
valid_class = member[1]
classes.append(valid_class)
except Exception:
logger.exception(
"unexpected error during import, skipping: %s", import_statement
)
return classes
def load_classes(path: Path, clazz: Generic[T]) -> List[T]:
""" """
Dynamically load classes for use within CORE. Dynamically load classes for use within CORE.
@ -346,50 +372,36 @@ def load_classes(path: str, clazz: Generic[T]) -> T:
:return: list of classes loaded :return: list of classes loaded
""" """
# validate path exists # validate path exists
logging.debug("attempting to load modules from path: %s", path) logger.debug("attempting to load modules from path: %s", path)
if not os.path.isdir(path): if not path.is_dir():
logging.warning("invalid custom module directory specified" ": %s", path) logger.warning("invalid custom module directory specified" ": %s", path)
# check if path is in sys.path # check if path is in sys.path
parent_path = os.path.dirname(path) parent = str(path.parent)
if parent_path not in sys.path: if parent not in sys.path:
logging.debug("adding parent path to allow imports: %s", parent_path) logger.debug("adding parent path to allow imports: %s", parent)
sys.path.append(parent_path) sys.path.append(parent)
# retrieve potential service modules, and filter out invalid modules
base_module = os.path.basename(path)
module_names = os.listdir(path)
module_names = filter(lambda x: _valid_module(path, x), module_names)
module_names = map(lambda x: x[:-3], module_names)
# import and add all service modules in the path # import and add all service modules in the path
classes = [] classes = []
for module_name in module_names: for p in path.iterdir():
import_statement = f"{base_module}.{module_name}" if not _valid_module(p):
logging.debug("importing custom module: %s", import_statement) continue
try: import_statement = f"{path.name}.{p.stem}"
module = importlib.import_module(import_statement) logger.debug("importing custom module: %s", import_statement)
members = inspect.getmembers(module, lambda x: _is_class(module, x, clazz)) loaded = load_module(import_statement, clazz)
for member in members: classes.extend(loaded)
valid_class = member[1]
classes.append(valid_class)
except Exception:
logging.exception(
"unexpected error during import, skipping: %s", import_statement
)
return classes return classes
def load_logging_config(config_path: str) -> None: def load_logging_config(config_path: Path) -> None:
""" """
Load CORE logging configuration file. Load CORE logging configuration file.
:param config_path: path to logging config file :param config_path: path to logging config file
:return: nothing :return: nothing
""" """
with open(config_path, "r") as log_config_file: with config_path.open("r") as f:
log_config = json.load(log_config_file) log_config = json.load(f)
logging.config.dictConfig(log_config) logging.config.dictConfig(log_config)
def threadpool( def threadpool(
@ -415,7 +427,7 @@ def threadpool(
result = future.result() result = future.result()
results.append(result) results.append(result)
except Exception as e: except Exception as e:
logging.exception("thread pool exception") logger.exception("thread pool exception")
exceptions.append(e) exceptions.append(e)
return results, exceptions return results, exceptions

View file

@ -1,4 +1,5 @@
import logging import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Type, TypeVar from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Type, TypeVar
from lxml import etree from lxml import etree
@ -16,6 +17,8 @@ from core.nodes.lxd import LxcNode
from core.nodes.network import CtrlNet, GreTapBridge, WlanNode from core.nodes.network import CtrlNet, GreTapBridge, WlanNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emane.emanemodel import EmaneModel from core.emane.emanemodel import EmaneModel
from core.emulator.session import Session from core.emulator.session import Session
@ -25,7 +28,7 @@ T = TypeVar("T")
def write_xml_file( def write_xml_file(
xml_element: etree.Element, file_path: str, doctype: str = None xml_element: etree.Element, file_path: Path, doctype: str = None
) -> None: ) -> None:
xml_data = etree.tostring( xml_data = etree.tostring(
xml_element, xml_element,
@ -34,8 +37,8 @@ def write_xml_file(
encoding="UTF-8", encoding="UTF-8",
doctype=doctype, doctype=doctype,
) )
with open(file_path, "wb") as xml_file: with file_path.open("wb") as f:
xml_file.write(xml_data) f.write(xml_data)
def get_type(element: etree.Element, name: str, _type: Generic[T]) -> Optional[T]: def get_type(element: etree.Element, name: str, _type: Generic[T]) -> Optional[T]:
@ -77,20 +80,6 @@ def create_iface_data(iface_element: etree.Element) -> InterfaceData:
) )
def create_emane_config(session: "Session") -> etree.Element:
emane_configuration = etree.Element("emane_global_configuration")
config = session.emane.get_configs()
emulator_element = etree.SubElement(emane_configuration, "emulator")
for emulator_config in session.emane.emane_config.emulator_config:
value = config[emulator_config.id]
add_configuration(emulator_element, emulator_config.id, value)
core_element = etree.SubElement(emane_configuration, "core")
for core_config in session.emane.emane_config.core_config:
value = config[core_config.id]
add_configuration(core_element, core_config.id, value)
return emane_configuration
def create_emane_model_config( def create_emane_model_config(
node_id: int, node_id: int,
model: "EmaneModelType", model: "EmaneModelType",
@ -101,22 +90,22 @@ def create_emane_model_config(
add_attribute(emane_element, "node", node_id) add_attribute(emane_element, "node", node_id)
add_attribute(emane_element, "iface", iface_id) add_attribute(emane_element, "iface", iface_id)
add_attribute(emane_element, "model", model.name) add_attribute(emane_element, "model", model.name)
platform_element = etree.SubElement(emane_element, "platform")
for platform_config in model.platform_config:
value = config[platform_config.id]
add_configuration(platform_element, platform_config.id, value)
mac_element = etree.SubElement(emane_element, "mac") mac_element = etree.SubElement(emane_element, "mac")
for mac_config in model.mac_config: for mac_config in model.mac_config:
value = config[mac_config.id] value = config[mac_config.id]
add_configuration(mac_element, mac_config.id, value) add_configuration(mac_element, mac_config.id, value)
phy_element = etree.SubElement(emane_element, "phy") phy_element = etree.SubElement(emane_element, "phy")
for phy_config in model.phy_config: for phy_config in model.phy_config:
value = config[phy_config.id] value = config[phy_config.id]
add_configuration(phy_element, phy_config.id, value) add_configuration(phy_element, phy_config.id, value)
external_element = etree.SubElement(emane_element, "external") external_element = etree.SubElement(emane_element, "external")
for external_config in model.external_config: for external_config in model.external_config:
value = config[external_config.id] value = config[external_config.id]
add_configuration(external_element, external_config.id, value) add_configuration(external_element, external_config.id, value)
return emane_element return emane_element
@ -293,13 +282,12 @@ class CoreXmlWriter:
self.write_session_metadata() self.write_session_metadata()
self.write_default_services() self.write_default_services()
def write(self, file_name: str) -> None: def write(self, path: Path) -> None:
self.scenario.set("name", file_name) self.scenario.set("name", str(path))
# write out generated xml # write out generated xml
xml_tree = etree.ElementTree(self.scenario) xml_tree = etree.ElementTree(self.scenario)
xml_tree.write( xml_tree.write(
file_name, xml_declaration=True, pretty_print=True, encoding="UTF-8" str(path), xml_declaration=True, pretty_print=True, encoding="UTF-8"
) )
def write_session_origin(self) -> None: def write_session_origin(self) -> None:
@ -374,22 +362,16 @@ class CoreXmlWriter:
self.scenario.append(metadata_elements) self.scenario.append(metadata_elements)
def write_emane_configs(self) -> None: def write_emane_configs(self) -> None:
emane_global_configuration = create_emane_config(self.session)
self.scenario.append(emane_global_configuration)
emane_configurations = etree.Element("emane_configurations") emane_configurations = etree.Element("emane_configurations")
for node_id in self.session.emane.nodes(): for node_id, model_configs in self.session.emane.node_configs.items():
all_configs = self.session.emane.get_all_configs(node_id)
if not all_configs:
continue
node_id, iface_id = utils.parse_iface_config_id(node_id) node_id, iface_id = utils.parse_iface_config_id(node_id)
for model_name in all_configs: for model_name, config in model_configs.items():
config = all_configs[model_name] logger.debug(
logging.debug(
"writing emane config node(%s) model(%s)", node_id, model_name "writing emane config node(%s) model(%s)", node_id, model_name
) )
model = self.session.emane.models[model_name] model_class = self.session.emane.get_model(model_name)
emane_configuration = create_emane_model_config( emane_configuration = create_emane_model_config(
node_id, model, config, iface_id node_id, model_class, config, iface_id
) )
emane_configurations.append(emane_configuration) emane_configurations.append(emane_configuration)
if emane_configurations.getchildren(): if emane_configurations.getchildren():
@ -404,7 +386,7 @@ class CoreXmlWriter:
for model_name in all_configs: for model_name in all_configs:
config = all_configs[model_name] config = all_configs[model_name]
logging.debug( logger.debug(
"writing mobility config node(%s) model(%s)", node_id, model_name "writing mobility config node(%s) model(%s)", node_id, model_name
) )
mobility_configuration = etree.SubElement( mobility_configuration = etree.SubElement(
@ -580,8 +562,8 @@ class CoreXmlReader:
self.session: "Session" = session self.session: "Session" = session
self.scenario: Optional[etree.ElementTree] = None self.scenario: Optional[etree.ElementTree] = None
def read(self, file_name: str) -> None: def read(self, file_path: Path) -> None:
xml_tree = etree.parse(file_name) xml_tree = etree.parse(str(file_path))
self.scenario = xml_tree.getroot() self.scenario = xml_tree.getroot()
# read xml session content # read xml session content
@ -593,7 +575,6 @@ class CoreXmlReader:
self.read_session_origin() self.read_session_origin()
self.read_service_configs() self.read_service_configs()
self.read_mobility_configs() self.read_mobility_configs()
self.read_emane_global_config()
self.read_nodes() self.read_nodes()
self.read_links() self.read_links()
self.read_emane_configs() self.read_emane_configs()
@ -609,7 +590,7 @@ class CoreXmlReader:
services = [] services = []
for service in node.iterchildren(): for service in node.iterchildren():
services.append(service.get("name")) services.append(service.get("name"))
logging.info( logger.info(
"reading default services for nodes(%s): %s", node_type, services "reading default services for nodes(%s): %s", node_type, services
) )
self.session.services.default_services[node_type] = services self.session.services.default_services[node_type] = services
@ -624,7 +605,7 @@ class CoreXmlReader:
name = data.get("name") name = data.get("name")
value = data.get("value") value = data.get("value")
configs[name] = value configs[name] = value
logging.info("reading session metadata: %s", configs) logger.info("reading session metadata: %s", configs)
self.session.metadata = configs self.session.metadata = configs
def read_session_options(self) -> None: def read_session_options(self) -> None:
@ -636,7 +617,7 @@ class CoreXmlReader:
name = configuration.get("name") name = configuration.get("name")
value = configuration.get("value") value = configuration.get("value")
xml_config[name] = value xml_config[name] = value
logging.info("reading session options: %s", xml_config) logger.info("reading session options: %s", xml_config)
config = self.session.options.get_configs() config = self.session.options.get_configs()
config.update(xml_config) config.update(xml_config)
@ -650,7 +631,7 @@ class CoreXmlReader:
state = get_int(hook, "state") state = get_int(hook, "state")
state = EventTypes(state) state = EventTypes(state)
data = hook.text data = hook.text
logging.info("reading hook: state(%s) name(%s)", state, name) logger.info("reading hook: state(%s) name(%s)", state, name)
self.session.add_hook(state, name, data) self.session.add_hook(state, name, data)
def read_servers(self) -> None: def read_servers(self) -> None:
@ -660,7 +641,7 @@ class CoreXmlReader:
for server in servers.iterchildren(): for server in servers.iterchildren():
name = server.get("name") name = server.get("name")
address = server.get("address") address = server.get("address")
logging.info("reading server: name(%s) address(%s)", name, address) logger.info("reading server: name(%s) address(%s)", name, address)
self.session.distributed.add_server(name, address) self.session.distributed.add_server(name, address)
def read_session_origin(self) -> None: def read_session_origin(self) -> None:
@ -672,19 +653,19 @@ class CoreXmlReader:
lon = get_float(session_origin, "lon") lon = get_float(session_origin, "lon")
alt = get_float(session_origin, "alt") alt = get_float(session_origin, "alt")
if all([lat, lon, alt]): if all([lat, lon, alt]):
logging.info("reading session reference geo: %s, %s, %s", lat, lon, alt) logger.info("reading session reference geo: %s, %s, %s", lat, lon, alt)
self.session.location.setrefgeo(lat, lon, alt) self.session.location.setrefgeo(lat, lon, alt)
scale = get_float(session_origin, "scale") scale = get_float(session_origin, "scale")
if scale: if scale:
logging.info("reading session reference scale: %s", scale) logger.info("reading session reference scale: %s", scale)
self.session.location.refscale = scale self.session.location.refscale = scale
x = get_float(session_origin, "x") x = get_float(session_origin, "x")
y = get_float(session_origin, "y") y = get_float(session_origin, "y")
z = get_float(session_origin, "z") z = get_float(session_origin, "z")
if all([x, y]): if all([x, y]):
logging.info("reading session reference xyz: %s, %s, %s", x, y, z) logger.info("reading session reference xyz: %s, %s, %s", x, y, z)
self.session.location.refxyz = (x, y, z) self.session.location.refxyz = (x, y, z)
def read_service_configs(self) -> None: def read_service_configs(self) -> None:
@ -695,7 +676,7 @@ class CoreXmlReader:
for service_configuration in service_configurations.iterchildren(): for service_configuration in service_configurations.iterchildren():
node_id = get_int(service_configuration, "node") node_id = get_int(service_configuration, "node")
service_name = service_configuration.get("name") service_name = service_configuration.get("name")
logging.info( logger.info(
"reading custom service(%s) for node(%s)", service_name, node_id "reading custom service(%s) for node(%s)", service_name, node_id
) )
self.session.services.set_service(node_id, service_name) self.session.services.set_service(node_id, service_name)
@ -731,28 +712,10 @@ class CoreXmlReader:
files.add(name) files.add(name)
service.configs = tuple(files) service.configs = tuple(files)
def read_emane_global_config(self) -> None:
emane_global_configuration = self.scenario.find("emane_global_configuration")
if emane_global_configuration is None:
return
emulator_configuration = emane_global_configuration.find("emulator")
configs = {}
for config in emulator_configuration.iterchildren():
name = config.get("name")
value = config.get("value")
configs[name] = value
core_configuration = emane_global_configuration.find("core")
for config in core_configuration.iterchildren():
name = config.get("name")
value = config.get("value")
configs[name] = value
self.session.emane.set_configs(config=configs)
def read_emane_configs(self) -> None: def read_emane_configs(self) -> None:
emane_configurations = self.scenario.find("emane_configurations") emane_configurations = self.scenario.find("emane_configurations")
if emane_configurations is None: if emane_configurations is None:
return return
for emane_configuration in emane_configurations.iterchildren(): for emane_configuration in emane_configurations.iterchildren():
node_id = get_int(emane_configuration, "node") node_id = get_int(emane_configuration, "node")
iface_id = get_int(emane_configuration, "iface") iface_id = get_int(emane_configuration, "iface")
@ -763,38 +726,39 @@ class CoreXmlReader:
node = self.session.nodes.get(node_id) node = self.session.nodes.get(node_id)
if not node: if not node:
raise CoreXmlError(f"node for emane config doesn't exist: {node_id}") raise CoreXmlError(f"node for emane config doesn't exist: {node_id}")
model = self.session.emane.models.get(model_name) self.session.emane.get_model(model_name)
if not model:
raise CoreXmlError(f"invalid emane model: {model_name}")
if iface_id is not None and iface_id not in node.ifaces: if iface_id is not None and iface_id not in node.ifaces:
raise CoreXmlError( raise CoreXmlError(
f"invalid interface id({iface_id}) for node({node.name})" f"invalid interface id({iface_id}) for node({node.name})"
) )
# read and set emane model configuration # read and set emane model configuration
platform_configuration = emane_configuration.find("platform")
for config in platform_configuration.iterchildren():
name = config.get("name")
value = config.get("value")
configs[name] = value
mac_configuration = emane_configuration.find("mac") mac_configuration = emane_configuration.find("mac")
for config in mac_configuration.iterchildren(): for config in mac_configuration.iterchildren():
name = config.get("name") name = config.get("name")
value = config.get("value") value = config.get("value")
configs[name] = value configs[name] = value
phy_configuration = emane_configuration.find("phy") phy_configuration = emane_configuration.find("phy")
for config in phy_configuration.iterchildren(): for config in phy_configuration.iterchildren():
name = config.get("name") name = config.get("name")
value = config.get("value") value = config.get("value")
configs[name] = value configs[name] = value
external_configuration = emane_configuration.find("external") external_configuration = emane_configuration.find("external")
for config in external_configuration.iterchildren(): for config in external_configuration.iterchildren():
name = config.get("name") name = config.get("name")
value = config.get("value") value = config.get("value")
configs[name] = value configs[name] = value
logging.info( logger.info(
"reading emane configuration node(%s) model(%s)", node_id, model_name "reading emane configuration node(%s) model(%s)", node_id, model_name
) )
node_id = utils.iface_config_id(node_id, iface_id) node_id = utils.iface_config_id(node_id, iface_id)
self.session.emane.set_model_config(node_id, model_name, configs) self.session.emane.set_config(node_id, model_name, configs)
def read_mobility_configs(self) -> None: def read_mobility_configs(self) -> None:
mobility_configurations = self.scenario.find("mobility_configurations") mobility_configurations = self.scenario.find("mobility_configurations")
@ -811,7 +775,7 @@ class CoreXmlReader:
value = config.get("value") value = config.get("value")
configs[name] = value configs[name] = value
logging.info( logger.info(
"reading mobility configuration node(%s) model(%s)", node_id, model_name "reading mobility configuration node(%s) model(%s)", node_id, model_name
) )
self.session.mobility.set_model_config(node_id, model_name, configs) self.session.mobility.set_model_config(node_id, model_name, configs)
@ -868,7 +832,7 @@ class CoreXmlReader:
if all([lat, lon, alt]): if all([lat, lon, alt]):
options.set_location(lat, lon, alt) options.set_location(lat, lon, alt)
logging.info("reading node id(%s) model(%s) name(%s)", node_id, model, name) logger.info("reading node id(%s) model(%s) name(%s)", node_id, model, name)
self.session.add_node(_class, node_id, options) self.session.add_node(_class, node_id, options)
def read_network(self, network_element: etree.Element) -> None: def read_network(self, network_element: etree.Element) -> None:
@ -896,7 +860,7 @@ class CoreXmlReader:
if all([lat, lon, alt]): if all([lat, lon, alt]):
options.set_location(lat, lon, alt) options.set_location(lat, lon, alt)
logging.info( logger.info(
"reading node id(%s) node_type(%s) name(%s)", node_id, node_type, name "reading node id(%s) node_type(%s) name(%s)", node_id, node_type, name
) )
self.session.add_node(_class, node_id, options) self.session.add_node(_class, node_id, options)
@ -926,7 +890,7 @@ class CoreXmlReader:
for template_element in templates_element.iterchildren(): for template_element in templates_element.iterchildren():
name = template_element.get("name") name = template_element.get("name")
template = template_element.text template = template_element.text
logging.info( logger.info(
"loading xml template(%s): %s", type(template), template "loading xml template(%s): %s", type(template), template
) )
service.set_template(name, template) service.set_template(name, template)
@ -978,12 +942,12 @@ class CoreXmlReader:
options.buffer = get_int(options_element, "buffer") options.buffer = get_int(options_element, "buffer")
if options.unidirectional == 1 and node_set in node_sets: if options.unidirectional == 1 and node_set in node_sets:
logging.info("updating link node1(%s) node2(%s)", node1_id, node2_id) logger.info("updating link node1(%s) node2(%s)", node1_id, node2_id)
self.session.update_link( self.session.update_link(
node1_id, node2_id, iface1_data.id, iface2_data.id, options node1_id, node2_id, iface1_data.id, iface2_data.id, options
) )
else: else:
logging.info("adding link node1(%s) node2(%s)", node1_id, node2_id) logger.info("adding link node1(%s) node2(%s)", node1_id, node2_id)
self.session.add_link( self.session.add_link(
node1_id, node2_id, iface1_data, iface2_data, options node1_id, node2_id, iface1_data, iface2_data, options
) )

View file

@ -1,5 +1,5 @@
import logging import logging
import os from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
@ -12,11 +12,11 @@ from core.emulator.distributed import DistributedServer
from core.errors import CoreError from core.errors import CoreError
from core.nodes.base import CoreNode, CoreNodeBase from core.nodes.base import CoreNode, CoreNodeBase
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.network import CtrlNet
from core.xml import corexml from core.xml import corexml
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emane.emanemanager import EmaneManager, StartData
from core.emane.emanemodel import EmaneModel from core.emane.emanemodel import EmaneModel
_MAC_PREFIX = "02:02" _MAC_PREFIX = "02:02"
@ -47,14 +47,14 @@ def _value_to_params(value: str) -> Optional[Tuple[str]]:
return None return None
return values return values
except SyntaxError: except SyntaxError:
logging.exception("error in value string to param list") logger.exception("error in value string to param list")
return None return None
def create_file( def create_file(
xml_element: etree.Element, xml_element: etree.Element,
doc_name: str, doc_name: str,
file_path: str, file_path: Path,
server: DistributedServer = None, server: DistributedServer = None,
) -> None: ) -> None:
""" """
@ -71,10 +71,11 @@ def create_file(
) )
if server: if server:
temp = NamedTemporaryFile(delete=False) temp = NamedTemporaryFile(delete=False)
corexml.write_xml_file(xml_element, temp.name, doctype=doctype) temp_path = Path(temp.name)
corexml.write_xml_file(xml_element, temp_path, doctype=doctype)
temp.close() temp.close()
server.remote_put(temp.name, file_path) server.remote_put(temp_path, file_path)
os.unlink(temp.name) temp_path.unlink()
else: else:
corexml.write_xml_file(xml_element, file_path, doctype=doctype) corexml.write_xml_file(xml_element, file_path, doctype=doctype)
@ -92,9 +93,9 @@ def create_node_file(
:return: :return:
""" """
if isinstance(node, CoreNode): if isinstance(node, CoreNode):
file_path = os.path.join(node.nodedir, file_name) file_path = node.directory / file_name
else: else:
file_path = os.path.join(node.session.session_dir, file_name) file_path = node.session.directory / file_name
create_file(xml_element, doc_name, file_path, node.server) create_file(xml_element, doc_name, file_path, node.server)
@ -143,74 +144,67 @@ def add_configurations(
def build_platform_xml( def build_platform_xml(
emane_manager: "EmaneManager", control_net: CtrlNet, data: "StartData" nem_id: int,
nem_port: int,
emane_net: EmaneNet,
iface: CoreInterface,
config: Dict[str, str],
) -> None: ) -> None:
""" """
Create platform xml for a specific node. Create platform xml for a nem/interface.
:param emane_manager: emane manager with emane :param nem_id: nem id for current node/interface
configurations :param nem_port: control port to configure for emane
:param control_net: control net node for this emane :param emane_net: emane network associate with node and interface
network :param iface: node interface to create platform xml for
:param data: start data for a node connected to emane and associated interfaces :param config: emane configuration for interface
:return: the next nem id that can be used for creating platform xml files :return: nothing
""" """
# create top level platform element # create top level platform element
transport_configs = {"otamanagerdevice", "eventservicedevice"}
platform_element = etree.Element("platform") platform_element = etree.Element("platform")
for configuration in emane_manager.emane_config.emulator_config: for configuration in emane_net.model.platform_config:
name = configuration.id name = configuration.id
if not isinstance(data.node, CoreNode) and name in transport_configs: value = config[configuration.id]
value = control_net.brname
else:
value = emane_manager.get_config(name)
add_param(platform_element, name, value) add_param(platform_element, name, value)
add_param(
platform_element, emane_net.model.platform_controlport, f"0.0.0.0:{nem_port}"
)
# create nem xml entries for all interfaces # build nem xml
for iface in data.ifaces: nem_definition = nem_file_name(iface)
emane_net = iface.net nem_element = etree.Element(
if not isinstance(emane_net, EmaneNet): "nem", id=str(nem_id), name=iface.localname, definition=nem_definition
raise CoreError( )
f"emane interface not connected to emane net: {emane_net.name}"
)
nem_id = emane_manager.next_nem_id()
emane_manager.set_nem(nem_id, iface)
emane_manager.write_nem(iface, nem_id)
config = emane_manager.get_iface_config(emane_net, iface)
emane_net.model.build_xml_files(config, iface)
# build nem xml # create model based xml files
nem_definition = nem_file_name(iface) emane_net.model.build_xml_files(config, iface)
nem_element = etree.Element(
"nem", id=str(nem_id), name=iface.localname, definition=nem_definition
)
# check if this is an external transport # check if this is an external transport
if is_external(config): if is_external(config):
nem_element.set("transport", "external") nem_element.set("transport", "external")
platform_endpoint = "platformendpoint" platform_endpoint = "platformendpoint"
add_param(nem_element, platform_endpoint, config[platform_endpoint]) add_param(nem_element, platform_endpoint, config[platform_endpoint])
transport_endpoint = "transportendpoint" transport_endpoint = "transportendpoint"
add_param(nem_element, transport_endpoint, config[transport_endpoint]) add_param(nem_element, transport_endpoint, config[transport_endpoint])
# define transport element # define transport element
transport_name = transport_file_name(iface) transport_name = transport_file_name(iface)
transport_element = etree.SubElement( transport_element = etree.SubElement(
nem_element, "transport", definition=transport_name nem_element, "transport", definition=transport_name
) )
add_param(transport_element, "device", iface.name) add_param(transport_element, "device", iface.name)
# add nem element to platform element # add nem element to platform element
platform_element.append(nem_element) platform_element.append(nem_element)
# generate and assign interface mac address based on nem id # generate and assign interface mac address based on nem id
mac = _MAC_PREFIX + ":00:00:" mac = _MAC_PREFIX + ":00:00:"
mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}"
iface.set_mac(mac) iface.set_mac(mac)
doc_name = "platform" doc_name = "platform"
file_name = f"{data.node.name}-platform.xml" file_name = platform_file_name(iface)
create_node_file(data.node, platform_element, doc_name, file_name) create_node_file(iface.node, platform_element, doc_name, file_name)
def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None: def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None:
@ -316,7 +310,7 @@ def create_event_service_xml(
group: str, group: str,
port: str, port: str,
device: str, device: str,
file_directory: str, file_directory: Path,
server: DistributedServer = None, server: DistributedServer = None,
) -> None: ) -> None:
""" """
@ -340,8 +334,7 @@ def create_event_service_xml(
): ):
sub_element = etree.SubElement(event_element, name) sub_element = etree.SubElement(event_element, name)
sub_element.text = value sub_element.text = value
file_name = "libemaneeventservice.xml" file_path = file_directory / "libemaneeventservice.xml"
file_path = os.path.join(file_directory, file_name)
create_file(event_element, "emaneeventmsgsvc", file_path, server) create_file(event_element, "emaneeventmsgsvc", file_path, server)
@ -394,3 +387,7 @@ def phy_file_name(iface: CoreInterface) -> str:
:return: phy xml file name :return: phy xml file name
""" """
return f"{iface.name}-phy.xml" return f"{iface.name}-phy.xml"
def platform_file_name(iface: CoreInterface) -> str:
return f"{iface.name}-platform.xml"

View file

@ -13,8 +13,21 @@
"format": "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" "format": "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s"
} }
}, },
"root": { "loggers": {
"level": "INFO", "": {
"handlers": ["console"] "level": "WARNING",
} "handlers": ["console"],
"propagate": false
},
"core": {
"level": "INFO",
"handlers": ["console"],
"propagate": false
},
"__main__": {
"level": "INFO",
"handlers": ["console"],
"propagate": false
}
}
} }

View file

@ -1,5 +0,0 @@
{
"bridge": "none",
"iptables": false
}

View file

@ -2,7 +2,7 @@ import argparse
import logging import logging
from core.api.grpc import client from core.api.grpc import client
from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState from core.api.grpc.wrappers import NodeType, Position, Server
def log_event(event): def log_event(event):
@ -10,62 +10,39 @@ def log_event(event):
def main(args): def main(args):
# helper to create interfaces
interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16")
# create grpc client and connect
core = client.CoreGrpcClient() core = client.CoreGrpcClient()
core.connect()
with core.context_connect(): # create session
# create session session = core.create_session()
response = core.create_session()
session_id = response.session_id
logging.info("created session: %s", response)
# add distributed server # add distributed server
server_name = "core2" server = Server(name="core2", host=args.server)
response = core.add_session_server(session_id, server_name, args.server) session.servers.append(server)
logging.info("added session server: %s", response)
# handle events session may broadcast # handle events session may broadcast
core.events(session_id, log_event) core.events(session.id, log_event)
# change session state # create switch node
response = core.set_session_state(session_id, SessionState.CONFIGURATION) position = Position(x=150, y=100)
logging.info("set session state: %s", response) switch = session.add_node(1, _type=NodeType.SWITCH, position=position)
position = Position(x=100, y=50)
node1 = session.add_node(2, position=position)
position = Position(x=200, y=50)
node2 = session.add_node(3, position=position, server=server.name)
# create switch node # create links
switch = Node(type=NodeType.SWITCH) iface1 = interface_helper.create_iface(node1.id, 0)
response = core.add_node(session_id, switch) session.add_link(node1=node1, node2=switch, iface1=iface1)
logging.info("created switch: %s", response) iface1 = interface_helper.create_iface(node2.id, 0)
switch_id = response.node_id session.add_link(node1=node2, node2=switch, iface1=iface1)
# helper to create interfaces # start session
interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") core.start_session(session)
# create node one
position = Position(x=100, y=50)
node = Node(position=position)
response = core.add_node(session_id, node)
logging.info("created node one: %s", response)
node1_id = response.node_id
# create link
interface1 = interface_helper.create_iface(node1_id, 0)
response = core.add_link(session_id, node1_id, switch_id, interface1)
logging.info("created link from node one to switch: %s", response)
# create node two
position = Position(x=200, y=50)
node = Node(position=position, server=server_name)
response = core.add_node(session_id, node)
logging.info("created node two: %s", response)
node2_id = response.node_id
# create link
interface1 = interface_helper.create_iface(node2_id, 0)
response = core.add_link(session_id, node2_id, switch_id, interface1)
logging.info("created link from node two to switch: %s", response)
# change session state
response = core.set_session_state(session_id, SessionState.INSTANTIATION)
logging.info("set session state: %s", response)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,7 +1,7 @@
# required imports # required imports
from core.api.grpc import client from core.api.grpc import client
from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState from core.api.grpc.wrappers import NodeType, Position
from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
# interface helper # interface helper
iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64")
@ -10,45 +10,29 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001
core = client.CoreGrpcClient() core = client.CoreGrpcClient()
core.connect() core.connect()
# create session and get id # add session
response = core.create_session() session = core.create_session()
session_id = response.session_id
# change session state to configuration so that nodes get started when added # create nodes
core.set_session_state(session_id, SessionState.CONFIGURATION)
# create emane node
position = Position(x=200, y=200) position = Position(x=200, y=200)
emane = Node(type=NodeType.EMANE, position=position, emane=EmaneIeee80211abgModel.name) emane = session.add_node(
response = core.add_node(session_id, emane) 1, _type=NodeType.EMANE, position=position, emane=EmaneIeee80211abgModel.name
emane_id = response.node_id )
# create node one
position = Position(x=100, y=100) position = Position(x=100, y=100)
n1 = Node(type=NodeType.DEFAULT, position=position, model="mdr") node1 = session.add_node(2, model="mdr", position=position)
response = core.add_node(session_id, n1)
n1_id = response.node_id
# create node two
position = Position(x=300, y=100) position = Position(x=300, y=100)
n2 = Node(type=NodeType.DEFAULT, position=position, model="mdr") node2 = session.add_node(3, model="mdr", position=position)
response = core.add_node(session_id, n2)
n2_id = response.node_id
# configure general emane settings # create links
core.set_emane_config(session_id, {"eventservicettl": "2"}) iface1 = iface_helper.create_iface(node1.id, 0)
session.add_link(node1=node1, node2=emane, iface1=iface1)
iface1 = iface_helper.create_iface(node2.id, 0)
session.add_link(node1=node2, node2=emane, iface1=iface1)
# configure emane model settings # setup emane configurations using a dict mapping currently support values as strings
# using a dict mapping currently support values as strings emane.set_emane_model(
core.set_emane_model_config( EmaneIeee80211abgModel.name, {"eventservicettl": "2", "unicastrate": "3"}
session_id, emane_id, EmaneIeee80211abgModel.name, {"unicastrate": "3"}
) )
# links nodes to emane # start session
iface1 = iface_helper.create_iface(n1_id, 0) core.start_session(session)
core.add_link(session_id, n1_id, emane_id, iface1)
iface1 = iface_helper.create_iface(n2_id, 0)
core.add_link(session_id, n2_id, emane_id, iface1)
# change session state
core.set_session_state(session_id, SessionState.INSTANTIATION)

View file

@ -1,5 +1,5 @@
from core.api.grpc import client from core.api.grpc import client
from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState from core.api.grpc.wrappers import Position
# interface helper # interface helper
iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64")
@ -8,29 +8,19 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001
core = client.CoreGrpcClient() core = client.CoreGrpcClient()
core.connect() core.connect()
# create session and get id # add session
response = core.create_session() session = core.create_session()
session_id = response.session_id
# change session state to configuration so that nodes get started when added # create nodes
core.set_session_state(session_id, SessionState.CONFIGURATION)
# create node one
position = Position(x=100, y=100) position = Position(x=100, y=100)
n1 = Node(type=NodeType.DEFAULT, position=position, model="PC") node1 = session.add_node(1, position=position)
response = core.add_node(session_id, n1)
n1_id = response.node_id
# create node two
position = Position(x=300, y=100) position = Position(x=300, y=100)
n2 = Node(type=NodeType.DEFAULT, position=position, model="PC") node2 = session.add_node(2, position=position)
response = core.add_node(session_id, n2)
n2_id = response.node_id
# links nodes together # create link
iface1 = iface_helper.create_iface(n1_id, 0) iface1 = iface_helper.create_iface(node1.id, 0)
iface2 = iface_helper.create_iface(n2_id, 0) iface2 = iface_helper.create_iface(node2.id, 0)
core.add_link(session_id, n1_id, n2_id, iface1, iface2) session.add_link(node1=node1, node2=node2, iface1=iface1, iface2=iface2)
# change session state # start session
core.set_session_state(session_id, SessionState.INSTANTIATION) core.start_session(session)

View file

@ -1,6 +1,5 @@
# required imports
from core.api.grpc import client from core.api.grpc import client
from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState from core.api.grpc.wrappers import NodeType, Position
# interface helper # interface helper
iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64")
@ -9,36 +8,22 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001
core = client.CoreGrpcClient() core = client.CoreGrpcClient()
core.connect() core.connect()
# create session and get id # add session
response = core.create_session() session = core.create_session()
session_id = response.session_id
# change session state to configuration so that nodes get started when added # create nodes
core.set_session_state(session_id, SessionState.CONFIGURATION)
# create switch node
position = Position(x=200, y=200) position = Position(x=200, y=200)
switch = Node(type=NodeType.SWITCH, position=position) switch = session.add_node(1, _type=NodeType.SWITCH, position=position)
response = core.add_node(session_id, switch)
switch_id = response.node_id
# create node one
position = Position(x=100, y=100) position = Position(x=100, y=100)
n1 = Node(type=NodeType.DEFAULT, position=position, model="PC") node1 = session.add_node(2, position=position)
response = core.add_node(session_id, n1)
n1_id = response.node_id
# create node two
position = Position(x=300, y=100) position = Position(x=300, y=100)
n2 = Node(type=NodeType.DEFAULT, position=position, model="PC") node2 = session.add_node(3, position=position)
response = core.add_node(session_id, n2)
n2_id = response.node_id
# links nodes to switch # create links
iface1 = iface_helper.create_iface(n1_id, 0) iface1 = iface_helper.create_iface(node1.id, 0)
core.add_link(session_id, n1_id, switch_id, iface1) session.add_link(node1=node1, node2=switch, iface1=iface1)
iface1 = iface_helper.create_iface(n2_id, 0) iface1 = iface_helper.create_iface(node2.id, 0)
core.add_link(session_id, n2_id, switch_id, iface1) session.add_link(node1=node2, node2=switch, iface1=iface1)
# change session state # start session
core.set_session_state(session_id, SessionState.INSTANTIATION) core.start_session(session)

View file

@ -1,6 +1,5 @@
# required imports
from core.api.grpc import client from core.api.grpc import client
from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState from core.api.grpc.wrappers import NodeType, Position
# interface helper # interface helper
iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64")
@ -9,50 +8,34 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001
core = client.CoreGrpcClient() core = client.CoreGrpcClient()
core.connect() core.connect()
# create session and get id # add session
response = core.create_session() session = core.create_session()
session_id = response.session_id
# change session state to configuration so that nodes get started when added # create nodes
core.set_session_state(session_id, SessionState.CONFIGURATION)
# create wlan node
position = Position(x=200, y=200) position = Position(x=200, y=200)
wlan = Node(type=NodeType.WIRELESS_LAN, position=position) wlan = session.add_node(1, _type=NodeType.WIRELESS_LAN, position=position)
response = core.add_node(session_id, wlan)
wlan_id = response.node_id
# create node one
position = Position(x=100, y=100) position = Position(x=100, y=100)
n1 = Node(type=NodeType.DEFAULT, position=position, model="mdr") node1 = session.add_node(2, model="mdr", position=position)
response = core.add_node(session_id, n1)
n1_id = response.node_id
# create node two
position = Position(x=300, y=100) position = Position(x=300, y=100)
n2 = Node(type=NodeType.DEFAULT, position=position, model="mdr") node2 = session.add_node(3, model="mdr", position=position)
response = core.add_node(session_id, n2)
n2_id = response.node_id
# configure wlan using a dict mapping currently # create links
iface1 = iface_helper.create_iface(node1.id, 0)
session.add_link(node1=node1, node2=wlan, iface1=iface1)
iface1 = iface_helper.create_iface(node2.id, 0)
session.add_link(node1=node2, node2=wlan, iface1=iface1)
# set wlan config using a dict mapping currently
# support values as strings # support values as strings
core.set_wlan_config( wlan.set_wlan(
session_id,
wlan_id,
{ {
"range": "280", "range": "280",
"bandwidth": "55000000", "bandwidth": "55000000",
"delay": "6000", "delay": "6000",
"jitter": "5", "jitter": "5",
"error": "5", "error": "5",
}, }
) )
# links nodes to wlan # start session
iface1 = iface_helper.create_iface(n1_id, 0) core.start_session(session)
core.add_link(session_id, n1_id, wlan_id, iface1)
iface1 = iface_helper.create_iface(n2_id, 0)
core.add_link(session_id, n2_id, wlan_id, iface1)
# change session state
core.set_session_state(session_id, SessionState.INSTANTIATION)

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