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

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

101
Dockerfile Normal file
View file

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

View file

@ -57,7 +57,7 @@ fpm -s dir -t deb -n core-distributed \
-d "procps" \
-d "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)

View file

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

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script.
# 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)

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -4,73 +4,107 @@ 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.
Represents a configuration option.
"""
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}"
)
@dataclass
class ConfigBool(Configuration):
"""
Represents a boolean configuration option.
"""
type: ConfigDataTypes = ConfigDataTypes.BOOL
@dataclass
class ConfigFloat(Configuration):
"""
Represents a float configuration option.
"""
type: ConfigDataTypes = ConfigDataTypes.FLOAT
@dataclass
class ConfigInt(Configuration):
"""
Represents an integer configuration option.
"""
type: ConfigDataTypes = ConfigDataTypes.INT32
@dataclass
class ConfigString(Configuration):
"""
Represents a string configuration option.
"""
type: ConfigDataTypes = ConfigDataTypes.STRING
class ConfigurableOptions:
@ -182,7 +216,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 +348,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 +377,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

View file

@ -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}"
)

View file

@ -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)

View file

@ -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

View file

@ -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)):

View file

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

View file

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

View file

@ -149,11 +149,13 @@ class DhcpService(ConfigService):
subnets = []
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)

View file

@ -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

View file

@ -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,

View file

@ -2,19 +2,21 @@
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.config import ConfigBool, ConfigGroup, ConfigString, 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
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 +25,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
@ -41,35 +54,45 @@ class EmaneModel(WirelessModel):
# support for external configurations
external_config: List[Configuration] = [
Configuration("external", ConfigDataTypes.BOOL, default="0"),
Configuration(
"platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001"
),
Configuration(
"transportendpoint", ConfigDataTypes.STRING, default="127.0.0.1:50002"
),
ConfigBool(id="external", default="0"),
ConfigString(id="platformendpoint", default="127.0.0.1:40001"),
ConfigString(id="transportendpoint", default="127.0.0.1:50002"),
]
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 +100,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 +111,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 +137,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 +156,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 +171,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)

View file

@ -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())

View file

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

View file

View file

@ -1,11 +1,11 @@
"""
EMANE Bypass model for CORE
"""
from pathlib import Path
from typing import List, Set
from core.config import Configuration
from core.config import ConfigBool, Configuration
from core.emane import emanemodel
from core.emulator.enumerations import ConfigDataTypes
class EmaneBypassModel(emanemodel.EmaneModel):
@ -17,9 +17,8 @@ class EmaneBypassModel(emanemodel.EmaneModel):
# mac definitions
mac_library: str = "bypassmaclayer"
mac_config: List[Configuration] = [
Configuration(
_id="none",
_type=ConfigDataTypes.BOOL,
ConfigBool(
id="none",
default="0",
label="There are no parameters for the bypass model.",
)
@ -30,6 +29,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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

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

View file

@ -4,7 +4,7 @@ share the same MAC+PHY model.
"""
import logging
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

View file

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

View file

@ -3,15 +3,21 @@ import logging
import os
import 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

View file

@ -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:
"""
@ -141,6 +142,7 @@ class InterfaceData:
ip4_mask: int = None
ip6: str = None
ip6_mask: int = None
mtu: int = None
def get_ips(self) -> List[str]:
"""

View file

@ -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()
@ -186,6 +187,7 @@ class DistributedController:
:return: nothing
"""
mtu = self.session.options.get_config_int("mtu")
for node_id in self.session.nodes:
node = self.session.nodes[node_id]
if not isinstance(node, CoreNetwork):
@ -194,17 +196,18 @@ class DistributedController:
continue
for name in self.servers:
server = self.servers[name]
self.create_gre_tunnel(node, server)
self.create_gre_tunnel(node, server, mtu, True)
def create_gre_tunnel(
self, node: CoreNetwork, server: DistributedServer
self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool
) -> Tuple[GreTap, GreTap]:
"""
Create gre tunnel using a pair of gre taps between the local and remote server.
:param node: node to create gre tunnel for
:param server: server to create
tunnel for
:param server: server to create tunnel for
:param mtu: mtu for gre taps
:param start: True to start gre taps, False otherwise
:return: local and remote gre taps created for tunnel
"""
host = server.host
@ -212,23 +215,20 @@ 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
)
local_tap = GreTap(session=self.session, remoteip=host, key=key)
logger.info("local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key)
local_tap = GreTap(self.session, host, key=key, mtu=mtu)
if start:
local_tap.startup()
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 = GreTap(self.session, self.address, key=key, server=server, mtu=mtu)
if start:
remote_tap.startup()
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 +244,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)

View file

@ -4,6 +4,7 @@ that manages a CORE session.
"""
import logging
import math
import os
import pwd
import shutil
@ -45,7 +46,7 @@ from core.location.geo import GeoLocation
from core.location.mobility import BasicRangeModel, MobilityManager
from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode
from core.nodes.interface import CoreInterface
from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.lxd import LxcNode
from core.nodes.network import (
CtrlNet,
@ -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,
)
@ -250,7 +253,12 @@ class Session:
node2 = self.get_node(node2_id, NodeBase)
iface1 = None
iface2 = None
# set mtu
mtu = self.options.get_config_int("mtu") or DEFAULT_MTU
if iface1_data:
iface1_data.mtu = mtu
if iface2_data:
iface2_data.mtu = mtu
# wireless link
if link_type == LinkTypes.WIRELESS:
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
@ -263,7 +271,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 +294,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 +311,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 +340,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 +417,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 +533,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,30 +550,40 @@ 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)
# set network mtu, if configured
mtu = self.options.get_config_int("mtu")
if isinstance(node, CoreNetworkBase) and mtu > 0:
node.mtu = mtu
# ensure default emane configuration
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
if isinstance(node, WlanNode):
self.mobility.set_model_config(_id, BasicRangeModel.name)
@ -575,51 +593,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 +631,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 +674,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 +686,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 +759,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 +770,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 +854,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 +869,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 +896,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 +930,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 +973,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 +991,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 +1001,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 +1015,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 +1044,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 +1104,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 +1130,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 +1228,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 +1248,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 +1295,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 +1316,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 +1344,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 +1389,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 +1403,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 +1426,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 +1440,7 @@ class Session:
else:
prefix = prefixes[0]
logging.info(
logger.info(
"controlnet(%s) prefix(%s) updown(%s) serverintf(%s)",
_id,
prefix,
@ -1508,6 +1496,7 @@ class Session:
mac=utils.random_mac(),
ip4=ip4,
ip4_mask=ip4_mask,
mtu=DEFAULT_MTU,
)
iface = node.new_iface(control_net, iface_data)
iface.control = True
@ -1515,7 +1504,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 +1522,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 +1537,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 +1566,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 +1578,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 +1597,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:

View file

@ -1,7 +1,14 @@
from typing import Any, List
from core.config import ConfigurableManager, ConfigurableOptions, Configuration
from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs
from core.config import (
ConfigBool,
ConfigInt,
ConfigString,
ConfigurableManager,
ConfigurableOptions,
Configuration,
)
from core.emulator.enumerations import RegisterTlvs
from core.plugins.sdt import Sdt
@ -12,53 +19,28 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
name: str = "session"
options: List[Configuration] = [
Configuration(
_id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network"
ConfigString(id="controlnet", label="Control Network"),
ConfigString(id="controlnet0", label="Control Network 0"),
ConfigString(id="controlnet1", label="Control Network 1"),
ConfigString(id="controlnet2", label="Control Network 2"),
ConfigString(id="controlnet3", label="Control Network 3"),
ConfigString(id="controlnet_updown_script", label="Control Network Script"),
ConfigBool(id="enablerj45", default="1", label="Enable RJ45s"),
ConfigBool(id="preservedir", default="0", label="Preserve session dir"),
ConfigBool(id="enablesdt", default="0", label="Enable SDT3D output"),
ConfigString(id="sdturl", default=Sdt.DEFAULT_SDT_URL, label="SDT3D URL"),
ConfigBool(id="ovs", default="0", label="Enable OVS"),
ConfigInt(id="platform_id_start", default="1", label="EMANE Platform ID Start"),
ConfigInt(id="nem_id_start", default="1", label="EMANE NEM ID Start"),
ConfigBool(id="link_enabled", default="1", label="EMANE Links?"),
ConfigInt(
id="loss_threshold", default="30", label="EMANE Link Loss Threshold (%)"
),
Configuration(
_id="controlnet0", _type=ConfigDataTypes.STRING, label="Control Network 0"
),
Configuration(
_id="controlnet1", _type=ConfigDataTypes.STRING, label="Control Network 1"
),
Configuration(
_id="controlnet2", _type=ConfigDataTypes.STRING, label="Control Network 2"
),
Configuration(
_id="controlnet3", _type=ConfigDataTypes.STRING, label="Control Network 3"
),
Configuration(
_id="controlnet_updown_script",
_type=ConfigDataTypes.STRING,
label="Control Network Script",
),
Configuration(
_id="enablerj45",
_type=ConfigDataTypes.BOOL,
default="1",
label="Enable RJ45s",
),
Configuration(
_id="preservedir",
_type=ConfigDataTypes.BOOL,
default="0",
label="Preserve session dir",
),
Configuration(
_id="enablesdt",
_type=ConfigDataTypes.BOOL,
default="0",
label="Enable SDT3D output",
),
Configuration(
_id="sdturl",
_type=ConfigDataTypes.STRING,
default=Sdt.DEFAULT_SDT_URL,
label="SDT3D URL",
),
Configuration(
_id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS"
ConfigInt(
id="link_interval", default="1", label="EMANE Link Check Interval (sec)"
),
ConfigInt(id="link_timeout", default="4", label="EMANE Link Timeout (sec)"),
ConfigInt(id="mtu", default="0", label="MTU for All Devices"),
]
config_type: RegisterTlvs = RegisterTlvs.UTILITY
@ -112,3 +94,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())

View file

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

View file

@ -7,15 +7,15 @@ SYSCTL: str = "sysctl"
IP: str = "ip"
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,

View file

@ -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()
)

View file

@ -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:

View file

@ -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)
def execute_script(self, script: str, options: str) -> None:
session_id = self.client.execute_script(script, options)
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

View file

@ -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()

View file

@ -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()

View file

@ -18,6 +18,8 @@ from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.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:

View file

@ -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,

View 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

View file

@ -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)
self.app.core.execute_script(file)
logger.info("Execute %s with options %s", file, options)
self.app.core.execute_script(file, options)
self.destroy()

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -17,6 +17,8 @@ from core.gui.dialogs.emaneconfig import EmaneModelDialog
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.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()

View file

@ -12,6 +12,8 @@ from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.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()

View file

@ -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()

View file

@ -9,6 +9,8 @@ from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts
from core.gui.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:

View file

@ -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)

View file

@ -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:

View file

@ -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()

View file

@ -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

View file

@ -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,

View file

@ -18,6 +18,8 @@ from core.gui.graph.node import CanvasNode, ShadowNode
from core.gui.graph.shape import Shape
from core.gui.graph.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:

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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]:

View file

@ -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

View file

@ -12,22 +12,27 @@ from pathlib import Path
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union
from core import utils
from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager
from core.config import (
ConfigBool,
ConfigFloat,
ConfigGroup,
ConfigInt,
ConfigString,
ConfigurableOptions,
Configuration,
ModelManager,
)
from core.emane.nodes import EmaneNet
from core.emulator.data import EventData, LinkData, LinkOptions
from core.emulator.enumerations import (
ConfigDataTypes,
EventTypes,
LinkTypes,
MessageFlags,
RegisterTlvs,
)
from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags, RegisterTlvs
from core.errors import CoreError
from core.executables import BASH
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 +47,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 +123,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 +137,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 +156,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 +166,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,
@ -235,39 +277,12 @@ class BasicRangeModel(WirelessModel):
name: str = "basic_range"
options: List[Configuration] = [
Configuration(
_id="range",
_type=ConfigDataTypes.UINT32,
default="275",
label="wireless range (pixels)",
),
Configuration(
_id="bandwidth",
_type=ConfigDataTypes.UINT64,
default="54000000",
label="bandwidth (bps)",
),
Configuration(
_id="jitter",
_type=ConfigDataTypes.UINT64,
default="0",
label="transmission jitter (usec)",
),
Configuration(
_id="delay",
_type=ConfigDataTypes.UINT64,
default="5000",
label="transmission delay (usec)",
),
Configuration(
_id="error", _type=ConfigDataTypes.STRING, default="0", label="loss (%)"
),
Configuration(
_id="promiscuous",
_type=ConfigDataTypes.BOOL,
default="0",
label="promiscuous mode",
),
ConfigInt(id="range", default="275", label="wireless range (pixels)"),
ConfigInt(id="bandwidth", default="54000000", label="bandwidth (bps)"),
ConfigInt(id="jitter", default="0", label="transmission jitter (usec)"),
ConfigInt(id="delay", default="5000", label="transmission delay (usec)"),
ConfigFloat(id="error", default="0.0", label="loss (%)"),
ConfigBool(id="promiscuous", default="0", label="promiscuous mode"),
]
@classmethod
@ -293,25 +308,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 +402,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 +442,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 +502,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
@ -867,43 +863,14 @@ class Ns2ScriptedMobility(WayPointMobility):
name: str = "ns2script"
options: List[Configuration] = [
Configuration(
_id="file", _type=ConfigDataTypes.STRING, label="mobility script file"
),
Configuration(
_id="refresh_ms",
_type=ConfigDataTypes.UINT32,
default="50",
label="refresh time (ms)",
),
Configuration(
_id="loop", _type=ConfigDataTypes.BOOL, default="1", label="loop"
),
Configuration(
_id="autostart",
_type=ConfigDataTypes.STRING,
label="auto-start seconds (0.0 for runtime)",
),
Configuration(
_id="map",
_type=ConfigDataTypes.STRING,
label="node mapping (optional, e.g. 0:1,1:2,2:3)",
),
Configuration(
_id="script_start",
_type=ConfigDataTypes.STRING,
label="script file to run upon start",
),
Configuration(
_id="script_pause",
_type=ConfigDataTypes.STRING,
label="script file to run upon pause",
),
Configuration(
_id="script_stop",
_type=ConfigDataTypes.STRING,
label="script file to run upon stop",
),
ConfigString(id="file", label="mobility script file"),
ConfigInt(id="refresh_ms", default="50", label="refresh time (ms)"),
ConfigBool(id="loop", default="1", label="loop"),
ConfigString(id="autostart", label="auto-start seconds (0.0 for runtime)"),
ConfigString(id="map", label="node mapping (optional, e.g. 0:1,1:2,2:3)"),
ConfigString(id="script_start", label="script file to run upon start"),
ConfigString(id="script_pause", label="script file to run upon pause"),
ConfigString(id="script_stop", label="script file to run upon stop"),
]
@classmethod
@ -920,7 +887,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 +895,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 +920,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 +944,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 +971,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 +1014,6 @@ class Ns2ScriptedMobility(WayPointMobility):
self.nodemap = {}
if mapstr.strip() == "":
return
for pair in mapstr.split(","):
parts = pair.split(":")
try:
@ -1055,7 +1021,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 +1043,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 +1065,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 +1086,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 +1098,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 +1118,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())

View file

@ -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
@ -18,9 +18,11 @@ from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes
from core.errors import CoreCommandError, CoreError
from core.executables import MOUNT, TEST, VNODED
from core.nodes.client import VnodeClient
from core.nodes.interface import CoreInterface, TunTap, Veth
from core.nodes.interface import DEFAULT_MTU, CoreInterface, TunTap, Veth
from core.nodes.netclient import LinuxNetClient, get_net_client
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:
"""
@ -675,64 +712,28 @@ class CoreNode(CoreNodeBase):
with self.lock:
return super().next_iface_id()
def newveth(self, iface_id: int = None, ifname: str = None) -> int:
def newveth(self, iface_id: int = None, ifname: str = None, mtu: int = None) -> int:
"""
Create a new interface.
:param iface_id: id for the new interface
:param ifname: name for the new interface
:param mtu: mtu for interface
:return: nothing
"""
with self.lock:
if iface_id is None:
iface_id = self.next_iface_id()
if ifname is None:
ifname = f"eth{iface_id}"
mtu = mtu if mtu is not None else DEFAULT_MTU
iface_id = iface_id if iface_id is not None else self.next_iface_id()
ifname = ifname if ifname is not None else f"eth{iface_id}"
sessionid = self.session.short_session_id()
try:
suffix = f"{self.id:x}.{iface_id}.{sessionid}"
except TypeError:
suffix = f"{self.id}.{iface_id}.{sessionid}"
localname = f"veth{suffix}"
if len(localname) >= 16:
raise ValueError(f"interface local name ({localname}) too long")
name = localname + "p"
if len(name) >= 16:
raise ValueError(f"interface name ({name}) too long")
veth = Veth(
self.session, self, name, localname, start=self.up, server=self.server
)
if self.up:
self.net_client.device_ns(veth.name, str(self.pid))
self.node_net_client.device_name(veth.name, ifname)
self.node_net_client.checksums_off(ifname)
veth.name = ifname
if self.up:
flow_id = self.node_net_client.get_ifindex(veth.name)
veth.flow_id = int(flow_id)
logging.debug("interface flow index: %s - %s", veth.name, veth.flow_id)
mac = self.node_net_client.get_mac(veth.name)
logging.debug("interface mac: %s - %s", veth.name, mac)
veth.set_mac(mac)
try:
# add network interface to the node. If unsuccessful, destroy the
# network interface and raise exception.
self.add_iface(veth, iface_id)
except ValueError as e:
veth.shutdown()
del veth
raise e
name = f"{localname}p"
veth = Veth(self.session, name, localname, mtu, self.server, self)
veth.adopt_node(iface_id, ifname, self.up)
return iface_id
def newtuntap(self, iface_id: int = None, ifname: str = None) -> int:
@ -744,24 +745,19 @@ class CoreNode(CoreNodeBase):
:return: interface index
"""
with self.lock:
if iface_id is None:
iface_id = self.next_iface_id()
if ifname is None:
ifname = f"eth{iface_id}"
iface_id = iface_id if iface_id is not None else self.next_iface_id()
ifname = ifname if ifname is not None else f"eth{iface_id}"
sessionid = self.session.short_session_id()
localname = f"tap{self.id}.{iface_id}.{sessionid}"
name = ifname
tuntap = TunTap(self.session, self, name, localname, start=self.up)
tuntap = TunTap(self.session, name, localname, node=self)
if self.up:
tuntap.startup()
try:
self.add_iface(tuntap, iface_id)
except ValueError as e:
except CoreError as e:
tuntap.shutdown()
del tuntap
raise e
return iface_id
def set_mac(self, iface_id: int, mac: str) -> None:
@ -842,7 +838,7 @@ class CoreNode(CoreNodeBase):
raise CoreError(
f"node({self.name}) already has interface({iface_id})"
)
iface_id = self.newveth(iface_id, iface_data.name)
iface_id = self.newveth(iface_id, iface_data.name, iface_data.mtu)
self.attachnet(iface_id, net)
if iface_data.mac:
self.set_mac(iface_id, iface_data.mac)
@ -851,86 +847,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):
@ -951,16 +960,17 @@ class CoreNetworkBase(NodeBase):
"""
Create a CoreNetworkBase instance.
:param session: CORE session object
:param session: session object
:param _id: object id
:param name: object name
:param server: remote server node
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.mtu: int = DEFAULT_MTU
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 +1039,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 +1051,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]:
"""

View file

@ -3,6 +3,7 @@ client.py: implementation of the VnodeClient class for issuing commands
over a control channel to the vnoded process running in a network namespace.
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:
"""

View file

@ -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}")

View file

@ -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
@ -30,25 +33,28 @@ class CoreInterface:
def __init__(
self,
session: "Session",
node: "CoreNode",
name: str,
localname: str,
mtu: int,
mtu: int = DEFAULT_MTU,
server: "DistributedServer" = None,
node: "CoreNode" = None,
) -> None:
"""
Creates a CoreInterface instance.
:param session: core session instance
:param node: node for interface
:param name: interface name
:param localname: interface local name
:param mtu: mtu value
:param server: remote server node
will run on, default is None for localhost
:param server: remote server node will run on, default is None for localhost
:param node: node for interface
"""
if len(name) >= 16:
raise CoreError(f"interface name ({name}) too long, max 16")
if len(localname) >= 16:
raise CoreError(f"interface local name ({localname}) too long, max 16")
self.session: "Session" = session
self.node: "CoreNode" = node
self.node: Optional["CoreNode"] = node
self.name: str = name
self.localname: str = localname
self.up: bool = False
@ -79,7 +85,7 @@ class CoreInterface:
self,
args: str,
env: Dict[str, str] = None,
cwd: str = None,
cwd: Path = None,
wait: bool = True,
shell: bool = False,
) -> str:
@ -125,7 +131,6 @@ class CoreInterface:
if self.net:
self.detachnet()
self.net = None
net.attach(self)
self.net = net
@ -273,14 +278,12 @@ 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
current_value = self._params.get(key)
if current_value is not None and current_value == value:
return False
self._params[key] = value
return True
@ -339,33 +342,32 @@ class Veth(CoreInterface):
Provides virtual ethernet functionality for core nodes.
"""
def __init__(
self,
session: "Session",
node: "CoreNode",
name: str,
localname: str,
mtu: int = DEFAULT_MTU,
server: "DistributedServer" = None,
start: bool = True,
) -> None:
def adopt_node(self, iface_id: int, name: str, start: bool) -> None:
"""
Creates a VEth instance.
Adopt this interface to the provided node, configuring and associating
with the node as needed.
:param session: core session instance
:param node: related core node
:param name: interface name
:param localname: interface local name
:param mtu: interface mtu
:param server: remote server node
will run on, default is None for localhost
:param start: start flag
:raises CoreCommandError: when there is a command exception
:param iface_id: interface id for node
:param name: name of interface fo rnode
:param start: True to start interface, False otherwise
:return: nothing
"""
# note that net arg is ignored
super().__init__(session, node, name, localname, mtu, server)
if start:
self.startup()
self.net_client.device_ns(self.name, str(self.node.pid))
self.node.node_net_client.checksums_off(self.name)
self.flow_id = self.node.node_net_client.get_ifindex(self.name)
logger.debug("interface flow index: %s - %s", self.name, self.flow_id)
mac = self.node.node_net_client.get_mac(self.name)
logger.debug("interface mac: %s - %s", self.name, mac)
self.set_mac(mac)
self.node.node_net_client.device_name(self.name, name)
self.name = name
try:
self.node.add_iface(self, iface_id)
except CoreError as e:
self.shutdown()
raise e
def startup(self) -> None:
"""
@ -375,6 +377,9 @@ class Veth(CoreInterface):
:raises CoreCommandError: when there is a command exception
"""
self.net_client.create_veth(self.localname, self.name)
if self.mtu > 0:
self.net_client.set_mtu(self.name, self.mtu)
self.net_client.set_mtu(self.localname, self.mtu)
self.net_client.device_up(self.localname)
self.up = True
@ -404,32 +409,6 @@ class TunTap(CoreInterface):
TUN/TAP virtual device in TAP mode
"""
def __init__(
self,
session: "Session",
node: "CoreNode",
name: str,
localname: str,
mtu: int = DEFAULT_MTU,
server: "DistributedServer" = None,
start: bool = True,
) -> None:
"""
Create a TunTap instance.
:param session: core session instance
:param node: related core node
:param name: interface name
:param localname: local interface name
:param mtu: interface mtu
:param server: remote server node
will run on, default is None for localhost
:param start: start flag
"""
super().__init__(session, node, name, localname, mtu, server)
if start:
self.startup()
def startup(self) -> None:
"""
Startup logic for a tunnel tap.
@ -452,12 +431,10 @@ class TunTap(CoreInterface):
"""
if not self.up:
return
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
def waitfor(
@ -481,14 +458,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 +476,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 +493,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:
@ -578,47 +555,55 @@ class GreTap(CoreInterface):
def __init__(
self,
session: "Session",
remoteip: str,
key: int = None,
node: "CoreNode" = None,
name: str = None,
session: "Session" = None,
mtu: int = 1458,
remoteip: str = None,
mtu: int = DEFAULT_MTU,
_id: int = None,
localip: str = None,
ttl: int = 255,
key: int = None,
start: bool = True,
server: "DistributedServer" = None,
) -> None:
"""
Creates a GreTap instance.
:param node: related core node
:param name: interface name
:param session: core session instance
:param mtu: interface mtu
:param remoteip: remote address
:param key: gre tap key
:param node: related core node
:param mtu: interface mtu
:param _id: object id
:param localip: local address
:param ttl: ttl value
:param key: gre tap key
:param start: start flag
:param server: remote server node
will run on, default is None for localhost
:raises CoreCommandError: when there is a command exception
"""
if _id is None:
_id = ((id(self) >> 16) ^ (id(self) & 0xFFFF)) & 0xFFFF
self.id = _id
self.id: int = _id
sessionid = session.short_session_id()
localname = f"gt.{self.id}.{sessionid}"
super().__init__(session, node, name, localname, mtu, server)
self.transport_type = TransportType.RAW
if not start:
return
if remoteip is None:
raise CoreError("missing remote IP required for GRE TAP device")
self.net_client.create_gretap(self.localname, remoteip, localip, ttl, key)
name = f"{localname}p"
super().__init__(session, name, localname, mtu, server, node)
self.transport_type: TransportType = TransportType.RAW
self.remote_ip: str = remoteip
self.ttl: int = ttl
self.key: Optional[int] = key
self.local_ip: Optional[str] = localip
def startup(self) -> None:
"""
Startup logic for a GreTap.
:return: nothing
"""
self.net_client.create_gretap(
self.localname, self.remote_ip, self.local_ip, self.ttl, self.key
)
if self.mtu > 0:
self.net_client.set_mtu(self.localname, self.mtu)
self.net_client.device_up(self.localname)
self.up = True
@ -633,5 +618,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

View file

@ -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)

View file

@ -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:
"""
@ -95,14 +95,14 @@ class LinuxNetClient:
"""
return self.run(f"cat /sys/class/net/{device}/address")
def get_ifindex(self, device: str) -> str:
def get_ifindex(self, device: str) -> int:
"""
Retrieve ifindex for a given device.
:param device: device to get ifindex for
:return: ifindex
"""
return self.run(f"cat /sys/class/net/{device}/ifindex")
return int(self.run(f"cat /sys/class/net/{device}/ifindex"))
def device_ns(self, device: str, namespace: str) -> None:
"""
@ -296,6 +296,16 @@ class LinuxNetClient:
"""
self.run(f"{IP} link set {name} type bridge ageing_time {value}")
def set_mtu(self, name: str, value: int) -> None:
"""
Sets the mtu value for a device.
:param name: name of device to set value for
:param value: mtu value to set
:return: nothing
"""
self.run(f"{IP} link set {name} mtu {value}")
class OvsNetClient(LinuxNetClient):
"""
@ -361,14 +371,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:

View file

@ -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,22 +283,24 @@ 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
def startup(self) -> None:
"""
Linux bridge starup logic.
Linux bridge startup logic.
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
self.net_client.create_bridge(self.brname)
self.has_ebtables_chain = False
if self.mtu > 0:
self.net_client.set_mtu(self.brname, self.mtu)
self.has_nftables_chain = False
self.up = True
ebq.startupdateloop(self)
nft_queue.start(self)
def shutdown(self) -> None:
"""
@ -333,27 +310,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 +346,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 +357,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 +368,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
@ -522,28 +484,22 @@ class CoreNetwork(CoreNetworkBase):
_id = f"{self.id:x}"
except TypeError:
_id = str(self.id)
try:
net_id = f"{net.id:x}"
except TypeError:
net_id = str(net.id)
localname = f"veth{_id}.{net_id}.{sessionid}"
if len(localname) >= 16:
raise ValueError(f"interface local name {localname} too long")
name = f"veth{net_id}.{_id}.{sessionid}"
if len(name) >= 16:
raise ValueError(f"interface name {name} too long")
iface = Veth(self.session, None, name, localname, start=self.up)
iface = Veth(self.session, name, localname)
if self.up:
iface.startup()
self.attach(iface)
if net.up and net.brname:
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
@ -616,14 +572,15 @@ class GreTapBridge(CoreNetwork):
self.localip: Optional[str] = localip
self.ttl: int = ttl
self.gretap: Optional[GreTap] = None
if remoteip is not None:
if self.remoteip is not None:
self.gretap = GreTap(
session,
remoteip,
key=self.grekey,
node=self,
session=session,
remoteip=remoteip,
localip=localip,
ttl=ttl,
key=self.grekey,
mtu=self.mtu,
)
def startup(self) -> None:
@ -634,6 +591,7 @@ class GreTapBridge(CoreNetwork):
"""
super().startup()
if self.gretap:
self.gretap.startup()
self.attach(self.gretap)
def shutdown(self) -> None:
@ -659,18 +617,20 @@ class GreTapBridge(CoreNetwork):
:return: nothing
"""
if self.gretap:
raise ValueError(f"gretap already exists for {self.name}")
raise CoreError(f"gretap already exists for {self.name}")
remoteip = ips[0].split("/")[0]
localip = None
if len(ips) > 1:
localip = ips[1].split("/")[0]
self.gretap = GreTap(
session=self.session,
remoteip=remoteip,
self.session,
remoteip,
key=self.grekey,
localip=localip,
ttl=self.ttl,
key=self.grekey,
mtu=self.mtu,
)
self.startup()
self.attach(self.gretap)
def setkey(self, key: int, iface_data: InterfaceData) -> None:
@ -769,7 +729,7 @@ class CtrlNet(CoreNetwork):
raise CoreError(f"old bridges exist for node: {self.id}")
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)

View file

@ -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
@ -14,7 +14,9 @@ from core.errors import CoreCommandError, CoreError
from core.executables import MOUNT, TEST, UMOUNT
from core.nodes.base import CoreNetworkBase, CoreNodeBase
from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.network import CoreNetwork, GreTap
from core.nodes.network import CoreNetwork
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
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:
@ -174,67 +173,46 @@ class PhysicalNode(CoreNodeBase):
name = iface_data.name
if name is None:
name = f"gt{iface_id}"
if self.up:
# this is reached when this node is linked to a network node
# tunnel to net not built yet, so build it now and adopt it
_, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server)
_, remote_tap = self.session.distributed.create_gre_tunnel(
net, self.server, iface_data.mtu, self.up
)
self.adopt_iface(remote_tap, iface_id, iface_data.mac, ips)
return remote_tap
else:
# this is reached when configuring services (self.up=False)
iface = GreTap(node=self, name=name, session=self.session, start=False)
self.adopt_iface(iface, iface_id, iface_data.mac, ips)
return iface
def privatedir(self, path: str) -> None:
if path[0] != "/":
raise ValueError(f"path not fully qualified: {path}")
hostpath = os.path.join(
self.nodedir, os.path.normpath(path).strip("/").replace("/", ".")
)
os.mkdir(hostpath)
self.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 +411,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 +421,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 +442,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:

View file

@ -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):

View file

@ -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():

View file

@ -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:

View file

@ -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

View file

@ -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,
)

View file

@ -15,6 +15,7 @@ import random
import shlex
import shutil
import sys
import threading
from pathlib import Path
from subprocess import PIPE, STDOUT, Popen
from typing import (
@ -36,7 +37,10 @@ import netaddr
from core.errors import CoreCommandError, CoreError
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from core.emulator.coreemu import CoreEmu
from core.emulator.session import Session
from core.nodes.base import CoreNode
T = TypeVar("T")
@ -45,12 +49,29 @@ DEVNULL = open(os.devnull, "wb")
IFACE_CONFIG_FACTOR: int = 1000
def execute_script(coreemu: "CoreEmu", file_path: Path, args: str) -> None:
"""
Provides utility function to execute a python script in context of the
provide coreemu instance.
:param coreemu: coreemu to provide to script
:param file_path: python script to execute
:param args: args to provide script
:return: nothing
"""
sys.argv = shlex.split(args)
thread = threading.Thread(
target=execute_file, args=(file_path, {"coreemu": coreemu}), daemon=True
)
thread.start()
thread.join()
def execute_file(
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 +80,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 +113,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 +140,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 +209,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 +226,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 +243,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 +295,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 +307,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 +345,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 +372,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 +427,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

View file

@ -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
)

View file

@ -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"

View file

@ -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
}
}
}

View file

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

View file

@ -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__":

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

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