Merge branch 'develop' into dependabot/pip/daemon/pyyaml-5.4
This commit is contained in:
commit
750489c2e1
145 changed files with 4867 additions and 9313 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 "libc6 >= 2.14" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "nftables" \
|
||||
-d "iproute2" \
|
||||
-d "libev4" \
|
||||
-d "openssh-server" \
|
||||
|
@ -77,7 +77,7 @@ fpm -s dir -t rpm -n core-distributed \
|
|||
-d "ethtool" \
|
||||
-d "procps-ng" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "nftables" \
|
||||
-d "iproute" \
|
||||
-d "libev" \
|
||||
-d "net-tools" \
|
||||
|
@ -123,7 +123,7 @@ all: change-files
|
|||
|
||||
.PHONY: 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,netns/setup.py)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
# 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
|
||||
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).])
|
||||
fi
|
||||
|
||||
AC_CHECK_PROG(ebtables_path, ebtables, $as_dir, no, $SEARCHPATH)
|
||||
if test "x$ebtables_path" = "xno" ; then
|
||||
AC_MSG_ERROR([Could not locate ebtables (from ebtables package).])
|
||||
AC_CHECK_PROG(nftables_path, nft, $as_dir, no, $SEARCHPATH)
|
||||
if test "x$nftables_path" = "xno" ; then
|
||||
AC_MSG_ERROR([Could not locate nftables (from nftables package).])
|
||||
fi
|
||||
|
||||
AC_CHECK_PROG(ip_path, ip, $as_dir, no, $SEARCHPATH)
|
||||
|
|
|
@ -2,6 +2,3 @@ import logging.config
|
|||
|
||||
# setup default null handler
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_node_event(node_data: NodeData) -> core_pb2.Event:
|
||||
"""
|
||||
|
@ -199,7 +201,7 @@ class EventStreamer:
|
|||
elif isinstance(data, FileData):
|
||||
event = handle_file_event(data)
|
||||
else:
|
||||
logging.error("unknown event: %s", data)
|
||||
logger.error("unknown event: %s", data)
|
||||
except Empty:
|
||||
pass
|
||||
if event:
|
||||
|
|
|
@ -7,10 +7,9 @@ import grpc
|
|||
from grpc import ServicerContext
|
||||
|
||||
from core import utils
|
||||
from core.api.grpc import common_pb2, core_pb2
|
||||
from core.api.grpc.common_pb2 import MappedConfig
|
||||
from core.api.grpc import common_pb2, core_pb2, wrappers
|
||||
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 (
|
||||
NodeServiceConfig,
|
||||
NodeServiceData,
|
||||
|
@ -28,9 +27,10 @@ from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
|||
from core.nodes.docker import DockerNode
|
||||
from core.nodes.interface import CoreInterface
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
WORKERS = 10
|
||||
|
||||
|
||||
|
@ -156,7 +156,7 @@ def create_nodes(
|
|||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
total = time.monotonic() - start
|
||||
logging.debug("grpc created nodes time: %s", total)
|
||||
logger.debug("grpc created nodes time: %s", total)
|
||||
return results, exceptions
|
||||
|
||||
|
||||
|
@ -180,7 +180,7 @@ def create_links(
|
|||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
total = time.monotonic() - start
|
||||
logging.debug("grpc created links time: %s", total)
|
||||
logger.debug("grpc created links time: %s", total)
|
||||
return results, exceptions
|
||||
|
||||
|
||||
|
@ -204,7 +204,7 @@ def edit_links(
|
|||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
total = time.monotonic() - start
|
||||
logging.debug("grpc edit links time: %s", total)
|
||||
logger.debug("grpc edit links time: %s", total)
|
||||
return results, exceptions
|
||||
|
||||
|
||||
|
@ -251,12 +251,15 @@ def get_config_options(
|
|||
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.
|
||||
|
||||
:param session: session containing node
|
||||
:param node: node to convert
|
||||
:param emane_configs: emane configs related to node
|
||||
:return: node proto
|
||||
"""
|
||||
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
|
||||
config_services = []
|
||||
if isinstance(node, CoreNodeBase):
|
||||
node_dir = node.nodedir
|
||||
node_dir = str(node.directory)
|
||||
config_services = [x for x in node.config_services]
|
||||
channel = None
|
||||
if isinstance(node, CoreNode):
|
||||
channel = node.ctrlchnlname
|
||||
channel = str(node.ctrlchnlname)
|
||||
emane_model = None
|
||||
if isinstance(node, EmaneNet):
|
||||
emane_model = node.model.name
|
||||
image = None
|
||||
if isinstance(node, (DockerNode, LxcNode)):
|
||||
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(
|
||||
id=node.id,
|
||||
name=node.name,
|
||||
|
@ -297,6 +336,11 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
|
|||
dir=node_dir,
|
||||
channel=channel,
|
||||
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
|
||||
|
||||
|
||||
def get_emane_model_configs(session: Session) -> List[GetEmaneModelConfig]:
|
||||
configs = []
|
||||
for _id in session.emane.node_configurations:
|
||||
if _id == -1:
|
||||
continue
|
||||
model_configs = session.emane.node_configurations[_id]
|
||||
def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]:
|
||||
configs = {}
|
||||
for _id, model_configs in session.emane.node_configs.items():
|
||||
for model_name in model_configs:
|
||||
model = session.emane.models[model_name]
|
||||
current_config = session.emane.get_model_config(_id, model_name)
|
||||
config = get_config_options(current_config, model)
|
||||
model_class = session.emane.get_model(model_name)
|
||||
current_config = session.emane.get_config(_id, model_name)
|
||||
config = get_config_options(current_config, model_class)
|
||||
node_id, iface_id = utils.parse_iface_config_id(_id)
|
||||
iface_id = iface_id if iface_id is not None else -1
|
||||
model_config = GetEmaneModelConfig(
|
||||
node_id=node_id, model=model_name, iface_id=iface_id, config=config
|
||||
node_config = NodeEmaneConfig(
|
||||
model=model_name, iface_id=iface_id, config=config
|
||||
)
|
||||
configs.append(model_config)
|
||||
return configs
|
||||
|
||||
|
||||
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
|
||||
node_configs = configs.setdefault(node_id, [])
|
||||
node_configs.append(node_config)
|
||||
return configs
|
||||
|
||||
|
||||
|
@ -590,15 +600,6 @@ def get_hooks(session: Session) -> List[core_pb2.Hook]:
|
|||
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]:
|
||||
default_services = []
|
||||
for name, services in session.services.default_services.items():
|
||||
|
@ -607,45 +608,6 @@ def get_default_services(session: Session) -> List[ServiceDefaults]:
|
|||
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(
|
||||
session: Session, node_id: int, context: ServicerContext
|
||||
) -> Union[WlanNode, EmaneNet]:
|
||||
|
@ -656,3 +618,88 @@ def get_mobility_node(
|
|||
return session.get_node(node_id, EmaneNet)
|
||||
except CoreError:
|
||||
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:
|
||||
templates: Dict[str, str]
|
||||
config: Dict[str, "ConfigOption"]
|
||||
modes: List[str]
|
||||
modes: Dict[str, Dict[str, str]]
|
||||
|
||||
@classmethod
|
||||
def from_proto(
|
||||
cls, proto: configservices_pb2.GetConfigServicesResponse
|
||||
cls, proto: configservices_pb2.GetConfigServiceDefaultsResponse
|
||||
) -> "ConfigServiceDefaults":
|
||||
config = ConfigOption.from_dict(proto.config)
|
||||
modes = {x.name: dict(x.config) for x in proto.modes}
|
||||
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
|
||||
class Service:
|
||||
group: str
|
||||
|
@ -205,16 +219,16 @@ class ServiceDefault:
|
|||
|
||||
@dataclass
|
||||
class NodeServiceData:
|
||||
executables: List[str]
|
||||
dependencies: List[str]
|
||||
dirs: List[str]
|
||||
configs: List[str]
|
||||
startup: List[str]
|
||||
validate: List[str]
|
||||
validation_mode: ServiceValidationMode
|
||||
validation_timer: int
|
||||
shutdown: List[str]
|
||||
meta: str
|
||||
executables: List[str] = field(default_factory=list)
|
||||
dependencies: List[str] = field(default_factory=list)
|
||||
dirs: List[str] = field(default_factory=list)
|
||||
configs: List[str] = field(default_factory=list)
|
||||
startup: List[str] = field(default_factory=list)
|
||||
validate: List[str] = field(default_factory=list)
|
||||
validation_mode: ServiceValidationMode = ServiceValidationMode.NON_BLOCKING
|
||||
validation_timer: int = 5
|
||||
shutdown: List[str] = field(default_factory=list)
|
||||
meta: str = None
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: services_pb2.NodeServiceData) -> "NodeServiceData":
|
||||
|
@ -225,12 +239,43 @@ class NodeServiceData:
|
|||
configs=proto.configs,
|
||||
startup=proto.startup,
|
||||
validate=proto.validate,
|
||||
validation_mode=proto.validation_mode,
|
||||
validation_mode=ServiceValidationMode(proto.validation_mode),
|
||||
validation_timer=proto.validation_timer,
|
||||
shutdown=proto.shutdown,
|
||||
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
|
||||
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
|
||||
class BridgeThroughput:
|
||||
node_id: int
|
||||
|
@ -364,11 +422,11 @@ class ExceptionEvent:
|
|||
|
||||
@dataclass
|
||||
class ConfigOption:
|
||||
label: str
|
||||
name: str
|
||||
value: str
|
||||
type: ConfigOptionType
|
||||
group: str
|
||||
label: str = None
|
||||
type: ConfigOptionType = None
|
||||
group: str = None
|
||||
select: List[str] = None
|
||||
|
||||
@classmethod
|
||||
|
@ -386,15 +444,27 @@ class ConfigOption:
|
|||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: common_pb2.ConfigOption) -> "ConfigOption":
|
||||
config_type = ConfigOptionType(proto.type) if proto.type is not None else None
|
||||
return ConfigOption(
|
||||
label=proto.label,
|
||||
name=proto.name,
|
||||
value=proto.value,
|
||||
type=ConfigOptionType(proto.type),
|
||||
type=config_type,
|
||||
group=proto.group,
|
||||
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
|
||||
class Interface:
|
||||
|
@ -598,11 +668,12 @@ class EmaneModelConfig:
|
|||
)
|
||||
|
||||
def to_proto(self) -> emane_pb2.EmaneModelConfig:
|
||||
config = ConfigOption.to_dict(self.config)
|
||||
return emane_pb2.EmaneModelConfig(
|
||||
node_id=self.node_id,
|
||||
model=self.model,
|
||||
iface_id=self.iface_id,
|
||||
config=self.config,
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
|
@ -635,11 +706,11 @@ class Geo:
|
|||
|
||||
@dataclass
|
||||
class Node:
|
||||
id: int
|
||||
name: str
|
||||
type: NodeType
|
||||
id: int = None
|
||||
name: str = None
|
||||
type: NodeType = NodeType.DEFAULT
|
||||
model: str = None
|
||||
position: Position = None
|
||||
position: Position = Position(x=0, y=0)
|
||||
services: Set[str] = field(default_factory=set)
|
||||
config_services: Set[str] = field(default_factory=set)
|
||||
emane: str = None
|
||||
|
@ -669,6 +740,23 @@ class Node:
|
|||
|
||||
@classmethod
|
||||
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(
|
||||
id=proto.id,
|
||||
name=proto.name,
|
||||
|
@ -685,9 +773,43 @@ class Node:
|
|||
dir=proto.dir,
|
||||
channel=proto.channel,
|
||||
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:
|
||||
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(
|
||||
id=self.id,
|
||||
name=self.name,
|
||||
|
@ -703,24 +825,50 @@ class Node:
|
|||
dir=self.dir,
|
||||
channel=self.channel,
|
||||
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
|
||||
class Session:
|
||||
id: int
|
||||
state: SessionState
|
||||
nodes: Dict[int, Node]
|
||||
links: List[Link]
|
||||
dir: str
|
||||
user: str
|
||||
default_services: Dict[str, Set[str]]
|
||||
location: SessionLocation
|
||||
hooks: Dict[str, Hook]
|
||||
emane_models: List[str]
|
||||
emane_config: Dict[str, ConfigOption]
|
||||
metadata: Dict[str, str]
|
||||
file: Path
|
||||
id: int = None
|
||||
state: SessionState = SessionState.DEFINITION
|
||||
nodes: Dict[int, Node] = field(default_factory=dict)
|
||||
links: List[Link] = field(default_factory=list)
|
||||
dir: str = None
|
||||
user: str = None
|
||||
default_services: Dict[str, Set[str]] = field(default_factory=dict)
|
||||
location: SessionLocation = SessionLocation(
|
||||
x=0.0, y=0.0, z=0.0, lat=47.57917, lon=-122.13232, alt=2.0, scale=150.0
|
||||
)
|
||||
hooks: Dict[str, Hook] = field(default_factory=dict)
|
||||
metadata: Dict[str, str] = field(default_factory=dict)
|
||||
file: Path = None
|
||||
options: Dict[str, ConfigOption] = field(default_factory=dict)
|
||||
servers: List[Server] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
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
|
||||
}
|
||||
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
|
||||
options = ConfigOption.from_dict(proto.options)
|
||||
servers = [Server.from_proto(x) for x in proto.servers]
|
||||
return Session(
|
||||
id=proto.id,
|
||||
state=SessionState(proto.state),
|
||||
|
@ -767,10 +891,107 @@ class Session:
|
|||
default_services=default_services,
|
||||
location=SessionLocation.from_proto(proto.location),
|
||||
hooks=hooks,
|
||||
emane_models=list(proto.emane_models),
|
||||
emane_config=ConfigOption.from_dict(proto.emane_config),
|
||||
metadata=dict(proto.metadata),
|
||||
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.emulator.data import ConfigData, NodeData
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def convert_node(node_data: NodeData):
|
||||
"""
|
||||
|
@ -139,9 +141,9 @@ class ConfigShim:
|
|||
captions = None
|
||||
data_types = []
|
||||
possible_values = []
|
||||
logging.debug("configurable: %s", configurable_options)
|
||||
logging.debug("configuration options: %s", configurable_options.configurations)
|
||||
logging.debug("configuration data: %s", config)
|
||||
logger.debug("configurable: %s", configurable_options)
|
||||
logger.debug("configuration options: %s", configurable_options.configurations)
|
||||
logger.debug("configuration data: %s", config)
|
||||
for configuration in configurable_options.configurations():
|
||||
if not captions:
|
||||
captions = configuration.label
|
||||
|
|
|
@ -4,6 +4,8 @@ Utilities for working with python struct data.
|
|||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def pack_values(clazz, packers):
|
||||
"""
|
||||
|
@ -15,7 +17,7 @@ def pack_values(clazz, packers):
|
|||
"""
|
||||
|
||||
# iterate through tuples of values to pack
|
||||
logging.debug("packing: %s", packers)
|
||||
logger.debug("packing: %s", packers)
|
||||
data = b""
|
||||
for packer in packers:
|
||||
# check if a transformer was provided for valid values
|
||||
|
@ -37,7 +39,7 @@ def pack_values(clazz, packers):
|
|||
value = transformer(value)
|
||||
|
||||
# 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)
|
||||
|
||||
return data
|
||||
|
|
|
@ -4,72 +4,70 @@ Common support for configurable CORE objects.
|
|||
|
||||
import logging
|
||||
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.emulator.enumerations import ConfigDataTypes
|
||||
from core.errors import CoreConfigError
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.location.mobility import WirelessModel
|
||||
|
||||
WirelessModelType = Type[WirelessModel]
|
||||
|
||||
_BOOL_OPTIONS: Set[str] = {"0", "1"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigGroup:
|
||||
"""
|
||||
Defines configuration group tabs used for display by ConfigurationOptions.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, start: int, stop: int) -> None:
|
||||
"""
|
||||
Creates a ConfigGroup object.
|
||||
|
||||
: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
|
||||
name: str
|
||||
start: int
|
||||
stop: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Configuration:
|
||||
"""
|
||||
Represents a configuration options.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
_id: str,
|
||||
_type: ConfigDataTypes,
|
||||
label: str = None,
|
||||
default: str = "",
|
||||
options: List[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Creates a Configuration object.
|
||||
id: str
|
||||
type: ConfigDataTypes
|
||||
label: str = None
|
||||
default: str = ""
|
||||
options: List[str] = field(default_factory=list)
|
||||
|
||||
:param _id: unique name for configuration
|
||||
:param _type: configuration data type
|
||||
:param label: configuration label for display
|
||||
:param default: default value for configuration
|
||||
:param options: list options if this is a configuration with a combobox
|
||||
"""
|
||||
self.id: str = _id
|
||||
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})"
|
||||
def __post_init__(self) -> None:
|
||||
self.label = self.label if self.label else self.id
|
||||
if self.type == ConfigDataTypes.BOOL:
|
||||
if self.default and self.default not in _BOOL_OPTIONS:
|
||||
raise CoreConfigError(
|
||||
f"{self.id} bool value must be one of: {_BOOL_OPTIONS}: "
|
||||
f"{self.default}"
|
||||
)
|
||||
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}"
|
||||
)
|
||||
|
||||
|
||||
|
@ -182,7 +180,7 @@ class ConfigurableManager:
|
|||
:param config_type: configuration type to store configuration for
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"setting config for node(%s) type(%s): %s", node_id, config_type, config
|
||||
)
|
||||
node_configs = self.node_configurations.setdefault(node_id, OrderedDict())
|
||||
|
@ -314,7 +312,7 @@ class ModelManager(ConfigurableManager):
|
|||
:param config: model configuration, None for default configuration
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"setting model(%s) for node(%s): %s", model_class.name, node.id, config
|
||||
)
|
||||
self.set_model_config(node.id, model_class.name, config)
|
||||
|
@ -343,5 +341,5 @@ class ModelManager(ConfigurableManager):
|
|||
model_class = self.models[model_name]
|
||||
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
|
||||
|
|
|
@ -2,9 +2,10 @@ import abc
|
|||
import enum
|
||||
import inspect
|
||||
import logging
|
||||
import pathlib
|
||||
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.lookup import TemplateLookup
|
||||
|
@ -14,6 +15,7 @@ from core.config import Configuration
|
|||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
TEMPLATES_DIR: str = "templates"
|
||||
|
||||
|
||||
|
@ -27,6 +29,14 @@ class ConfigServiceBootError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShadowDir:
|
||||
path: str
|
||||
src: Optional[str] = None
|
||||
templates: bool = False
|
||||
has_node_paths: bool = False
|
||||
|
||||
|
||||
class ConfigService(abc.ABC):
|
||||
"""
|
||||
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
|
||||
validation_timer: int = 5
|
||||
|
||||
# directories to shadow and copy files from
|
||||
shadow_directories: List[ShadowDir] = []
|
||||
|
||||
def __init__(self, node: CoreNode) -> None:
|
||||
"""
|
||||
Create ConfigService instance.
|
||||
|
@ -46,7 +59,7 @@ class ConfigService(abc.ABC):
|
|||
"""
|
||||
self.node: CoreNode = node
|
||||
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.config: Dict[str, Configuration] = {}
|
||||
self.custom_templates: Dict[str, str] = {}
|
||||
|
@ -133,7 +146,8 @@ class ConfigService(abc.ABC):
|
|||
:return: nothing
|
||||
: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_files()
|
||||
wait = self.validation_mode == ConfigServiceMode.BLOCKING
|
||||
|
@ -154,7 +168,7 @@ class ConfigService(abc.ABC):
|
|||
try:
|
||||
self.node.cmd(cmd)
|
||||
except CoreCommandError:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"failed shutdown: {cmd}"
|
||||
)
|
||||
|
@ -168,6 +182,64 @@ class ConfigService(abc.ABC):
|
|||
self.stop()
|
||||
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:
|
||||
"""
|
||||
Creates directories for service.
|
||||
|
@ -175,10 +247,12 @@ class ConfigService(abc.ABC):
|
|||
:return: nothing
|
||||
: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:
|
||||
self.node.privatedir(directory)
|
||||
except (CoreCommandError, ValueError):
|
||||
self.node.create_dir(dir_path)
|
||||
except (CoreCommandError, CoreError):
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"failure to create service directory: {directory}"
|
||||
|
@ -219,17 +293,21 @@ class ConfigService(abc.ABC):
|
|||
:return: mapping of files to templates
|
||||
"""
|
||||
templates = {}
|
||||
for name in self.files:
|
||||
basename = pathlib.Path(name).name
|
||||
if name in self.custom_templates:
|
||||
template = self.custom_templates[name]
|
||||
template = self.clean_text(template)
|
||||
elif self.templates.has_template(basename):
|
||||
template = self.templates.get_template(basename).source
|
||||
for file in self.files:
|
||||
file_path = Path(file)
|
||||
if file_path.is_absolute():
|
||||
template_path = str(file_path.relative_to("/"))
|
||||
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)
|
||||
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
|
||||
|
||||
def create_files(self) -> None:
|
||||
|
@ -239,24 +317,20 @@ class ConfigService(abc.ABC):
|
|||
:return: nothing
|
||||
"""
|
||||
data = self.data()
|
||||
for name in self.files:
|
||||
basename = pathlib.Path(name).name
|
||||
if name in self.custom_templates:
|
||||
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,
|
||||
for file in sorted(self.files):
|
||||
logger.debug(
|
||||
"node(%s) service(%s) template(%s)", self.node.name, self.name, file
|
||||
)
|
||||
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:
|
||||
"""
|
||||
|
@ -300,7 +374,7 @@ class ConfigService(abc.ABC):
|
|||
del cmds[index]
|
||||
index += 1
|
||||
except CoreCommandError:
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"validate command failed: {cmd}"
|
||||
)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List, Set
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.configservice.base import ConfigService
|
||||
|
||||
|
@ -41,7 +43,7 @@ class ConfigServiceDependencies:
|
|||
for name in self.node_services:
|
||||
service = self.node_services[name]
|
||||
if service.name in self.started:
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"skipping service that will already be started: %s", service.name
|
||||
)
|
||||
continue
|
||||
|
@ -75,7 +77,7 @@ class ConfigServiceDependencies:
|
|||
:param service: service to check dependencies for
|
||||
: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()
|
||||
return self._visit(service)
|
||||
|
||||
|
@ -86,7 +88,7 @@ class ConfigServiceDependencies:
|
|||
:param current_service: service being visited
|
||||
: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.visiting.add(current_service.name)
|
||||
|
||||
|
@ -109,7 +111,7 @@ class ConfigServiceDependencies:
|
|||
self._visit(service)
|
||||
|
||||
# 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.path.append(current_service)
|
||||
self.visiting.remove(current_service.name)
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import logging
|
||||
import pathlib
|
||||
import pkgutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Type
|
||||
|
||||
from core import utils
|
||||
from core import configservices, utils
|
||||
from core.configservice.base import ConfigService
|
||||
from core.errors import CoreError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigServiceManager:
|
||||
"""
|
||||
|
@ -28,7 +32,7 @@ class ConfigServiceManager:
|
|||
"""
|
||||
service_class = self.services.get(name)
|
||||
if service_class is None:
|
||||
raise CoreError(f"service does not exit {name}")
|
||||
raise CoreError(f"service does not exist {name}")
|
||||
return service_class
|
||||
|
||||
def add(self, service: Type[ConfigService]) -> None:
|
||||
|
@ -40,7 +44,7 @@ class ConfigServiceManager:
|
|||
:raises CoreError: when service is a duplicate or has unmet executables
|
||||
"""
|
||||
name = service.name
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"loading service: class(%s) name(%s)", service.__class__.__name__, name
|
||||
)
|
||||
|
||||
|
@ -58,24 +62,43 @@ class ConfigServiceManager:
|
|||
# make service available
|
||||
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
|
||||
:return: list errors when loading and adding services
|
||||
:return: list errors when loading services
|
||||
"""
|
||||
path = pathlib.Path(path)
|
||||
subdirs = [x for x in path.iterdir() if x.is_dir()]
|
||||
subdirs.append(path)
|
||||
service_errors = []
|
||||
for subdir in subdirs:
|
||||
logging.debug("loading config services from: %s", subdir)
|
||||
services = utils.load_classes(str(subdir), ConfigService)
|
||||
logger.debug("loading config services from: %s", subdir)
|
||||
services = utils.load_classes(subdir, ConfigService)
|
||||
for service in services:
|
||||
try:
|
||||
self.add(service)
|
||||
except CoreError as e:
|
||||
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
|
||||
|
|
|
@ -9,6 +9,7 @@ from core.nodes.base import CoreNodeBase
|
|||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
GROUP: str = "Quagga"
|
||||
QUAGGA_STATE_DIR: str = "/var/run/quagga"
|
||||
|
||||
|
@ -101,9 +102,9 @@ class Zebra(ConfigService):
|
|||
ip4s = []
|
||||
ip6s = []
|
||||
for ip4 in iface.ip4s:
|
||||
ip4s.append(str(ip4.ip))
|
||||
ip4s.append(str(ip4))
|
||||
for ip6 in iface.ip6s:
|
||||
ip6s.append(str(ip6.ip))
|
||||
ip6s.append(str(ip6))
|
||||
ifaces.append((iface, ip4s, ip6s, iface.control))
|
||||
|
||||
return dict(
|
||||
|
@ -226,12 +227,6 @@ class Ospfv3mdr(Ospfv3):
|
|||
|
||||
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:
|
||||
config = super().quagga_iface_config(iface)
|
||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
|
|
|
@ -20,20 +20,20 @@ class VpnClient(ConfigService):
|
|||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="keydir",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="keydir",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="Key Dir",
|
||||
default="/etc/core/keys",
|
||||
),
|
||||
Configuration(
|
||||
_id="keyname",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="keyname",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="Key Name",
|
||||
default="client1",
|
||||
),
|
||||
Configuration(
|
||||
_id="server",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="server",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="Server",
|
||||
default="10.0.2.10",
|
||||
),
|
||||
|
@ -54,20 +54,20 @@ class VpnServer(ConfigService):
|
|||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="keydir",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="keydir",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="Key Dir",
|
||||
default="/etc/core/keys",
|
||||
),
|
||||
Configuration(
|
||||
_id="keyname",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="keyname",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="Key Name",
|
||||
default="server",
|
||||
),
|
||||
Configuration(
|
||||
_id="subnet",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="subnet",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="Subnet",
|
||||
default="10.0.200.0",
|
||||
),
|
||||
|
|
|
@ -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 = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
if ip4.size == 1:
|
||||
continue
|
||||
# divide the address space in half
|
||||
index = (ip4.size - 2) / 2
|
||||
rangelow = ip4[index]
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
COREDPY_VERSION = "@PACKAGE_VERSION@"
|
||||
CORE_CONF_DIR = "@CORE_CONF_DIR@"
|
||||
CORE_DATA_DIR = "@CORE_DATA_DIR@"
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
from core.config import Configuration
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
manifest = None
|
||||
try:
|
||||
from emane.shell import manifest
|
||||
|
@ -12,7 +15,7 @@ except ImportError:
|
|||
from emanesh import manifest
|
||||
except ImportError:
|
||||
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:
|
||||
|
@ -71,9 +74,10 @@ def _get_default(config_type_name: str, config_value: List[str]) -> str:
|
|||
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 defaults: used to override default values for configurations
|
||||
|
@ -85,7 +89,7 @@ def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]:
|
|||
return []
|
||||
|
||||
# load configuration file
|
||||
manifest_file = manifest.Manifest(manifest_path)
|
||||
manifest_file = manifest.Manifest(str(manifest_path))
|
||||
manifest_configurations = manifest_file.getAllConfiguration()
|
||||
|
||||
configurations = []
|
||||
|
@ -116,8 +120,8 @@ def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]:
|
|||
config_descriptions = f"{config_descriptions} file"
|
||||
|
||||
configuration = Configuration(
|
||||
_id=config_name,
|
||||
_type=config_type_value,
|
||||
id=config_name,
|
||||
type=config_type_value,
|
||||
default=config_default,
|
||||
options=possible,
|
||||
label=config_descriptions,
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
Defines Emane Models used within CORE.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from core.config import ConfigGroup, Configuration
|
||||
from core.emane import emanemanifest
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import LinkOptions
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
from core.errors import CoreError
|
||||
|
@ -15,6 +14,10 @@ from core.location.mobility import WirelessModel
|
|||
from core.nodes.interface import CoreInterface
|
||||
from core.xml import emanexml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
DEFAULT_DEV: str = "ctrl0"
|
||||
MANIFEST_PATH: str = "share/emane/manifest"
|
||||
|
||||
|
||||
class EmaneModel(WirelessModel):
|
||||
"""
|
||||
|
@ -23,6 +26,17 @@ class EmaneModel(WirelessModel):
|
|||
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
|
||||
mac_library: Optional[str] = None
|
||||
mac_xml: Optional[str] = None
|
||||
|
@ -53,23 +67,37 @@ class EmaneModel(WirelessModel):
|
|||
config_ignore: Set[str] = set()
|
||||
|
||||
@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
|
||||
parsing xml files.
|
||||
Called after being loaded within the EmaneManager. Provides configured
|
||||
emane_prefix for parsing xml files.
|
||||
|
||||
:param emane_prefix: configured emane prefix path
|
||||
:return: nothing
|
||||
"""
|
||||
manifest_path = "share/emane/manifest"
|
||||
cls._load_platform_config(emane_prefix)
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
@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
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
"""
|
||||
|
@ -77,7 +105,9 @@ class EmaneModel(WirelessModel):
|
|||
|
||||
: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
|
||||
def config_groups(cls) -> List[ConfigGroup]:
|
||||
|
@ -86,11 +116,13 @@ class EmaneModel(WirelessModel):
|
|||
|
||||
: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
|
||||
config_len = len(cls.configurations())
|
||||
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("External Parameters", phy_len + 1, config_len),
|
||||
]
|
||||
|
@ -110,13 +142,14 @@ class EmaneModel(WirelessModel):
|
|||
emanexml.create_phy_xml(self, 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.
|
||||
|
||||
:param iface: interface for post startup
|
||||
: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:
|
||||
"""
|
||||
|
@ -128,10 +161,9 @@ class EmaneModel(WirelessModel):
|
|||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
emane_net = self.session.get_node(self.id, EmaneNet)
|
||||
emane_net.setnempositions(moved_ifaces)
|
||||
self.session.emane.set_nem_positions(moved_ifaces)
|
||||
except CoreError:
|
||||
logging.exception("error during update")
|
||||
logger.exception("error during update")
|
||||
|
||||
def linkconfig(
|
||||
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||
|
@ -144,4 +176,4 @@ class EmaneModel(WirelessModel):
|
|||
:param iface2: interface two
|
||||
: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 core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import LinkData
|
||||
from core.emulator.enumerations import LinkTypes, MessageFlags
|
||||
from core.nodes.network import CtrlNet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from emane import shell
|
||||
except ImportError:
|
||||
|
@ -17,12 +20,11 @@ except ImportError:
|
|||
from emanesh import shell
|
||||
except ImportError:
|
||||
shell = None
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emane.emanemanager import EmaneManager
|
||||
|
||||
DEFAULT_PORT: int = 47_000
|
||||
MAC_COMPONENT_INDEX: int = 1
|
||||
EMANE_RFPIPE: str = "rfpipemaclayer"
|
||||
EMANE_80211: str = "ieee80211abgmaclayer"
|
||||
|
@ -77,10 +79,10 @@ class EmaneLink:
|
|||
|
||||
|
||||
class EmaneClient:
|
||||
def __init__(self, address: str) -> None:
|
||||
def __init__(self, address: str, port: int) -> None:
|
||||
self.address: str = address
|
||||
self.client: shell.ControlPortClient = shell.ControlPortClient(
|
||||
self.address, DEFAULT_PORT
|
||||
self.address, port
|
||||
)
|
||||
self.nems: Dict[int, LossTable] = {}
|
||||
self.setup()
|
||||
|
@ -91,7 +93,7 @@ class EmaneClient:
|
|||
# get mac config
|
||||
mac_id, _, emane_model = components[MAC_COMPONENT_INDEX]
|
||||
mac_config = self.client.getConfiguration(mac_id)
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"address(%s) nem(%s) emane(%s)", self.address, nem_id, emane_model
|
||||
)
|
||||
|
||||
|
@ -101,9 +103,9 @@ class EmaneClient:
|
|||
elif emane_model == EMANE_RFPIPE:
|
||||
loss_table = self.handle_rfpipe(mac_config)
|
||||
else:
|
||||
logging.warning("unknown emane link model: %s", emane_model)
|
||||
logger.warning("unknown emane link model: %s", emane_model)
|
||||
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
|
||||
self.nems[nem_id] = loss_table
|
||||
|
||||
|
@ -138,12 +140,12 @@ class EmaneClient:
|
|||
|
||||
def handle_tdma(self, config: Dict[str, Tuple]):
|
||||
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:
|
||||
unicastrate = config["unicastrate"][0][0]
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logging.debug("80211 pcr: %s", pcr)
|
||||
logger.debug("80211 pcr: %s", pcr)
|
||||
tree = etree.parse(pcr)
|
||||
root = tree.getroot()
|
||||
table = root.find("table")
|
||||
|
@ -159,7 +161,7 @@ class EmaneClient:
|
|||
|
||||
def handle_rfpipe(self, config: Dict[str, Tuple]) -> LossTable:
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logging.debug("rfpipe pcr: %s", pcr)
|
||||
logger.debug("rfpipe pcr: %s", pcr)
|
||||
tree = etree.parse(pcr)
|
||||
root = tree.getroot()
|
||||
table = root.find("table")
|
||||
|
@ -187,12 +189,13 @@ class EmaneLinkMonitor:
|
|||
self.running: bool = False
|
||||
|
||||
def start(self) -> None:
|
||||
self.loss_threshold = int(self.emane_manager.get_config("loss_threshold"))
|
||||
self.link_interval = int(self.emane_manager.get_config("link_interval"))
|
||||
self.link_timeout = int(self.emane_manager.get_config("link_timeout"))
|
||||
options = self.emane_manager.session.options
|
||||
self.loss_threshold = options.get_config_int("loss_threshold")
|
||||
self.link_interval = options.get_config_int("link_interval")
|
||||
self.link_timeout = options.get_config_int("link_timeout")
|
||||
self.initialize()
|
||||
if not self.clients:
|
||||
logging.info("no valid emane models to monitor links")
|
||||
logger.info("no valid emane models to monitor links")
|
||||
return
|
||||
self.scheduler = sched.scheduler()
|
||||
self.scheduler.enter(0, 0, self.check_links)
|
||||
|
@ -202,22 +205,28 @@ class EmaneLinkMonitor:
|
|||
|
||||
def initialize(self) -> None:
|
||||
addresses = self.get_addresses()
|
||||
for address in addresses:
|
||||
client = EmaneClient(address)
|
||||
for address, port in addresses:
|
||||
client = EmaneClient(address, port)
|
||||
if client.nems:
|
||||
self.clients.append(client)
|
||||
|
||||
def get_addresses(self) -> List[str]:
|
||||
def get_addresses(self) -> List[Tuple[str, int]]:
|
||||
addresses = []
|
||||
nodes = self.emane_manager.getnodes()
|
||||
for node in nodes:
|
||||
control = None
|
||||
ports = []
|
||||
for iface in node.get_ifaces():
|
||||
if isinstance(iface.net, CtrlNet):
|
||||
ip4 = iface.get_ip4()
|
||||
if ip4:
|
||||
address = str(ip4.ip)
|
||||
addresses.append(address)
|
||||
break
|
||||
control = str(ip4.ip)
|
||||
if isinstance(iface.net, EmaneNet):
|
||||
port = self.emane_manager.get_nem_port(iface)
|
||||
ports.append(port)
|
||||
if control:
|
||||
for port in ports:
|
||||
addresses.append((control, port))
|
||||
return addresses
|
||||
|
||||
def check_links(self) -> None:
|
||||
|
@ -228,7 +237,7 @@ class EmaneLinkMonitor:
|
|||
client.check_links(self.links, self.loss_threshold)
|
||||
except shell.ControlPortException:
|
||||
if self.running:
|
||||
logging.exception("link monitor error")
|
||||
logger.exception("link monitor error")
|
||||
|
||||
# find new links
|
||||
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,6 +1,7 @@
|
|||
"""
|
||||
EMANE Bypass model for CORE
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import List, Set
|
||||
|
||||
from core.config import Configuration
|
||||
|
@ -18,8 +19,8 @@ class EmaneBypassModel(emanemodel.EmaneModel):
|
|||
mac_library: str = "bypassmaclayer"
|
||||
mac_config: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="none",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
id="none",
|
||||
type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
label="There are no parameters for the bypass model.",
|
||||
)
|
||||
|
@ -30,6 +31,5 @@ class EmaneBypassModel(emanemodel.EmaneModel):
|
|||
phy_config: List[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
# ignore default logic
|
||||
pass
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls._load_platform_config(emane_prefix)
|
|
@ -3,7 +3,7 @@ commeffect.py: EMANE CommEffect model for CORE
|
|||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
from lxml import etree
|
||||
|
@ -14,6 +14,8 @@ from core.emulator.data import LinkOptions
|
|||
from core.nodes.interface import CoreInterface
|
||||
from core.xml import emanexml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from emane.events.commeffectevent import CommEffectEvent
|
||||
except ImportError:
|
||||
|
@ -21,7 +23,7 @@ except ImportError:
|
|||
from emanesh.events.commeffectevent import CommEffectEvent
|
||||
except ImportError:
|
||||
CommEffectEvent = None
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
def convert_none(x: float) -> int:
|
||||
|
@ -48,17 +50,26 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
external_config: List[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
shim_xml_path = os.path.join(emane_prefix, "share/emane/manifest", cls.shim_xml)
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
return cls.config_shim
|
||||
return cls.platform_config + cls.config_shim
|
||||
|
||||
@classmethod
|
||||
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:
|
||||
"""
|
||||
|
@ -111,21 +122,15 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
Generate CommEffect events when a Link Message is received having
|
||||
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:
|
||||
logging.warning("%s: missing NEM information", self.name)
|
||||
logger.warning("%s: missing NEM information", self.name)
|
||||
return
|
||||
|
||||
# TODO: batch these into multiple events per transmission
|
||||
# TODO: may want to split out seconds portion of delay and jitter
|
||||
event = CommEffectEvent()
|
||||
nem1 = self.session.emane.get_nem_id(iface)
|
||||
nem2 = self.session.emane.get_nem_id(iface2)
|
||||
logging.info("sending comm effect event")
|
||||
logger.info("sending comm effect event")
|
||||
event.append(
|
||||
nem1,
|
||||
latency=convert_none(options.delay),
|
||||
|
@ -135,4 +140,4 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
unicast=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
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
@ -15,8 +15,8 @@ class EmaneIeee80211abgModel(emanemodel.EmaneModel):
|
|||
mac_xml: str = "ieee80211abgmaclayer.xml"
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix, "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml"
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = str(
|
||||
emane_prefix / "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml"
|
||||
)
|
||||
super().load(emane_prefix)
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
rfpipe.py: EMANE RF-PIPE model for CORE
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
@ -15,8 +15,8 @@ class EmaneRfPipeModel(emanemodel.EmaneModel):
|
|||
mac_xml: str = "rfpipemaclayer.xml"
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix, "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = str(
|
||||
emane_prefix / "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
|
||||
)
|
||||
super().load(emane_prefix)
|
68
daemon/core/emane/models/tdma.py
Normal file
68
daemon/core/emane/models/tdma.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
"""
|
||||
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 Configuration
|
||||
from core.emane import emanemodel
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
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 = Configuration(
|
||||
id=cls.schedule_name,
|
||||
type=ConfigDataTypes.STRING,
|
||||
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
|
||||
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.distributed import DistributedServer
|
||||
|
@ -19,6 +19,8 @@ from core.errors import CoreError
|
|||
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||
from core.nodes.interface import CoreInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emane.emanemodel import EmaneModel
|
||||
from core.emulator.session import Session
|
||||
|
@ -34,7 +36,7 @@ except ImportError:
|
|||
from emanesh.events import LocationEvent
|
||||
except ImportError:
|
||||
LocationEvent = None
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
class EmaneNet(CoreNetworkBase):
|
||||
|
@ -92,9 +94,7 @@ class EmaneNet(CoreNetworkBase):
|
|||
def updatemodel(self, config: Dict[str, str]) -> None:
|
||||
if not self.model:
|
||||
raise CoreError(f"no model set to update for node({self.name})")
|
||||
logging.info(
|
||||
"node(%s) updating model(%s): %s", self.id, self.model.name, config
|
||||
)
|
||||
logger.info("node(%s) updating model(%s): %s", self.id, self.model.name, config)
|
||||
self.model.update_config(config)
|
||||
|
||||
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.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]:
|
||||
links = super().links(flags)
|
||||
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 signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Type
|
||||
|
||||
import core.services
|
||||
from core import configservices, utils
|
||||
from core import utils
|
||||
from core.configservice.manager import ConfigServiceManager
|
||||
from core.emane.modelmanager import EmaneModelManager
|
||||
from core.emulator.session import Session
|
||||
from core.executables import get_requirements
|
||||
from core.services.coreservices import ServiceManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_EMANE_PREFIX: str = "/usr"
|
||||
|
||||
|
||||
def signal_handler(signal_number: int, _) -> None:
|
||||
"""
|
||||
|
@ -21,7 +27,7 @@ def signal_handler(signal_number: int, _) -> None:
|
|||
:param _: ignored
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info("caught signal: %s", signal_number)
|
||||
logger.info("caught signal: %s", signal_number)
|
||||
sys.exit(signal_number)
|
||||
|
||||
|
||||
|
@ -47,8 +53,7 @@ class CoreEmu:
|
|||
os.umask(0)
|
||||
|
||||
# configuration
|
||||
if config is None:
|
||||
config = {}
|
||||
config = config if config else {}
|
||||
self.config: Dict[str, str] = config
|
||||
|
||||
# session management
|
||||
|
@ -56,15 +61,12 @@ class CoreEmu:
|
|||
|
||||
# load services
|
||||
self.service_errors: List[str] = []
|
||||
self.load_services()
|
||||
|
||||
# config services
|
||||
self.service_manager: ConfigServiceManager = ConfigServiceManager()
|
||||
config_services_path = os.path.abspath(os.path.dirname(configservices.__file__))
|
||||
self.service_manager.load(config_services_path)
|
||||
custom_dir = self.config.get("custom_config_services_dir")
|
||||
if custom_dir:
|
||||
self.service_manager.load(custom_dir)
|
||||
self._load_services()
|
||||
|
||||
# check and load emane
|
||||
self.has_emane: bool = False
|
||||
self._load_emane()
|
||||
|
||||
# check executables exist on path
|
||||
self._validate_env()
|
||||
|
@ -83,7 +85,7 @@ class CoreEmu:
|
|||
for requirement in get_requirements(use_ovs):
|
||||
utils.which(requirement, required=True)
|
||||
|
||||
def load_services(self) -> None:
|
||||
def _load_services(self) -> None:
|
||||
"""
|
||||
Loads default and custom services for use within CORE.
|
||||
|
||||
|
@ -91,15 +93,46 @@ class CoreEmu:
|
|||
"""
|
||||
# load default services
|
||||
self.service_errors = core.services.load()
|
||||
|
||||
# load custom services
|
||||
service_paths = self.config.get("custom_services_dir")
|
||||
logging.debug("custom service paths: %s", service_paths)
|
||||
if service_paths:
|
||||
logger.debug("custom service paths: %s", service_paths)
|
||||
if service_paths is not None:
|
||||
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)
|
||||
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:
|
||||
"""
|
||||
|
@ -107,7 +140,7 @@ class CoreEmu:
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info("shutting down all sessions")
|
||||
logger.info("shutting down all sessions")
|
||||
sessions = self.sessions.copy()
|
||||
self.sessions.clear()
|
||||
for _id in sessions:
|
||||
|
@ -128,7 +161,7 @@ class CoreEmu:
|
|||
_id += 1
|
||||
session = _cls(_id, config=self.config)
|
||||
session.service_manager = self.service_manager
|
||||
logging.info("created session: %s", _id)
|
||||
logger.info("created session: %s", _id)
|
||||
self.sessions[_id] = session
|
||||
return session
|
||||
|
||||
|
@ -139,14 +172,14 @@ class CoreEmu:
|
|||
:param _id: session id to delete
|
||||
:return: True if deleted, False otherwise
|
||||
"""
|
||||
logging.info("deleting session: %s", _id)
|
||||
logger.info("deleting session: %s", _id)
|
||||
session = self.sessions.pop(_id, None)
|
||||
result = False
|
||||
if session:
|
||||
logging.info("shutting session down: %s", _id)
|
||||
logger.info("shutting session down: %s", _id)
|
||||
session.data_collect()
|
||||
session.shutdown()
|
||||
result = True
|
||||
else:
|
||||
logging.error("session to delete did not exist: %s", _id)
|
||||
logger.error("session to delete did not exist: %s", _id)
|
||||
return result
|
||||
|
|
|
@ -91,6 +91,7 @@ class NodeOptions:
|
|||
server: str = None
|
||||
image: str = None
|
||||
emane: str = None
|
||||
legacy: bool = False
|
||||
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
"""
|
||||
|
|
|
@ -6,6 +6,7 @@ import logging
|
|||
import os
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
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.network import CoreNetwork, CtrlNet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
@ -61,7 +64,7 @@ class DistributedServer:
|
|||
replace_env = env is not None
|
||||
if not wait:
|
||||
cmd += " &"
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"remote cmd server(%s) cwd(%s) wait(%s): %s", self.host, cwd, wait, cmd
|
||||
)
|
||||
try:
|
||||
|
@ -79,23 +82,23 @@ class DistributedServer:
|
|||
stdout, stderr = e.streams_for_display()
|
||||
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.
|
||||
|
||||
:param source: source file to push
|
||||
:param destination: destination file location
|
||||
:param src_path: source file to push
|
||||
:param dst_path: destination file location
|
||||
:return: nothing
|
||||
"""
|
||||
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
|
||||
intermediate step.
|
||||
|
||||
:param destination: file destination for data
|
||||
:param dst_path: file destination for data
|
||||
:param data: data to store in remote file
|
||||
:return: nothing
|
||||
"""
|
||||
|
@ -103,7 +106,7 @@ class DistributedServer:
|
|||
temp = NamedTemporaryFile(delete=False)
|
||||
temp.write(data.encode("utf-8"))
|
||||
temp.close()
|
||||
self.conn.put(temp.name, destination)
|
||||
self.conn.put(temp.name, str(dst_path))
|
||||
os.unlink(temp.name)
|
||||
|
||||
|
||||
|
@ -144,7 +147,7 @@ class DistributedController:
|
|||
f"command({requirement})"
|
||||
)
|
||||
self.servers[name] = server
|
||||
cmd = f"mkdir -p {self.session.session_dir}"
|
||||
cmd = f"mkdir -p {self.session.directory}"
|
||||
server.remote_cmd(cmd)
|
||||
|
||||
def execute(self, func: Callable[[DistributedServer], None]) -> None:
|
||||
|
@ -170,13 +173,11 @@ class DistributedController:
|
|||
tunnels = self.tunnels[key]
|
||||
for tunnel in tunnels:
|
||||
tunnel.shutdown()
|
||||
|
||||
# remove all remote session directories
|
||||
for name in self.servers:
|
||||
server = self.servers[name]
|
||||
cmd = f"rm -rf {self.session.session_dir}"
|
||||
cmd = f"rm -rf {self.session.directory}"
|
||||
server.remote_cmd(cmd)
|
||||
|
||||
# clear tunnels
|
||||
self.tunnels.clear()
|
||||
|
||||
|
@ -212,23 +213,18 @@ class DistributedController:
|
|||
tunnel = self.tunnels.get(key)
|
||||
if tunnel is not None:
|
||||
return tunnel
|
||||
|
||||
# local to server
|
||||
logging.info(
|
||||
"local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key
|
||||
)
|
||||
logger.info("local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key)
|
||||
local_tap = GreTap(session=self.session, remoteip=host, key=key)
|
||||
local_tap.net_client.set_iface_master(node.brname, local_tap.localname)
|
||||
|
||||
# server to local
|
||||
logging.info(
|
||||
logger.info(
|
||||
"remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key
|
||||
)
|
||||
remote_tap = GreTap(
|
||||
session=self.session, remoteip=self.address, key=key, server=server
|
||||
)
|
||||
remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
|
||||
|
||||
# save tunnels for shutdown
|
||||
tunnel = (local_tap, remote_tap)
|
||||
self.tunnels[key] = tunnel
|
||||
|
@ -244,7 +240,7 @@ class DistributedController:
|
|||
:param node2_id: node two id
|
||||
: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 = (
|
||||
(self.session.id << 16)
|
||||
^ utils.hashkey(node1_id)
|
||||
|
|
|
@ -4,6 +4,7 @@ that manages a CORE session.
|
|||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import pwd
|
||||
import shutil
|
||||
|
@ -62,6 +63,8 @@ from core.services.coreservices import CoreServices
|
|||
from core.xml import corexml, corexmldeployment
|
||||
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
|
||||
NODES: Dict[NodeTypes, Type[NodeBase]] = {
|
||||
NodeTypes.DEFAULT: CoreNode,
|
||||
|
@ -103,13 +106,13 @@ class Session:
|
|||
self.id: int = _id
|
||||
|
||||
# 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:
|
||||
os.mkdir(self.session_dir)
|
||||
self.directory.mkdir()
|
||||
|
||||
self.name: Optional[str] = None
|
||||
self.file_name: Optional[str] = None
|
||||
self.thumbnail: Optional[str] = None
|
||||
self.file_path: Optional[Path] = None
|
||||
self.thumbnail: Optional[Path] = None
|
||||
self.user: Optional[str] = None
|
||||
self.event_loop: EventLoop = EventLoop()
|
||||
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
|
||||
networks are found
|
||||
"""
|
||||
logging.info(
|
||||
logger.info(
|
||||
"handling wireless linking node1(%s) node2(%s): %s",
|
||||
node1.name,
|
||||
node2.name,
|
||||
|
@ -208,7 +211,7 @@ class Session:
|
|||
raise CoreError("no common network found for wireless link/unlink")
|
||||
for common_network, iface1, iface2 in common_networks:
|
||||
if not isinstance(common_network, (WlanNode, EmaneNet)):
|
||||
logging.info(
|
||||
logger.info(
|
||||
"skipping common network that is not wireless/emane: %s",
|
||||
common_network,
|
||||
)
|
||||
|
@ -263,7 +266,7 @@ class Session:
|
|||
else:
|
||||
# peer to peer link
|
||||
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()
|
||||
ptp = self.create_node(PtpNet, start)
|
||||
iface1 = node1.new_iface(ptp, iface1_data)
|
||||
|
@ -286,7 +289,7 @@ class Session:
|
|||
elif isinstance(node1, CoreNetworkBase) and isinstance(
|
||||
node2, CoreNetworkBase
|
||||
):
|
||||
logging.info(
|
||||
logger.info(
|
||||
"linking network to network: %s - %s", node1.name, node2.name
|
||||
)
|
||||
iface1 = node1.linknet(node2)
|
||||
|
@ -303,10 +306,10 @@ class Session:
|
|||
# configure tunnel nodes
|
||||
key = options.key
|
||||
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)
|
||||
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)
|
||||
self.sdt.add_link(node1_id, node2_id)
|
||||
return iface1, iface2
|
||||
|
@ -332,7 +335,7 @@ class Session:
|
|||
"""
|
||||
node1 = self.get_node(node1_id, NodeBase)
|
||||
node2 = self.get_node(node2_id, NodeBase)
|
||||
logging.info(
|
||||
logger.info(
|
||||
"deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)",
|
||||
link_type.name,
|
||||
node1.name,
|
||||
|
@ -409,7 +412,7 @@ class Session:
|
|||
options = LinkOptions()
|
||||
node1 = self.get_node(node1_id, NodeBase)
|
||||
node2 = self.get_node(node2_id, NodeBase)
|
||||
logging.info(
|
||||
logger.info(
|
||||
"update link(%s) node(%s):interface(%s) node(%s):interface(%s)",
|
||||
link_type.name,
|
||||
node1.name,
|
||||
|
@ -525,7 +528,7 @@ class Session:
|
|||
raise CoreError(f"invalid distributed server: {options.server}")
|
||||
|
||||
# create node
|
||||
logging.info(
|
||||
logger.info(
|
||||
"creating node(%s) id(%s) name(%s) start(%s)",
|
||||
_class.__name__,
|
||||
_id,
|
||||
|
@ -542,28 +545,32 @@ class Session:
|
|||
node.canvas = options.canvas
|
||||
|
||||
# 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
|
||||
if isinstance(node, (CoreNode, PhysicalNode)):
|
||||
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)
|
||||
|
||||
# add config services
|
||||
logging.info("setting node config services: %s", options.config_services)
|
||||
for name in options.config_services:
|
||||
config_services = 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)
|
||||
node.add_config_service(service_class)
|
||||
|
||||
# ensure default emane configuration
|
||||
if isinstance(node, EmaneNet) and options.emane:
|
||||
model = self.emane.models.get(options.emane)
|
||||
if not model:
|
||||
raise CoreError(
|
||||
f"node({node.name}) emane model({options.emane}) does not exist"
|
||||
)
|
||||
node.model = model(self, node.id)
|
||||
model_class = self.emane.get_model(options.emane)
|
||||
node.model = model_class(self, node.id)
|
||||
if self.state == EventTypes.RUNTIME_STATE:
|
||||
self.emane.add_node(node)
|
||||
# set default wlan config if needed
|
||||
|
@ -575,51 +582,26 @@ class Session:
|
|||
if self.state == EventTypes.RUNTIME_STATE and is_boot_node:
|
||||
self.write_nodes()
|
||||
self.add_remove_control_iface(node, remove=False)
|
||||
self.services.boot_services(node)
|
||||
self.boot_node(node)
|
||||
|
||||
self.sdt.add_node(node)
|
||||
return node
|
||||
|
||||
def edit_node(self, node_id: int, options: NodeOptions) -> None:
|
||||
"""
|
||||
Edit node information.
|
||||
def set_node_pos(self, node: NodeBase, x: float, y: float) -> None:
|
||||
node.setposition(x, y, None)
|
||||
self.sdt.edit_node(
|
||||
node, node.position.lon, node.position.lat, node.position.alt
|
||||
)
|
||||
|
||||
:param node_id: id of node to update
|
||||
: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:
|
||||
def set_node_geo(self, node: NodeBase, lon: float, lat: float, alt: float) -> None:
|
||||
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.position.set_geo(lon, lat, alt)
|
||||
self.broadcast_node(node)
|
||||
elif not has_empty_position:
|
||||
node.setposition(x, y, None)
|
||||
self.sdt.edit_node(node, lon, lat, alt)
|
||||
|
||||
def start_mobility(self, node_ids: List[int] = None) -> None:
|
||||
"""
|
||||
|
@ -638,45 +620,42 @@ class Session:
|
|||
:return: True if active, False otherwise
|
||||
"""
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
: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
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info("opening xml: %s", file_name)
|
||||
|
||||
logger.info("opening xml: %s", file_path)
|
||||
# clear out existing session
|
||||
self.clear()
|
||||
|
||||
# set state and read xml
|
||||
state = EventTypes.CONFIGURATION_STATE if start else EventTypes.DEFINITION_STATE
|
||||
self.set_state(state)
|
||||
self.name = os.path.basename(file_name)
|
||||
self.file_name = file_name
|
||||
CoreXmlReader(self).read(file_name)
|
||||
|
||||
self.name = file_path.name
|
||||
self.file_path = file_path
|
||||
CoreXmlReader(self).read(file_path)
|
||||
# start session if needed
|
||||
if start:
|
||||
self.set_state(EventTypes.INSTANTIATION_STATE)
|
||||
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.
|
||||
|
||||
:param file_name: file name to write session xml to
|
||||
:param file_path: file name to write session xml to
|
||||
:return: nothing
|
||||
"""
|
||||
CoreXmlWriter(self).write(file_name)
|
||||
CoreXmlWriter(self).write(file_path)
|
||||
|
||||
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:
|
||||
"""
|
||||
Store a hook from a received file message.
|
||||
|
@ -684,11 +663,11 @@ class Session:
|
|||
:param state: when to run hook
|
||||
:param file_name: file name for hook
|
||||
:param data: hook data
|
||||
:param source_name: source name
|
||||
:param src_name: source name
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info(
|
||||
"setting state hook: %s - %s source(%s)", state, file_name, source_name
|
||||
logger.info(
|
||||
"setting state hook: %s - %s source(%s)", state, file_name, src_name
|
||||
)
|
||||
hook = file_name, data
|
||||
state_hooks = self.hooks.setdefault(state, [])
|
||||
|
@ -696,26 +675,26 @@ class Session:
|
|||
|
||||
# immediately run a hook if it is in the current state
|
||||
if self.state == state:
|
||||
logging.info("immediately running new state hook")
|
||||
logger.info("immediately running new state hook")
|
||||
self.run_hook(hook)
|
||||
|
||||
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:
|
||||
"""
|
||||
Add a file to a node.
|
||||
|
||||
:param node_id: node to add file to
|
||||
:param source_name: source file name
|
||||
:param file_name: file name to add
|
||||
:param src_path: source file path
|
||||
:param file_path: file path to add
|
||||
:param data: file data
|
||||
:return: nothing
|
||||
"""
|
||||
node = self.get_node(node_id, CoreNodeBase)
|
||||
if source_name is not None:
|
||||
node.addfile(source_name, file_name)
|
||||
node = self.get_node(node_id, CoreNode)
|
||||
if src_path is not None:
|
||||
node.addfile(src_path, file_path)
|
||||
elif data is not None:
|
||||
node.nodefile(file_name, data)
|
||||
node.create_file(file_path, data)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""
|
||||
|
@ -769,9 +748,9 @@ class Session:
|
|||
Shutdown all session nodes and remove the session directory.
|
||||
"""
|
||||
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:
|
||||
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)
|
||||
# clear out current core session
|
||||
self.clear()
|
||||
|
@ -780,7 +759,7 @@ class Session:
|
|||
# remove this sessions working directory
|
||||
preserve = self.options.get_config("preservedir") == "1"
|
||||
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:
|
||||
"""
|
||||
|
@ -864,7 +843,7 @@ class Session:
|
|||
return
|
||||
self.state = state
|
||||
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.run_hooks(state)
|
||||
self.run_state_hooks(state)
|
||||
|
@ -879,12 +858,12 @@ class Session:
|
|||
:param state: state to write to file
|
||||
:return: nothing
|
||||
"""
|
||||
state_file = os.path.join(self.session_dir, "state")
|
||||
state_file = self.directory / "state"
|
||||
try:
|
||||
with open(state_file, "w") as f:
|
||||
with state_file.open("w") as f:
|
||||
f.write(f"{state.value} {state.name}\n")
|
||||
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:
|
||||
"""
|
||||
|
@ -906,24 +885,24 @@ class Session:
|
|||
:return: nothing
|
||||
"""
|
||||
file_name, data = hook
|
||||
logging.info("running hook %s", file_name)
|
||||
file_path = os.path.join(self.session_dir, file_name)
|
||||
log_path = os.path.join(self.session_dir, f"{file_name}.log")
|
||||
logger.info("running hook %s", file_name)
|
||||
file_path = self.directory / file_name
|
||||
log_path = self.directory / f"{file_name}.log"
|
||||
try:
|
||||
with open(file_path, "w") as f:
|
||||
with file_path.open("w") as f:
|
||||
f.write(data)
|
||||
with open(log_path, "w") as f:
|
||||
with log_path.open("w") as f:
|
||||
args = ["/bin/sh", file_name]
|
||||
subprocess.check_call(
|
||||
args,
|
||||
stdout=f,
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
cwd=self.session_dir,
|
||||
cwd=self.directory,
|
||||
env=self.get_environment(),
|
||||
)
|
||||
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:
|
||||
"""
|
||||
|
@ -940,7 +919,7 @@ class Session:
|
|||
hook(state)
|
||||
except Exception:
|
||||
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)
|
||||
|
||||
def add_state_hook(
|
||||
|
@ -983,10 +962,10 @@ class Session:
|
|||
"""
|
||||
self.emane.poststartup()
|
||||
# create session deployed xml
|
||||
xml_file_name = os.path.join(self.session_dir, "session-deployed.xml")
|
||||
xml_writer = corexml.CoreXmlWriter(self)
|
||||
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]:
|
||||
"""
|
||||
|
@ -1001,9 +980,9 @@ class Session:
|
|||
env["CORE_PYTHON"] = sys.executable
|
||||
env["SESSION"] = str(self.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_FILENAME"] = str(self.file_name)
|
||||
env["SESSION_FILENAME"] = str(self.file_path)
|
||||
env["SESSION_USER"] = str(self.user)
|
||||
if state:
|
||||
env["SESSION_STATE"] = str(self.state)
|
||||
|
@ -1011,8 +990,8 @@ class Session:
|
|||
# /etc/core/environment
|
||||
# /home/user/.core/environment
|
||||
# /tmp/pycore.<session id>/environment
|
||||
core_env_path = Path(constants.CORE_CONF_DIR) / "environment"
|
||||
session_env_path = Path(self.session_dir) / "environment"
|
||||
core_env_path = constants.CORE_CONF_DIR / "environment"
|
||||
session_env_path = self.directory / "environment"
|
||||
if self.user:
|
||||
user_home_path = Path(f"~{self.user}").expanduser()
|
||||
user_env1 = user_home_path / ".core" / "environment"
|
||||
|
@ -1025,23 +1004,23 @@ class Session:
|
|||
try:
|
||||
utils.load_config(path, env)
|
||||
except IOError:
|
||||
logging.exception("error reading environment file: %s", path)
|
||||
logger.exception("error reading environment file: %s", path)
|
||||
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.
|
||||
|
||||
:param thumb_file: tumbnail file to set for session
|
||||
:return: nothing
|
||||
"""
|
||||
if not os.path.exists(thumb_file):
|
||||
logging.error("thumbnail file to set does not exist: %s", thumb_file)
|
||||
if not thumb_file.is_file():
|
||||
logger.error("thumbnail file to set does not exist: %s", thumb_file)
|
||||
self.thumbnail = None
|
||||
return
|
||||
destination_file = os.path.join(self.session_dir, os.path.basename(thumb_file))
|
||||
shutil.copy(thumb_file, destination_file)
|
||||
self.thumbnail = destination_file
|
||||
dst_path = self.directory / thumb_file.name
|
||||
shutil.copy(thumb_file, dst_path)
|
||||
self.thumbnail = dst_path
|
||||
|
||||
def set_user(self, user: str) -> None:
|
||||
"""
|
||||
|
@ -1054,10 +1033,10 @@ class Session:
|
|||
if user:
|
||||
try:
|
||||
uid = pwd.getpwnam(user).pw_uid
|
||||
gid = os.stat(self.session_dir).st_gid
|
||||
os.chown(self.session_dir, uid, gid)
|
||||
gid = self.directory.stat().st_gid
|
||||
os.chown(self.directory, uid, gid)
|
||||
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
|
||||
|
||||
def create_node(
|
||||
|
@ -1114,7 +1093,7 @@ class Session:
|
|||
with self.nodes_lock:
|
||||
if _id in self.nodes:
|
||||
node = self.nodes.pop(_id)
|
||||
logging.info("deleted node(%s)", node.name)
|
||||
logger.info("deleted node(%s)", node.name)
|
||||
if node:
|
||||
node.shutdown()
|
||||
self.sdt.delete_node(_id)
|
||||
|
@ -1140,14 +1119,14 @@ class Session:
|
|||
Write nodes to a 'nodes' file in the session dir.
|
||||
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:
|
||||
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():
|
||||
f.write(f"{_id} {node.name} {node.apitype} {type(node)}\n")
|
||||
except IOError:
|
||||
logging.exception("error writing nodes file")
|
||||
logger.exception("error writing nodes file")
|
||||
|
||||
def exception(
|
||||
self, level: ExceptionLevels, source: str, text: str, node_id: int = None
|
||||
|
@ -1238,13 +1217,13 @@ class Session:
|
|||
"""
|
||||
# this is called from instantiate() after receiving an event message
|
||||
# for the instantiation state
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"session(%s) checking if not in runtime state, current state: %s",
|
||||
self.id,
|
||||
self.state.name,
|
||||
)
|
||||
if self.state == EventTypes.RUNTIME_STATE:
|
||||
logging.info("valid runtime state found, returning")
|
||||
logger.info("valid runtime state found, returning")
|
||||
return
|
||||
# start event loop and set to runtime
|
||||
self.event_loop.run()
|
||||
|
@ -1258,23 +1237,21 @@ class Session:
|
|||
:return: nothing
|
||||
"""
|
||||
if self.state.already_collected():
|
||||
logging.info(
|
||||
logger.info(
|
||||
"session(%s) state(%s) already data collected", self.id, self.state
|
||||
)
|
||||
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)
|
||||
|
||||
# stop event loop
|
||||
self.event_loop.stop()
|
||||
|
||||
# stop node services
|
||||
# stop mobility and node services
|
||||
with self.nodes_lock:
|
||||
funcs = []
|
||||
for node_id in self.nodes:
|
||||
node = self.nodes[node_id]
|
||||
if not isinstance(node, CoreNodeBase) or not node.up:
|
||||
continue
|
||||
for node in self.nodes.values():
|
||||
if isinstance(node, CoreNodeBase) and node.up:
|
||||
args = (node,)
|
||||
funcs.append((self.services.stop_services, args, {}))
|
||||
utils.threadpool(funcs)
|
||||
|
@ -1307,7 +1284,7 @@ class Session:
|
|||
:param node: node to boot
|
||||
: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)
|
||||
node.start_config_services()
|
||||
|
||||
|
@ -1328,7 +1305,7 @@ class Session:
|
|||
funcs.append((self.boot_node, (node,), {}))
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
total = time.monotonic() - start
|
||||
logging.debug("boot run time: %s", total)
|
||||
logger.debug("boot run time: %s", total)
|
||||
if not exceptions:
|
||||
self.update_control_iface_hosts()
|
||||
return exceptions
|
||||
|
@ -1356,7 +1333,7 @@ class Session:
|
|||
"""
|
||||
d0 = self.options.get_config("controlnetif0")
|
||||
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")
|
||||
d2 = self.options.get_config("controlnetif2")
|
||||
d3 = self.options.get_config("controlnetif3")
|
||||
|
@ -1401,7 +1378,7 @@ class Session:
|
|||
:param conf_required: flag to check if conf is required
|
||||
:return: control net node
|
||||
"""
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"add/remove control net: index(%s) remove(%s) conf_required(%s)",
|
||||
net_index,
|
||||
remove,
|
||||
|
@ -1415,7 +1392,7 @@ class Session:
|
|||
return None
|
||||
else:
|
||||
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]
|
||||
|
||||
# return any existing controlnet bridge
|
||||
|
@ -1438,7 +1415,7 @@ class Session:
|
|||
if net_index == 0:
|
||||
updown_script = self.options.get_config("controlnet_updown_script")
|
||||
if not updown_script:
|
||||
logging.debug("controlnet updown script not configured")
|
||||
logger.debug("controlnet updown script not configured")
|
||||
|
||||
prefixes = prefix_spec.split()
|
||||
if len(prefixes) > 1:
|
||||
|
@ -1452,7 +1429,7 @@ class Session:
|
|||
else:
|
||||
prefix = prefixes[0]
|
||||
|
||||
logging.info(
|
||||
logger.info(
|
||||
"controlnet(%s) prefix(%s) updown(%s) serverintf(%s)",
|
||||
_id,
|
||||
prefix,
|
||||
|
@ -1515,7 +1492,7 @@ class Session:
|
|||
msg = f"Control interface not added to node {node.id}. "
|
||||
msg += f"Invalid control network prefix ({control_net.prefix}). "
|
||||
msg += "A longer prefix length may be required for this many nodes."
|
||||
logging.exception(msg)
|
||||
logger.exception(msg)
|
||||
|
||||
def update_control_iface_hosts(
|
||||
self, net_index: int = 0, remove: bool = False
|
||||
|
@ -1533,12 +1510,12 @@ class Session:
|
|||
try:
|
||||
control_net = self.get_control_net(net_index)
|
||||
except CoreError:
|
||||
logging.exception("error retrieving control net node")
|
||||
logger.exception("error retrieving control net node")
|
||||
return
|
||||
|
||||
header = f"CORE session {self.id} host entries"
|
||||
if remove:
|
||||
logging.info("Removing /etc/hosts file entries.")
|
||||
logger.info("Removing /etc/hosts file entries.")
|
||||
utils.file_demunge("/etc/hosts", header)
|
||||
return
|
||||
|
||||
|
@ -1548,7 +1525,7 @@ class Session:
|
|||
for ip in iface.ips():
|
||||
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")
|
||||
|
||||
def runtime(self) -> float:
|
||||
|
@ -1577,7 +1554,7 @@ class Session:
|
|||
current_time = self.runtime()
|
||||
if current_time > 0:
|
||||
if event_time <= current_time:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"could not schedule past event for time %s (run time is now %s)",
|
||||
event_time,
|
||||
current_time,
|
||||
|
@ -1589,7 +1566,7 @@ class Session:
|
|||
)
|
||||
if not name:
|
||||
name = ""
|
||||
logging.info(
|
||||
logger.info(
|
||||
"scheduled event %s at time %s data=%s",
|
||||
name,
|
||||
event_time + current_time,
|
||||
|
@ -1608,12 +1585,12 @@ class Session:
|
|||
:return: nothing
|
||||
"""
|
||||
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
|
||||
now = self.runtime()
|
||||
if not 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:
|
||||
utils.mute_detach(data)
|
||||
else:
|
||||
|
|
|
@ -13,51 +13,87 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
|||
name: str = "session"
|
||||
options: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network"
|
||||
id="controlnet", type=ConfigDataTypes.STRING, label="Control Network"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet0", _type=ConfigDataTypes.STRING, label="Control Network 0"
|
||||
id="controlnet0", type=ConfigDataTypes.STRING, label="Control Network 0"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet1", _type=ConfigDataTypes.STRING, label="Control Network 1"
|
||||
id="controlnet1", type=ConfigDataTypes.STRING, label="Control Network 1"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet2", _type=ConfigDataTypes.STRING, label="Control Network 2"
|
||||
id="controlnet2", type=ConfigDataTypes.STRING, label="Control Network 2"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet3", _type=ConfigDataTypes.STRING, label="Control Network 3"
|
||||
id="controlnet3", type=ConfigDataTypes.STRING, label="Control Network 3"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet_updown_script",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="controlnet_updown_script",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="Control Network Script",
|
||||
),
|
||||
Configuration(
|
||||
_id="enablerj45",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
id="enablerj45",
|
||||
type=ConfigDataTypes.BOOL,
|
||||
default="1",
|
||||
label="Enable RJ45s",
|
||||
),
|
||||
Configuration(
|
||||
_id="preservedir",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
id="preservedir",
|
||||
type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
label="Preserve session dir",
|
||||
),
|
||||
Configuration(
|
||||
_id="enablesdt",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
id="enablesdt",
|
||||
type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
label="Enable SDT3D output",
|
||||
),
|
||||
Configuration(
|
||||
_id="sdturl",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="sdturl",
|
||||
type=ConfigDataTypes.STRING,
|
||||
default=Sdt.DEFAULT_SDT_URL,
|
||||
label="SDT3D URL",
|
||||
),
|
||||
Configuration(
|
||||
_id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS"
|
||||
id="ovs", type=ConfigDataTypes.BOOL, default="0", label="Enable OVS"
|
||||
),
|
||||
Configuration(
|
||||
id="platform_id_start",
|
||||
type=ConfigDataTypes.INT32,
|
||||
default="1",
|
||||
label="EMANE Platform ID Start",
|
||||
),
|
||||
Configuration(
|
||||
id="nem_id_start",
|
||||
type=ConfigDataTypes.INT32,
|
||||
default="1",
|
||||
label="EMANE NEM ID Start",
|
||||
),
|
||||
Configuration(
|
||||
id="link_enabled",
|
||||
type=ConfigDataTypes.BOOL,
|
||||
default="1",
|
||||
label="EMANE Links?",
|
||||
),
|
||||
Configuration(
|
||||
id="loss_threshold",
|
||||
type=ConfigDataTypes.INT32,
|
||||
default="30",
|
||||
label="EMANE Link Loss Threshold (%)",
|
||||
),
|
||||
Configuration(
|
||||
id="link_interval",
|
||||
type=ConfigDataTypes.INT32,
|
||||
default="1",
|
||||
label="EMANE Link Check Interval (sec)",
|
||||
),
|
||||
Configuration(
|
||||
id="link_timeout",
|
||||
type=ConfigDataTypes.INT32,
|
||||
default="4",
|
||||
label="EMANE Link Timeout (sec)",
|
||||
),
|
||||
]
|
||||
config_type: RegisterTlvs = RegisterTlvs.UTILITY
|
||||
|
@ -112,3 +148,13 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
|||
if value is not None:
|
||||
value = int(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
|
||||
|
||||
|
||||
class CoreConfigError(Exception):
|
||||
"""
|
||||
Used when there is an error defining a configurable option.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
@ -7,15 +7,15 @@ SYSCTL: str = "sysctl"
|
|||
IP: str = "ip"
|
||||
ETHTOOL: str = "ethtool"
|
||||
TC: str = "tc"
|
||||
EBTABLES: str = "ebtables"
|
||||
MOUNT: str = "mount"
|
||||
UMOUNT: str = "umount"
|
||||
OVS_VSCTL: str = "ovs-vsctl"
|
||||
TEST: str = "test"
|
||||
NFTABLES: str = "nft"
|
||||
|
||||
COMMON_REQUIREMENTS: List[str] = [
|
||||
BASH,
|
||||
EBTABLES,
|
||||
NFTABLES,
|
||||
ETHTOOL,
|
||||
IP,
|
||||
MOUNT,
|
||||
|
|
|
@ -22,6 +22,7 @@ from core.gui.statusbar import StatusBar
|
|||
from core.gui.themes import PADY
|
||||
from core.gui.toolbar import Toolbar
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
WIDTH: int = 1000
|
||||
HEIGHT: int = 800
|
||||
|
||||
|
@ -171,7 +172,7 @@ class Application(ttk.Frame):
|
|||
def show_grpc_exception(
|
||||
self, message: str, e: grpc.RpcError, blocking: bool = False
|
||||
) -> 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())
|
||||
if blocking:
|
||||
dialog.show()
|
||||
|
@ -179,7 +180,7 @@ class Application(ttk.Frame):
|
|||
self.after(0, lambda: dialog.show())
|
||||
|
||||
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(
|
||||
0, lambda: ErrorDialog(self, "App Exception", message, str(e)).show()
|
||||
)
|
||||
|
|
|
@ -120,25 +120,18 @@ class IpConfigs(yaml.YAMLObject):
|
|||
yaml_tag: str = "!IpConfigs"
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ip4: str = None,
|
||||
ip6: str = None,
|
||||
ip4s: List[str] = None,
|
||||
ip6s: List[str] = None,
|
||||
) -> None:
|
||||
if ip4s is None:
|
||||
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
|
||||
self.ip4s: List[str] = ip4s
|
||||
if ip6s is None:
|
||||
ip6s = ["2001::", "2002::", "a::"]
|
||||
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
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__setstate__(kwargs)
|
||||
|
||||
def __setstate__(self, kwargs):
|
||||
self.ip4s: List[str] = kwargs.get(
|
||||
"ip4s", ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
|
||||
)
|
||||
self.ip4: str = kwargs.get("ip4", self.ip4s[0])
|
||||
self.ip6s: List[str] = kwargs.get("ip6s", ["2001::", "2002::", "a::"])
|
||||
self.ip6: str = kwargs.get("ip6", self.ip6s[0])
|
||||
self.enable_ip4: bool = kwargs.get("enable_ip4", True)
|
||||
self.enable_ip6: bool = kwargs.get("enable_ip6", True)
|
||||
|
||||
|
||||
class GuiConfig(yaml.YAMLObject):
|
||||
|
@ -223,7 +216,7 @@ def check_directory() -> None:
|
|||
|
||||
def read() -> GuiConfig:
|
||||
with CONFIG_PATH.open("r") as f:
|
||||
return yaml.load(f, Loader=yaml.SafeLoader)
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def save(config: GuiConfig) -> None:
|
||||
|
|
|
@ -6,23 +6,18 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from tkinter import messagebox
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import (
|
||||
client,
|
||||
configservices_pb2,
|
||||
core_pb2,
|
||||
emane_pb2,
|
||||
mobility_pb2,
|
||||
services_pb2,
|
||||
wlan_pb2,
|
||||
)
|
||||
from core.api.grpc import client, configservices_pb2, core_pb2
|
||||
from core.api.grpc.wrappers import (
|
||||
ConfigOption,
|
||||
ConfigService,
|
||||
EmaneModelConfig,
|
||||
Event,
|
||||
ExceptionEvent,
|
||||
Link,
|
||||
LinkEvent,
|
||||
|
@ -33,6 +28,9 @@ from core.api.grpc.wrappers import (
|
|||
NodeServiceData,
|
||||
NodeType,
|
||||
Position,
|
||||
Server,
|
||||
ServiceConfig,
|
||||
ServiceFileConfig,
|
||||
Session,
|
||||
SessionLocation,
|
||||
SessionState,
|
||||
|
@ -45,10 +43,11 @@ from core.gui.dialogs.mobilityplayer import MobilityPlayer
|
|||
from core.gui.dialogs.sessions import SessionsDialog
|
||||
from core.gui.graph.edges import CanvasEdge
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.graph.shape import Shape
|
||||
from core.gui.interface import InterfaceManager
|
||||
from core.gui.nodeutils import NodeDraw
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -77,6 +76,7 @@ class CoreClient:
|
|||
self.config_services: Dict[str, ConfigService] = {}
|
||||
|
||||
# loaded configuration data
|
||||
self.emane_models: List[str] = []
|
||||
self.servers: Dict[str, CoreServer] = {}
|
||||
self.custom_nodes: Dict[str, NodeDraw] = {}
|
||||
self.custom_observers: Dict[str, Observer] = {}
|
||||
|
@ -98,8 +98,7 @@ class CoreClient:
|
|||
@property
|
||||
def client(self) -> client.CoreGrpcClient:
|
||||
if self.session:
|
||||
response = self._client.check_session(self.session.id)
|
||||
if not response.result:
|
||||
if not self._client.check_session(self.session.id):
|
||||
throughputs_enabled = self.handling_throughputs is not None
|
||||
self.cancel_throughputs()
|
||||
self.cancel_events()
|
||||
|
@ -150,22 +149,20 @@ class CoreClient:
|
|||
for observer in self.app.guiconfig.observers:
|
||||
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:
|
||||
return
|
||||
if event.session_id != self.session.id:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"ignoring event session(%s) current(%s)",
|
||||
event.session_id,
|
||||
self.session.id,
|
||||
)
|
||||
return
|
||||
|
||||
if event.HasField("link_event"):
|
||||
link_event = LinkEvent.from_proto(event.link_event)
|
||||
self.app.after(0, self.handle_link_event, link_event)
|
||||
elif event.HasField("session_event"):
|
||||
logging.info("session event: %s", event)
|
||||
if event.link_event:
|
||||
self.app.after(0, self.handle_link_event, event.link_event)
|
||||
elif event.session_event:
|
||||
logger.info("session event: %s", event)
|
||||
session_event = event.session_event
|
||||
if session_event.event <= SessionState.SHUTDOWN.value:
|
||||
self.session.state = SessionState(session_event.event)
|
||||
|
@ -180,24 +177,22 @@ class CoreClient:
|
|||
else:
|
||||
dialog.set_pause()
|
||||
else:
|
||||
logging.warning("unknown session event: %s", session_event)
|
||||
elif event.HasField("node_event"):
|
||||
node_event = NodeEvent.from_proto(event.node_event)
|
||||
self.app.after(0, self.handle_node_event, node_event)
|
||||
elif event.HasField("config_event"):
|
||||
logging.info("config event: %s", event)
|
||||
elif event.HasField("exception_event"):
|
||||
event = ExceptionEvent.from_proto(event.session_id, event.exception_event)
|
||||
self.handle_exception_event(event)
|
||||
logger.warning("unknown session event: %s", session_event)
|
||||
elif event.node_event:
|
||||
self.app.after(0, self.handle_node_event, event.node_event)
|
||||
elif event.config_event:
|
||||
logger.info("config event: %s", event)
|
||||
elif event.exception_event:
|
||||
self.handle_exception_event(event.exception_event)
|
||||
else:
|
||||
logging.info("unhandled event: %s", event)
|
||||
logger.info("unhandled event: %s", event)
|
||||
|
||||
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
|
||||
node2_id = event.link.node2_id
|
||||
if node1_id == node2_id:
|
||||
logging.warning("ignoring links with loops: %s", event)
|
||||
logger.warning("ignoring links with loops: %s", event)
|
||||
return
|
||||
canvas_node1 = self.canvas_nodes[node1_id]
|
||||
canvas_node2 = self.canvas_nodes[node2_id]
|
||||
|
@ -215,7 +210,7 @@ class CoreClient:
|
|||
canvas_node1, canvas_node2, event.link
|
||||
)
|
||||
else:
|
||||
logging.warning("unknown link event: %s", event)
|
||||
logger.warning("unknown link event: %s", event)
|
||||
else:
|
||||
if event.message_type == MessageType.ADD:
|
||||
self.app.manager.add_wired_edge(canvas_node1, canvas_node2, event.link)
|
||||
|
@ -224,10 +219,10 @@ class CoreClient:
|
|||
elif event.message_type == MessageType.NONE:
|
||||
self.app.manager.update_wired_edge(event.link)
|
||||
else:
|
||||
logging.warning("unknown link event: %s", event)
|
||||
logger.warning("unknown link event: %s", event)
|
||||
|
||||
def handle_node_event(self, event: NodeEvent) -> None:
|
||||
logging.debug("node event: %s", event)
|
||||
logger.debug("node event: %s", event)
|
||||
node = event.node
|
||||
if event.message_type == MessageType.NONE:
|
||||
canvas_node = self.canvas_nodes[node.id]
|
||||
|
@ -238,15 +233,13 @@ class CoreClient:
|
|||
canvas_node.update_icon(node.icon)
|
||||
elif event.message_type == MessageType.DELETE:
|
||||
canvas_node = self.canvas_nodes[node.id]
|
||||
canvas_node.canvas.clear_selection()
|
||||
canvas_node.canvas.select_object(canvas_node.id)
|
||||
canvas_node.canvas.delete_selected_objects()
|
||||
canvas_node.canvas_delete()
|
||||
elif event.message_type == MessageType.ADD:
|
||||
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)
|
||||
else:
|
||||
logging.warning("unknown node event: %s", event)
|
||||
logger.warning("unknown node event: %s", event)
|
||||
|
||||
def enable_throughputs(self) -> None:
|
||||
self.handling_throughputs = self.client.throughputs(
|
||||
|
@ -278,34 +271,35 @@ class CoreClient:
|
|||
CPU_USAGE_DELAY, self.handle_cpu_event
|
||||
)
|
||||
|
||||
def handle_throughputs(self, event: core_pb2.ThroughputsEvent) -> None:
|
||||
event = ThroughputsEvent.from_proto(event)
|
||||
def handle_throughputs(self, event: ThroughputsEvent) -> None:
|
||||
if event.session_id != self.session.id:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"ignoring throughput event session(%s) current(%s)",
|
||||
event.session_id,
|
||||
self.session.id,
|
||||
)
|
||||
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)
|
||||
|
||||
def handle_cpu_event(self, event: core_pb2.CpuUsageEvent) -> None:
|
||||
self.app.after(0, self.app.statusbar.set_cpu, event.usage)
|
||||
|
||||
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)
|
||||
|
||||
def join_session(self, session_id: int) -> 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)
|
||||
def update_session_title(self) -> None:
|
||||
title_file = self.session.file.name if self.session.file else ""
|
||||
self.master.title(f"CORE Session({self.session.id}) {title_file}")
|
||||
|
||||
def join_session(self, session_id: int) -> None:
|
||||
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.session.id, self.handle_events
|
||||
)
|
||||
|
@ -320,53 +314,14 @@ class CoreClient:
|
|||
def is_runtime(self) -> bool:
|
||||
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:
|
||||
"""
|
||||
Create a new session
|
||||
"""
|
||||
try:
|
||||
response = self.client.create_session()
|
||||
logging.info("created session: %s", response)
|
||||
self.join_session(response.session_id)
|
||||
session = self.client.create_session()
|
||||
logger.info("created session: %s", session.id)
|
||||
self.join_session(session.id)
|
||||
location_config = self.app.guiconfig.location
|
||||
self.session.location = SessionLocation(
|
||||
x=location_config.x,
|
||||
|
@ -387,7 +342,7 @@ class CoreClient:
|
|||
session_id = self.session.id
|
||||
try:
|
||||
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:
|
||||
self.app.show_grpc_exception("Delete Session Error", e)
|
||||
|
||||
|
@ -397,23 +352,21 @@ class CoreClient:
|
|||
"""
|
||||
try:
|
||||
self.client.connect()
|
||||
# get all available services
|
||||
response = self.client.get_services()
|
||||
for service in response.services:
|
||||
# get current core configurations services/config services
|
||||
core_config = self.client.get_config()
|
||||
self.emane_models = sorted(core_config.emane_models)
|
||||
for service in core_config.services:
|
||||
group_services = self.services.setdefault(service.group, set())
|
||||
group_services.add(service.name)
|
||||
# get config service informations
|
||||
response = self.client.get_config_services()
|
||||
for service in response.services:
|
||||
self.config_services[service.name] = ConfigService.from_proto(service)
|
||||
for service in core_config.config_services:
|
||||
self.config_services[service.name] = service
|
||||
group_services = self.config_services_groups.setdefault(
|
||||
service.group, set()
|
||||
)
|
||||
group_services.add(service.name)
|
||||
# join provided session, create new session, or show dialog to select an
|
||||
# existing session
|
||||
response = self.client.get_sessions()
|
||||
sessions = response.sessions
|
||||
sessions = self.client.get_sessions()
|
||||
if session_id:
|
||||
session_ids = set(x.id for x in sessions)
|
||||
if session_id not in session_ids:
|
||||
|
@ -432,71 +385,50 @@ class CoreClient:
|
|||
dialog = SessionsDialog(self.app, True)
|
||||
dialog.show()
|
||||
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.close()
|
||||
|
||||
def edit_node(self, core_node: Node) -> None:
|
||||
try:
|
||||
position = core_node.position.to_proto()
|
||||
self.client.edit_node(
|
||||
self.session.id, core_node.id, position, source=GUI_SOURCE
|
||||
self.client.move_node(
|
||||
self.session.id, core_node.id, core_node.position, source=GUI_SOURCE
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Edit Node Error", e)
|
||||
|
||||
def send_servers(self) -> None:
|
||||
for server in self.servers.values():
|
||||
self.client.add_session_server(self.session.id, server.name, server.address)
|
||||
|
||||
def start_session(self) -> Tuple[bool, List[str]]:
|
||||
def get_links(self, definition: bool = False) -> List[Link]:
|
||||
if not definition:
|
||||
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 = []
|
||||
asymmetric_links = []
|
||||
for edge in self.links.values():
|
||||
link = edge.link
|
||||
if not definition:
|
||||
if link.iface1 and not link.iface1.mac:
|
||||
link.iface1.mac = self.ifaces_manager.next_mac()
|
||||
if link.iface2 and not link.iface2.mac:
|
||||
link.iface2.mac = self.ifaces_manager.next_mac()
|
||||
links.append(link.to_proto())
|
||||
links.append(link)
|
||||
if edge.asymmetric_link:
|
||||
asymmetric_links.append(edge.asymmetric_link.to_proto())
|
||||
wlan_configs = self.get_wlan_configs_proto()
|
||||
mobility_configs = self.get_mobility_configs_proto()
|
||||
emane_model_configs = self.get_emane_model_configs_proto()
|
||||
hooks = [x.to_proto() for x in self.session.hooks.values()]
|
||||
service_configs = self.get_service_configs_proto()
|
||||
file_configs = self.get_service_file_configs_proto()
|
||||
config_service_configs = self.get_config_service_configs_proto()
|
||||
emane_config = to_dict(self.session.emane_config)
|
||||
links.append(edge.asymmetric_link)
|
||||
return links
|
||||
|
||||
def start_session(self, definition: bool = False) -> Tuple[bool, List[str]]:
|
||||
self.session.links = self.get_links(definition)
|
||||
self.session.metadata = self.get_metadata()
|
||||
self.session.servers.clear()
|
||||
for server in self.servers.values():
|
||||
self.session.servers.append(Server(name=server.name, host=server.address))
|
||||
result = False
|
||||
exceptions = []
|
||||
try:
|
||||
self.send_servers()
|
||||
response = self.client.start_session(
|
||||
result, exceptions = self.client.start_session(self.session, definition)
|
||||
logger.info(
|
||||
"start session(%s) definition(%s), result: %s",
|
||||
self.session.id,
|
||||
nodes,
|
||||
links,
|
||||
self.session.location.to_proto(),
|
||||
hooks,
|
||||
emane_config,
|
||||
emane_model_configs,
|
||||
wlan_configs,
|
||||
mobility_configs,
|
||||
service_configs,
|
||||
file_configs,
|
||||
asymmetric_links,
|
||||
config_service_configs,
|
||||
definition,
|
||||
result,
|
||||
)
|
||||
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:
|
||||
self.app.show_grpc_exception("Start Session Error", e)
|
||||
return result, exceptions
|
||||
|
@ -506,9 +438,8 @@ class CoreClient:
|
|||
session_id = self.session.id
|
||||
result = False
|
||||
try:
|
||||
response = self.client.stop_session(session_id)
|
||||
logging.info("stopped session(%s), result: %s", session_id, response)
|
||||
result = response.result
|
||||
result = self.client.stop_session(session_id)
|
||||
logger.info("stopped session(%s), result: %s", session_id, result)
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Stop Session Error", e)
|
||||
return result
|
||||
|
@ -522,7 +453,7 @@ class CoreClient:
|
|||
self.mobility_players[node.id] = mobility_player
|
||||
mobility_player.show()
|
||||
|
||||
def set_metadata(self) -> None:
|
||||
def get_metadata(self) -> Dict[str, str]:
|
||||
# create canvas data
|
||||
canvas_config = self.app.manager.get_metadata()
|
||||
canvas_config = json.dumps(canvas_config)
|
||||
|
@ -548,11 +479,9 @@ class CoreClient:
|
|||
hidden = json.dumps(hidden)
|
||||
|
||||
# save metadata
|
||||
metadata = dict(
|
||||
return dict(
|
||||
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:
|
||||
try:
|
||||
|
@ -564,9 +493,9 @@ class CoreClient:
|
|||
parent=self.app,
|
||||
)
|
||||
return
|
||||
response = self.client.get_node_terminal(self.session.id, node_id)
|
||||
cmd = f"{terminal} {response.terminal} &"
|
||||
logging.info("launching terminal %s", cmd)
|
||||
node_term = self.client.get_node_terminal(self.session.id, node_id)
|
||||
cmd = f"{terminal} {node_term} &"
|
||||
logger.info("launching terminal %s", cmd)
|
||||
os.system(cmd)
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Node Terminal Error", e)
|
||||
|
@ -574,189 +503,82 @@ class CoreClient:
|
|||
def get_xml_dir(self) -> str:
|
||||
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
|
||||
"""
|
||||
if not file_path and not self.session.file:
|
||||
logging.error("trying to save xml for session with no file")
|
||||
return
|
||||
logger.error("trying to save xml for session with no file")
|
||||
return False
|
||||
if not file_path:
|
||||
file_path = str(self.session.file)
|
||||
file_path = self.session.file
|
||||
result = False
|
||||
try:
|
||||
if not self.is_runtime():
|
||||
logging.debug("Send session data to the daemon")
|
||||
self.send_data()
|
||||
response = self.client.save_xml(self.session.id, file_path)
|
||||
logging.info("saved xml file %s, result: %s", file_path, response)
|
||||
logger.debug("sending session data to the daemon")
|
||||
result, exceptions = self.start_session(definition=True)
|
||||
if not result:
|
||||
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:
|
||||
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
|
||||
"""
|
||||
try:
|
||||
response = self._client.open_xml(file_path)
|
||||
logging.info("open xml file %s, response: %s", file_path, response)
|
||||
self.join_session(response.session_id)
|
||||
result, session_id = self._client.open_xml(file_path)
|
||||
logger.info(
|
||||
"open xml file %s, result(%s) session(%s)",
|
||||
file_path,
|
||||
result,
|
||||
session_id,
|
||||
)
|
||||
self.join_session(session_id)
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Open XML Error", e)
|
||||
|
||||
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)
|
||||
logging.debug(
|
||||
"get node(%s) %s service, response: %s", node_id, service_name, response
|
||||
node_service = self.client.get_node_service(
|
||||
self.session.id, node_id, service_name
|
||||
)
|
||||
return NodeServiceData.from_proto(response.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,
|
||||
logger.debug(
|
||||
"get node(%s) service(%s): %s", node_id, service_name, node_service
|
||||
)
|
||||
logging.info(
|
||||
"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)
|
||||
return node_service
|
||||
|
||||
def get_node_service_file(
|
||||
self, node_id: int, service_name: str, file_name: 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
|
||||
)
|
||||
logging.debug(
|
||||
"get service file for node(%s), service: %s, file: %s, result: %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",
|
||||
logger.debug(
|
||||
"get service file for node(%s), service: %s, file: %s, data: %s",
|
||||
node_id,
|
||||
service_name,
|
||||
file_name,
|
||||
data,
|
||||
response,
|
||||
)
|
||||
|
||||
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()
|
||||
return data
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Clean ups when done using grpc
|
||||
"""
|
||||
logging.debug("close grpc")
|
||||
logger.debug("close grpc")
|
||||
self.client.close()
|
||||
|
||||
def next_node_id(self) -> int:
|
||||
|
@ -783,11 +605,11 @@ class CoreClient:
|
|||
image = "ubuntu:latest"
|
||||
emane = None
|
||||
if node_type == NodeType.EMANE:
|
||||
if not self.session.emane_models:
|
||||
if not self.emane_models:
|
||||
dialog = EmaneInstallDialog(self.app)
|
||||
dialog.show()
|
||||
return
|
||||
emane = self.session.emane_models[0]
|
||||
emane = self.emane_models[0]
|
||||
name = f"emane{node_id}"
|
||||
elif node_type == NodeType.WIRELESS_LAN:
|
||||
name = f"wlan{node_id}"
|
||||
|
@ -806,13 +628,13 @@ class CoreClient:
|
|||
)
|
||||
if nutils.is_custom(node):
|
||||
services = nutils.get_custom_services(self.app.guiconfig, model)
|
||||
node.services = set(services)
|
||||
node.config_services = set(services)
|
||||
# assign default services to CORE node
|
||||
else:
|
||||
services = self.session.default_services.get(model)
|
||||
if services:
|
||||
node.services = services.copy()
|
||||
logging.info(
|
||||
node.config_services = set(services)
|
||||
logger.info(
|
||||
"add node(%s) to session(%s), coordinates(%s, %s)",
|
||||
node.name,
|
||||
self.session.id,
|
||||
|
@ -850,7 +672,7 @@ class CoreClient:
|
|||
dst_iface_id = edge.link.iface2.id
|
||||
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 = []
|
||||
for node in self.session.nodes.values():
|
||||
if node.type != NodeType.WIRELESS_LAN:
|
||||
|
@ -858,11 +680,10 @@ class CoreClient:
|
|||
if not node.wlan_config:
|
||||
continue
|
||||
config = ConfigOption.to_dict(node.wlan_config)
|
||||
wlan_config = wlan_pb2.WlanConfig(node_id=node.id, config=config)
|
||||
configs.append(wlan_config)
|
||||
configs.append((node.id, config))
|
||||
return configs
|
||||
|
||||
def get_mobility_configs_proto(self) -> List[mobility_pb2.MobilityConfig]:
|
||||
def get_mobility_configs(self) -> List[Tuple[int, Dict[str, str]]]:
|
||||
configs = []
|
||||
for node in self.session.nodes.values():
|
||||
if not nutils.is_mobility(node):
|
||||
|
@ -870,27 +691,24 @@ class CoreClient:
|
|||
if not node.mobility_config:
|
||||
continue
|
||||
config = ConfigOption.to_dict(node.mobility_config)
|
||||
mobility_config = mobility_pb2.MobilityConfig(
|
||||
node_id=node.id, config=config
|
||||
)
|
||||
configs.append(mobility_config)
|
||||
configs.append((node.id, config))
|
||||
return configs
|
||||
|
||||
def get_emane_model_configs_proto(self) -> List[emane_pb2.EmaneModelConfig]:
|
||||
def get_emane_model_configs(self) -> List[EmaneModelConfig]:
|
||||
configs = []
|
||||
for node in self.session.nodes.values():
|
||||
for key, config in node.emane_model_configs.items():
|
||||
model, iface_id = key
|
||||
config = ConfigOption.to_dict(config)
|
||||
# config = ConfigOption.to_dict(config)
|
||||
if iface_id is None:
|
||||
iface_id = -1
|
||||
config_proto = emane_pb2.EmaneModelConfig(
|
||||
node_id=node.id, iface_id=iface_id, model=model, config=config
|
||||
config = EmaneModelConfig(
|
||||
node_id=node.id, model=model, iface_id=iface_id, config=config
|
||||
)
|
||||
configs.append(config_proto)
|
||||
configs.append(config)
|
||||
return configs
|
||||
|
||||
def get_service_configs_proto(self) -> List[services_pb2.ServiceConfig]:
|
||||
def get_service_configs(self) -> List[ServiceConfig]:
|
||||
configs = []
|
||||
for node in self.session.nodes.values():
|
||||
if not nutils.is_container(node):
|
||||
|
@ -898,19 +716,19 @@ class CoreClient:
|
|||
if not node.service_configs:
|
||||
continue
|
||||
for name, config in node.service_configs.items():
|
||||
config_proto = services_pb2.ServiceConfig(
|
||||
config = ServiceConfig(
|
||||
node_id=node.id,
|
||||
service=name,
|
||||
directories=config.dirs,
|
||||
files=config.configs,
|
||||
directories=config.dirs,
|
||||
startup=config.startup,
|
||||
validate=config.validate,
|
||||
shutdown=config.shutdown,
|
||||
)
|
||||
configs.append(config_proto)
|
||||
configs.append(config)
|
||||
return configs
|
||||
|
||||
def get_service_file_configs_proto(self) -> List[services_pb2.ServiceFileConfig]:
|
||||
def get_service_file_configs(self) -> List[ServiceFileConfig]:
|
||||
configs = []
|
||||
for node in self.session.nodes.values():
|
||||
if not nutils.is_container(node):
|
||||
|
@ -919,10 +737,8 @@ class CoreClient:
|
|||
continue
|
||||
for service, file_configs in node.service_file_configs.items():
|
||||
for file, data in file_configs.items():
|
||||
config_proto = services_pb2.ServiceFileConfig(
|
||||
node_id=node.id, service=service, file=file, data=data
|
||||
)
|
||||
configs.append(config_proto)
|
||||
config = ServiceFileConfig(node.id, service, file, data)
|
||||
configs.append(config)
|
||||
return configs
|
||||
|
||||
def get_config_service_configs_proto(
|
||||
|
@ -945,39 +761,37 @@ class CoreClient:
|
|||
return config_service_protos
|
||||
|
||||
def run(self, node_id: int) -> str:
|
||||
logging.info("running node(%s) cmd: %s", node_id, self.observer)
|
||||
return self.client.node_command(self.session.id, node_id, self.observer).output
|
||||
logger.info("running node(%s) cmd: %s", node_id, self.observer)
|
||||
_, 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]:
|
||||
response = self.client.get_wlan_config(self.session.id, node_id)
|
||||
config = response.config
|
||||
logging.debug(
|
||||
config = self.client.get_wlan_config(self.session.id, node_id)
|
||||
logger.debug(
|
||||
"get wlan configuration from node %s, result configuration: %s",
|
||||
node_id,
|
||||
config,
|
||||
)
|
||||
return ConfigOption.from_dict(config)
|
||||
return config
|
||||
|
||||
def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]:
|
||||
response = self.client.get_mobility_config(self.session.id, node_id)
|
||||
config = response.config
|
||||
logging.debug(
|
||||
config = self.client.get_mobility_config(self.session.id, node_id)
|
||||
logger.debug(
|
||||
"get mobility config from node %s, result configuration: %s",
|
||||
node_id,
|
||||
config,
|
||||
)
|
||||
return ConfigOption.from_dict(config)
|
||||
return config
|
||||
|
||||
def get_emane_model_config(
|
||||
self, node_id: int, model: str, iface_id: int = None
|
||||
) -> Dict[str, ConfigOption]:
|
||||
if iface_id is None:
|
||||
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
|
||||
)
|
||||
config = response.config
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"get emane model config: node id: %s, EMANE model: %s, "
|
||||
"interface: %s, config: %s",
|
||||
node_id,
|
||||
|
@ -985,42 +799,21 @@ class CoreClient:
|
|||
iface_id,
|
||||
config,
|
||||
)
|
||||
return ConfigOption.from_dict(config)
|
||||
return config
|
||||
|
||||
def execute_script(self, script) -> None:
|
||||
response = self.client.execute_script(script)
|
||||
logging.info("execute python script %s", response)
|
||||
if response.session_id != -1:
|
||||
self.join_session(response.session_id)
|
||||
session_id = self.client.execute_script(script)
|
||||
logger.info("execute python script %s", session_id)
|
||||
if session_id != -1:
|
||||
self.join_session(session_id)
|
||||
|
||||
def add_link(self, link: Link) -> None:
|
||||
iface1 = link.iface1.to_proto() if link.iface1 else None
|
||||
iface2 = link.iface2.to_proto() if link.iface2 else None
|
||||
options = link.options.to_proto() if link.options else None
|
||||
response = self.client.add_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)
|
||||
result, _, _ = self.client.add_link(self.session.id, link, source=GUI_SOURCE)
|
||||
logger.debug("added link: %s", result)
|
||||
if not result:
|
||||
logger.error("error adding link: %s", link)
|
||||
|
||||
def edit_link(self, link: Link) -> None:
|
||||
iface1_id = link.iface1.id if link.iface1 else None
|
||||
iface2_id = link.iface2.id if link.iface2 else None
|
||||
response = self.client.edit_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)
|
||||
result = self.client.edit_link(self.session.id, link, source=GUI_SOURCE)
|
||||
if not result:
|
||||
logger.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")
|
||||
self.manager: CanvasManager = self.app.manager
|
||||
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_height: tk.IntVar = tk.IntVar(value=height)
|
||||
location = self.app.core.session.location
|
||||
|
@ -189,7 +189,7 @@ class SizeAndScaleDialog(Dialog):
|
|||
|
||||
def click_apply(self) -> None:
|
||||
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.x = self.x.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.widgets import image_chooser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -167,5 +169,5 @@ class CanvasWallpaperDialog(Dialog):
|
|||
try:
|
||||
self.canvas.set_wallpaper(filename)
|
||||
except FileNotFoundError:
|
||||
logging.error("invalid background: %s", filename)
|
||||
logger.error("invalid background: %s", filename)
|
||||
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.widgets import CodeText, ConfigFrame, ListboxScroll
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.coreclient import CoreClient
|
||||
|
@ -73,7 +75,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
|
||||
def load(self) -> None:
|
||||
try:
|
||||
self.core.create_nodes_and_links()
|
||||
self.core.start_session(definition=True)
|
||||
service = self.core.config_services[self.service_name]
|
||||
self.dependencies = service.dependencies[:]
|
||||
self.executables = service.executables[:]
|
||||
|
@ -86,18 +88,18 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.validation_time = service.validation_timer
|
||||
self.validation_period.set(service.validation_period)
|
||||
|
||||
response = self.core.client.get_config_service_defaults(self.service_name)
|
||||
self.original_service_files = response.templates
|
||||
defaults = self.core.client.get_config_service_defaults(self.service_name)
|
||||
self.original_service_files = defaults.templates
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
self.modes = sorted(x.name for x in response.modes)
|
||||
self.mode_configs = {x.name: x.config for x in response.modes}
|
||||
self.config = ConfigOption.from_dict(response.config)
|
||||
self.modes = sorted(defaults.modes)
|
||||
self.mode_configs = defaults.modes
|
||||
self.config = ConfigOption.from_dict(defaults.config)
|
||||
self.default_config = {x.name: x.value for x in self.config.values()}
|
||||
service_config = self.node.config_service_configs.get(self.service_name)
|
||||
if service_config:
|
||||
for key, value in service_config.config.items():
|
||||
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():
|
||||
self.modified_files.add(file)
|
||||
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.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.draw_config()
|
||||
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
|
@ -308,9 +310,9 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
|
||||
self.destroy()
|
||||
return
|
||||
service_config = self.node.config_service_configs.get(self.service_name)
|
||||
if not service_config:
|
||||
service_config = ConfigServiceData()
|
||||
service_config = self.node.config_service_configs.setdefault(
|
||||
self.service_name, ConfigServiceData()
|
||||
)
|
||||
if self.config_frame:
|
||||
self.config_frame.parse_config()
|
||||
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:
|
||||
mode = self.modes_combobox.get()
|
||||
config = self.mode_configs[mode]
|
||||
logging.info("mode config: %s", config)
|
||||
logger.info("mode config: %s", config)
|
||||
self.config_frame.set_values(config)
|
||||
|
||||
def update_template_file_data(self, event: tk.Event) -> None:
|
||||
|
@ -350,7 +352,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
|
||||
def click_defaults(self) -> 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
|
||||
)
|
||||
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.insert("end", self.temp_service_files[filename])
|
||||
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)
|
||||
|
||||
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.widgets import CheckboxList, ListboxScroll, image_chooser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -209,7 +211,7 @@ class CustomNodesDialog(Dialog):
|
|||
name, node_draw.image_file, list(node_draw.services)
|
||||
)
|
||||
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.destroy()
|
||||
|
||||
|
@ -219,7 +221,7 @@ class CustomNodesDialog(Dialog):
|
|||
image_file = str(Path(self.image_file).absolute())
|
||||
custom_node = CustomNode(name, image_file, list(self.services))
|
||||
node_draw = NodeDraw.from_custom(custom_node)
|
||||
logging.info(
|
||||
logger.info(
|
||||
"created new custom node (%s), image file (%s), services: (%s)",
|
||||
name,
|
||||
image_file,
|
||||
|
@ -239,7 +241,7 @@ class CustomNodesDialog(Dialog):
|
|||
node_draw.image_file = str(Path(self.image_file).absolute())
|
||||
node_draw.image = self.image
|
||||
node_draw.services = set(self.services)
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"edit custom node (%s), image: (%s), services (%s)",
|
||||
node_draw.model,
|
||||
node_draw.image_file,
|
||||
|
|
|
@ -19,40 +19,6 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -115,7 +81,7 @@ class EmaneConfigDialog(Dialog):
|
|||
self.radiovar: tk.IntVar = tk.IntVar()
|
||||
self.radiovar.set(1)
|
||||
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]
|
||||
self.emane_model: tk.StringVar = tk.StringVar(value=model)
|
||||
|
@ -179,9 +145,7 @@ class EmaneConfigDialog(Dialog):
|
|||
def draw_emane_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
frame.columnconfigure(0, weight=1)
|
||||
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
|
||||
self.emane_model_button = ttk.Button(
|
||||
frame,
|
||||
|
@ -191,18 +155,7 @@ class EmaneConfigDialog(Dialog):
|
|||
command=self.click_model_config,
|
||||
)
|
||||
self.emane_model_button.image = image
|
||||
self.emane_model_button.grid(row=0, column=0, 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)
|
||||
self.emane_model_button.grid(padx=PADX, sticky=tk.EW)
|
||||
|
||||
def draw_apply_and_cancel(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
|
@ -215,10 +168,6 @@ class EmaneConfigDialog(Dialog):
|
|||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
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:
|
||||
"""
|
||||
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.themes import FRAME_PAD, PADX
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -83,6 +85,6 @@ class ExecutePythonDialog(Dialog):
|
|||
def script_execute(self) -> None:
|
||||
file = self.file_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.destroy()
|
||||
|
|
|
@ -6,6 +6,8 @@ from typing import TYPE_CHECKING, Optional
|
|||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -139,8 +141,8 @@ class FindDialog(Dialog):
|
|||
_x, _y, _, _ = canvas_node.canvas.bbox(canvas_node.id)
|
||||
oid = canvas_node.canvas.find_withtag("rectangle")
|
||||
x0, y0, x1, y1 = canvas_node.canvas.bbox(oid[0])
|
||||
logging.debug("Dist to most left: %s", abs(x0 - _x))
|
||||
logging.debug("White canvas width: %s", abs(x0 - x1))
|
||||
logger.debug("Dist to most left: %s", abs(x0 - _x))
|
||||
logger.debug("White canvas width: %s", abs(x0 - x1))
|
||||
|
||||
# calculate the node's location
|
||||
# (as fractions of white canvas's width and height)
|
||||
|
|
|
@ -23,6 +23,8 @@ class IpConfigDialog(Dialog):
|
|||
self.ip4_listbox: Optional[ListboxScroll] = None
|
||||
self.ip6_entry: Optional[ttk.Entry] = 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()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
@ -36,10 +38,19 @@ class IpConfigDialog(Dialog):
|
|||
frame.rowconfigure(0, weight=1)
|
||||
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.columnconfigure(0, weight=1)
|
||||
ip4_frame.rowconfigure(0, weight=1)
|
||||
ip4_frame.grid(row=0, column=0, stick="nsew")
|
||||
ip4_frame.rowconfigure(1, weight=1)
|
||||
ip4_frame.grid(row=1, column=0, stick=tk.NSEW)
|
||||
self.ip4_listbox = ListboxScroll(ip4_frame)
|
||||
self.ip4_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip4)
|
||||
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.columnconfigure(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.listbox.bind("<<ListboxSelect>>", self.select_ip6)
|
||||
self.ip6_listbox.grid(sticky=tk.NSEW, pady=PADY)
|
||||
|
@ -86,7 +97,7 @@ class IpConfigDialog(Dialog):
|
|||
|
||||
# draw buttons
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(stick="ew")
|
||||
frame.grid(stick=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
|
@ -142,10 +153,18 @@ class IpConfigDialog(Dialog):
|
|||
ip6 = self.ip6_listbox.listbox.get(index)
|
||||
ip6s.append(ip6)
|
||||
ip_config = self.app.guiconfig.ips
|
||||
ip_changed = False
|
||||
if ip_config.ip4 != self.ip4:
|
||||
ip_config.ip4 = self.ip4
|
||||
ip_changed = True
|
||||
if ip_config.ip6 != self.ip6:
|
||||
ip_config.ip6 = self.ip6
|
||||
ip_changed = True
|
||||
ip_config.ip4s = ip4s
|
||||
ip_config.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.save_config()
|
||||
self.destroy()
|
||||
|
|
|
@ -134,7 +134,7 @@ class MobilityPlayerDialog(Dialog):
|
|||
session_id = self.app.core.session.id
|
||||
try:
|
||||
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:
|
||||
self.app.show_grpc_exception("Mobility Error", e)
|
||||
|
@ -144,7 +144,7 @@ class MobilityPlayerDialog(Dialog):
|
|||
session_id = self.app.core.session.id
|
||||
try:
|
||||
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:
|
||||
self.app.show_grpc_exception("Mobility Error", e)
|
||||
|
@ -154,7 +154,7 @@ class MobilityPlayerDialog(Dialog):
|
|||
session_id = self.app.core.session.id
|
||||
try:
|
||||
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:
|
||||
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.widgets import ListboxScroll, image_chooser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
@ -260,17 +262,17 @@ class NodeConfigDialog(Dialog):
|
|||
row += 1
|
||||
|
||||
if nutils.is_rj45(self.node):
|
||||
response = self.app.core.client.get_ifaces()
|
||||
logging.debug("host machine available interfaces: %s", response)
|
||||
ifaces = ListboxScroll(frame)
|
||||
ifaces.listbox.config(state=state)
|
||||
ifaces.grid(
|
||||
ifaces = self.app.core.client.get_ifaces()
|
||||
logger.debug("host machine available interfaces: %s", ifaces)
|
||||
ifaces_scroll = ListboxScroll(frame)
|
||||
ifaces_scroll.listbox.config(state=state)
|
||||
ifaces_scroll.grid(
|
||||
row=row, column=0, columnspan=2, sticky=tk.EW, padx=PADX, pady=PADY
|
||||
)
|
||||
for inf in sorted(response.ifaces[:]):
|
||||
ifaces.listbox.insert(tk.END, inf)
|
||||
for inf in sorted(ifaces):
|
||||
ifaces_scroll.listbox.insert(tk.END, inf)
|
||||
row += 1
|
||||
ifaces.listbox.bind("<<ListboxSelect>>", self.iface_select)
|
||||
ifaces_scroll.listbox.bind("<<ListboxSelect>>", self.iface_select)
|
||||
|
||||
# interfaces
|
||||
if self.canvas_node.ifaces:
|
||||
|
@ -296,10 +298,9 @@ class NodeConfigDialog(Dialog):
|
|||
emane_node = self.canvas_node.has_emane_link(iface.id)
|
||||
if emane_node:
|
||||
emane_model = emane_node.emane.split("_")[1]
|
||||
command = partial(self.click_emane_config, emane_model, iface.id)
|
||||
button = ttk.Button(
|
||||
tab,
|
||||
text=f"Configure EMANE {emane_model}",
|
||||
command=lambda: self.click_emane_config(emane_model, iface.id),
|
||||
tab, text=f"Configure EMANE {emane_model}", command=command
|
||||
)
|
||||
button.grid(row=row, sticky=tk.EW, columnspan=3, pady=PADY)
|
||||
row += 1
|
||||
|
@ -365,6 +366,7 @@ class NodeConfigDialog(Dialog):
|
|||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
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.show()
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ from core.gui.dialogs.dialog import Dialog
|
|||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CheckboxList, ListboxScroll
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -29,6 +31,7 @@ class NodeConfigServiceDialog(Dialog):
|
|||
if services is None:
|
||||
services = set(node.config_services)
|
||||
self.current_services: Set[str] = services
|
||||
self.protocol("WM_DELETE_WINDOW", self.click_cancel)
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
@ -100,6 +103,7 @@ class NodeConfigServiceDialog(Dialog):
|
|||
self.current_services.add(name)
|
||||
elif not var.get() and name in self.current_services:
|
||||
self.current_services.remove(name)
|
||||
self.node.config_service_configs.pop(name, None)
|
||||
self.draw_current_services()
|
||||
self.node.config_services = self.current_services.copy()
|
||||
|
||||
|
@ -131,7 +135,7 @@ class NodeConfigServiceDialog(Dialog):
|
|||
|
||||
def click_save(self) -> None:
|
||||
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()
|
||||
|
||||
def click_cancel(self) -> None:
|
||||
|
@ -144,6 +148,7 @@ class NodeConfigServiceDialog(Dialog):
|
|||
service = self.current.listbox.get(cur[0])
|
||||
self.current.listbox.delete(cur[0])
|
||||
self.current_services.remove(service)
|
||||
self.node.config_service_configs.pop(service, None)
|
||||
for checkbutton in self.services.frame.winfo_children():
|
||||
if checkbutton["text"] == service:
|
||||
checkbutton.invoke()
|
||||
|
|
|
@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
|||
|
||||
class NodeServiceDialog(Dialog):
|
||||
def __init__(self, app: "Application", node: Node) -> None:
|
||||
title = f"{node.name} Services"
|
||||
title = f"{node.name} Services (Deprecated)"
|
||||
super().__init__(app, title)
|
||||
self.node: Node = node
|
||||
self.groups: Optional[ListboxScroll] = None
|
||||
|
@ -25,6 +25,7 @@ class NodeServiceDialog(Dialog):
|
|||
self.current: Optional[ListboxScroll] = None
|
||||
services = set(node.services)
|
||||
self.current_services: Set[str] = services
|
||||
self.protocol("WM_DELETE_WINDOW", self.click_cancel)
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
@ -77,7 +78,7 @@ class NodeServiceDialog(Dialog):
|
|||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Remove", command=self.click_remove)
|
||||
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)
|
||||
|
||||
# trigger group change
|
||||
|
@ -98,6 +99,8 @@ class NodeServiceDialog(Dialog):
|
|||
self.current_services.add(name)
|
||||
elif not var.get() and name in self.current_services:
|
||||
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)
|
||||
for name in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, name)
|
||||
|
@ -125,6 +128,9 @@ class NodeServiceDialog(Dialog):
|
|||
"Service Configuration", "Select a service to configure", parent=self
|
||||
)
|
||||
|
||||
def click_cancel(self) -> None:
|
||||
self.destroy()
|
||||
|
||||
def click_save(self) -> None:
|
||||
self.node.services = self.current_services.copy()
|
||||
self.destroy()
|
||||
|
@ -135,6 +141,8 @@ class NodeServiceDialog(Dialog):
|
|||
service = self.current.listbox.get(cur[0])
|
||||
self.current.listbox.delete(cur[0])
|
||||
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():
|
||||
if checkbutton["text"] == service:
|
||||
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.validation import LARGEST_SCALE, SMALLEST_SCALE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -102,7 +104,7 @@ class PreferencesDialog(Dialog):
|
|||
|
||||
def theme_change(self, event: tk.Event) -> None:
|
||||
theme = self.theme.get()
|
||||
logging.info("changing theme: %s", theme)
|
||||
logger.info("changing theme: %s", theme)
|
||||
self.app.style.theme_use(theme)
|
||||
|
||||
def click_save(self) -> None:
|
||||
|
|
|
@ -106,10 +106,8 @@ class RunToolDialog(Dialog):
|
|||
for selection in self.node_list.listbox.curselection():
|
||||
node_name = self.node_list.listbox.get(selection)
|
||||
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.result.text.insert(
|
||||
tk.END, f"> {node_name} > {command}:\n{response.output}\n"
|
||||
)
|
||||
self.result.text.insert(tk.END, f"> {node_name} > {command}:\n{output}\n")
|
||||
self.result.text.config(state=tk.DISABLED)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
import os
|
||||
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
|
||||
|
||||
import grpc
|
||||
|
@ -15,6 +15,8 @@ from core.gui.images import ImageEnum
|
|||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ListboxScroll
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.coreclient import CoreClient
|
||||
|
@ -26,7 +28,7 @@ class ServiceConfigDialog(Dialog):
|
|||
def __init__(
|
||||
self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node
|
||||
) -> None:
|
||||
title = f"{service_name} Service"
|
||||
title = f"{service_name} Service (Deprecated)"
|
||||
super().__init__(app, title, master=master)
|
||||
self.core: "CoreClient" = app.core
|
||||
self.node: Node = node
|
||||
|
@ -76,7 +78,7 @@ class ServiceConfigDialog(Dialog):
|
|||
|
||||
def load(self) -> None:
|
||||
try:
|
||||
self.app.core.create_nodes_and_links()
|
||||
self.core.start_session(definition=True)
|
||||
default_config = self.app.core.get_node_service(
|
||||
self.node.id, self.service_name
|
||||
)
|
||||
|
@ -388,7 +390,7 @@ class ServiceConfigDialog(Dialog):
|
|||
1.0, "end"
|
||||
)
|
||||
else:
|
||||
logging.debug("file already existed")
|
||||
logger.debug("file already existed")
|
||||
|
||||
def delete_filename(self) -> None:
|
||||
cbb = self.filename_combobox
|
||||
|
@ -447,36 +449,31 @@ class ServiceConfigDialog(Dialog):
|
|||
self.current_service_color("")
|
||||
self.destroy()
|
||||
return
|
||||
|
||||
try:
|
||||
files = set(self.filenames)
|
||||
if (
|
||||
self.is_custom_command()
|
||||
or self.has_new_files()
|
||||
or self.is_custom_directory()
|
||||
):
|
||||
startup, validate, shutdown = self.get_commands()
|
||||
config = self.core.set_node_service(
|
||||
self.node.id,
|
||||
self.service_name,
|
||||
files = set(self.filename_combobox["values"])
|
||||
service_data = NodeServiceData(
|
||||
configs=list(files),
|
||||
dirs=self.temp_directories,
|
||||
files=list(self.filename_combobox["values"]),
|
||||
startups=startup,
|
||||
validations=validate,
|
||||
shutdowns=shutdown,
|
||||
startup=startup,
|
||||
validate=validate,
|
||||
shutdown=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:
|
||||
if file not in files:
|
||||
continue
|
||||
file_configs = self.node.service_file_configs.setdefault(
|
||||
self.service_name, {}
|
||||
)
|
||||
file_configs[file] = self.temp_service_files[file]
|
||||
# TODO: check if this is really needed
|
||||
self.app.core.set_node_service_file(
|
||||
self.node.id, self.service_name, file, self.temp_service_files[file]
|
||||
)
|
||||
self.current_service_color("green")
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Save Service Config Error", e)
|
||||
self.destroy()
|
||||
|
||||
def display_service_file_data(self, event: tk.Event) -> None:
|
||||
|
@ -579,11 +576,13 @@ class ServiceConfigDialog(Dialog):
|
|||
self.directory_entry.insert("end", d)
|
||||
|
||||
def add_directory(self) -> None:
|
||||
d = self.directory_entry.get()
|
||||
if os.path.isdir(d):
|
||||
if d not in self.temp_directories:
|
||||
self.dir_list.listbox.insert("end", d)
|
||||
self.temp_directories.append(d)
|
||||
directory = Path(self.directory_entry.get())
|
||||
if directory.is_absolute():
|
||||
if str(directory) not in self.temp_directories:
|
||||
self.dir_list.listbox.insert("end", directory)
|
||||
self.temp_directories.append(str(directory))
|
||||
else:
|
||||
messagebox.showerror("Add Directory", "Path must be absolute!", parent=self)
|
||||
|
||||
def remove_directory(self) -> None:
|
||||
d = self.directory_entry.get()
|
||||
|
@ -594,7 +593,7 @@ class ServiceConfigDialog(Dialog):
|
|||
i = dirs.index(d)
|
||||
self.dir_list.listbox.delete(i)
|
||||
except ValueError:
|
||||
logging.debug("directory is not in the list")
|
||||
logger.debug("directory is not in the list")
|
||||
self.directory_entry.delete(0, "end")
|
||||
|
||||
def directory_select(self, event) -> None:
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import logging
|
||||
import tkinter as tk
|
||||
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.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -19,25 +18,15 @@ class SessionOptionsDialog(Dialog):
|
|||
super().__init__(app, "Session Options")
|
||||
self.config_frame: Optional[ConfigFrame] = None
|
||||
self.has_error: bool = False
|
||||
self.config: Dict[str, ConfigOption] = self.get_config()
|
||||
self.enabled: bool = not self.app.core.is_runtime()
|
||||
if not self.has_error:
|
||||
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:
|
||||
self.top.columnconfigure(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.grid(sticky=tk.NSEW, pady=PADY)
|
||||
|
||||
|
@ -53,10 +42,6 @@ class SessionOptionsDialog(Dialog):
|
|||
|
||||
def save(self) -> None:
|
||||
config = self.config_frame.parse_config()
|
||||
try:
|
||||
session_id = self.app.core.session.id
|
||||
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)
|
||||
for key, value in config.items():
|
||||
self.app.core.session.options[key].value = value
|
||||
self.destroy()
|
||||
|
|
|
@ -12,6 +12,8 @@ from core.gui.images import ImageEnum
|
|||
from core.gui.task import ProgressTask
|
||||
from core.gui.themes import PADX, PADY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -30,10 +32,9 @@ class SessionsDialog(Dialog):
|
|||
|
||||
def get_sessions(self) -> List[SessionSummary]:
|
||||
try:
|
||||
response = self.app.core.client.get_sessions()
|
||||
logging.info("sessions: %s", response)
|
||||
sessions = sorted(response.sessions, key=lambda x: x.id)
|
||||
return [SessionSummary.from_proto(x) for x in sessions]
|
||||
sessions = self.app.core.client.get_sessions()
|
||||
logger.info("sessions: %s", sessions)
|
||||
return sorted(sessions, key=lambda x: x.id)
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Get Sessions Error", e)
|
||||
self.destroy()
|
||||
|
@ -176,7 +177,7 @@ class SessionsDialog(Dialog):
|
|||
self.selected_id = None
|
||||
self.delete_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:
|
||||
if not self.selected_session:
|
||||
|
@ -200,7 +201,7 @@ class SessionsDialog(Dialog):
|
|||
def click_delete(self) -> None:
|
||||
if not self.selected_session:
|
||||
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.app.core.delete_session(self.selected_session)
|
||||
session_id = None
|
||||
|
|
|
@ -11,6 +11,8 @@ from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
|
|||
from core.gui.graph import tags
|
||||
from core.gui.utils import bandwidth_text, delay_jitter_text
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
|
@ -393,7 +395,7 @@ class Edge:
|
|||
self.dst.canvas.coords(self.dst_label2, *dst_pos)
|
||||
|
||||
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.src_label)
|
||||
self.src.canvas.delete(self.dst_label)
|
||||
|
@ -488,7 +490,7 @@ class CanvasWirelessEdge(Edge):
|
|||
token: str,
|
||||
link: Link,
|
||||
) -> 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)
|
||||
self.src.wireless_edges.add(self)
|
||||
self.dst.wireless_edges.add(self)
|
||||
|
@ -622,7 +624,7 @@ class CanvasEdge(Edge):
|
|||
self.draw_link_options()
|
||||
|
||||
def complete(self, dst: "CanvasNode", link: Link = None) -> None:
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"completing wired link from node(%s) to node(%s)",
|
||||
self.src.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.shapeutils import ShapeType, is_draw_shape, is_marker
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
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
|
||||
"""
|
||||
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("<ButtonRelease-1>", self.click_release)
|
||||
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("<Double-Button-1>", self.double_click)
|
||||
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
|
||||
"""
|
||||
logging.debug("click release")
|
||||
logger.debug("click release")
|
||||
x, y = self.canvas_xy(event)
|
||||
if not self.inside_canvas(x, y):
|
||||
return
|
||||
|
@ -210,7 +217,7 @@ class CanvasGraph(tk.Canvas):
|
|||
else:
|
||||
self.focus_set()
|
||||
self.selected = self.get_selected(event)
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"click release selected(%s) mode(%s)", self.selected, self.manager.mode
|
||||
)
|
||||
if self.manager.mode == GraphMode.EDGE:
|
||||
|
@ -228,7 +235,7 @@ class CanvasGraph(tk.Canvas):
|
|||
edge = self.drawing_edge
|
||||
self.drawing_edge = None
|
||||
# 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)
|
||||
if not dst_node:
|
||||
edge.delete()
|
||||
|
@ -275,7 +282,7 @@ class CanvasGraph(tk.Canvas):
|
|||
if select_id is not None:
|
||||
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()
|
||||
nodes = []
|
||||
for object_id in self.selection:
|
||||
|
@ -305,7 +312,7 @@ class CanvasGraph(tk.Canvas):
|
|||
self.selection.clear()
|
||||
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:
|
||||
# delete selection box
|
||||
selection_id = self.selection[object_id]
|
||||
|
@ -331,8 +338,8 @@ class CanvasGraph(tk.Canvas):
|
|||
self.offset[0] * factor + event.x * (1 - factor),
|
||||
self.offset[1] * factor + event.y * (1 - factor),
|
||||
)
|
||||
logging.debug("ratio: %s", self.ratio)
|
||||
logging.debug("offset: %s", self.offset)
|
||||
logger.debug("ratio: %s", self.ratio)
|
||||
logger.debug("offset: %s", self.offset)
|
||||
self.app.statusbar.set_zoom(self.ratio)
|
||||
if self.wallpaper:
|
||||
self.redraw_wallpaper()
|
||||
|
@ -347,10 +354,10 @@ class CanvasGraph(tk.Canvas):
|
|||
|
||||
self.cursor = x, y
|
||||
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]
|
||||
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
|
||||
if self.manager.mode == GraphMode.EDGE and is_node:
|
||||
node = self.nodes[selected]
|
||||
|
@ -387,7 +394,7 @@ class CanvasGraph(tk.Canvas):
|
|||
node = self.nodes[selected]
|
||||
self.select_object(node.id)
|
||||
self.selected = selected
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"selected node(%s), coords: (%s, %s)",
|
||||
node.core_node.name,
|
||||
node.core_node.position.x,
|
||||
|
@ -397,7 +404,7 @@ class CanvasGraph(tk.Canvas):
|
|||
shadow_node = self.shadow_nodes[selected]
|
||||
self.select_object(shadow_node.id)
|
||||
self.selected = selected
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"selected shadow node(%s), coords: (%s, %s)",
|
||||
shadow_node.node.core_node.name,
|
||||
shadow_node.node.core_node.position.x,
|
||||
|
@ -418,7 +425,7 @@ class CanvasGraph(tk.Canvas):
|
|||
self.cursor = x, y
|
||||
|
||||
# handle multiple selections
|
||||
logging.debug("control left click: %s", event)
|
||||
logger.debug("control left click: %s", event)
|
||||
selected = self.get_selected(event)
|
||||
if (
|
||||
selected not in self.selection
|
||||
|
@ -485,17 +492,6 @@ class CanvasGraph(tk.Canvas):
|
|||
if self.select_box and self.manager.mode == GraphMode.SELECT:
|
||||
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:
|
||||
selected = self.get_selected(event)
|
||||
if selected is not None and selected in self.shapes:
|
||||
|
@ -606,10 +602,10 @@ class CanvasGraph(tk.Canvas):
|
|||
self.draw_wallpaper(image)
|
||||
|
||||
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
|
||||
logging.debug("resetting scaling: %s %s", self.ratio, self.offset)
|
||||
logger.debug("resetting scaling: %s %s", self.ratio, self.offset)
|
||||
factor = 1 / self.ratio
|
||||
self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor)
|
||||
self.move(tk.ALL, -self.offset[0], -self.offset[1])
|
||||
|
@ -628,11 +624,11 @@ class CanvasGraph(tk.Canvas):
|
|||
|
||||
def redraw_wallpaper(self) -> None:
|
||||
if self.adjust_to_dim.get():
|
||||
logging.debug("drawing wallpaper to canvas dimensions")
|
||||
logger.debug("drawing wallpaper to canvas dimensions")
|
||||
self.resize_to_wallpaper()
|
||||
else:
|
||||
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:
|
||||
self.wallpaper_upper_left()
|
||||
elif option == ScaleOption.CENTERED:
|
||||
|
@ -640,7 +636,7 @@ class CanvasGraph(tk.Canvas):
|
|||
elif option == ScaleOption.SCALED:
|
||||
self.wallpaper_scaled()
|
||||
elif option == ScaleOption.TILED:
|
||||
logging.warning("tiled background not implemented yet")
|
||||
logger.warning("tiled background not implemented yet")
|
||||
self.organize()
|
||||
|
||||
def organize(self) -> None:
|
||||
|
@ -648,7 +644,7 @@ class CanvasGraph(tk.Canvas):
|
|||
self.tag_raise(tag)
|
||||
|
||||
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:
|
||||
img = Image.open(filename)
|
||||
self.wallpaper = img
|
||||
|
@ -671,20 +667,38 @@ class CanvasGraph(tk.Canvas):
|
|||
edge.complete(dst)
|
||||
return edge
|
||||
|
||||
def copy(self) -> None:
|
||||
def copy_selected(self, _event: tk.Event = None) -> None:
|
||||
if self.core.is_runtime():
|
||||
logging.debug("copy is disabled during runtime state")
|
||||
logger.debug("copy is disabled during runtime state")
|
||||
return
|
||||
if self.selection:
|
||||
logging.debug("to copy nodes: %s", self.selection)
|
||||
logger.debug("to copy nodes: %s", self.selection)
|
||||
self.to_copy.clear()
|
||||
for node_id in self.selection.keys():
|
||||
canvas_node = self.nodes[node_id]
|
||||
self.to_copy.append(canvas_node)
|
||||
|
||||
def paste(self) -> None:
|
||||
def cut_selected(self, _event: tk.Event = None) -> None:
|
||||
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
|
||||
# maps original node canvas id to copy node canvas id
|
||||
copy_map = {}
|
||||
|
@ -813,6 +827,7 @@ class CanvasGraph(tk.Canvas):
|
|||
wallpaper=wallpaper_path,
|
||||
wallpaper_style=self.scale_option.get(),
|
||||
fit_image=self.adjust_to_dim.get(),
|
||||
dimensions=self.current_dimensions,
|
||||
)
|
||||
|
||||
def parse_metadata(self, config: Dict[str, Any]) -> None:
|
||||
|
@ -820,12 +835,15 @@ class CanvasGraph(tk.Canvas):
|
|||
self.adjust_to_dim.set(fit_image)
|
||||
wallpaper_style = config.get("wallpaper_style", 1)
|
||||
self.scale_option.set(wallpaper_style)
|
||||
dimensions = config.get("dimensions")
|
||||
if dimensions:
|
||||
self.redraw_canvas(dimensions)
|
||||
wallpaper = config.get("wallpaper")
|
||||
if wallpaper:
|
||||
wallpaper = Path(wallpaper)
|
||||
if not wallpaper.is_file():
|
||||
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():
|
||||
self.set_wallpaper(str(wallpaper))
|
||||
else:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
import logging
|
||||
import tkinter as tk
|
||||
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.graph import CanvasGraph
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.graph.shape import Shape
|
||||
from core.gui.graph.shapeutils import ShapeType
|
||||
from core.gui.nodeutils import NodeDraw
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.coreclient import CoreClient
|
||||
|
@ -85,7 +89,6 @@ class CanvasManager:
|
|||
self.app.guiconfig.preferences.width,
|
||||
self.app.guiconfig.preferences.height,
|
||||
)
|
||||
self.current_dimensions: Tuple[int, int] = self.default_dimensions
|
||||
self.show_node_labels: ShowVar = ShowNodeLabels(
|
||||
self, tags.NODE_LABEL, value=True
|
||||
)
|
||||
|
@ -166,7 +169,7 @@ class CanvasManager:
|
|||
canvas_id = self._next_id()
|
||||
self.notebook.add(tab, text=f"Canvas {canvas_id}")
|
||||
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.unique_ids[canvas_id] = unique_id
|
||||
|
||||
|
@ -205,7 +208,7 @@ class CanvasManager:
|
|||
edge.delete()
|
||||
|
||||
def join(self, session: Session) -> None:
|
||||
# clear out all canvas
|
||||
# clear out all canvases
|
||||
for canvas_id in self.notebook.tabs():
|
||||
self.notebook.forget(canvas_id)
|
||||
self.canvases.clear()
|
||||
|
@ -213,7 +216,7 @@ class CanvasManager:
|
|||
self.unique_ids.clear()
|
||||
self.edges.clear()
|
||||
self.wireless_edges.clear()
|
||||
logging.info("cleared canvases")
|
||||
logger.info("cleared canvases")
|
||||
|
||||
# reset settings
|
||||
self.show_node_labels.set(True)
|
||||
|
@ -232,6 +235,10 @@ class CanvasManager:
|
|||
self.draw_session(session)
|
||||
|
||||
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
|
||||
for core_node in session.nodes.values():
|
||||
# add node, avoiding ignored nodes
|
||||
|
@ -254,50 +261,92 @@ class CanvasManager:
|
|||
else:
|
||||
self.add_wired_edge(node1, node2, link)
|
||||
|
||||
# parse metadata and organize canvases
|
||||
self.core.parse_metadata()
|
||||
# organize canvas order
|
||||
for canvas in self.canvases.values():
|
||||
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
|
||||
if not self.canvases:
|
||||
self.add_canvas()
|
||||
|
||||
def redraw_canvases(self, dimensions: Tuple[int, int]) -> None:
|
||||
for canvas in self.canvases.values():
|
||||
def redraw_canvas(self, dimensions: Tuple[int, int]) -> None:
|
||||
canvas = self.current()
|
||||
canvas.redraw_canvas(dimensions)
|
||||
if canvas.wallpaper:
|
||||
canvas.redraw_wallpaper()
|
||||
|
||||
def get_metadata(self) -> Dict[str, Any]:
|
||||
canvases = [x.get_metadata() for x in self.all()]
|
||||
return dict(
|
||||
gridlines=self.app.manager.show_grid.get(),
|
||||
dimensions=self.app.manager.current_dimensions,
|
||||
canvases=canvases,
|
||||
)
|
||||
return dict(gridlines=self.show_grid.get(), 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
|
||||
dimensions = self.default_dimensions
|
||||
dimensions = config.get("dimensions", dimensions)
|
||||
gridlines = config.get("gridlines", True)
|
||||
gridlines = canvas_config.get("gridlines", True)
|
||||
self.show_grid.set(gridlines)
|
||||
self.redraw_canvases(dimensions)
|
||||
|
||||
# get background configurations
|
||||
for canvas_config in config.get("canvases", []):
|
||||
for canvas_config in canvas_config.get("canvases", []):
|
||||
canvas_id = canvas_config.get("id")
|
||||
if canvas_id is None:
|
||||
logging.error("canvas config id not provided")
|
||||
logger.error("canvas config id not provided")
|
||||
continue
|
||||
canvas = self.get(canvas_id)
|
||||
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:
|
||||
# get canvas tab for node
|
||||
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)
|
||||
image = nutils.get_icon(core_node, self.app)
|
||||
x = core_node.position.x
|
||||
|
@ -354,7 +403,7 @@ class CanvasManager:
|
|||
network_id = link.network_id if link.network_id else None
|
||||
token = create_wireless_token(src.id, dst.id, network_id)
|
||||
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
|
||||
edge = CanvasWirelessEdge(self.app, src, dst, network_id, token, link)
|
||||
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.images import ImageEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
|
@ -87,7 +89,7 @@ class CanvasNode:
|
|||
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
|
||||
|
||||
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.text_id)
|
||||
self.delete_antennas()
|
||||
|
@ -110,7 +112,7 @@ class CanvasNode:
|
|||
"""
|
||||
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:
|
||||
antenna_id = self.antennas.pop()
|
||||
self.canvas.delete(antenna_id)
|
||||
|
@ -120,7 +122,7 @@ class CanvasNode:
|
|||
"""
|
||||
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:
|
||||
self.canvas.delete(antenna_id)
|
||||
self.antennas.clear()
|
||||
|
@ -253,10 +255,12 @@ class CanvasNode:
|
|||
else:
|
||||
self.context.add_command(label="Configure", command=self.show_config)
|
||||
if nutils.is_container(self.core_node):
|
||||
self.context.add_command(label="Services", command=self.show_services)
|
||||
self.context.add_command(
|
||||
label="Config Services", command=self.show_config_services
|
||||
)
|
||||
self.context.add_command(
|
||||
label="Services (Deprecated)", command=self.show_services
|
||||
)
|
||||
if is_emane:
|
||||
self.context.add_command(
|
||||
label="EMANE Config", command=self.show_emane_config
|
||||
|
@ -334,7 +338,7 @@ class CanvasNode:
|
|||
def canvas_copy(self) -> None:
|
||||
self.canvas.clear_selection()
|
||||
self.canvas.select_object(self.id)
|
||||
self.canvas.copy()
|
||||
self.canvas.copy_selected()
|
||||
|
||||
def show_config(self) -> None:
|
||||
dialog = NodeConfigDialog(self.app, self)
|
||||
|
@ -400,7 +404,7 @@ class CanvasNode:
|
|||
|
||||
def update_icon(self, icon_path: str) -> None:
|
||||
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
|
||||
self.core_node.icon = icon_path
|
||||
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:
|
||||
session_id = self.app.core.session.id
|
||||
try:
|
||||
response = self.app.core.client.service_action(
|
||||
result = self.app.core.client.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!")
|
||||
except grpc.RpcError as 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.shapeutils import ShapeType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
|
@ -92,7 +94,7 @@ class Shape:
|
|||
shape = Shape(app, canvas, shape_type, *coords, data=data)
|
||||
canvas.shapes[shape.id] = shape
|
||||
except ValueError:
|
||||
logging.exception("unknown shape: %s", shape_type)
|
||||
logger.exception("unknown shape: %s", shape_type)
|
||||
|
||||
def draw(self) -> None:
|
||||
if self.created:
|
||||
|
@ -139,7 +141,7 @@ class Shape:
|
|||
state=self.app.manager.show_annotations.state(),
|
||||
)
|
||||
else:
|
||||
logging.error("unknown shape type: %s", self.shape_type)
|
||||
logger.error("unknown shape type: %s", self.shape_type)
|
||||
self.created = True
|
||||
|
||||
def get_font(self) -> List[Union[int, str]]:
|
||||
|
@ -192,7 +194,7 @@ class Shape:
|
|||
self.canvas.move(self.text_id, x_offset, y_offset)
|
||||
|
||||
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.text_id)
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ from core.gui import nodeutils as nutils
|
|||
from core.gui.graph.edges import CanvasEdge
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -158,11 +160,18 @@ class InterfaceManager:
|
|||
index += 1
|
||||
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)
|
||||
ip4 = self.current_subnets.ip4[index]
|
||||
ip6 = self.current_subnets.ip6[index]
|
||||
return str(ip4), str(ip6)
|
||||
if enable_ip4:
|
||||
ip4 = str(self.current_subnets.ip4[index])
|
||||
if enable_ip6:
|
||||
ip6 = str(self.current_subnets.ip6[index])
|
||||
return ip4, ip6
|
||||
|
||||
def get_subnets(self, iface: Interface) -> Subnets:
|
||||
ip4_subnet = self.ip4_subnets
|
||||
|
@ -196,12 +205,12 @@ class InterfaceManager:
|
|||
else:
|
||||
self.current_subnets = self.next_subnets()
|
||||
else:
|
||||
logging.info("ignoring subnet change for link between network nodes")
|
||||
logger.info("ignoring subnet change for link between network nodes")
|
||||
|
||||
def find_subnets(
|
||||
self, canvas_node: CanvasNode, visited: Set[int] = None
|
||||
) -> 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
|
||||
if not visited:
|
||||
visited = set()
|
||||
|
@ -220,7 +229,7 @@ class InterfaceManager:
|
|||
else:
|
||||
subnets = self.find_subnets(check_node, visited)
|
||||
if subnets:
|
||||
logging.info("found subnets: %s", subnets)
|
||||
logger.info("found subnets: %s", subnets)
|
||||
break
|
||||
return subnets
|
||||
|
||||
|
@ -244,7 +253,7 @@ class InterfaceManager:
|
|||
iface1=src_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
|
||||
|
||||
def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface:
|
||||
|
@ -266,5 +275,5 @@ class InterfaceManager:
|
|||
ip6=ip6,
|
||||
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
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import logging
|
||||
import os
|
||||
import tkinter as tk
|
||||
import webbrowser
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, messagebox
|
||||
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.task import ProgressTask
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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())
|
||||
menu.add_command(label="Save", accelerator="Ctrl+S", command=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(
|
||||
label="Open...", command=self.click_open_xml, accelerator="Ctrl+O"
|
||||
)
|
||||
|
@ -84,7 +86,7 @@ class Menubar(tk.Menu):
|
|||
self.recent_menu = tk.Menu(menu)
|
||||
for i in self.app.guiconfig.recentfiles:
|
||||
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_separator()
|
||||
|
@ -120,11 +122,6 @@ class Menubar(tk.Menu):
|
|||
)
|
||||
menu.add_command(label="Hide", accelerator="Ctrl+H", command=self.click_hide)
|
||||
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
|
||||
|
||||
def draw_canvas_menu(self) -> None:
|
||||
|
@ -272,27 +269,28 @@ class Menubar(tk.Menu):
|
|||
menu.add_command(label="About", command=self.click_about)
|
||||
self.add_cascade(label="Help", menu=menu)
|
||||
|
||||
def open_recent_files(self, filename: str) -> None:
|
||||
if os.path.isfile(filename):
|
||||
logging.debug("Open recent file %s", filename)
|
||||
self.open_xml_task(filename)
|
||||
def open_recent_files(self, file_path: Path) -> None:
|
||||
if file_path.is_file():
|
||||
logger.debug("Open recent file %s", file_path)
|
||||
self.open_xml_task(file_path)
|
||||
else:
|
||||
logging.warning("File does not exist %s", filename)
|
||||
logger.warning("File does not exist %s", file_path)
|
||||
|
||||
def update_recent_files(self) -> None:
|
||||
self.recent_menu.delete(0, tk.END)
|
||||
for i in self.app.guiconfig.recentfiles:
|
||||
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:
|
||||
self.core.save_xml()
|
||||
if self.core.save_xml():
|
||||
self.add_recent_file_to_gui_config(self.core.session.file)
|
||||
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()
|
||||
file_path = filedialog.asksaveasfilename(
|
||||
initialdir=init_dir,
|
||||
|
@ -301,8 +299,9 @@ class Menubar(tk.Menu):
|
|||
defaultextension=".xml",
|
||||
)
|
||||
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.core.save_xml(file_path)
|
||||
|
||||
def click_open_xml(self, _event: tk.Event = None) -> None:
|
||||
init_dir = self.core.get_xml_dir()
|
||||
|
@ -312,9 +311,10 @@ class Menubar(tk.Menu):
|
|||
filetypes=(("XML Files", "*.xml"), ("All Files", "*")),
|
||||
)
|
||||
if file_path:
|
||||
file_path = Path(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.prompt_save_running_session()
|
||||
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.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
|
||||
num_files = len(recent_files)
|
||||
if num_files == 0:
|
||||
recent_files.insert(0, file_path)
|
||||
elif 0 < num_files <= MAX_FILES:
|
||||
file_path = str(file_path)
|
||||
if file_path in recent_files:
|
||||
recent_files.remove(file_path)
|
||||
recent_files.insert(0, file_path)
|
||||
else:
|
||||
if num_files == MAX_FILES:
|
||||
if len(recent_files) > MAX_FILES:
|
||||
recent_files.pop()
|
||||
recent_files.insert(0, file_path)
|
||||
else:
|
||||
logging.error("unexpected number of recent files")
|
||||
self.app.save_config()
|
||||
self.app.menubar.update_recent_files()
|
||||
|
||||
|
@ -411,47 +404,47 @@ class Menubar(tk.Menu):
|
|||
|
||||
def click_copy(self, _event: tk.Event = None) -> None:
|
||||
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.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.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.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.copy()
|
||||
canvas.delete_selected_objects()
|
||||
canvas.copy_selected(event)
|
||||
canvas.delete_selected(event)
|
||||
|
||||
def click_show_hidden(self, _event: tk.Event = None) -> None:
|
||||
for canvas in self.manager.all():
|
||||
canvas.show_hidden()
|
||||
|
||||
def click_session_options(self) -> None:
|
||||
logging.debug("Click options")
|
||||
logger.debug("Click options")
|
||||
dialog = SessionOptionsDialog(self.app)
|
||||
if not dialog.has_error:
|
||||
dialog.show()
|
||||
|
||||
def click_sessions(self) -> None:
|
||||
logging.debug("Click change sessions")
|
||||
logger.debug("Click change sessions")
|
||||
dialog = SessionsDialog(self.app)
|
||||
dialog.show()
|
||||
|
||||
def click_hooks(self) -> None:
|
||||
logging.debug("Click hooks")
|
||||
logger.debug("Click hooks")
|
||||
dialog = HooksDialog(self.app)
|
||||
dialog.show()
|
||||
|
||||
def click_servers(self) -> None:
|
||||
logging.debug("Click emulation servers")
|
||||
logger.debug("Click emulation servers")
|
||||
dialog = ServersDialog(self.app)
|
||||
dialog.show()
|
||||
|
||||
|
@ -460,11 +453,11 @@ class Menubar(tk.Menu):
|
|||
dialog.show()
|
||||
|
||||
def click_autogrid(self) -> None:
|
||||
width, height = self.manager.current_dimensions
|
||||
width, height = self.manager.current().current_dimensions
|
||||
padding = (images.NODE_SIZE / 2) + 10
|
||||
layout_size = padding + images.NODE_SIZE
|
||||
col_count = width // layout_size
|
||||
logging.info(
|
||||
logger.info(
|
||||
"auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count
|
||||
)
|
||||
canvas = self.manager.current()
|
||||
|
|
|
@ -8,6 +8,8 @@ from core.gui import images
|
|||
from core.gui.appconfig import CustomNode, GuiConfig
|
||||
from core.gui.images import ImageEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -29,10 +31,9 @@ ANTENNA_ICON: Optional[PhotoImage] = None
|
|||
def setup() -> None:
|
||||
global ANTENNA_ICON
|
||||
nodes = [
|
||||
(ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"),
|
||||
(ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"),
|
||||
(ImageEnum.PC, NodeType.DEFAULT, "PC", "PC"),
|
||||
(ImageEnum.MDR, NodeType.DEFAULT, "MDR", "mdr"),
|
||||
(ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"),
|
||||
(ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"),
|
||||
(ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None),
|
||||
(ImageEnum.LXC, NodeType.LXC, "LXC", None),
|
||||
|
@ -118,11 +119,11 @@ def get_icon(node: Node, app: "Application") -> PhotoImage:
|
|||
try:
|
||||
image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale)
|
||||
except OSError:
|
||||
logging.error("invalid icon: %s", node.icon)
|
||||
logger.error("invalid icon: %s", node.icon)
|
||||
# custom node
|
||||
elif is_custom(node):
|
||||
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:
|
||||
image = images.from_file(image_file, width=images.NODE_SIZE, scale=scale)
|
||||
# built in node
|
||||
|
|
|
@ -4,6 +4,8 @@ import time
|
|||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -43,7 +45,7 @@ class ProgressTask:
|
|||
if self.callback:
|
||||
self.app.after(0, self.callback, *values)
|
||||
except Exception as e:
|
||||
logging.exception("progress task exception")
|
||||
logger.exception("progress task exception")
|
||||
self.app.show_exception("Task Error", e)
|
||||
finally:
|
||||
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.tooltip import Tooltip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -304,7 +306,6 @@ class Toolbar(ttk.Frame):
|
|||
def start_callback(self, result: bool, exceptions: List[str]) -> None:
|
||||
if result:
|
||||
self.set_runtime()
|
||||
self.app.core.set_metadata()
|
||||
self.app.core.show_mobility_players()
|
||||
else:
|
||||
enable_buttons(self.design_frame, enabled=True)
|
||||
|
@ -338,7 +339,7 @@ class Toolbar(ttk.Frame):
|
|||
type_enum: NodeTypeEnum,
|
||||
image: PhotoImage,
|
||||
) -> None:
|
||||
logging.debug("update button(%s): %s", button, node_draw)
|
||||
logger.debug("update button(%s): %s", button, node_draw)
|
||||
button.configure(image=image)
|
||||
button.image = image
|
||||
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
|
||||
"""
|
||||
logging.info("clicked stop button")
|
||||
logger.info("clicked stop button")
|
||||
self.app.menubar.set_state(is_runtime=False)
|
||||
self.app.core.close_mobility_players()
|
||||
enable_buttons(self.runtime_frame, enabled=False)
|
||||
|
@ -415,7 +416,7 @@ class Toolbar(ttk.Frame):
|
|||
def update_annotation(
|
||||
self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage
|
||||
) -> None:
|
||||
logging.debug("clicked annotation")
|
||||
logger.debug("clicked annotation")
|
||||
self.annotation_button.configure(image=image)
|
||||
self.annotation_button.image = image
|
||||
self.app.manager.annotation_type = shape_type
|
||||
|
@ -433,7 +434,7 @@ class Toolbar(ttk.Frame):
|
|||
self.marker_frame.grid()
|
||||
|
||||
def click_run_button(self) -> None:
|
||||
logging.debug("Click on RUN button")
|
||||
logger.debug("Click on RUN button")
|
||||
dialog = RunToolDialog(self.app)
|
||||
dialog.show()
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ from core.gui import appconfig, themes, validation
|
|||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -161,7 +163,7 @@ class ConfigFrame(ttk.Notebook):
|
|||
)
|
||||
entry.grid(row=index, column=1, sticky=tk.EW)
|
||||
else:
|
||||
logging.error("unhandled config option type: %s", option.type)
|
||||
logger.error("unhandled config option type: %s", option.type)
|
||||
self.values[option.name] = value
|
||||
|
||||
def parse_config(self) -> Dict[str, str]:
|
||||
|
|
|
@ -10,6 +10,7 @@ from pyproj import Transformer
|
|||
|
||||
from core.emulator.enumerations import RegisterTlvs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
SCALE_FACTOR: float = 100.0
|
||||
CRS_WGS84: int = 4326
|
||||
CRS_PROJ: int = 3857
|
||||
|
@ -92,7 +93,7 @@ class GeoLocation:
|
|||
:param alt: altitude value
|
||||
: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 -= self.refproj[0]
|
||||
py -= self.refproj[1]
|
||||
|
@ -100,7 +101,7 @@ class GeoLocation:
|
|||
x = self.meters2pixels(px) + self.refxyz[0]
|
||||
y = -(self.meters2pixels(py) + self.refxyz[1])
|
||||
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
|
||||
|
||||
def getgeo(self, x: float, y: float, z: float) -> Tuple[float, float, float]:
|
||||
|
@ -112,7 +113,7 @@ class GeoLocation:
|
|||
:param z: z value
|
||||
: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]
|
||||
y = -(y - self.refxyz[1])
|
||||
if z is None:
|
||||
|
@ -123,5 +124,5 @@ class GeoLocation:
|
|||
py = self.refproj[1] + self.pixels2meters(y)
|
||||
lon, lat = self.to_geo.transform(px, py)
|
||||
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
|
||||
|
|
|
@ -28,6 +28,8 @@ from core.nodes.base import CoreNode
|
|||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
@ -42,6 +44,43 @@ def get_mobility_node(session: "Session", node_id: int) -> Union[WlanNode, Emane
|
|||
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):
|
||||
"""
|
||||
Member of session class for handling configuration data for mobility and
|
||||
|
@ -81,7 +120,7 @@ class MobilityManager(ModelManager):
|
|||
if node_ids is None:
|
||||
node_ids = self.nodes()
|
||||
for node_id in node_ids:
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"node(%s) mobility startup: %s", node_id, self.get_all_configs(node_id)
|
||||
)
|
||||
try:
|
||||
|
@ -95,8 +134,8 @@ class MobilityManager(ModelManager):
|
|||
if node.mobility:
|
||||
self.session.event_loop.add_event(0.0, node.mobility.startup)
|
||||
except CoreError:
|
||||
logging.exception("mobility startup error")
|
||||
logging.warning(
|
||||
logger.exception("mobility startup error")
|
||||
logger.warning(
|
||||
"skipping mobility configuration for unknown node: %s", node_id
|
||||
)
|
||||
|
||||
|
@ -114,7 +153,7 @@ class MobilityManager(ModelManager):
|
|||
try:
|
||||
node = get_mobility_node(self.session, node_id)
|
||||
except CoreError:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
"ignoring event for model(%s), unknown node(%s)", name, node_id
|
||||
)
|
||||
return
|
||||
|
@ -124,17 +163,17 @@ class MobilityManager(ModelManager):
|
|||
for model in models:
|
||||
cls = self.models.get(model)
|
||||
if not cls:
|
||||
logging.warning("ignoring event for unknown model '%s'", model)
|
||||
logger.warning("ignoring event for unknown model '%s'", model)
|
||||
continue
|
||||
if cls.config_type in [RegisterTlvs.WIRELESS, RegisterTlvs.MOBILITY]:
|
||||
model = node.mobility
|
||||
else:
|
||||
continue
|
||||
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
|
||||
if cls.name != model.name:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"ignoring event for %s wrong model %s,%s",
|
||||
node.name,
|
||||
cls.name,
|
||||
|
@ -236,35 +275,35 @@ class BasicRangeModel(WirelessModel):
|
|||
name: str = "basic_range"
|
||||
options: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="range",
|
||||
_type=ConfigDataTypes.UINT32,
|
||||
id="range",
|
||||
type=ConfigDataTypes.UINT32,
|
||||
default="275",
|
||||
label="wireless range (pixels)",
|
||||
),
|
||||
Configuration(
|
||||
_id="bandwidth",
|
||||
_type=ConfigDataTypes.UINT64,
|
||||
id="bandwidth",
|
||||
type=ConfigDataTypes.UINT64,
|
||||
default="54000000",
|
||||
label="bandwidth (bps)",
|
||||
),
|
||||
Configuration(
|
||||
_id="jitter",
|
||||
_type=ConfigDataTypes.UINT64,
|
||||
id="jitter",
|
||||
type=ConfigDataTypes.UINT64,
|
||||
default="0",
|
||||
label="transmission jitter (usec)",
|
||||
),
|
||||
Configuration(
|
||||
_id="delay",
|
||||
_type=ConfigDataTypes.UINT64,
|
||||
id="delay",
|
||||
type=ConfigDataTypes.UINT64,
|
||||
default="5000",
|
||||
label="transmission delay (usec)",
|
||||
),
|
||||
Configuration(
|
||||
_id="error", _type=ConfigDataTypes.STRING, default="0", label="loss (%)"
|
||||
id="error", type=ConfigDataTypes.STRING, default="0", label="loss (%)"
|
||||
),
|
||||
Configuration(
|
||||
_id="promiscuous",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
id="promiscuous",
|
||||
type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
label="promiscuous mode",
|
||||
),
|
||||
|
@ -293,25 +332,6 @@ class BasicRangeModel(WirelessModel):
|
|||
self.jitter: Optional[int] = None
|
||||
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:
|
||||
"""
|
||||
Apply link parameters to all interfaces. This is invoked from
|
||||
|
@ -406,20 +426,20 @@ class BasicRangeModel(WirelessModel):
|
|||
a = min(iface, iface2)
|
||||
b = max(iface, iface2)
|
||||
|
||||
with self.wlan._linked_lock:
|
||||
linked = self.wlan.linked(a, b)
|
||||
with self.wlan.linked_lock:
|
||||
linked = self.wlan.is_linked(a, b)
|
||||
if d > self.range:
|
||||
if linked:
|
||||
logging.debug("was linked, unlinking")
|
||||
logger.debug("was linked, unlinking")
|
||||
self.wlan.unlink(a, b)
|
||||
self.sendlinkmsg(a, b, unlink=True)
|
||||
else:
|
||||
if not linked:
|
||||
logging.debug("was not linked, linking")
|
||||
logger.debug("was not linked, linking")
|
||||
self.wlan.link(a, b)
|
||||
self.sendlinkmsg(a, b)
|
||||
except KeyError:
|
||||
logging.exception("error getting interfaces during calclinkS")
|
||||
logger.exception("error getting interfaces during calclink")
|
||||
|
||||
@staticmethod
|
||||
def calcdistance(
|
||||
|
@ -446,15 +466,15 @@ class BasicRangeModel(WirelessModel):
|
|||
:param config: values to update configuration
|
||||
: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:
|
||||
self.range = 0
|
||||
logging.debug("wlan %s set range to %s", self.wlan.name, self.range)
|
||||
self.bw = self._get_config(self.bw, config, "bandwidth")
|
||||
self.delay = self._get_config(self.delay, config, "delay")
|
||||
self.loss = self._get_config(self.loss, config, "error")
|
||||
self.jitter = self._get_config(self.jitter, config, "jitter")
|
||||
promiscuous = config["promiscuous"] == "1"
|
||||
logger.debug("wlan %s set range to %s", self.wlan.name, self.range)
|
||||
self.bw = get_config_int(self.bw, config, "bandwidth")
|
||||
self.delay = get_config_int(self.delay, config, "delay")
|
||||
self.loss = get_config_float(self.loss, config, "error")
|
||||
self.jitter = get_config_int(self.jitter, config, "jitter")
|
||||
promiscuous = config.get("promiscuous", "0") == "1"
|
||||
if self.promiscuous and not promiscuous:
|
||||
self.wlan.net_client.set_mac_learning(self.wlan.brname, LEARNING_ENABLED)
|
||||
elif not self.promiscuous and promiscuous:
|
||||
|
@ -506,10 +526,10 @@ class BasicRangeModel(WirelessModel):
|
|||
:return: all link data
|
||||
"""
|
||||
all_links = []
|
||||
with self.wlan._linked_lock:
|
||||
for a in self.wlan._linked:
|
||||
for b in self.wlan._linked[a]:
|
||||
if self.wlan._linked[a][b]:
|
||||
with self.wlan.linked_lock:
|
||||
for a in self.wlan.linked:
|
||||
for b in self.wlan.linked[a]:
|
||||
if self.wlan.linked[a][b]:
|
||||
all_links.append(self.create_link_data(a, b, flags))
|
||||
return all_links
|
||||
|
||||
|
@ -868,40 +888,38 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
name: str = "ns2script"
|
||||
options: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="file", _type=ConfigDataTypes.STRING, label="mobility script file"
|
||||
id="file", type=ConfigDataTypes.STRING, label="mobility script file"
|
||||
),
|
||||
Configuration(
|
||||
_id="refresh_ms",
|
||||
_type=ConfigDataTypes.UINT32,
|
||||
id="refresh_ms",
|
||||
type=ConfigDataTypes.UINT32,
|
||||
default="50",
|
||||
label="refresh time (ms)",
|
||||
),
|
||||
Configuration(id="loop", type=ConfigDataTypes.BOOL, default="1", label="loop"),
|
||||
Configuration(
|
||||
_id="loop", _type=ConfigDataTypes.BOOL, default="1", label="loop"
|
||||
),
|
||||
Configuration(
|
||||
_id="autostart",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="autostart",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="auto-start seconds (0.0 for runtime)",
|
||||
),
|
||||
Configuration(
|
||||
_id="map",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="map",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="node mapping (optional, e.g. 0:1,1:2,2:3)",
|
||||
),
|
||||
Configuration(
|
||||
_id="script_start",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="script_start",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="script file to run upon start",
|
||||
),
|
||||
Configuration(
|
||||
_id="script_pause",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="script_pause",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="script file to run upon pause",
|
||||
),
|
||||
Configuration(
|
||||
_id="script_stop",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
id="script_stop",
|
||||
type=ConfigDataTypes.STRING,
|
||||
label="script file to run upon stop",
|
||||
),
|
||||
]
|
||||
|
@ -920,7 +938,7 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
:param _id: object id
|
||||
"""
|
||||
super().__init__(session, _id)
|
||||
self.file: Optional[str] = None
|
||||
self.file: Optional[Path] = None
|
||||
self.autostart: Optional[str] = None
|
||||
self.nodemap: Dict[int, int] = {}
|
||||
self.script_start: Optional[str] = None
|
||||
|
@ -928,8 +946,8 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
self.script_stop: Optional[str] = None
|
||||
|
||||
def update_config(self, config: Dict[str, str]) -> None:
|
||||
self.file = config["file"]
|
||||
logging.info(
|
||||
self.file = Path(config["file"])
|
||||
logger.info(
|
||||
"ns-2 scripted mobility configured for WLAN %d using file: %s",
|
||||
self.id,
|
||||
self.file,
|
||||
|
@ -953,15 +971,15 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
filename = self.findfile(self.file)
|
||||
file_path = self.findfile(self.file)
|
||||
try:
|
||||
f = open(filename, "r")
|
||||
f = file_path.open("r")
|
||||
except IOError:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
"ns-2 scripted mobility failed to load file: %s", self.file
|
||||
)
|
||||
return
|
||||
logging.info("reading ns-2 script file: %s", filename)
|
||||
logger.info("reading ns-2 script file: %s", file_path)
|
||||
ln = 0
|
||||
ix = iy = iz = None
|
||||
inodenum = None
|
||||
|
@ -977,13 +995,13 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
# waypoints:
|
||||
# $ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0"
|
||||
parts = line.split()
|
||||
time = float(parts[2])
|
||||
line_time = float(parts[2])
|
||||
nodenum = parts[3][1 + parts[3].index("(") : parts[3].index(")")]
|
||||
x = float(parts[5])
|
||||
y = float(parts[6])
|
||||
z = None
|
||||
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_(":
|
||||
# initial position (time=0, speed=0):
|
||||
# $node_(6) set X_ 780.0
|
||||
|
@ -1004,38 +1022,38 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
else:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
"skipping line %d of file %s '%s'", ln, self.file, line
|
||||
)
|
||||
continue
|
||||
if ix is not None and iy is not None:
|
||||
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
|
||||
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
|
||||
:raises CoreError: when file is not found
|
||||
"""
|
||||
file_path = Path(file_name).expanduser()
|
||||
file_path = file_path.expanduser()
|
||||
if file_path.exists():
|
||||
return str(file_path)
|
||||
if self.session.file_name:
|
||||
file_path = Path(self.session.file_name).parent / file_name
|
||||
if file_path.exists():
|
||||
return str(file_path)
|
||||
return file_path
|
||||
if self.session.file_path:
|
||||
session_file_path = self.session.file_path.parent / file_path
|
||||
if session_file_path.exists():
|
||||
return session_file_path
|
||||
if self.session.user:
|
||||
user_path = Path(f"~{self.session.user}").expanduser()
|
||||
file_path = user_path / ".core" / "configs" / file_name
|
||||
if file_path.exists():
|
||||
return str(file_path)
|
||||
file_path = user_path / ".coregui" / "mobility" / file_name
|
||||
if file_path.exists():
|
||||
return str(file_path)
|
||||
raise CoreError(f"invalid file: {file_name}")
|
||||
configs_path = user_path / ".core" / "configs" / file_path
|
||||
if configs_path.exists():
|
||||
return configs_path
|
||||
mobility_path = user_path / ".coregui" / "mobility" / file_path
|
||||
if mobility_path.exists():
|
||||
return mobility_path
|
||||
raise CoreError(f"invalid file: {file_path}")
|
||||
|
||||
def parsemap(self, mapstr: str) -> None:
|
||||
"""
|
||||
|
@ -1047,7 +1065,6 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
self.nodemap = {}
|
||||
if mapstr.strip() == "":
|
||||
return
|
||||
|
||||
for pair in mapstr.split(","):
|
||||
parts = pair.split(":")
|
||||
try:
|
||||
|
@ -1055,7 +1072,7 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
raise ValueError
|
||||
self.nodemap[int(parts[0])] = int(parts[1])
|
||||
except ValueError:
|
||||
logging.exception("ns-2 mobility node map error")
|
||||
logger.exception("ns-2 mobility node map error")
|
||||
|
||||
def map(self, nodenum: str) -> int:
|
||||
"""
|
||||
|
@ -1077,19 +1094,19 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
:return: nothing
|
||||
"""
|
||||
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
|
||||
try:
|
||||
t = float(self.autostart)
|
||||
except ValueError:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
"Invalid auto-start seconds specified '%s' for %s",
|
||||
self.autostart,
|
||||
self.net.name,
|
||||
)
|
||||
return
|
||||
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.session.event_loop.add_event(t, self.run)
|
||||
|
||||
|
@ -1099,7 +1116,7 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info("starting script: %s", self.file)
|
||||
logger.info("starting script: %s", self.file)
|
||||
laststate = self.state
|
||||
super().start()
|
||||
if laststate == self.STATE_PAUSED:
|
||||
|
@ -1120,7 +1137,7 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info("pausing script: %s", self.file)
|
||||
logger.info("pausing script: %s", self.file)
|
||||
super().pause()
|
||||
self.statescript("pause")
|
||||
|
||||
|
@ -1132,7 +1149,7 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
position
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info("stopping script: %s", self.file)
|
||||
logger.info("stopping script: %s", self.file)
|
||||
super().stop(move_initial=move_initial)
|
||||
self.statescript("stop")
|
||||
|
||||
|
@ -1152,8 +1169,7 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
filename = self.script_stop
|
||||
if filename is None or filename == "":
|
||||
return
|
||||
filename = Path(filename)
|
||||
filename = self.findfile(filename)
|
||||
args = f"{BASH} {filename} {typestr}"
|
||||
utils.cmd(
|
||||
args, cwd=self.session.session_dir, env=self.session.get_environment()
|
||||
)
|
||||
utils.cmd(args, cwd=self.session.directory, env=self.session.get_environment())
|
||||
|
|
|
@ -3,9 +3,9 @@ Defines the base logic for nodes used within core.
|
|||
"""
|
||||
import abc
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from threading import RLock
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union
|
||||
|
||||
|
@ -21,6 +21,8 @@ from core.nodes.client import VnodeClient
|
|||
from core.nodes.interface import CoreInterface, TunTap, Veth
|
||||
from core.nodes.netclient import LinuxNetClient, get_net_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.distributed import DistributedServer
|
||||
from core.emulator.session import Session
|
||||
|
@ -30,6 +32,8 @@ if TYPE_CHECKING:
|
|||
CoreServices = List[Union[CoreService, Type[CoreService]]]
|
||||
ConfigServiceType = Type[ConfigService]
|
||||
|
||||
PRIVATE_DIRS: List[Path] = [Path("/var/run"), Path("/var/log")]
|
||||
|
||||
|
||||
class NodeBase(abc.ABC):
|
||||
"""
|
||||
|
@ -97,7 +101,7 @@ class NodeBase(abc.ABC):
|
|||
self,
|
||||
args: str,
|
||||
env: Dict[str, str] = None,
|
||||
cwd: str = None,
|
||||
cwd: Path = None,
|
||||
wait: bool = True,
|
||||
shell: bool = False,
|
||||
) -> str:
|
||||
|
@ -221,7 +225,7 @@ class CoreNodeBase(NodeBase):
|
|||
"""
|
||||
super().__init__(session, _id, name, server)
|
||||
self.config_services: Dict[str, "ConfigService"] = {}
|
||||
self.nodedir: Optional[str] = None
|
||||
self.directory: Optional[Path] = None
|
||||
self.tmpnodedir: bool = False
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -233,11 +237,21 @@ class CoreNodeBase(NodeBase):
|
|||
raise NotImplementedError
|
||||
|
||||
@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.
|
||||
|
||||
:param filename: name of file to create
|
||||
:param file_path: name of file to create
|
||||
:param contents: contents of file
|
||||
:param mode: mode for file
|
||||
:return: nothing
|
||||
|
@ -245,12 +259,25 @@ class CoreNodeBase(NodeBase):
|
|||
raise NotImplementedError
|
||||
|
||||
@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.
|
||||
|
||||
:param srcname: source file name
|
||||
:param filename: file name to add
|
||||
:param src_path: source file path
|
||||
:param file_path: file name to add
|
||||
:return: nothing
|
||||
:raises CoreCommandError: when a non-zero exit status occurs
|
||||
"""
|
||||
|
@ -302,6 +329,21 @@ class CoreNodeBase(NodeBase):
|
|||
"""
|
||||
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:
|
||||
"""
|
||||
Adds a configuration service to the node.
|
||||
|
@ -345,9 +387,9 @@ class CoreNodeBase(NodeBase):
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
if self.nodedir is None:
|
||||
self.nodedir = os.path.join(self.session.session_dir, self.name + ".conf")
|
||||
self.host_cmd(f"mkdir -p {self.nodedir}")
|
||||
if self.directory is None:
|
||||
self.directory = self.session.directory / f"{self.name}.conf"
|
||||
self.host_cmd(f"mkdir -p {self.directory}")
|
||||
self.tmpnodedir = True
|
||||
else:
|
||||
self.tmpnodedir = False
|
||||
|
@ -362,7 +404,7 @@ class CoreNodeBase(NodeBase):
|
|||
if preserve:
|
||||
return
|
||||
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:
|
||||
"""
|
||||
|
@ -387,7 +429,7 @@ class CoreNodeBase(NodeBase):
|
|||
if iface_id not in self.ifaces:
|
||||
raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
|
||||
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.shutdown()
|
||||
|
||||
|
@ -458,7 +500,7 @@ class CoreNode(CoreNodeBase):
|
|||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
nodedir: str = None,
|
||||
directory: Path = None,
|
||||
server: "DistributedServer" = None,
|
||||
) -> None:
|
||||
"""
|
||||
|
@ -467,19 +509,17 @@ class CoreNode(CoreNodeBase):
|
|||
:param session: core session instance
|
||||
:param _id: object id
|
||||
:param name: object name
|
||||
:param nodedir: node directory
|
||||
:param directory: node directory
|
||||
:param server: remote server node
|
||||
will run on, default is None for localhost
|
||||
"""
|
||||
super().__init__(session, _id, name, server)
|
||||
self.nodedir: Optional[str] = nodedir
|
||||
self.ctrlchnlname: str = os.path.abspath(
|
||||
os.path.join(self.session.session_dir, self.name)
|
||||
)
|
||||
self.directory: Optional[Path] = directory
|
||||
self.ctrlchnlname: Path = self.session.directory / self.name
|
||||
self.client: Optional[VnodeClient] = None
|
||||
self.pid: Optional[int] = None
|
||||
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.session.use_ovs()
|
||||
)
|
||||
|
@ -524,33 +564,33 @@ class CoreNode(CoreNodeBase):
|
|||
f"{VNODED} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log "
|
||||
f"-p {self.ctrlchnlname}.pid"
|
||||
)
|
||||
if self.nodedir:
|
||||
vnoded += f" -C {self.nodedir}"
|
||||
if self.directory:
|
||||
vnoded += f" -C {self.directory}"
|
||||
env = self.session.get_environment(state=False)
|
||||
env["NODE_NUMBER"] = str(self.id)
|
||||
env["NODE_NAME"] = str(self.name)
|
||||
|
||||
output = self.host_cmd(vnoded, env=env)
|
||||
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
|
||||
self.client = VnodeClient(self.name, self.ctrlchnlname)
|
||||
|
||||
# bring up the loopback interface
|
||||
logging.debug("bringing up loopback interface")
|
||||
logger.debug("bringing up loopback interface")
|
||||
self.node_net_client.device_up("lo")
|
||||
|
||||
# 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)
|
||||
|
||||
# mark node as up
|
||||
self.up = True
|
||||
|
||||
# create private directories
|
||||
self.privatedir("/var/run")
|
||||
self.privatedir("/var/log")
|
||||
for dir_path in PRIVATE_DIRS:
|
||||
self.create_dir(dir_path)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
|
@ -561,35 +601,30 @@ class CoreNode(CoreNodeBase):
|
|||
# nothing to do if node is not up
|
||||
if not self.up:
|
||||
return
|
||||
|
||||
with self.lock:
|
||||
try:
|
||||
# unmount all targets (NOTE: non-persistent mount namespaces are
|
||||
# removed by the kernel when last referencing process is killed)
|
||||
self._mounts = []
|
||||
|
||||
# shutdown all interfaces
|
||||
for iface in self.get_ifaces():
|
||||
iface.shutdown()
|
||||
|
||||
# kill node process if present
|
||||
try:
|
||||
self.host_cmd(f"kill -9 {self.pid}")
|
||||
except CoreCommandError:
|
||||
logging.exception("error killing process")
|
||||
|
||||
logger.exception("error killing process")
|
||||
# remove node directory if present
|
||||
try:
|
||||
self.host_cmd(f"rm -rf {self.ctrlchnlname}")
|
||||
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
|
||||
self.ifaces.clear()
|
||||
self.client.close()
|
||||
self.up = False
|
||||
except OSError:
|
||||
logging.exception("error during shutdown")
|
||||
logger.exception("error during shutdown")
|
||||
finally:
|
||||
self.rmnodedir()
|
||||
|
||||
|
@ -636,35 +671,37 @@ class CoreNode(CoreNodeBase):
|
|||
else:
|
||||
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
|
||||
"""
|
||||
if path[0] != "/":
|
||||
raise ValueError(f"path not fully qualified: {path}")
|
||||
hostpath = os.path.join(
|
||||
self.nodedir, os.path.normpath(path).strip("/").replace("/", ".")
|
||||
)
|
||||
self.host_cmd(f"mkdir -p {hostpath}")
|
||||
self.mount(hostpath, path)
|
||||
if not dir_path.is_absolute():
|
||||
raise CoreError(f"private directory path not fully qualified: {dir_path}")
|
||||
logger.debug("node(%s) creating private directory: %s", self.name, dir_path)
|
||||
parent_path = self._find_parent_path(dir_path)
|
||||
if parent_path:
|
||||
self.host_cmd(f"mkdir -p {parent_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.
|
||||
|
||||
:param source: source directory to mount
|
||||
:param target: target directory to create
|
||||
:param src_path: source directory to mount
|
||||
:param target_path: target directory to create
|
||||
:return: nothing
|
||||
:raises CoreCommandError: when a non-zero exit status occurs
|
||||
"""
|
||||
source = os.path.abspath(source)
|
||||
logging.debug("node(%s) mounting: %s at %s", self.name, source, target)
|
||||
self.cmd(f"mkdir -p {target}")
|
||||
self.cmd(f"{MOUNT} -n --bind {source} {target}")
|
||||
self._mounts.append((source, target))
|
||||
logger.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path)
|
||||
self.cmd(f"mkdir -p {target_path}")
|
||||
self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}")
|
||||
self._mounts.append((src_path, target_path))
|
||||
|
||||
def next_iface_id(self) -> int:
|
||||
"""
|
||||
|
@ -719,9 +756,9 @@ class CoreNode(CoreNodeBase):
|
|||
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)
|
||||
logger.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)
|
||||
logger.debug("interface mac: %s - %s", veth.name, mac)
|
||||
veth.set_mac(mac)
|
||||
|
||||
try:
|
||||
|
@ -851,86 +888,99 @@ class CoreNode(CoreNodeBase):
|
|||
self.ifup(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.
|
||||
|
||||
:param srcname: source file name
|
||||
:param filename: file name to add
|
||||
:param src_path: source file path
|
||||
:param file_path: file name to add
|
||||
:return: nothing
|
||||
:raises CoreCommandError: when a non-zero exit status occurs
|
||||
"""
|
||||
logging.info("adding file from %s to %s", srcname, filename)
|
||||
directory = os.path.dirname(filename)
|
||||
logger.info("adding file from %s to %s", src_path, file_path)
|
||||
directory = file_path.parent
|
||||
if self.server is None:
|
||||
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")
|
||||
else:
|
||||
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
|
||||
:return: path to file
|
||||
:param path: existing parent path to use
|
||||
:return: exist parent path if exists, None otherwise
|
||||
"""
|
||||
dirname, basename = os.path.split(filename)
|
||||
if not basename:
|
||||
raise ValueError(f"no basename for filename: {filename}")
|
||||
if dirname and dirname[0] == "/":
|
||||
dirname = dirname[1:]
|
||||
dirname = dirname.replace("/", ".")
|
||||
dirname = os.path.join(self.nodedir, dirname)
|
||||
return os.path.join(dirname, basename)
|
||||
logger.debug("looking for existing parent: %s", path)
|
||||
existing_path = None
|
||||
for parent in path.parents:
|
||||
node_path = self.host_path(parent, is_dir=True)
|
||||
if node_path == self.directory:
|
||||
break
|
||||
if self.path_exists(str(node_path)):
|
||||
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 mode: mode for file
|
||||
:param mode: mode to create file with
|
||||
:return: nothing
|
||||
"""
|
||||
hostfilename = self.hostfilename(filename)
|
||||
dirname, _basename = os.path.split(hostfilename)
|
||||
if self.server is None:
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname, mode=0o755)
|
||||
with open(hostfilename, "w") as open_file:
|
||||
open_file.write(contents)
|
||||
os.chmod(open_file.name, mode)
|
||||
logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode)
|
||||
host_path = self._find_parent_path(file_path)
|
||||
if host_path:
|
||||
self.host_cmd(f"mkdir -p {host_path.parent}")
|
||||
else:
|
||||
self.host_cmd(f"mkdir -m {0o755:o} -p {dirname}")
|
||||
self.server.remote_put_temp(hostfilename, contents)
|
||||
self.host_cmd(f"chmod {mode:o} {hostfilename}")
|
||||
logging.debug(
|
||||
"node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode
|
||||
host_path = self.host_path(file_path)
|
||||
directory = host_path.parent
|
||||
if self.server is None:
|
||||
if not directory.exists():
|
||||
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,
|
||||
)
|
||||
|
||||
def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None:
|
||||
"""
|
||||
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)
|
||||
host_path = self._find_parent_path(dst_path)
|
||||
if host_path:
|
||||
self.host_cmd(f"mkdir -p {host_path.parent}")
|
||||
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:
|
||||
self.host_cmd(f"chmod {mode:o} {hostfilename}")
|
||||
logging.info(
|
||||
"node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode
|
||||
)
|
||||
self.host_cmd(f"chmod {mode:o} {host_path}")
|
||||
|
||||
|
||||
class CoreNetworkBase(NodeBase):
|
||||
|
@ -958,9 +1008,9 @@ class CoreNetworkBase(NodeBase):
|
|||
will run on, default is None for localhost
|
||||
"""
|
||||
super().__init__(session, _id, name, server)
|
||||
self.brname = None
|
||||
self._linked = {}
|
||||
self._linked_lock = threading.Lock()
|
||||
self.brname: Optional[str] = None
|
||||
self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {}
|
||||
self.linked_lock: threading.Lock = threading.Lock()
|
||||
|
||||
@abc.abstractmethod
|
||||
def startup(self) -> None:
|
||||
|
@ -1029,8 +1079,8 @@ class CoreNetworkBase(NodeBase):
|
|||
i = self.next_iface_id()
|
||||
self.ifaces[i] = iface
|
||||
iface.net_id = i
|
||||
with self._linked_lock:
|
||||
self._linked[iface] = {}
|
||||
with self.linked_lock:
|
||||
self.linked[iface] = {}
|
||||
|
||||
def detach(self, iface: CoreInterface) -> None:
|
||||
"""
|
||||
|
@ -1041,8 +1091,8 @@ class CoreNetworkBase(NodeBase):
|
|||
"""
|
||||
del self.ifaces[iface.net_id]
|
||||
iface.net_id = None
|
||||
with self._linked_lock:
|
||||
del self._linked[iface]
|
||||
with self.linked_lock:
|
||||
del self.linked[iface]
|
||||
|
||||
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.
|
||||
The control channel can be accessed via calls using the vcmd shell.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from core import utils
|
||||
from core.executables import BASH, VCMD
|
||||
|
@ -13,7 +14,7 @@ class VnodeClient:
|
|||
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.
|
||||
|
||||
|
@ -21,7 +22,7 @@ class VnodeClient:
|
|||
:param ctrlchnlname: control channel name
|
||||
"""
|
||||
self.name: str = name
|
||||
self.ctrlchnlname: str = ctrlchnlname
|
||||
self.ctrlchnlname: Path = ctrlchnlname
|
||||
|
||||
def _verify_connection(self) -> None:
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
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.netclient import LinuxNetClient, get_net_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
@ -50,7 +52,7 @@ class DockerClient:
|
|||
self.run(f"docker rm -f {self.name}")
|
||||
|
||||
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)
|
||||
|
||||
def create_ns_cmd(self, cmd: str) -> str:
|
||||
|
@ -60,11 +62,11 @@ class DockerClient:
|
|||
args = f"docker inspect -f '{{{{.State.Pid}}}}' {self.name}"
|
||||
output = self.run(args)
|
||||
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
|
||||
|
||||
def copy_file(self, source: str, destination: str) -> str:
|
||||
args = f"docker cp {source} {self.name}:{destination}"
|
||||
def copy_file(self, src_path: Path, dst_path: Path) -> str:
|
||||
args = f"docker cp {src_path} {self.name}:{dst_path}"
|
||||
return self.run(args)
|
||||
|
||||
|
||||
|
@ -76,7 +78,7 @@ class DockerNode(CoreNode):
|
|||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
nodedir: str = None,
|
||||
directory: str = None,
|
||||
server: DistributedServer = None,
|
||||
image: str = None,
|
||||
) -> None:
|
||||
|
@ -86,7 +88,7 @@ class DockerNode(CoreNode):
|
|||
:param session: core session instance
|
||||
:param _id: object id
|
||||
:param name: object name
|
||||
:param nodedir: node directory
|
||||
:param directory: node directory
|
||||
:param server: remote server node
|
||||
will run on, default is None for localhost
|
||||
:param image: image to start container with
|
||||
|
@ -94,7 +96,7 @@ class DockerNode(CoreNode):
|
|||
if image is None:
|
||||
image = "ubuntu"
|
||||
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:
|
||||
"""
|
||||
|
@ -162,77 +164,73 @@ class DockerNode(CoreNode):
|
|||
"""
|
||||
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.
|
||||
|
||||
:param path: path to create
|
||||
:param dir_path: path to create
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug("creating node dir: %s", path)
|
||||
args = f"mkdir -p {path}"
|
||||
logger.debug("creating node dir: %s", dir_path)
|
||||
args = f"mkdir -p {dir_path}"
|
||||
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.
|
||||
|
||||
:param source: source directory to mount
|
||||
:param target: target directory to create
|
||||
:param src_path: source directory to mount
|
||||
:param target_path: target directory to create
|
||||
:return: nothing
|
||||
:raises CoreCommandError: when a non-zero exit status occurs
|
||||
"""
|
||||
logging.debug("mounting source(%s) target(%s)", source, target)
|
||||
logger.debug("mounting source(%s) target(%s)", src_path, target_path)
|
||||
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.
|
||||
|
||||
:param filename: name of file to create
|
||||
:param file_path: name of file to create
|
||||
:param contents: contents of file
|
||||
:param mode: mode for file
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug("nodefile filename(%s) mode(%s)", filename, mode)
|
||||
directory = os.path.dirname(filename)
|
||||
logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode)
|
||||
temp = NamedTemporaryFile(delete=False)
|
||||
temp.write(contents.encode("utf-8"))
|
||||
temp.close()
|
||||
|
||||
if directory:
|
||||
temp_path = Path(temp.name)
|
||||
directory = file_path.name
|
||||
if str(directory) != ".":
|
||||
self.cmd(f"mkdir -m {0o755:o} -p {directory}")
|
||||
if self.server is not None:
|
||||
self.server.remote_put(temp.name, temp.name)
|
||||
self.client.copy_file(temp.name, filename)
|
||||
self.cmd(f"chmod {mode:o} {filename}")
|
||||
self.server.remote_put(temp_path, temp_path)
|
||||
self.client.copy_file(temp_path, file_path)
|
||||
self.cmd(f"chmod {mode:o} {file_path}")
|
||||
if self.server is not None:
|
||||
self.host_cmd(f"rm -f {temp.name}")
|
||||
os.unlink(temp.name)
|
||||
logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode)
|
||||
self.host_cmd(f"rm -f {temp_path}")
|
||||
temp_path.unlink()
|
||||
|
||||
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.
|
||||
Change file mode if specified.
|
||||
|
||||
:param filename: file name to copy file to
|
||||
:param srcfilename: file to copy
|
||||
:param dst_path: file name to copy file to
|
||||
:param src_path: file to copy
|
||||
:param mode: mode to copy to
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info(
|
||||
"node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode
|
||||
logger.info(
|
||||
"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 {directory}")
|
||||
|
||||
if self.server is None:
|
||||
source = srcfilename
|
||||
else:
|
||||
self.cmd(f"mkdir -p {dst_path.parent}")
|
||||
if self.server:
|
||||
temp = NamedTemporaryFile(delete=False)
|
||||
source = temp.name
|
||||
self.server.remote_put(source, temp.name)
|
||||
|
||||
self.client.copy_file(source, filename)
|
||||
self.cmd(f"chmod {mode:o} {filename}")
|
||||
temp_path = Path(temp.name)
|
||||
src_path = temp_path
|
||||
self.server.remote_put(src_path, temp_path)
|
||||
self.client.copy_file(src_path, dst_path)
|
||||
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 time
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import netaddr
|
||||
|
@ -14,6 +15,8 @@ from core.emulator.enumerations import TransportType
|
|||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.netclient import LinuxNetClient, get_net_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.distributed import DistributedServer
|
||||
from core.emulator.session import Session
|
||||
|
@ -79,7 +82,7 @@ class CoreInterface:
|
|||
self,
|
||||
args: str,
|
||||
env: Dict[str, str] = None,
|
||||
cwd: str = None,
|
||||
cwd: Path = None,
|
||||
wait: bool = True,
|
||||
shell: bool = False,
|
||||
) -> str:
|
||||
|
@ -273,7 +276,7 @@ class CoreInterface:
|
|||
:return: True if parameter changed, False otherwise
|
||||
"""
|
||||
# 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:
|
||||
return False
|
||||
|
||||
|
@ -456,7 +459,7 @@ class TunTap(CoreInterface):
|
|||
try:
|
||||
self.node.node_net_client.device_flush(self.name)
|
||||
except CoreCommandError:
|
||||
logging.exception("error shutting down tunnel tap")
|
||||
logger.exception("error shutting down tunnel tap")
|
||||
|
||||
self.up = False
|
||||
|
||||
|
@ -481,14 +484,14 @@ class TunTap(CoreInterface):
|
|||
msg = f"attempt {i} failed with nonzero exit status {r}"
|
||||
if i < attempts + 1:
|
||||
msg += ", retrying..."
|
||||
logging.info(msg)
|
||||
logger.info(msg)
|
||||
time.sleep(delay)
|
||||
delay += delay
|
||||
if delay > maxretrydelay:
|
||||
delay = maxretrydelay
|
||||
else:
|
||||
msg += ", giving up"
|
||||
logging.info(msg)
|
||||
logger.info(msg)
|
||||
|
||||
return result
|
||||
|
||||
|
@ -499,7 +502,7 @@ class TunTap(CoreInterface):
|
|||
|
||||
: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():
|
||||
try:
|
||||
|
@ -516,7 +519,7 @@ class TunTap(CoreInterface):
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug("waiting for device node: %s", self.name)
|
||||
logger.debug("waiting for device node: %s", self.name)
|
||||
|
||||
def nodedevexists():
|
||||
try:
|
||||
|
@ -633,5 +636,5 @@ class GreTap(CoreInterface):
|
|||
self.net_client.device_down(self.localname)
|
||||
self.net_client.delete_device(self.localname)
|
||||
except CoreCommandError:
|
||||
logging.exception("error during shutdown")
|
||||
logger.exception("error during shutdown")
|
||||
self.localname = None
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
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.interface import CoreInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
@ -57,11 +59,10 @@ class LxdClient:
|
|||
args = self.create_cmd(cmd)
|
||||
return utils.cmd(args, wait=wait, shell=shell)
|
||||
|
||||
def copy_file(self, source: str, destination: str) -> None:
|
||||
if destination[0] != "/":
|
||||
destination = os.path.join("/root/", destination)
|
||||
|
||||
args = f"lxc file push {source} {self.name}/{destination}"
|
||||
def copy_file(self, src_path: Path, dst_path: Path) -> None:
|
||||
if not str(dst_path).startswith("/"):
|
||||
dst_path = Path("/root/") / dst_path
|
||||
args = f"lxc file push {src_path} {self.name}/{dst_path}"
|
||||
self.run(args)
|
||||
|
||||
|
||||
|
@ -73,7 +74,7 @@ class LxcNode(CoreNode):
|
|||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
nodedir: str = None,
|
||||
directory: str = None,
|
||||
server: DistributedServer = None,
|
||||
image: str = None,
|
||||
) -> None:
|
||||
|
@ -83,7 +84,7 @@ class LxcNode(CoreNode):
|
|||
:param session: core session instance
|
||||
:param _id: object id
|
||||
:param name: object name
|
||||
:param nodedir: node directory
|
||||
:param directory: node directory
|
||||
:param server: remote server node
|
||||
will run on, default is None for localhost
|
||||
:param image: image to start container with
|
||||
|
@ -91,7 +92,7 @@ class LxcNode(CoreNode):
|
|||
if image is None:
|
||||
image = "ubuntu"
|
||||
self.image: str = image
|
||||
super().__init__(session, _id, name, nodedir, server)
|
||||
super().__init__(session, _id, name, directory, server)
|
||||
|
||||
def alive(self) -> bool:
|
||||
"""
|
||||
|
@ -139,81 +140,77 @@ class LxcNode(CoreNode):
|
|||
"""
|
||||
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.
|
||||
|
||||
:param path: path to create
|
||||
:param dir_path: path to create
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info("creating node dir: %s", path)
|
||||
args = f"mkdir -p {path}"
|
||||
logger.info("creating node dir: %s", dir_path)
|
||||
args = f"mkdir -p {dir_path}"
|
||||
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.
|
||||
|
||||
:param source: source directory to mount
|
||||
:param target: target directory to create
|
||||
:param src_path: source directory to mount
|
||||
:param target_path: target directory to create
|
||||
:return: nothing
|
||||
:raises CoreCommandError: when a non-zero exit status occurs
|
||||
"""
|
||||
logging.debug("mounting source(%s) target(%s)", source, target)
|
||||
logger.debug("mounting source(%s) target(%s)", src_path, target_path)
|
||||
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.
|
||||
|
||||
:param filename: name of file to create
|
||||
:param file_path: name of file to create
|
||||
:param contents: contents of file
|
||||
:param mode: mode for file
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug("nodefile filename(%s) mode(%s)", filename, mode)
|
||||
|
||||
directory = os.path.dirname(filename)
|
||||
logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode)
|
||||
temp = NamedTemporaryFile(delete=False)
|
||||
temp.write(contents.encode("utf-8"))
|
||||
temp.close()
|
||||
|
||||
if directory:
|
||||
temp_path = Path(temp.name)
|
||||
directory = file_path.parent
|
||||
if str(directory) != ".":
|
||||
self.cmd(f"mkdir -m {0o755:o} -p {directory}")
|
||||
if self.server is not None:
|
||||
self.server.remote_put(temp.name, temp.name)
|
||||
self.client.copy_file(temp.name, filename)
|
||||
self.cmd(f"chmod {mode:o} {filename}")
|
||||
self.server.remote_put(temp_path, temp_path)
|
||||
self.client.copy_file(temp_path, file_path)
|
||||
self.cmd(f"chmod {mode:o} {file_path}")
|
||||
if self.server is not None:
|
||||
self.host_cmd(f"rm -f {temp.name}")
|
||||
os.unlink(temp.name)
|
||||
logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode)
|
||||
self.host_cmd(f"rm -f {temp_path}")
|
||||
temp_path.unlink()
|
||||
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.
|
||||
Change file mode if specified.
|
||||
|
||||
:param filename: file name to copy file to
|
||||
:param srcfilename: file to copy
|
||||
:param dst_path: file name to copy file to
|
||||
:param src_path: file to copy
|
||||
:param mode: mode to copy to
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info(
|
||||
"node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode
|
||||
logger.info(
|
||||
"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 {directory}")
|
||||
|
||||
if self.server is None:
|
||||
source = srcfilename
|
||||
else:
|
||||
self.cmd(f"mkdir -p {dst_path.parent}")
|
||||
if self.server:
|
||||
temp = NamedTemporaryFile(delete=False)
|
||||
source = temp.name
|
||||
self.server.remote_put(source, temp.name)
|
||||
|
||||
self.client.copy_file(source, filename)
|
||||
self.cmd(f"chmod {mode:o} {filename}")
|
||||
temp_path = Path(temp.name)
|
||||
src_path = temp_path
|
||||
self.server.remote_put(src_path, temp_path)
|
||||
self.client.copy_file(src_path, dst_path)
|
||||
if mode is not None:
|
||||
self.cmd(f"chmod {mode:o} {dst_path}")
|
||||
|
||||
def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
|
||||
super().add_iface(iface, iface_id)
|
||||
|
|
|
@ -38,7 +38,7 @@ class LinuxNetClient:
|
|||
:param device: device to add route to
|
||||
: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:
|
||||
"""
|
||||
|
@ -361,14 +361,15 @@ class OvsNetClient(LinuxNetClient):
|
|||
return True
|
||||
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 value: ageing time value
|
||||
: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:
|
||||
|
|
|
@ -6,7 +6,10 @@ import logging
|
|||
import math
|
||||
import threading
|
||||
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
|
||||
|
||||
|
@ -20,11 +23,13 @@ from core.emulator.enumerations import (
|
|||
RegisterTlvs,
|
||||
)
|
||||
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.interface import CoreInterface, GreTap, Veth
|
||||
from core.nodes.netclient import get_net_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.distributed import DistributedServer
|
||||
from core.emulator.session import Session
|
||||
|
@ -33,224 +38,194 @@ if TYPE_CHECKING:
|
|||
WirelessModelType = Type[WirelessModel]
|
||||
|
||||
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
|
||||
many WLAN link updates.
|
||||
"""
|
||||
|
||||
# update rate is every 300ms
|
||||
rate: float = 0.3
|
||||
# ebtables
|
||||
atomic_file: str = "/tmp/pycore.ebtables.atomic"
|
||||
atomic_file: str = "/tmp/pycore.nftables.atomic"
|
||||
chain: str = "forward"
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Initialize the helper class, but don't start the update thread
|
||||
until a WLAN is instantiated.
|
||||
"""
|
||||
self.doupdateloop: bool = False
|
||||
self.updatethread: Optional[threading.Thread] = None
|
||||
self.running: bool = False
|
||||
self.run_thread: Optional[threading.Thread] = None
|
||||
# this lock protects cmds and updates lists
|
||||
self.updatelock: threading.Lock = threading.Lock()
|
||||
# list of pending ebtables commands
|
||||
self.lock: threading.Lock = threading.Lock()
|
||||
# list of pending nftables commands
|
||||
self.cmds: List[str] = []
|
||||
# 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
|
||||
# using this queue
|
||||
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
|
||||
"""
|
||||
with self.updatelock:
|
||||
self.last_update_time[wlan] = time.monotonic()
|
||||
if self.doupdateloop:
|
||||
with self.lock:
|
||||
self.last_update_time[net] = time.monotonic()
|
||||
if self.running:
|
||||
return
|
||||
self.doupdateloop = True
|
||||
self.updatethread = threading.Thread(target=self.updateloop, daemon=True)
|
||||
self.updatethread.start()
|
||||
self.running = True
|
||||
self.run_thread = threading.Thread(target=self.run, daemon=True)
|
||||
self.run_thread.start()
|
||||
|
||||
def stopupdateloop(self, wlan: "CoreNetwork") -> None:
|
||||
def stop(self, net: "CoreNetwork") -> None:
|
||||
"""
|
||||
Kill the update loop thread if there are no more WLANs using it.
|
||||
|
||||
Stop updates for network, when no networks remain, stop update thread.
|
||||
:param net: network to stop watching updates
|
||||
:return: nothing
|
||||
"""
|
||||
with self.updatelock:
|
||||
try:
|
||||
del self.last_update_time[wlan]
|
||||
except KeyError:
|
||||
logging.exception(
|
||||
"error deleting last update time for wlan, ignored before: %s", wlan
|
||||
)
|
||||
if len(self.last_update_time) > 0:
|
||||
with self.lock:
|
||||
self.last_update_time.pop(net, None)
|
||||
if self.last_update_time:
|
||||
return
|
||||
self.doupdateloop = False
|
||||
if self.updatethread:
|
||||
self.updatethread.join()
|
||||
self.updatethread = None
|
||||
self.running = False
|
||||
if self.run_thread:
|
||||
self.updates.put(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.
|
||||
|
||||
:param cmd: ebtable command
|
||||
:return: ebtable atomic command
|
||||
Return the time elapsed since this network was last updated.
|
||||
:param net: network node
|
||||
:return: elapsed time
|
||||
"""
|
||||
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.
|
||||
|
||||
:param wlan: wlan entity
|
||||
:return: elpased time
|
||||
"""
|
||||
try:
|
||||
elapsed = time.monotonic() - self.last_update_time[wlan]
|
||||
except KeyError:
|
||||
self.last_update_time[wlan] = time.monotonic()
|
||||
elapsed = 0.0
|
||||
|
||||
return elapsed
|
||||
|
||||
def updated(self, wlan: "CoreNetwork") -> None:
|
||||
"""
|
||||
Keep track of when this WLAN was last updated.
|
||||
|
||||
:param wlan: wlan entity
|
||||
:return: nothing
|
||||
"""
|
||||
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.
|
||||
Thread target that looks for networks needing update, and
|
||||
rate limits the amount of nftables activity. Only one userspace program
|
||||
should use nftables at any given time, or results can be unpredictable.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
while self.doupdateloop:
|
||||
with self.updatelock:
|
||||
for wlan in self.updates:
|
||||
# Check if wlan is from a previously closed session. Because of the
|
||||
# rate limiting scheme employed here, this may happen if a new session
|
||||
# is started soon after closing a previous session.
|
||||
# TODO: if these are WlanNodes, this will never throw an exception
|
||||
try:
|
||||
wlan.session
|
||||
except Exception:
|
||||
# Just mark as updated to remove from self.updates.
|
||||
self.updated(wlan)
|
||||
continue
|
||||
while self.running:
|
||||
net = self.updates.get()
|
||||
if net is None:
|
||||
break
|
||||
if not net.up:
|
||||
self.last_update_time[net] = time.monotonic()
|
||||
elif self.last_update(net) > self.rate:
|
||||
with self.lock:
|
||||
self.build_cmds(net)
|
||||
self.commit(net)
|
||||
self.last_update_time[net] = time.monotonic()
|
||||
|
||||
if self.lastupdate(wlan) > self.rate:
|
||||
self.buildcmds(wlan)
|
||||
self.ebcommit(wlan)
|
||||
self.updated(wlan)
|
||||
|
||||
time.sleep(self.rate)
|
||||
|
||||
def ebcommit(self, wlan: "CoreNetwork") -> None:
|
||||
def commit(self, net: "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
|
||||
"""
|
||||
# save kernel ebtables snapshot to a file
|
||||
args = self.ebatomiccmd("--atomic-save")
|
||||
wlan.host_cmd(args)
|
||||
if not self.cmds:
|
||||
return
|
||||
# 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
|
||||
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:
|
||||
def update(self, net: "CoreNetwork") -> None:
|
||||
"""
|
||||
Flag a change to the given WLAN's _linked dict, so the ebtables
|
||||
chain will be rebuilt at the next interval.
|
||||
|
||||
Flag this network has an update, so the nftables chain will be rebuilt.
|
||||
:param net: wlan network
|
||||
:return: nothing
|
||||
"""
|
||||
with self.updatelock:
|
||||
if wlan not in self.updates:
|
||||
self.updates.append(wlan)
|
||||
self.updates.put(net)
|
||||
|
||||
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
|
||||
"""
|
||||
with wlan._linked_lock:
|
||||
if wlan.has_ebtables_chain:
|
||||
# flush the chain
|
||||
self.cmds.append(f"-F {wlan.brname}")
|
||||
with self.lock:
|
||||
net.host_cmd(f"{NFTABLES} delete table bridge {net.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:
|
||||
wlan.has_ebtables_chain = True
|
||||
self.cmds.extend(
|
||||
[
|
||||
f"-N {wlan.brname} -P {wlan.policy.value}",
|
||||
f"-A FORWARD --logical-in {wlan.brname} -j {wlan.brname}",
|
||||
]
|
||||
net.has_nftables_chain = True
|
||||
policy = net.policy.value.lower()
|
||||
self.cmds.append(f"add table bridge {net.brname}")
|
||||
self.cmds.append(
|
||||
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
|
||||
for iface1, v in wlan._linked.items():
|
||||
for oface2, linked in v.items():
|
||||
if wlan.policy == NetworkPolicy.DROP and linked:
|
||||
self.cmds.extend(
|
||||
[
|
||||
f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j ACCEPT",
|
||||
f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j ACCEPT",
|
||||
]
|
||||
for iface1, v in net.linked.items():
|
||||
for iface2, linked in v.items():
|
||||
policy = None
|
||||
if net.policy == NetworkPolicy.DROP and linked:
|
||||
policy = "accept"
|
||||
elif net.policy == NetworkPolicy.ACCEPT and not linked:
|
||||
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.extend(
|
||||
[
|
||||
f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j DROP",
|
||||
f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j DROP",
|
||||
]
|
||||
self.cmds.append(
|
||||
f"add rule bridge {net.brname} {self.chain} "
|
||||
f"oif {iface1.localname} iif {iface2.localname} "
|
||||
f"{policy}"
|
||||
)
|
||||
|
||||
|
||||
# a global object because all WLANs share the same queue
|
||||
# cannot have multiple threads invoking the ebtables commnd
|
||||
ebq: EbtablesQueue = EbtablesQueue()
|
||||
|
||||
|
||||
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)
|
||||
# a global object because all networks share the same queue
|
||||
# cannot have multiple threads invoking the nftables commnd
|
||||
nft_queue: NftablesQueue = NftablesQueue()
|
||||
|
||||
|
||||
class CoreNetwork(CoreNetworkBase):
|
||||
|
@ -282,17 +257,17 @@ class CoreNetwork(CoreNetworkBase):
|
|||
if name is None:
|
||||
name = str(self.id)
|
||||
if policy is not None:
|
||||
self.policy = policy
|
||||
self.policy: NetworkPolicy = policy
|
||||
self.name: Optional[str] = name
|
||||
sessionid = self.session.short_session_id()
|
||||
self.brname: str = f"b.{self.id}.{sessionid}"
|
||||
self.has_ebtables_chain: bool = False
|
||||
self.has_nftables_chain: bool = False
|
||||
|
||||
def host_cmd(
|
||||
self,
|
||||
args: str,
|
||||
env: Dict[str, str] = None,
|
||||
cwd: str = None,
|
||||
cwd: Path = None,
|
||||
wait: bool = True,
|
||||
shell: bool = False,
|
||||
) -> str:
|
||||
|
@ -308,7 +283,7 @@ class CoreNetwork(CoreNetworkBase):
|
|||
:return: combined stdout and stderr
|
||||
: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)
|
||||
self.session.distributed.execute(lambda x: x.remote_cmd(args, env, cwd, wait))
|
||||
return output
|
||||
|
@ -321,9 +296,9 @@ class CoreNetwork(CoreNetworkBase):
|
|||
:raises CoreCommandError: when there is a command exception
|
||||
"""
|
||||
self.net_client.create_bridge(self.brname)
|
||||
self.has_ebtables_chain = False
|
||||
self.has_nftables_chain = False
|
||||
self.up = True
|
||||
ebq.startupdateloop(self)
|
||||
nft_queue.start(self)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
|
@ -333,27 +308,18 @@ class CoreNetwork(CoreNetworkBase):
|
|||
"""
|
||||
if not self.up:
|
||||
return
|
||||
|
||||
ebq.stopupdateloop(self)
|
||||
|
||||
nft_queue.stop(self)
|
||||
try:
|
||||
self.net_client.delete_bridge(self.brname)
|
||||
if self.has_ebtables_chain:
|
||||
cmds = [
|
||||
f"{EBTABLES} -D FORWARD --logical-in {self.brname} -j {self.brname}",
|
||||
f"{EBTABLES} -X {self.brname}",
|
||||
]
|
||||
ebtablescmds(self.host_cmd, cmds)
|
||||
if self.has_nftables_chain:
|
||||
nft_queue.delete_table(self)
|
||||
except CoreCommandError:
|
||||
logging.exception("error during shutdown")
|
||||
|
||||
# removes veth pairs used for bridge-to-bridge connections
|
||||
for iface in self.get_ifaces():
|
||||
iface.shutdown()
|
||||
|
||||
self.ifaces.clear()
|
||||
self._linked.clear()
|
||||
del self.session
|
||||
self.linked.clear()
|
||||
self.up = False
|
||||
|
||||
def attach(self, iface: CoreInterface) -> None:
|
||||
|
@ -378,7 +344,7 @@ class CoreNetwork(CoreNetworkBase):
|
|||
iface.net_client.delete_iface(self.brname, iface.localname)
|
||||
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.
|
||||
|
||||
|
@ -389,12 +355,10 @@ class CoreNetwork(CoreNetworkBase):
|
|||
# check if the network interfaces are attached to this network
|
||||
if self.ifaces[iface1.net_id] != iface1:
|
||||
raise ValueError(f"inconsistency for interface {iface1.name}")
|
||||
|
||||
if self.ifaces[iface2.net_id] != iface2:
|
||||
raise ValueError(f"inconsistency for interface {iface2.name}")
|
||||
|
||||
try:
|
||||
linked = self._linked[iface1][iface2]
|
||||
linked = self.linked[iface1][iface2]
|
||||
except KeyError:
|
||||
if self.policy == NetworkPolicy.ACCEPT:
|
||||
linked = True
|
||||
|
@ -402,41 +366,37 @@ class CoreNetwork(CoreNetworkBase):
|
|||
linked = False
|
||||
else:
|
||||
raise Exception(f"unknown policy: {self.policy.value}")
|
||||
self._linked[iface1][iface2] = linked
|
||||
|
||||
self.linked[iface1][iface2] = linked
|
||||
return linked
|
||||
|
||||
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.
|
||||
|
||||
:param iface1: interface one
|
||||
:param iface2: interface two
|
||||
:return: nothing
|
||||
"""
|
||||
with self._linked_lock:
|
||||
if not self.linked(iface1, iface2):
|
||||
with self.linked_lock:
|
||||
if self.is_linked(iface1, iface2):
|
||||
return
|
||||
self._linked[iface1][iface2] = False
|
||||
|
||||
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)
|
||||
self.linked[iface1][iface2] = True
|
||||
nft_queue.update(self)
|
||||
|
||||
def linkconfig(
|
||||
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||
|
@ -542,8 +502,8 @@ class CoreNetwork(CoreNetworkBase):
|
|||
iface.net_client.set_iface_master(net.brname, iface.name)
|
||||
i = net.next_iface_id()
|
||||
net.ifaces[i] = iface
|
||||
with net._linked_lock:
|
||||
net._linked[iface] = {}
|
||||
with net.linked_lock:
|
||||
net.linked[iface] = {}
|
||||
iface.net = self
|
||||
iface.othernet = net
|
||||
return iface
|
||||
|
@ -769,7 +729,7 @@ class CtrlNet(CoreNetwork):
|
|||
raise CoreError(f"old bridges exist for node: {self.id}")
|
||||
|
||||
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:
|
||||
self.add_addresses(self.hostid)
|
||||
|
@ -777,7 +737,7 @@ class CtrlNet(CoreNetwork):
|
|||
self.add_addresses(-2)
|
||||
|
||||
if self.updown_script:
|
||||
logging.info(
|
||||
logger.info(
|
||||
"interface %s updown script (%s startup) called",
|
||||
self.brname,
|
||||
self.updown_script,
|
||||
|
@ -797,7 +757,7 @@ class CtrlNet(CoreNetwork):
|
|||
try:
|
||||
self.net_client.delete_iface(self.brname, self.serverintf)
|
||||
except CoreCommandError:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
"error deleting server interface %s from bridge %s",
|
||||
self.serverintf,
|
||||
self.brname,
|
||||
|
@ -805,14 +765,14 @@ class CtrlNet(CoreNetwork):
|
|||
|
||||
if self.updown_script is not None:
|
||||
try:
|
||||
logging.info(
|
||||
logger.info(
|
||||
"interface %s updown script (%s shutdown) called",
|
||||
self.brname,
|
||||
self.updown_script,
|
||||
)
|
||||
self.host_cmd(f"{self.updown_script} {self.brname} shutdown")
|
||||
except CoreCommandError:
|
||||
logging.exception("error issuing shutdown script shutdown")
|
||||
logger.exception("error issuing shutdown script shutdown")
|
||||
|
||||
super().shutdown()
|
||||
|
||||
|
@ -990,7 +950,7 @@ class WlanNode(CoreNetwork):
|
|||
:return: nothing
|
||||
"""
|
||||
super().startup()
|
||||
ebq.ebchange(self)
|
||||
nft_queue.update(self)
|
||||
|
||||
def attach(self, iface: CoreInterface) -> None:
|
||||
"""
|
||||
|
@ -1012,7 +972,7 @@ class WlanNode(CoreNetwork):
|
|||
:param config: configuration for model being set
|
||||
: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:
|
||||
self.model = model(session=self.session, _id=self.id)
|
||||
for iface in self.get_ifaces():
|
||||
|
@ -1031,7 +991,7 @@ class WlanNode(CoreNetwork):
|
|||
def updatemodel(self, config: Dict[str, str]) -> None:
|
||||
if not self.model:
|
||||
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
|
||||
)
|
||||
self.model.update_config(config)
|
||||
|
|
|
@ -3,9 +3,9 @@ PhysicalNode class for including real systems in the emulated network.
|
|||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
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.distributed import DistributedServer
|
||||
|
@ -16,6 +16,8 @@ from core.nodes.base import CoreNetworkBase, CoreNodeBase
|
|||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.nodes.network import CoreNetwork, GreTap
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
@ -26,15 +28,15 @@ class PhysicalNode(CoreNodeBase):
|
|||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
nodedir: str = None,
|
||||
directory: Path = None,
|
||||
server: DistributedServer = None,
|
||||
) -> None:
|
||||
super().__init__(session, _id, name, server)
|
||||
if not self.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._mounts: List[Tuple[str, str]] = []
|
||||
self._mounts: List[Tuple[Path, Path]] = []
|
||||
|
||||
def startup(self) -> None:
|
||||
with self.lock:
|
||||
|
@ -44,15 +46,12 @@ class PhysicalNode(CoreNodeBase):
|
|||
def shutdown(self) -> None:
|
||||
if not self.up:
|
||||
return
|
||||
|
||||
with self.lock:
|
||||
while self._mounts:
|
||||
_source, target = self._mounts.pop(-1)
|
||||
self.umount(target)
|
||||
|
||||
_, target_path = self._mounts.pop(-1)
|
||||
self.umount(target_path)
|
||||
for iface in self.get_ifaces():
|
||||
iface.shutdown()
|
||||
|
||||
self.rmnodedir()
|
||||
|
||||
def path_exists(self, path: str) -> bool:
|
||||
|
@ -166,7 +165,7 @@ class PhysicalNode(CoreNodeBase):
|
|||
def new_iface(
|
||||
self, net: CoreNetworkBase, iface_data: InterfaceData
|
||||
) -> CoreInterface:
|
||||
logging.info("creating interface")
|
||||
logger.info("creating interface")
|
||||
ips = iface_data.get_ips()
|
||||
iface_id = iface_data.id
|
||||
if iface_id is None:
|
||||
|
@ -186,55 +185,40 @@ class PhysicalNode(CoreNodeBase):
|
|||
self.adopt_iface(iface, iface_id, iface_data.mac, ips)
|
||||
return iface
|
||||
|
||||
def privatedir(self, path: str) -> None:
|
||||
if path[0] != "/":
|
||||
raise ValueError(f"path not fully qualified: {path}")
|
||||
hostpath = os.path.join(
|
||||
self.nodedir, os.path.normpath(path).strip("/").replace("/", ".")
|
||||
)
|
||||
os.mkdir(hostpath)
|
||||
self.mount(hostpath, path)
|
||||
def privatedir(self, dir_path: Path) -> None:
|
||||
if not str(dir_path).startswith("/"):
|
||||
raise CoreError(f"private directory path not fully qualified: {dir_path}")
|
||||
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:
|
||||
source = os.path.abspath(source)
|
||||
logging.info("mounting %s at %s", source, target)
|
||||
os.makedirs(target)
|
||||
self.host_cmd(f"{MOUNT} --bind {source} {target}", cwd=self.nodedir)
|
||||
self._mounts.append((source, target))
|
||||
def mount(self, src_path: Path, target_path: Path) -> None:
|
||||
logger.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path)
|
||||
self.cmd(f"mkdir -p {target_path}")
|
||||
self.host_cmd(f"{MOUNT} --bind {src_path} {target_path}", cwd=self.directory)
|
||||
self._mounts.append((src_path, target_path))
|
||||
|
||||
def umount(self, target: str) -> None:
|
||||
logging.info("unmounting '%s'", target)
|
||||
def umount(self, target_path: Path) -> None:
|
||||
logger.info("unmounting '%s'", target_path)
|
||||
try:
|
||||
self.host_cmd(f"{UMOUNT} -l {target}", cwd=self.nodedir)
|
||||
self.host_cmd(f"{UMOUNT} -l {target_path}", cwd=self.directory)
|
||||
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:
|
||||
dirname, basename = os.path.split(filename)
|
||||
if not basename:
|
||||
raise ValueError("no basename for filename: " + filename)
|
||||
|
||||
if dirname and dirname[0] == "/":
|
||||
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:
|
||||
def nodefile(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
|
||||
host_path = self.host_path(file_path)
|
||||
directory = host_path.parent
|
||||
if not directory.is_dir():
|
||||
directory.mkdir(parents=True, mode=0o755)
|
||||
with host_path.open("w") as f:
|
||||
f.write(contents)
|
||||
os.chmod(f.name, mode)
|
||||
logging.info("created nodefile: '%s'; mode: 0%o", f.name, mode)
|
||||
host_path.chmod(mode)
|
||||
logger.info("created nodefile: '%s'; mode: 0%o", host_path, mode)
|
||||
|
||||
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
|
||||
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")
|
||||
|
||||
|
||||
|
@ -433,7 +417,7 @@ class Rj45Node(CoreNodeBase):
|
|||
if items[1][:4] == "fe80":
|
||||
continue
|
||||
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:
|
||||
"""
|
||||
|
@ -443,7 +427,7 @@ class Rj45Node(CoreNodeBase):
|
|||
:raises CoreCommandError: when there is a command exception
|
||||
"""
|
||||
localname = self.iface.localname
|
||||
logging.info("restoring rj45 state: %s", localname)
|
||||
logger.info("restoring rj45 state: %s", localname)
|
||||
for addr in self.old_addrs:
|
||||
self.net_client.create_address(localname, addr[0], addr[1])
|
||||
if self.old_up:
|
||||
|
@ -464,10 +448,10 @@ class Rj45Node(CoreNodeBase):
|
|||
def termcmdstring(self, sh: str) -> str:
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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.network import WlanNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
@ -109,7 +111,7 @@ class Sdt:
|
|||
return False
|
||||
|
||||
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:
|
||||
try:
|
||||
if self.protocol.lower() == "udp":
|
||||
|
@ -119,7 +121,7 @@ class Sdt:
|
|||
# Default to tcp
|
||||
self.sock = socket.create_connection(self.address, 5)
|
||||
except IOError:
|
||||
logging.exception("SDT socket connect error")
|
||||
logger.exception("SDT socket connect error")
|
||||
return False
|
||||
|
||||
if not self.initialize():
|
||||
|
@ -157,7 +159,7 @@ class Sdt:
|
|||
try:
|
||||
self.sock.close()
|
||||
except IOError:
|
||||
logging.error("error closing socket")
|
||||
logger.error("error closing socket")
|
||||
finally:
|
||||
self.sock = None
|
||||
|
||||
|
@ -191,11 +193,11 @@ class Sdt:
|
|||
|
||||
try:
|
||||
cmd = f"{cmdstr}\n".encode()
|
||||
logging.debug("sdt cmd: %s", cmd)
|
||||
logger.debug("sdt cmd: %s", cmd)
|
||||
self.sock.sendall(cmd)
|
||||
return True
|
||||
except IOError:
|
||||
logging.exception("SDT connection error")
|
||||
logger.exception("SDT connection error")
|
||||
self.sock = None
|
||||
self.connected = False
|
||||
return False
|
||||
|
@ -250,7 +252,7 @@ class Sdt:
|
|||
:param node: node to add
|
||||
: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():
|
||||
return
|
||||
pos = self.get_node_position(node)
|
||||
|
@ -262,8 +264,8 @@ class Sdt:
|
|||
icon = node.icon
|
||||
if icon:
|
||||
node_type = node.name
|
||||
icon = icon.replace("$CORE_DATA_DIR", CORE_DATA_DIR)
|
||||
icon = icon.replace("$CORE_CONF_DIR", CORE_CONF_DIR)
|
||||
icon = icon.replace("$CORE_DATA_DIR", str(CORE_DATA_DIR))
|
||||
icon = icon.replace("$CORE_CONF_DIR", str(CORE_CONF_DIR))
|
||||
self.cmd(f"sprite {node_type} image {icon}")
|
||||
self.cmd(
|
||||
f'node {node.id} nodeLayer "{NODE_LAYER}" '
|
||||
|
@ -280,7 +282,7 @@ class Sdt:
|
|||
:param alt: node altitude
|
||||
: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():
|
||||
return
|
||||
|
||||
|
@ -300,7 +302,7 @@ class Sdt:
|
|||
:param node_id: node id to delete
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug("sdt delete node: %s", node_id)
|
||||
logger.debug("sdt delete node: %s", node_id)
|
||||
if not self.connect():
|
||||
return
|
||||
self.cmd(f"delete node,{node_id}")
|
||||
|
@ -315,7 +317,7 @@ class Sdt:
|
|||
if not self.connect():
|
||||
return
|
||||
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:
|
||||
self.cmd(f"delete node,{node.id}")
|
||||
else:
|
||||
|
@ -356,7 +358,7 @@ class Sdt:
|
|||
:param label: label for link
|
||||
: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():
|
||||
return
|
||||
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
|
||||
: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():
|
||||
return
|
||||
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
|
||||
: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():
|
||||
return
|
||||
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
|
||||
__all__ is automatically loaded by the main core module.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from core.services.coreservices import ServiceManager
|
||||
|
||||
_PATH = os.path.abspath(os.path.dirname(__file__))
|
||||
_PATH: Path = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def load():
|
||||
|
|
|
@ -10,6 +10,7 @@ services.
|
|||
import enum
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
|
@ -33,6 +34,8 @@ from core.errors import (
|
|||
)
|
||||
from core.nodes.base import CoreNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
@ -160,7 +163,7 @@ class ServiceShim:
|
|||
cls.setvalue(service, key, values[cls.keys.index(key)])
|
||||
except IndexError:
|
||||
# old config does not need to have new keys
|
||||
logging.exception("error indexing into key")
|
||||
logger.exception("error indexing into key")
|
||||
|
||||
@classmethod
|
||||
def setvalue(cls, service: "CoreService", key: str, value: str) -> None:
|
||||
|
@ -227,7 +230,7 @@ class ServiceManager:
|
|||
:raises ValueError: when service cannot be loaded
|
||||
"""
|
||||
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
|
||||
if name in cls.services:
|
||||
|
@ -244,7 +247,7 @@ class ServiceManager:
|
|||
try:
|
||||
service.on_load()
|
||||
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)
|
||||
|
||||
# make service available
|
||||
|
@ -264,7 +267,7 @@ class ServiceManager:
|
|||
return service
|
||||
|
||||
@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.
|
||||
|
||||
|
@ -276,12 +279,11 @@ class ServiceManager:
|
|||
for service in services:
|
||||
if not service.name:
|
||||
continue
|
||||
|
||||
try:
|
||||
cls.add(service)
|
||||
except (CoreError, ValueError) as e:
|
||||
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
|
||||
|
||||
|
||||
|
@ -329,14 +331,14 @@ class CoreServices:
|
|||
:param node_type: node type to get default services for
|
||||
:return: default services
|
||||
"""
|
||||
logging.debug("getting default services for type: %s", node_type)
|
||||
logger.debug("getting default services for type: %s", node_type)
|
||||
results = []
|
||||
defaults = self.default_services.get(node_type, [])
|
||||
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)
|
||||
if not service:
|
||||
logging.warning("default service %s is unknown", name)
|
||||
logger.warning("default service %s is unknown", name)
|
||||
else:
|
||||
results.append(service)
|
||||
return results
|
||||
|
@ -369,7 +371,7 @@ class CoreServices:
|
|||
:param service_name: name of service to set
|
||||
: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)
|
||||
if not service:
|
||||
service_class = ServiceManager.get(service_name)
|
||||
|
@ -391,15 +393,15 @@ class CoreServices:
|
|||
:return: nothing
|
||||
"""
|
||||
if not services:
|
||||
logging.info(
|
||||
logger.info(
|
||||
"using default services for node(%s) type(%s)", node.name, 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:
|
||||
service = self.get_service(node.id, service_name, default_service=True)
|
||||
if not service:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"unknown service(%s) for node(%s)", service_name, node.name
|
||||
)
|
||||
continue
|
||||
|
@ -457,7 +459,7 @@ class CoreServices:
|
|||
raise CoreServiceBootError(*exceptions)
|
||||
|
||||
def _boot_service_path(self, node: CoreNode, boot_path: List["CoreServiceType"]):
|
||||
logging.info(
|
||||
logger.info(
|
||||
"booting node(%s) services: %s",
|
||||
node.name,
|
||||
" -> ".join([x.name for x in boot_path]),
|
||||
|
@ -467,7 +469,7 @@ class CoreServices:
|
|||
try:
|
||||
self.boot_service(node, service)
|
||||
except Exception as e:
|
||||
logging.exception("exception booting service: %s", service.name)
|
||||
logger.exception("exception booting service: %s", service.name)
|
||||
raise CoreServiceBootError(e)
|
||||
|
||||
def boot_service(self, node: CoreNode, service: "CoreServiceType") -> None:
|
||||
|
@ -479,7 +481,7 @@ class CoreServices:
|
|||
:param service: service to start
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info(
|
||||
logger.info(
|
||||
"starting node(%s) service(%s) validation(%s)",
|
||||
node.name,
|
||||
service.name,
|
||||
|
@ -488,10 +490,11 @@ class CoreServices:
|
|||
|
||||
# create service directories
|
||||
for directory in service.dirs:
|
||||
dir_path = Path(directory)
|
||||
try:
|
||||
node.privatedir(directory)
|
||||
except (CoreCommandError, ValueError) as e:
|
||||
logging.warning(
|
||||
node.create_dir(dir_path)
|
||||
except (CoreCommandError, CoreError) as e:
|
||||
logger.warning(
|
||||
"error mounting private dir '%s' for service '%s': %s",
|
||||
directory,
|
||||
service.name,
|
||||
|
@ -534,14 +537,14 @@ class CoreServices:
|
|||
"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
|
||||
config references an existing file that should be copied.
|
||||
Returns True for local files, False for generated.
|
||||
|
||||
: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
|
||||
:return: True if successful, False otherwise
|
||||
"""
|
||||
|
@ -550,7 +553,7 @@ class CoreServices:
|
|||
src = src.split("\n")[0]
|
||||
src = utils.expand_corepath(src, node.session, node)
|
||||
# TODO: glob here
|
||||
node.nodefilecopy(filename, src, mode=0o644)
|
||||
node.copy_file(src, file_path, mode=0o644)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -562,21 +565,21 @@ class CoreServices:
|
|||
:param service: service to validate
|
||||
: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
|
||||
if not service.custom:
|
||||
cmds = service.get_validate(node)
|
||||
|
||||
status = 0
|
||||
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:
|
||||
node.cmd(cmd)
|
||||
except CoreCommandError as e:
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"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
|
||||
break
|
||||
|
||||
|
@ -611,7 +614,7 @@ class CoreServices:
|
|||
f"error stopping service {service.name}: {e.stderr}",
|
||||
node.id,
|
||||
)
|
||||
logging.exception("error running stop command %s", args)
|
||||
logger.exception("error running stop command %s", args)
|
||||
status = -1
|
||||
return status
|
||||
|
||||
|
@ -679,13 +682,13 @@ class CoreServices:
|
|||
# retrieve custom service
|
||||
service = self.get_service(node_id, service_name)
|
||||
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
|
||||
|
||||
# validate file being set is valid
|
||||
config_files = service.configs
|
||||
if file_name not in config_files:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"received unknown file(%s) for service(%s)", file_name, service_name
|
||||
)
|
||||
return
|
||||
|
@ -713,7 +716,7 @@ class CoreServices:
|
|||
try:
|
||||
node.cmd(cmd, wait)
|
||||
except CoreCommandError:
|
||||
logging.exception("error starting command")
|
||||
logger.exception("error starting command")
|
||||
status = -1
|
||||
return status
|
||||
|
||||
|
@ -729,27 +732,25 @@ class CoreServices:
|
|||
config_files = service.configs
|
||||
if not service.custom:
|
||||
config_files = service.get_configs(node)
|
||||
|
||||
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
|
||||
)
|
||||
if service.custom:
|
||||
cfg = service.config_data.get(file_name)
|
||||
if cfg is None:
|
||||
cfg = service.generate_config(node, file_name)
|
||||
|
||||
# cfg may have a file:/// url for copying from a file
|
||||
try:
|
||||
if self.copy_service_file(node, file_name, cfg):
|
||||
if self.copy_service_file(node, file_path, cfg):
|
||||
continue
|
||||
except IOError:
|
||||
logging.exception("error copying service file: %s", file_name)
|
||||
logger.exception("error copying service file: %s", file_name)
|
||||
continue
|
||||
else:
|
||||
cfg = service.generate_config(node, file_name)
|
||||
|
||||
node.nodefile(file_name, cfg)
|
||||
node.create_file(file_path, cfg)
|
||||
|
||||
def service_reconfigure(self, node: CoreNode, service: "CoreService") -> None:
|
||||
"""
|
||||
|
@ -762,17 +763,15 @@ class CoreServices:
|
|||
config_files = service.configs
|
||||
if not service.custom:
|
||||
config_files = service.get_configs(node)
|
||||
|
||||
for file_name in config_files:
|
||||
file_path = Path(file_name)
|
||||
if file_name[:7] == "file:///":
|
||||
# TODO: implement this
|
||||
raise NotImplementedError
|
||||
|
||||
cfg = service.config_data.get(file_name)
|
||||
if cfg is None:
|
||||
cfg = service.generate_config(node, file_name)
|
||||
|
||||
node.nodefile(file_name, cfg)
|
||||
node.create_file(file_path, cfg)
|
||||
|
||||
|
||||
class CoreService:
|
||||
|
|
|
@ -11,6 +11,8 @@ from core.nodes.base import CoreNode
|
|||
from core.nodes.interface import CoreInterface
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VPNClient(CoreService):
|
||||
name: str = "VPNClient"
|
||||
|
@ -33,7 +35,7 @@ class VPNClient(CoreService):
|
|||
with open(fname, "r") as f:
|
||||
cfg += f.read()
|
||||
except IOError:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
"error opening VPN client configuration template (%s)", fname
|
||||
)
|
||||
return cfg
|
||||
|
@ -61,7 +63,7 @@ class VPNServer(CoreService):
|
|||
with open(fname, "r") as f:
|
||||
cfg += f.read()
|
||||
except IOError:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
"Error opening VPN server configuration template (%s)", fname
|
||||
)
|
||||
return cfg
|
||||
|
@ -89,7 +91,7 @@ class IPsec(CoreService):
|
|||
with open(fname, "r") as f:
|
||||
cfg += f.read()
|
||||
except IOError:
|
||||
logging.exception("Error opening IPsec configuration template (%s)", fname)
|
||||
logger.exception("Error opening IPsec configuration template (%s)", fname)
|
||||
return cfg
|
||||
|
||||
|
||||
|
@ -112,7 +114,7 @@ class Firewall(CoreService):
|
|||
with open(fname, "r") as f:
|
||||
cfg += f.read()
|
||||
except IOError:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
"Error opening Firewall configuration template (%s)", fname
|
||||
)
|
||||
return cfg
|
||||
|
|
|
@ -236,7 +236,7 @@ max-lease-time 7200;
|
|||
ddns-update-style none;
|
||||
"""
|
||||
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"
|
||||
return cfg
|
||||
|
||||
|
@ -246,10 +246,8 @@ ddns-update-style none;
|
|||
Generate a subnet declaration block given an IPv4 prefix string
|
||||
for inclusion in the dhcpd3 config file.
|
||||
"""
|
||||
address = str(ip.ip)
|
||||
if netaddr.valid_ipv6(address):
|
||||
if ip.size == 1:
|
||||
return ""
|
||||
else:
|
||||
# divide the address space in half
|
||||
index = (ip.size - 2) / 2
|
||||
rangelow = ip[index]
|
||||
|
@ -263,11 +261,11 @@ subnet %s netmask %s {
|
|||
}
|
||||
}
|
||||
""" % (
|
||||
ip.ip,
|
||||
ip.cidr.ip,
|
||||
ip.netmask,
|
||||
rangelow,
|
||||
rangehigh,
|
||||
address,
|
||||
ip.ip,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -36,6 +36,8 @@ import netaddr
|
|||
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
from core.nodes.base import CoreNode
|
||||
|
@ -46,11 +48,10 @@ IFACE_CONFIG_FACTOR: int = 1000
|
|||
|
||||
|
||||
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:
|
||||
"""
|
||||
Provides an alternative way to run execfile to be compatible for
|
||||
both python2/3.
|
||||
Provides a way to execute a file.
|
||||
|
||||
:param path: path of file to execute
|
||||
:param exec_globals: globals values to pass to execution
|
||||
|
@ -59,8 +60,8 @@ def execute_file(
|
|||
"""
|
||||
if exec_globals is None:
|
||||
exec_globals = {}
|
||||
exec_globals.update({"__file__": path, "__name__": "__main__"})
|
||||
with open(path, "rb") as f:
|
||||
exec_globals.update({"__file__": str(path), "__name__": "__main__"})
|
||||
with path.open("rb") as f:
|
||||
data = compile(f.read(), path, "exec")
|
||||
exec(data, exec_globals, exec_locals)
|
||||
|
||||
|
@ -92,24 +93,19 @@ def _detach_init() -> None:
|
|||
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.
|
||||
|
||||
:param path: path to file
|
||||
:param file_name: file name to check
|
||||
:return: True if a valid python module file, False otherwise
|
||||
"""
|
||||
file_path = os.path.join(path, file_name)
|
||||
if not os.path.isfile(file_path):
|
||||
if not path.is_file():
|
||||
return False
|
||||
|
||||
if file_name.startswith("_"):
|
||||
if path.name.startswith("_"):
|
||||
return False
|
||||
|
||||
if not file_name.endswith(".py"):
|
||||
if not path.suffix == ".py":
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -124,13 +120,10 @@ def _is_class(module: Any, member: Type, clazz: Type) -> bool:
|
|||
"""
|
||||
if not inspect.isclass(member):
|
||||
return False
|
||||
|
||||
if not issubclass(member, clazz):
|
||||
return False
|
||||
|
||||
if member.__module__ != module.__name__:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -196,7 +189,7 @@ def mute_detach(args: str, **kwargs: Dict[str, Any]) -> int:
|
|||
def cmd(
|
||||
args: str,
|
||||
env: Dict[str, str] = None,
|
||||
cwd: str = None,
|
||||
cwd: Path = None,
|
||||
wait: bool = True,
|
||||
shell: bool = False,
|
||||
) -> str:
|
||||
|
@ -213,7 +206,7 @@ def cmd(
|
|||
:raises CoreCommandError: when there is a non-zero exit status or the file to
|
||||
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:
|
||||
args = shlex.split(args)
|
||||
try:
|
||||
|
@ -230,7 +223,7 @@ def cmd(
|
|||
else:
|
||||
return ""
|
||||
except OSError as e:
|
||||
logging.error("cmd error: %s", e.strerror)
|
||||
logger.error("cmd error: %s", e.strerror)
|
||||
raise CoreCommandError(1, args, "", e.strerror)
|
||||
|
||||
|
||||
|
@ -282,7 +275,7 @@ def file_demunge(pathname: str, header: str) -> None:
|
|||
|
||||
def expand_corepath(
|
||||
pathname: str, session: "Session" = None, node: "CoreNode" = None
|
||||
) -> str:
|
||||
) -> Path:
|
||||
"""
|
||||
Expand a file path given session information.
|
||||
|
||||
|
@ -294,14 +287,12 @@ def expand_corepath(
|
|||
if session is not None:
|
||||
pathname = pathname.replace("~", f"/home/{session.user}")
|
||||
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)
|
||||
|
||||
if node is not None:
|
||||
pathname = pathname.replace("%NODE%", str(node.id))
|
||||
pathname = pathname.replace("%NODENAME%", node.name)
|
||||
|
||||
return pathname
|
||||
return Path(pathname)
|
||||
|
||||
|
||||
def sysctl_devname(devname: str) -> Optional[str]:
|
||||
|
@ -334,10 +325,25 @@ def load_config(file_path: Path, d: Dict[str, str]) -> None:
|
|||
key, value = line.split("=", 1)
|
||||
d[key] = value.strip()
|
||||
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.
|
||||
|
||||
|
@ -346,49 +352,35 @@ def load_classes(path: str, clazz: Generic[T]) -> T:
|
|||
:return: list of classes loaded
|
||||
"""
|
||||
# validate path exists
|
||||
logging.debug("attempting to load modules from path: %s", path)
|
||||
if not os.path.isdir(path):
|
||||
logging.warning("invalid custom module directory specified" ": %s", path)
|
||||
logger.debug("attempting to load modules from path: %s", path)
|
||||
if not path.is_dir():
|
||||
logger.warning("invalid custom module directory specified" ": %s", path)
|
||||
# check if path is in sys.path
|
||||
parent_path = os.path.dirname(path)
|
||||
if parent_path not in sys.path:
|
||||
logging.debug("adding parent path to allow imports: %s", parent_path)
|
||||
sys.path.append(parent_path)
|
||||
|
||||
# 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)
|
||||
|
||||
parent = str(path.parent)
|
||||
if parent not in sys.path:
|
||||
logger.debug("adding parent path to allow imports: %s", parent)
|
||||
sys.path.append(parent)
|
||||
# import and add all service modules in the path
|
||||
classes = []
|
||||
for module_name in module_names:
|
||||
import_statement = f"{base_module}.{module_name}"
|
||||
logging.debug("importing custom module: %s", import_statement)
|
||||
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:
|
||||
logging.exception(
|
||||
"unexpected error during import, skipping: %s", import_statement
|
||||
)
|
||||
|
||||
for p in path.iterdir():
|
||||
if not _valid_module(p):
|
||||
continue
|
||||
import_statement = f"{path.name}.{p.stem}"
|
||||
logger.debug("importing custom module: %s", import_statement)
|
||||
loaded = load_module(import_statement, clazz)
|
||||
classes.extend(loaded)
|
||||
return classes
|
||||
|
||||
|
||||
def load_logging_config(config_path: str) -> None:
|
||||
def load_logging_config(config_path: Path) -> None:
|
||||
"""
|
||||
Load CORE logging configuration file.
|
||||
|
||||
:param config_path: path to logging config file
|
||||
:return: nothing
|
||||
"""
|
||||
with open(config_path, "r") as log_config_file:
|
||||
log_config = json.load(log_config_file)
|
||||
with config_path.open("r") as f:
|
||||
log_config = json.load(f)
|
||||
logging.config.dictConfig(log_config)
|
||||
|
||||
|
||||
|
@ -415,7 +407,7 @@ def threadpool(
|
|||
result = future.result()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
logging.exception("thread pool exception")
|
||||
logger.exception("thread pool exception")
|
||||
exceptions.append(e)
|
||||
return results, exceptions
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Type, TypeVar
|
||||
|
||||
from lxml import etree
|
||||
|
@ -16,6 +17,8 @@ from core.nodes.lxd import LxcNode
|
|||
from core.nodes.network import CtrlNet, GreTapBridge, WlanNode
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emane.emanemodel import EmaneModel
|
||||
from core.emulator.session import Session
|
||||
|
@ -25,7 +28,7 @@ T = TypeVar("T")
|
|||
|
||||
|
||||
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:
|
||||
xml_data = etree.tostring(
|
||||
xml_element,
|
||||
|
@ -34,8 +37,8 @@ def write_xml_file(
|
|||
encoding="UTF-8",
|
||||
doctype=doctype,
|
||||
)
|
||||
with open(file_path, "wb") as xml_file:
|
||||
xml_file.write(xml_data)
|
||||
with file_path.open("wb") as f:
|
||||
f.write(xml_data)
|
||||
|
||||
|
||||
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(
|
||||
node_id: int,
|
||||
model: "EmaneModelType",
|
||||
|
@ -101,22 +90,22 @@ def create_emane_model_config(
|
|||
add_attribute(emane_element, "node", node_id)
|
||||
add_attribute(emane_element, "iface", iface_id)
|
||||
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")
|
||||
for mac_config in model.mac_config:
|
||||
value = config[mac_config.id]
|
||||
add_configuration(mac_element, mac_config.id, value)
|
||||
|
||||
phy_element = etree.SubElement(emane_element, "phy")
|
||||
for phy_config in model.phy_config:
|
||||
value = config[phy_config.id]
|
||||
add_configuration(phy_element, phy_config.id, value)
|
||||
|
||||
external_element = etree.SubElement(emane_element, "external")
|
||||
for external_config in model.external_config:
|
||||
value = config[external_config.id]
|
||||
add_configuration(external_element, external_config.id, value)
|
||||
|
||||
return emane_element
|
||||
|
||||
|
||||
|
@ -293,13 +282,12 @@ class CoreXmlWriter:
|
|||
self.write_session_metadata()
|
||||
self.write_default_services()
|
||||
|
||||
def write(self, file_name: str) -> None:
|
||||
self.scenario.set("name", file_name)
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
self.scenario.set("name", str(path))
|
||||
# write out generated xml
|
||||
xml_tree = etree.ElementTree(self.scenario)
|
||||
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:
|
||||
|
@ -374,22 +362,16 @@ class CoreXmlWriter:
|
|||
self.scenario.append(metadata_elements)
|
||||
|
||||
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")
|
||||
for node_id in self.session.emane.nodes():
|
||||
all_configs = self.session.emane.get_all_configs(node_id)
|
||||
if not all_configs:
|
||||
continue
|
||||
for node_id, model_configs in self.session.emane.node_configs.items():
|
||||
node_id, iface_id = utils.parse_iface_config_id(node_id)
|
||||
for model_name in all_configs:
|
||||
config = all_configs[model_name]
|
||||
logging.debug(
|
||||
for model_name, config in model_configs.items():
|
||||
logger.debug(
|
||||
"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(
|
||||
node_id, model, config, iface_id
|
||||
node_id, model_class, config, iface_id
|
||||
)
|
||||
emane_configurations.append(emane_configuration)
|
||||
if emane_configurations.getchildren():
|
||||
|
@ -404,7 +386,7 @@ class CoreXmlWriter:
|
|||
|
||||
for model_name in all_configs:
|
||||
config = all_configs[model_name]
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"writing mobility config node(%s) model(%s)", node_id, model_name
|
||||
)
|
||||
mobility_configuration = etree.SubElement(
|
||||
|
@ -580,8 +562,8 @@ class CoreXmlReader:
|
|||
self.session: "Session" = session
|
||||
self.scenario: Optional[etree.ElementTree] = None
|
||||
|
||||
def read(self, file_name: str) -> None:
|
||||
xml_tree = etree.parse(file_name)
|
||||
def read(self, file_path: Path) -> None:
|
||||
xml_tree = etree.parse(str(file_path))
|
||||
self.scenario = xml_tree.getroot()
|
||||
|
||||
# read xml session content
|
||||
|
@ -593,7 +575,6 @@ class CoreXmlReader:
|
|||
self.read_session_origin()
|
||||
self.read_service_configs()
|
||||
self.read_mobility_configs()
|
||||
self.read_emane_global_config()
|
||||
self.read_nodes()
|
||||
self.read_links()
|
||||
self.read_emane_configs()
|
||||
|
@ -609,7 +590,7 @@ class CoreXmlReader:
|
|||
services = []
|
||||
for service in node.iterchildren():
|
||||
services.append(service.get("name"))
|
||||
logging.info(
|
||||
logger.info(
|
||||
"reading default services for nodes(%s): %s", node_type, services
|
||||
)
|
||||
self.session.services.default_services[node_type] = services
|
||||
|
@ -624,7 +605,7 @@ class CoreXmlReader:
|
|||
name = data.get("name")
|
||||
value = data.get("value")
|
||||
configs[name] = value
|
||||
logging.info("reading session metadata: %s", configs)
|
||||
logger.info("reading session metadata: %s", configs)
|
||||
self.session.metadata = configs
|
||||
|
||||
def read_session_options(self) -> None:
|
||||
|
@ -636,7 +617,7 @@ class CoreXmlReader:
|
|||
name = configuration.get("name")
|
||||
value = configuration.get("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.update(xml_config)
|
||||
|
||||
|
@ -650,7 +631,7 @@ class CoreXmlReader:
|
|||
state = get_int(hook, "state")
|
||||
state = EventTypes(state)
|
||||
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)
|
||||
|
||||
def read_servers(self) -> None:
|
||||
|
@ -660,7 +641,7 @@ class CoreXmlReader:
|
|||
for server in servers.iterchildren():
|
||||
name = server.get("name")
|
||||
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)
|
||||
|
||||
def read_session_origin(self) -> None:
|
||||
|
@ -672,19 +653,19 @@ class CoreXmlReader:
|
|||
lon = get_float(session_origin, "lon")
|
||||
alt = get_float(session_origin, "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)
|
||||
|
||||
scale = get_float(session_origin, "scale")
|
||||
if scale:
|
||||
logging.info("reading session reference scale: %s", scale)
|
||||
logger.info("reading session reference scale: %s", scale)
|
||||
self.session.location.refscale = scale
|
||||
|
||||
x = get_float(session_origin, "x")
|
||||
y = get_float(session_origin, "y")
|
||||
z = get_float(session_origin, "z")
|
||||
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)
|
||||
|
||||
def read_service_configs(self) -> None:
|
||||
|
@ -695,7 +676,7 @@ class CoreXmlReader:
|
|||
for service_configuration in service_configurations.iterchildren():
|
||||
node_id = get_int(service_configuration, "node")
|
||||
service_name = service_configuration.get("name")
|
||||
logging.info(
|
||||
logger.info(
|
||||
"reading custom service(%s) for node(%s)", service_name, node_id
|
||||
)
|
||||
self.session.services.set_service(node_id, service_name)
|
||||
|
@ -731,28 +712,10 @@ class CoreXmlReader:
|
|||
files.add(name)
|
||||
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:
|
||||
emane_configurations = self.scenario.find("emane_configurations")
|
||||
if emane_configurations is None:
|
||||
return
|
||||
|
||||
for emane_configuration in emane_configurations.iterchildren():
|
||||
node_id = get_int(emane_configuration, "node")
|
||||
iface_id = get_int(emane_configuration, "iface")
|
||||
|
@ -763,38 +726,39 @@ class CoreXmlReader:
|
|||
node = self.session.nodes.get(node_id)
|
||||
if not node:
|
||||
raise CoreXmlError(f"node for emane config doesn't exist: {node_id}")
|
||||
model = self.session.emane.models.get(model_name)
|
||||
if not model:
|
||||
raise CoreXmlError(f"invalid emane model: {model_name}")
|
||||
self.session.emane.get_model(model_name)
|
||||
if iface_id is not None and iface_id not in node.ifaces:
|
||||
raise CoreXmlError(
|
||||
f"invalid interface id({iface_id}) for node({node.name})"
|
||||
)
|
||||
|
||||
# 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")
|
||||
for config in mac_configuration.iterchildren():
|
||||
name = config.get("name")
|
||||
value = config.get("value")
|
||||
configs[name] = value
|
||||
|
||||
phy_configuration = emane_configuration.find("phy")
|
||||
for config in phy_configuration.iterchildren():
|
||||
name = config.get("name")
|
||||
value = config.get("value")
|
||||
configs[name] = value
|
||||
|
||||
external_configuration = emane_configuration.find("external")
|
||||
for config in external_configuration.iterchildren():
|
||||
name = config.get("name")
|
||||
value = config.get("value")
|
||||
configs[name] = value
|
||||
|
||||
logging.info(
|
||||
logger.info(
|
||||
"reading emane configuration node(%s) model(%s)", node_id, model_name
|
||||
)
|
||||
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:
|
||||
mobility_configurations = self.scenario.find("mobility_configurations")
|
||||
|
@ -811,7 +775,7 @@ class CoreXmlReader:
|
|||
value = config.get("value")
|
||||
configs[name] = value
|
||||
|
||||
logging.info(
|
||||
logger.info(
|
||||
"reading mobility configuration node(%s) model(%s)", node_id, model_name
|
||||
)
|
||||
self.session.mobility.set_model_config(node_id, model_name, configs)
|
||||
|
@ -868,7 +832,7 @@ class CoreXmlReader:
|
|||
if all([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)
|
||||
|
||||
def read_network(self, network_element: etree.Element) -> None:
|
||||
|
@ -896,7 +860,7 @@ class CoreXmlReader:
|
|||
if all([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
|
||||
)
|
||||
self.session.add_node(_class, node_id, options)
|
||||
|
@ -926,7 +890,7 @@ class CoreXmlReader:
|
|||
for template_element in templates_element.iterchildren():
|
||||
name = template_element.get("name")
|
||||
template = template_element.text
|
||||
logging.info(
|
||||
logger.info(
|
||||
"loading xml template(%s): %s", type(template), template
|
||||
)
|
||||
service.set_template(name, template)
|
||||
|
@ -978,12 +942,12 @@ class CoreXmlReader:
|
|||
options.buffer = get_int(options_element, "buffer")
|
||||
|
||||
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(
|
||||
node1_id, node2_id, iface1_data.id, iface2_data.id, options
|
||||
)
|
||||
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(
|
||||
node1_id, node2_id, iface1_data, iface2_data, options
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
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.nodes.base import CoreNode, CoreNodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import CtrlNet
|
||||
from core.xml import corexml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emane.emanemanager import EmaneManager, StartData
|
||||
from core.emane.emanemodel import EmaneModel
|
||||
|
||||
_MAC_PREFIX = "02:02"
|
||||
|
@ -47,14 +47,14 @@ def _value_to_params(value: str) -> Optional[Tuple[str]]:
|
|||
return None
|
||||
return values
|
||||
except SyntaxError:
|
||||
logging.exception("error in value string to param list")
|
||||
logger.exception("error in value string to param list")
|
||||
return None
|
||||
|
||||
|
||||
def create_file(
|
||||
xml_element: etree.Element,
|
||||
doc_name: str,
|
||||
file_path: str,
|
||||
file_path: Path,
|
||||
server: DistributedServer = None,
|
||||
) -> None:
|
||||
"""
|
||||
|
@ -71,10 +71,11 @@ def create_file(
|
|||
)
|
||||
if server:
|
||||
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()
|
||||
server.remote_put(temp.name, file_path)
|
||||
os.unlink(temp.name)
|
||||
server.remote_put(temp_path, file_path)
|
||||
temp_path.unlink()
|
||||
else:
|
||||
corexml.write_xml_file(xml_element, file_path, doctype=doctype)
|
||||
|
||||
|
@ -92,9 +93,9 @@ def create_node_file(
|
|||
:return:
|
||||
"""
|
||||
if isinstance(node, CoreNode):
|
||||
file_path = os.path.join(node.nodedir, file_name)
|
||||
file_path = node.directory / file_name
|
||||
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)
|
||||
|
||||
|
||||
|
@ -143,41 +144,31 @@ def add_configurations(
|
|||
|
||||
|
||||
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:
|
||||
"""
|
||||
Create platform xml for a specific node.
|
||||
Create platform xml for a nem/interface.
|
||||
|
||||
:param emane_manager: emane manager with emane
|
||||
configurations
|
||||
:param control_net: control net node for this emane
|
||||
network
|
||||
:param data: start data for a node connected to emane and associated interfaces
|
||||
:return: the next nem id that can be used for creating platform xml files
|
||||
:param nem_id: nem id for current node/interface
|
||||
:param nem_port: control port to configure for emane
|
||||
:param emane_net: emane network associate with node and interface
|
||||
:param iface: node interface to create platform xml for
|
||||
:param config: emane configuration for interface
|
||||
:return: nothing
|
||||
"""
|
||||
# create top level platform element
|
||||
transport_configs = {"otamanagerdevice", "eventservicedevice"}
|
||||
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
|
||||
if not isinstance(data.node, CoreNode) and name in transport_configs:
|
||||
value = control_net.brname
|
||||
else:
|
||||
value = emane_manager.get_config(name)
|
||||
value = config[configuration.id]
|
||||
add_param(platform_element, name, value)
|
||||
|
||||
# create nem xml entries for all interfaces
|
||||
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}"
|
||||
add_param(
|
||||
platform_element, emane_net.model.platform_controlport, f"0.0.0.0:{nem_port}"
|
||||
)
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
# create model based xml files
|
||||
emane_net.model.build_xml_files(config, iface)
|
||||
|
||||
# check if this is an external transport
|
||||
if is_external(config):
|
||||
nem_element.set("transport", "external")
|
||||
|
@ -209,8 +203,8 @@ def build_platform_xml(
|
|||
iface.set_mac(mac)
|
||||
|
||||
doc_name = "platform"
|
||||
file_name = f"{data.node.name}-platform.xml"
|
||||
create_node_file(data.node, platform_element, doc_name, file_name)
|
||||
file_name = platform_file_name(iface)
|
||||
create_node_file(iface.node, platform_element, doc_name, file_name)
|
||||
|
||||
|
||||
def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None:
|
||||
|
@ -316,7 +310,7 @@ def create_event_service_xml(
|
|||
group: str,
|
||||
port: str,
|
||||
device: str,
|
||||
file_directory: str,
|
||||
file_directory: Path,
|
||||
server: DistributedServer = None,
|
||||
) -> None:
|
||||
"""
|
||||
|
@ -340,8 +334,7 @@ def create_event_service_xml(
|
|||
):
|
||||
sub_element = etree.SubElement(event_element, name)
|
||||
sub_element.text = value
|
||||
file_name = "libemaneeventservice.xml"
|
||||
file_path = os.path.join(file_directory, file_name)
|
||||
file_path = file_directory / "libemaneeventservice.xml"
|
||||
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 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"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"loggers": {
|
||||
"": {
|
||||
"level": "WARNING",
|
||||
"handlers": ["console"],
|
||||
"propagate": false
|
||||
},
|
||||
"core": {
|
||||
"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
|
||||
|
||||
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):
|
||||
|
@ -10,62 +10,39 @@ def log_event(event):
|
|||
|
||||
|
||||
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
|
||||
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)
|
||||
node = Node(position=position)
|
||||
response = core.add_node(session_id, node)
|
||||
logging.info("created node one: %s", response)
|
||||
node1_id = response.node_id
|
||||
|
||||
# create link
|
||||
interface1 = interface_helper.create_iface(node1_id, 0)
|
||||
response = core.add_link(session_id, node1_id, switch_id, interface1)
|
||||
logging.info("created link from node one to switch: %s", response)
|
||||
|
||||
# create node two
|
||||
node1 = session.add_node(2, position=position)
|
||||
position = Position(x=200, y=50)
|
||||
node = Node(position=position, server=server_name)
|
||||
response = core.add_node(session_id, node)
|
||||
logging.info("created node two: %s", response)
|
||||
node2_id = response.node_id
|
||||
node2 = session.add_node(3, position=position, server=server.name)
|
||||
|
||||
# create link
|
||||
interface1 = interface_helper.create_iface(node2_id, 0)
|
||||
response = core.add_link(session_id, node2_id, switch_id, interface1)
|
||||
logging.info("created link from node two to switch: %s", response)
|
||||
# create links
|
||||
iface1 = interface_helper.create_iface(node1.id, 0)
|
||||
session.add_link(node1=node1, node2=switch, iface1=iface1)
|
||||
iface1 = interface_helper.create_iface(node2.id, 0)
|
||||
session.add_link(node1=node2, node2=switch, iface1=iface1)
|
||||
|
||||
# change session state
|
||||
response = core.set_session_state(session_id, SessionState.INSTANTIATION)
|
||||
logging.info("set session state: %s", response)
|
||||
# start session
|
||||
core.start_session(session)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# required imports
|
||||
from core.api.grpc import client
|
||||
from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState
|
||||
from core.emane.ieee80211abg import EmaneIeee80211abgModel
|
||||
from core.api.grpc.wrappers import NodeType, Position
|
||||
from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
|
||||
|
||||
# interface helper
|
||||
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.connect()
|
||||
|
||||
# create session and get id
|
||||
response = core.create_session()
|
||||
session_id = response.session_id
|
||||
# add session
|
||||
session = core.create_session()
|
||||
|
||||
# change session state to configuration so that nodes get started when added
|
||||
core.set_session_state(session_id, SessionState.CONFIGURATION)
|
||||
|
||||
# create emane node
|
||||
# create nodes
|
||||
position = Position(x=200, y=200)
|
||||
emane = Node(type=NodeType.EMANE, position=position, emane=EmaneIeee80211abgModel.name)
|
||||
response = core.add_node(session_id, emane)
|
||||
emane_id = response.node_id
|
||||
|
||||
# create node one
|
||||
emane = session.add_node(
|
||||
1, _type=NodeType.EMANE, position=position, emane=EmaneIeee80211abgModel.name
|
||||
)
|
||||
position = Position(x=100, y=100)
|
||||
n1 = Node(type=NodeType.DEFAULT, position=position, model="mdr")
|
||||
response = core.add_node(session_id, n1)
|
||||
n1_id = response.node_id
|
||||
|
||||
# create node two
|
||||
node1 = session.add_node(2, model="mdr", position=position)
|
||||
position = Position(x=300, y=100)
|
||||
n2 = Node(type=NodeType.DEFAULT, position=position, model="mdr")
|
||||
response = core.add_node(session_id, n2)
|
||||
n2_id = response.node_id
|
||||
node2 = session.add_node(3, model="mdr", position=position)
|
||||
|
||||
# configure general emane settings
|
||||
core.set_emane_config(session_id, {"eventservicettl": "2"})
|
||||
# create links
|
||||
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
|
||||
# using a dict mapping currently support values as strings
|
||||
core.set_emane_model_config(
|
||||
session_id, emane_id, EmaneIeee80211abgModel.name, {"unicastrate": "3"}
|
||||
# setup emane configurations using a dict mapping currently support values as strings
|
||||
emane.set_emane_model(
|
||||
EmaneIeee80211abgModel.name, {"eventservicettl": "2", "unicastrate": "3"}
|
||||
)
|
||||
|
||||
# links nodes to emane
|
||||
iface1 = iface_helper.create_iface(n1_id, 0)
|
||||
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)
|
||||
# start session
|
||||
core.start_session(session)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
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.connect()
|
||||
|
||||
# create session and get id
|
||||
response = core.create_session()
|
||||
session_id = response.session_id
|
||||
# add session
|
||||
session = core.create_session()
|
||||
|
||||
# change session state to configuration so that nodes get started when added
|
||||
core.set_session_state(session_id, SessionState.CONFIGURATION)
|
||||
|
||||
# create node one
|
||||
# create nodes
|
||||
position = Position(x=100, y=100)
|
||||
n1 = Node(type=NodeType.DEFAULT, position=position, model="PC")
|
||||
response = core.add_node(session_id, n1)
|
||||
n1_id = response.node_id
|
||||
|
||||
# create node two
|
||||
node1 = session.add_node(1, position=position)
|
||||
position = Position(x=300, y=100)
|
||||
n2 = Node(type=NodeType.DEFAULT, position=position, model="PC")
|
||||
response = core.add_node(session_id, n2)
|
||||
n2_id = response.node_id
|
||||
node2 = session.add_node(2, position=position)
|
||||
|
||||
# links nodes together
|
||||
iface1 = iface_helper.create_iface(n1_id, 0)
|
||||
iface2 = iface_helper.create_iface(n2_id, 0)
|
||||
core.add_link(session_id, n1_id, n2_id, iface1, iface2)
|
||||
# create link
|
||||
iface1 = iface_helper.create_iface(node1.id, 0)
|
||||
iface2 = iface_helper.create_iface(node2.id, 0)
|
||||
session.add_link(node1=node1, node2=node2, iface1=iface1, iface2=iface2)
|
||||
|
||||
# change session state
|
||||
core.set_session_state(session_id, SessionState.INSTANTIATION)
|
||||
# start session
|
||||
core.start_session(session)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# required imports
|
||||
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
|
||||
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.connect()
|
||||
|
||||
# create session and get id
|
||||
response = core.create_session()
|
||||
session_id = response.session_id
|
||||
# add session
|
||||
session = core.create_session()
|
||||
|
||||
# change session state to configuration so that nodes get started when added
|
||||
core.set_session_state(session_id, SessionState.CONFIGURATION)
|
||||
|
||||
# create switch node
|
||||
# create nodes
|
||||
position = Position(x=200, y=200)
|
||||
switch = Node(type=NodeType.SWITCH, position=position)
|
||||
response = core.add_node(session_id, switch)
|
||||
switch_id = response.node_id
|
||||
|
||||
# create node one
|
||||
switch = session.add_node(1, _type=NodeType.SWITCH, position=position)
|
||||
position = Position(x=100, y=100)
|
||||
n1 = Node(type=NodeType.DEFAULT, position=position, model="PC")
|
||||
response = core.add_node(session_id, n1)
|
||||
n1_id = response.node_id
|
||||
|
||||
# create node two
|
||||
node1 = session.add_node(2, position=position)
|
||||
position = Position(x=300, y=100)
|
||||
n2 = Node(type=NodeType.DEFAULT, position=position, model="PC")
|
||||
response = core.add_node(session_id, n2)
|
||||
n2_id = response.node_id
|
||||
node2 = session.add_node(3, position=position)
|
||||
|
||||
# links nodes to switch
|
||||
iface1 = iface_helper.create_iface(n1_id, 0)
|
||||
core.add_link(session_id, n1_id, switch_id, iface1)
|
||||
iface1 = iface_helper.create_iface(n2_id, 0)
|
||||
core.add_link(session_id, n2_id, switch_id, iface1)
|
||||
# create links
|
||||
iface1 = iface_helper.create_iface(node1.id, 0)
|
||||
session.add_link(node1=node1, node2=switch, iface1=iface1)
|
||||
iface1 = iface_helper.create_iface(node2.id, 0)
|
||||
session.add_link(node1=node2, node2=switch, iface1=iface1)
|
||||
|
||||
# change session state
|
||||
core.set_session_state(session_id, SessionState.INSTANTIATION)
|
||||
# start session
|
||||
core.start_session(session)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# required imports
|
||||
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
|
||||
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.connect()
|
||||
|
||||
# create session and get id
|
||||
response = core.create_session()
|
||||
session_id = response.session_id
|
||||
# add session
|
||||
session = core.create_session()
|
||||
|
||||
# change session state to configuration so that nodes get started when added
|
||||
core.set_session_state(session_id, SessionState.CONFIGURATION)
|
||||
|
||||
# create wlan node
|
||||
# create nodes
|
||||
position = Position(x=200, y=200)
|
||||
wlan = Node(type=NodeType.WIRELESS_LAN, position=position)
|
||||
response = core.add_node(session_id, wlan)
|
||||
wlan_id = response.node_id
|
||||
|
||||
# create node one
|
||||
wlan = session.add_node(1, _type=NodeType.WIRELESS_LAN, position=position)
|
||||
position = Position(x=100, y=100)
|
||||
n1 = Node(type=NodeType.DEFAULT, position=position, model="mdr")
|
||||
response = core.add_node(session_id, n1)
|
||||
n1_id = response.node_id
|
||||
|
||||
# create node two
|
||||
node1 = session.add_node(2, model="mdr", position=position)
|
||||
position = Position(x=300, y=100)
|
||||
n2 = Node(type=NodeType.DEFAULT, position=position, model="mdr")
|
||||
response = core.add_node(session_id, n2)
|
||||
n2_id = response.node_id
|
||||
node2 = session.add_node(3, model="mdr", position=position)
|
||||
|
||||
# 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
|
||||
core.set_wlan_config(
|
||||
session_id,
|
||||
wlan_id,
|
||||
wlan.set_wlan(
|
||||
{
|
||||
"range": "280",
|
||||
"bandwidth": "55000000",
|
||||
"delay": "6000",
|
||||
"jitter": "5",
|
||||
"error": "5",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# links nodes to wlan
|
||||
iface1 = iface_helper.create_iface(n1_id, 0)
|
||||
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)
|
||||
# start session
|
||||
core.start_session(session)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Example custom emane model.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from core.config import Configuration
|
||||
|
@ -39,17 +40,34 @@ class ExampleModel(emanemodel.EmaneModel):
|
|||
|
||||
name: str = "emane_example"
|
||||
mac_library: str = "rfpipemaclayer"
|
||||
mac_xml: str = "/usr/share/emane/manifest/rfpipemaclayer.xml"
|
||||
mac_xml: str = "rfpipemaclayer.xml"
|
||||
mac_defaults: Dict[str, str] = {
|
||||
"pcrcurveuri": "/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
|
||||
}
|
||||
mac_config: List[Configuration] = emanemanifest.parse(mac_xml, mac_defaults)
|
||||
mac_config: List[Configuration] = []
|
||||
phy_library: Optional[str] = None
|
||||
phy_xml: str = "/usr/share/emane/manifest/emanephy.xml"
|
||||
phy_xml: str = "emanephy.xml"
|
||||
phy_defaults: Dict[str, str] = {
|
||||
"subid": "1",
|
||||
"propagationmodel": "2ray",
|
||||
"noisemode": "none",
|
||||
}
|
||||
phy_config: List[Configuration] = emanemanifest.parse(phy_xml, phy_defaults)
|
||||
phy_config: List[Configuration] = []
|
||||
config_ignore: Set[str] = set()
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
"""
|
||||
Called after being loaded within the EmaneManager. Provides configured
|
||||
emane_prefix for parsing xml files.
|
||||
|
||||
:param emane_prefix: configured emane prefix path
|
||||
:return: nothing
|
||||
"""
|
||||
manifest_path = "share/emane/manifest"
|
||||
# load mac configuration
|
||||
mac_xml_path = emane_prefix / manifest_path / cls.mac_xml
|
||||
cls.mac_config = emanemanifest.parse(mac_xml_path, cls.mac_defaults)
|
||||
# load phy configuration
|
||||
phy_xml_path = emane_prefix / manifest_path / cls.phy_xml
|
||||
cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue