Merge branch 'develop' into dependabot/pip/daemon/lxml-4.6.5
This commit is contained in:
commit
fdc009699e
150 changed files with 5356 additions and 9820 deletions
101
Dockerfile
Normal file
101
Dockerfile
Normal 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
|
|
@ -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)
|
||||||
|
|
||||||
|
|
20
README.md
20
README.md
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
@ -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:
|
||||||
|
|
|
@ -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
|
@ -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
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
if not options:
|
|
||||||
options = []
|
|
||||||
self.options: List[str] = options
|
|
||||||
if not label:
|
|
||||||
label = _id
|
|
||||||
self.label: str = label
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return (
|
|
||||||
f"{self.__class__.__name__}(id={self.id}, type={self.type}, "
|
|
||||||
f"default={self.default}, options={self.options})"
|
|
||||||
)
|
)
|
||||||
|
elif self.type == ConfigDataTypes.FLOAT:
|
||||||
|
if self.default:
|
||||||
|
try:
|
||||||
|
float(self.default)
|
||||||
|
except ValueError:
|
||||||
|
raise CoreConfigError(
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ConfigBool(Configuration):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
|
@ -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}"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,24 +62,43 @@ class ConfigServiceManager:
|
||||||
# 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
|
||||||
|
|
|
@ -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)):
|
||||||
|
|
|
@ -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]] = {}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
"""
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
70
daemon/core/emane/modelmanager.py
Normal file
70
daemon/core/emane/modelmanager.py
Normal 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
|
0
daemon/core/emane/models/__init__.py
Normal file
0
daemon/core/emane/models/__init__.py
Normal 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
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
66
daemon/core/emane/models/tdma.py
Normal file
66
daemon/core/emane/models/tdma.py
Normal 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}")
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
|
@ -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
|
||||||
|
|
|
@ -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]:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
logger.debug("set node type: %s", node.type)
|
||||||
self.services.add_services(node, node.type, options.services)
|
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
|
|
||||||
:return: nothing
|
|
||||||
:raises core.CoreError: when node to update does not exist
|
|
||||||
"""
|
|
||||||
node = self.get_node(node_id, NodeBase)
|
|
||||||
node.icon = options.icon
|
|
||||||
self.set_node_position(node, options)
|
|
||||||
self.sdt.edit_node(node, options.lon, options.lat, options.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)
|
x, y, _ = self.location.getxyz(lat, lon, alt)
|
||||||
|
if math.isinf(x) or math.isinf(y):
|
||||||
|
raise CoreError(
|
||||||
|
f"invalid geo for current reference/scale: {lon},{lat},{alt}"
|
||||||
|
)
|
||||||
node.setposition(x, y, None)
|
node.setposition(x, y, None)
|
||||||
node.position.set_geo(lon, lat, alt)
|
node.position.set_geo(lon, lat, alt)
|
||||||
self.broadcast_node(node)
|
self.sdt.edit_node(node, lon, lat, alt)
|
||||||
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,23 +1248,21 @@ 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:
|
|
||||||
continue
|
|
||||||
args = (node,)
|
args = (node,)
|
||||||
funcs.append((self.services.stop_services, args, {}))
|
funcs.append((self.services.stop_services, args, {}))
|
||||||
utils.threadpool(funcs)
|
utils.threadpool(funcs)
|
||||||
|
@ -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:
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -46,3 +46,11 @@ class CoreServiceBootError(Exception):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfigError(Exception):
|
||||||
|
"""
|
||||||
|
Used when there is an error defining a configurable option.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 join_session(self, session_id: int) -> None:
|
def update_session_title(self) -> None:
|
||||||
logging.info("joining session(%s)", session_id)
|
|
||||||
self.reset()
|
|
||||||
try:
|
|
||||||
response = self.client.get_session(session_id)
|
|
||||||
self.session = Session.from_proto(response.session)
|
|
||||||
self.client.set_session_user(self.session.id, self.user)
|
|
||||||
title_file = self.session.file.name if self.session.file else ""
|
title_file = self.session.file.name if self.session.file else ""
|
||||||
self.master.title(f"CORE Session({self.session.id}) {title_file}")
|
self.master.title(f"CORE Session({self.session.id}) {title_file}")
|
||||||
|
|
||||||
|
def join_session(self, session_id: int) -> None:
|
||||||
|
logger.info("joining session(%s)", session_id)
|
||||||
|
self.reset()
|
||||||
|
try:
|
||||||
|
self.session = self.client.get_session(session_id)
|
||||||
|
self.session.user = self.user
|
||||||
|
self.update_session_title()
|
||||||
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)
|
|
||||||
|
|
||||||
def start_session(self) -> Tuple[bool, List[str]]:
|
|
||||||
self.ifaces_manager.set_macs([x.link for x in self.links.values()])
|
self.ifaces_manager.set_macs([x.link for x in self.links.values()])
|
||||||
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 not definition:
|
||||||
if link.iface1 and not link.iface1.mac:
|
if link.iface1 and not link.iface1.mac:
|
||||||
link.iface1.mac = self.ifaces_manager.next_mac()
|
link.iface1.mac = self.ifaces_manager.next_mac()
|
||||||
if link.iface2 and not link.iface2.mac:
|
if link.iface2 and not link.iface2.mac:
|
||||||
link.iface2.mac = self.ifaces_manager.next_mac()
|
link.iface2.mac = self.ifaces_manager.next_mac()
|
||||||
links.append(link.to_proto())
|
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
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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_changed = False
|
||||||
|
if ip_config.ip4 != self.ip4:
|
||||||
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_config.ip6 = self.ip6
|
||||||
|
ip_changed = True
|
||||||
ip_config.ip4s = ip4s
|
ip_config.ip4s = ip4s
|
||||||
ip_config.ip6s = ip6s
|
ip_config.ip6s = ip6s
|
||||||
|
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.core.ifaces_manager.update_ips(self.ip4, self.ip6)
|
||||||
self.app.save_config()
|
self.app.save_config()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
config = self.core.set_node_service(
|
files = set(self.filename_combobox["values"])
|
||||||
self.node.id,
|
service_data = NodeServiceData(
|
||||||
self.service_name,
|
configs=list(files),
|
||||||
dirs=self.temp_directories,
|
dirs=self.temp_directories,
|
||||||
files=list(self.filename_combobox["values"]),
|
startup=startup,
|
||||||
startups=startup,
|
validate=validate,
|
||||||
validations=validate,
|
shutdown=shutdown,
|
||||||
shutdowns=shutdown,
|
|
||||||
)
|
)
|
||||||
self.node.service_configs[self.service_name] = config
|
logger.info("setting service data: %s", service_data)
|
||||||
|
self.node.service_configs[self.service_name] = service_data
|
||||||
for file in self.modified_files:
|
for file in self.modified_files:
|
||||||
|
if file not in files:
|
||||||
|
continue
|
||||||
file_configs = self.node.service_file_configs.setdefault(
|
file_configs = self.node.service_file_configs.setdefault(
|
||||||
self.service_name, {}
|
self.service_name, {}
|
||||||
)
|
)
|
||||||
file_configs[file] = self.temp_service_files[file]
|
file_configs[file] = self.temp_service_files[file]
|
||||||
# TODO: check if this is really needed
|
|
||||||
self.app.core.set_node_service_file(
|
|
||||||
self.node.id, self.service_name, file, self.temp_service_files[file]
|
|
||||||
)
|
|
||||||
self.current_service_color("green")
|
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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
file_path = Path(file_path)
|
||||||
|
if self.core.save_xml(file_path):
|
||||||
self.add_recent_file_to_gui_config(file_path)
|
self.add_recent_file_to_gui_config(file_path)
|
||||||
self.core.save_xml(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:
|
|
||||||
recent_files.insert(0, file_path)
|
|
||||||
elif 0 < num_files <= MAX_FILES:
|
|
||||||
if file_path in recent_files:
|
if file_path in recent_files:
|
||||||
recent_files.remove(file_path)
|
recent_files.remove(file_path)
|
||||||
recent_files.insert(0, file_path)
|
recent_files.insert(0, file_path)
|
||||||
else:
|
if len(recent_files) > MAX_FILES:
|
||||||
if num_files == MAX_FILES:
|
|
||||||
recent_files.pop()
|
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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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]:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
|
||||||
)
|
|
||||||
|
|
|
@ -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]:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
with self.updatelock:
|
with self.lock:
|
||||||
try:
|
self.last_update_time.pop(net, None)
|
||||||
del self.last_update_time[wlan]
|
if self.last_update_time:
|
||||||
except KeyError:
|
|
||||||
logging.exception(
|
|
||||||
"error deleting last update time for wlan, ignored before: %s", wlan
|
|
||||||
)
|
|
||||||
if len(self.last_update_time) > 0:
|
|
||||||
return
|
return
|
||||||
self.doupdateloop = False
|
self.running = False
|
||||||
if self.updatethread:
|
if self.run_thread:
|
||||||
self.updatethread.join()
|
self.updates.put(None)
|
||||||
self.updatethread = None
|
self.run_thread.join()
|
||||||
|
self.run_thread = None
|
||||||
|
|
||||||
def ebatomiccmd(self, cmd: str) -> str:
|
def last_update(self, net: "CoreNetwork") -> float:
|
||||||
"""
|
"""
|
||||||
Helper for building ebtables atomic file command list.
|
Return the time elapsed since this network was last updated.
|
||||||
|
:param net: network node
|
||||||
:param cmd: ebtable command
|
:return: elapsed time
|
||||||
:return: ebtable atomic command
|
|
||||||
"""
|
"""
|
||||||
return f"{EBTABLES} --atomic-file {self.atomic_file} {cmd}"
|
now = time.monotonic()
|
||||||
|
last_update = self.last_update_time.setdefault(net, now)
|
||||||
|
return now - last_update
|
||||||
|
|
||||||
def lastupdate(self, wlan: "CoreNetwork") -> float:
|
def run(self) -> None:
|
||||||
"""
|
"""
|
||||||
Return the time elapsed since this WLAN was last updated.
|
Thread target that looks for networks needing update, and
|
||||||
|
rate limits the amount of nftables activity. Only one userspace program
|
||||||
:param wlan: wlan entity
|
should use nftables at any given time, or results can be unpredictable.
|
||||||
: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
|
|
||||||
"""
|
|
||||||
self.last_update_time[wlan] = time.monotonic()
|
|
||||||
self.updates.remove(wlan)
|
|
||||||
|
|
||||||
def updateloop(self) -> None:
|
|
||||||
"""
|
|
||||||
Thread target that looks for WLANs needing update, and
|
|
||||||
rate limits the amount of ebtables activity. Only one userspace program
|
|
||||||
should use ebtables at any given time, or results can be unpredictable.
|
|
||||||
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
while self.doupdateloop:
|
while self.running:
|
||||||
with self.updatelock:
|
net = self.updates.get()
|
||||||
for wlan in self.updates:
|
if net is None:
|
||||||
# Check if wlan is from a previously closed session. Because of the
|
break
|
||||||
# rate limiting scheme employed here, this may happen if a new session
|
if not net.up:
|
||||||
# is started soon after closing a previous session.
|
self.last_update_time[net] = time.monotonic()
|
||||||
# TODO: if these are WlanNodes, this will never throw an exception
|
elif self.last_update(net) > self.rate:
|
||||||
try:
|
with self.lock:
|
||||||
wlan.session
|
self.build_cmds(net)
|
||||||
except Exception:
|
self.commit(net)
|
||||||
# Just mark as updated to remove from self.updates.
|
self.last_update_time[net] = time.monotonic()
|
||||||
self.updated(wlan)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.lastupdate(wlan) > self.rate:
|
def commit(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.
|
Commit changes to nftables for the provided network.
|
||||||
|
:param net: network to commit nftables changes
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
# save kernel ebtables snapshot to a file
|
if not self.cmds:
|
||||||
args = self.ebatomiccmd("--atomic-save")
|
return
|
||||||
wlan.host_cmd(args)
|
# write out nft commands to file
|
||||||
|
for cmd in self.cmds:
|
||||||
|
net.host_cmd(f"echo {cmd} >> {self.atomic_file}", shell=True)
|
||||||
|
# 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()
|
||||||
|
|
||||||
# modify the table file using queued ebtables commands
|
def update(self, net: "CoreNetwork") -> None:
|
||||||
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
|
Flag this network has an update, so the nftables chain will be rebuilt.
|
||||||
chain will be rebuilt at the next interval.
|
:param net: wlan network
|
||||||
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
with self.updatelock:
|
self.updates.put(net)
|
||||||
if wlan not in self.updates:
|
|
||||||
self.updates.append(wlan)
|
|
||||||
|
|
||||||
def buildcmds(self, wlan: "CoreNetwork") -> None:
|
def delete_table(self, net: "CoreNetwork") -> None:
|
||||||
"""
|
"""
|
||||||
Inspect a _linked dict from a wlan, and rebuild the ebtables chain for that WLAN.
|
Delete nftable bridge rule table.
|
||||||
|
|
||||||
|
:param net: network to delete table for
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
with wlan._linked_lock:
|
with self.lock:
|
||||||
if wlan.has_ebtables_chain:
|
net.host_cmd(f"{NFTABLES} delete table bridge {net.brname}")
|
||||||
# flush the chain
|
|
||||||
self.cmds.append(f"-F {wlan.brname}")
|
def build_cmds(self, net: "CoreNetwork") -> None:
|
||||||
|
"""
|
||||||
|
Inspect linked nodes for a network, and rebuild the nftables chain commands.
|
||||||
|
:param net: network to build commands for
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
with net.linked_lock:
|
||||||
|
if net.has_nftables_chain:
|
||||||
|
self.cmds.append(f"flush table bridge {net.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)
|
||||||
|
|
|
@ -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)
|
self.adopt_iface(remote_tap, iface_id, iface_data.mac, ips)
|
||||||
return remote_tap
|
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:
|
def privatedir(self, dir_path: Path) -> None:
|
||||||
if path[0] != "/":
|
if not str(dir_path).startswith("/"):
|
||||||
raise ValueError(f"path not fully qualified: {path}")
|
raise CoreError(f"private directory path not fully qualified: {dir_path}")
|
||||||
hostpath = os.path.join(
|
host_path = self.host_path(dir_path, is_dir=True)
|
||||||
self.nodedir, os.path.normpath(path).strip("/").replace("/", ".")
|
self.host_cmd(f"mkdir -p {host_path}")
|
||||||
)
|
self.mount(host_path, dir_path)
|
||||||
os.mkdir(hostpath)
|
|
||||||
self.mount(hostpath, path)
|
|
||||||
|
|
||||||
def mount(self, source: str, target: str) -> None:
|
def mount(self, src_path: Path, target_path: Path) -> None:
|
||||||
source = os.path.abspath(source)
|
logger.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path)
|
||||||
logging.info("mounting %s at %s", source, target)
|
self.cmd(f"mkdir -p {target_path}")
|
||||||
os.makedirs(target)
|
self.host_cmd(f"{MOUNT} --bind {src_path} {target_path}", cwd=self.directory)
|
||||||
self.host_cmd(f"{MOUNT} --bind {source} {target}", cwd=self.nodedir)
|
self._mounts.append((src_path, target_path))
|
||||||
self._mounts.append((source, target))
|
|
||||||
|
|
||||||
def umount(self, target: str) -> None:
|
def umount(self, target_path: Path) -> None:
|
||||||
logging.info("unmounting '%s'", target)
|
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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,10 +246,8 @@ 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]
|
||||||
|
@ -263,11 +261,11 @@ subnet %s netmask %s {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""" % (
|
""" % (
|
||||||
ip.ip,
|
ip.cidr.ip,
|
||||||
ip.netmask,
|
ip.netmask,
|
||||||
rangelow,
|
rangelow,
|
||||||
rangehigh,
|
rangehigh,
|
||||||
address,
|
ip.ip,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,8 +80,8 @@ 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)
|
||||||
|
|
||||||
|
@ -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,49 +372,35 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,41 +144,31 @@ 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(
|
||||||
# create nem xml entries for all interfaces
|
platform_element, emane_net.model.platform_controlport, f"0.0.0.0:{nem_port}"
|
||||||
for iface in data.ifaces:
|
|
||||||
emane_net = iface.net
|
|
||||||
if not isinstance(emane_net, EmaneNet):
|
|
||||||
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
|
# build nem xml
|
||||||
nem_definition = nem_file_name(iface)
|
nem_definition = nem_file_name(iface)
|
||||||
|
@ -185,6 +176,9 @@ def build_platform_xml(
|
||||||
"nem", id=str(nem_id), name=iface.localname, definition=nem_definition
|
"nem", id=str(nem_id), name=iface.localname, definition=nem_definition
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# create model based xml files
|
||||||
|
emane_net.model.build_xml_files(config, iface)
|
||||||
|
|
||||||
# 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")
|
||||||
|
@ -209,8 +203,8 @@ def build_platform_xml(
|
||||||
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"
|
||||||
|
|
|
@ -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": "WARNING",
|
||||||
|
"handlers": ["console"],
|
||||||
|
"propagate": false
|
||||||
|
},
|
||||||
|
"core": {
|
||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
"handlers": ["console"]
|
"handlers": ["console"],
|
||||||
|
"propagate": false
|
||||||
|
},
|
||||||
|
"__main__": {
|
||||||
|
"level": "INFO",
|
||||||
|
"handlers": ["console"],
|
||||||
|
"propagate": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"bridge": "none",
|
|
||||||
"iptables": false
|
|
||||||
|
|
||||||
}
|
|
|
@ -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):
|
||||||
core = client.CoreGrpcClient()
|
|
||||||
|
|
||||||
with core.context_connect():
|
|
||||||
# create session
|
|
||||||
response = core.create_session()
|
|
||||||
session_id = response.session_id
|
|
||||||
logging.info("created session: %s", response)
|
|
||||||
|
|
||||||
# add distributed server
|
|
||||||
server_name = "core2"
|
|
||||||
response = core.add_session_server(session_id, server_name, args.server)
|
|
||||||
logging.info("added session server: %s", response)
|
|
||||||
|
|
||||||
# handle events session may broadcast
|
|
||||||
core.events(session_id, log_event)
|
|
||||||
|
|
||||||
# change session state
|
|
||||||
response = core.set_session_state(session_id, SessionState.CONFIGURATION)
|
|
||||||
logging.info("set session state: %s", response)
|
|
||||||
|
|
||||||
# create switch node
|
|
||||||
switch = Node(type=NodeType.SWITCH)
|
|
||||||
response = core.add_node(session_id, switch)
|
|
||||||
logging.info("created switch: %s", response)
|
|
||||||
switch_id = response.node_id
|
|
||||||
|
|
||||||
# helper to create interfaces
|
# helper to create interfaces
|
||||||
interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16")
|
interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16")
|
||||||
|
|
||||||
# create node one
|
# create grpc client and connect
|
||||||
|
core = client.CoreGrpcClient()
|
||||||
|
core.connect()
|
||||||
|
|
||||||
|
# create session
|
||||||
|
session = core.create_session()
|
||||||
|
|
||||||
|
# add distributed server
|
||||||
|
server = Server(name="core2", host=args.server)
|
||||||
|
session.servers.append(server)
|
||||||
|
|
||||||
|
# handle events session may broadcast
|
||||||
|
core.events(session.id, log_event)
|
||||||
|
|
||||||
|
# create switch node
|
||||||
|
position = Position(x=150, y=100)
|
||||||
|
switch = session.add_node(1, _type=NodeType.SWITCH, position=position)
|
||||||
position = Position(x=100, y=50)
|
position = Position(x=100, y=50)
|
||||||
node = Node(position=position)
|
node1 = session.add_node(2, 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)
|
position = Position(x=200, y=50)
|
||||||
node = Node(position=position, server=server_name)
|
node2 = session.add_node(3, 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
|
# create links
|
||||||
interface1 = interface_helper.create_iface(node2_id, 0)
|
iface1 = interface_helper.create_iface(node1.id, 0)
|
||||||
response = core.add_link(session_id, node2_id, switch_id, interface1)
|
session.add_link(node1=node1, node2=switch, iface1=iface1)
|
||||||
logging.info("created link from node two to switch: %s", response)
|
iface1 = interface_helper.create_iface(node2.id, 0)
|
||||||
|
session.add_link(node1=node2, node2=switch, iface1=iface1)
|
||||||
|
|
||||||
# change session state
|
# start session
|
||||||
response = core.set_session_state(session_id, SessionState.INSTANTIATION)
|
core.start_session(session)
|
||||||
logging.info("set session state: %s", response)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue