Merge pull request #712 from coreemu/develop

merge develop for 9.0.0 release
This commit is contained in:
bharnden 2022-11-18 14:46:29 -08:00 committed by GitHub
commit 5ab71377cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
386 changed files with 6440 additions and 53764 deletions

View file

@ -4,13 +4,13 @@ on: [push]
jobs: jobs:
build: build:
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Set up Python 3.6 - name: Set up Python 3.9
uses: actions/setup-python@v1 uses: actions/setup-python@v1
with: with:
python-version: 3.6 python-version: 3.9
- name: install poetry - name: install poetry
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip

4
.gitignore vendored
View file

@ -14,6 +14,7 @@ config.h.in
config.log config.log
config.status config.status
configure configure
configure~
debian debian
stamp-h1 stamp-h1
@ -58,3 +59,6 @@ daemon/setup.py
# python # python
__pycache__ __pycache__
# ignore core player files
*.core

View file

@ -1,3 +1,46 @@
## 2022-11-18 CORE 9.0.0
* Breaking Changes
* removed session nodes file
* removed session state file
* emane now runs in one process per nem with unique control ports
* grpc client has been refactored and updated
* removed tcl/legacy gui, imn file support and the tlv api
* link configuration is now different, but consistent, for wired links
* Installation
* added packaging for single file distribution
* python3.9 is now the minimum required version
* updated Dockerfile examples
* updated various python dependencies
* virtual environment is now installed to /opt/core/venv
* Documentation
* updated emane invoke task examples
* revamped install documentation
* added wireless node notes
* core-gui
* updated config services to display rendered templated and allow editing
* fixed node icon issue when updating preferences
* \#89 - throughput widget now works for hubs/switches
* \#691 - fixed custom nodes to properly use config services
* gRPC API
* add linked call to support linking and unlinking interfaces without destroying them
* fixed issue during start session clearing out session options
* added call to get rendered config service files
* removed get_node_links from links from client
* nem id and nem port have been added to GetNode and AddLink calls
* core-daemon
* wired links always create two veth pairs joined by a bridge
* node interfaces are now configured within the container to apply to outgoing traffic
* session.add_node now uses NodeOptions, allowing for node specific options
* fixed issue with xml reading node canvas values
* removed Session.add_node_file
* fixed get requirements logic
* fixed docker/lxd node support terminal commands on remote servers
* improved docker node command execution time using nsenter
* new wireless node type added to support dynamic loss based on distance
* \#513 - add and deleting distributed links during runtime is now supported
* \#703 - fixed issue not starting emane event listening service
## 2022-03-21 CORE 8.2.0 ## 2022-03-21 CORE 8.2.0
* core-gui * core-gui

View file

@ -1,100 +0,0 @@
# 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=master
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"]

View file

@ -6,10 +6,6 @@ if WANT_DOCS
DOCS = docs man DOCS = docs man
endif endif
if WANT_GUI
GUI = gui
endif
if WANT_DAEMON if WANT_DAEMON
DAEMON = daemon DAEMON = daemon
endif endif
@ -19,12 +15,13 @@ if WANT_NETNS
endif endif
# keep docs last due to dependencies on binaries # keep docs last due to dependencies on binaries
SUBDIRS = $(GUI) $(DAEMON) $(NETNS) $(DOCS) SUBDIRS = $(DAEMON) $(NETNS) $(DOCS)
ACLOCAL_AMFLAGS = -I config ACLOCAL_AMFLAGS = -I config
# extra files to include with distribution tarball # extra files to include with distribution tarball
EXTRA_DIST = bootstrap.sh \ EXTRA_DIST = bootstrap.sh \
package \
LICENSE \ LICENSE \
README.md \ README.md \
ASSIGNMENT_OF_COPYRIGHT.pdf \ ASSIGNMENT_OF_COPYRIGHT.pdf \
@ -51,7 +48,7 @@ fpm -s dir -t deb -n core-distributed \
--description "Common Open Research Emulator Distributed Package" \ --description "Common Open Research Emulator Distributed Package" \
--url https://github.com/coreemu/core \ --url https://github.com/coreemu/core \
--vendor "$(PACKAGE_VENDOR)" \ --vendor "$(PACKAGE_VENDOR)" \
-p core_distributed_VERSION_ARCH.deb \ -p core-distributed_VERSION_ARCH.deb \
-v $(PACKAGE_VERSION) \ -v $(PACKAGE_VERSION) \
-d "ethtool" \ -d "ethtool" \
-d "procps" \ -d "procps" \
@ -62,7 +59,8 @@ fpm -s dir -t deb -n core-distributed \
-d "libev4" \ -d "libev4" \
-d "openssh-server" \ -d "openssh-server" \
-d "xterm" \ -d "xterm" \
-C $(DESTDIR) netns/vnoded=/usr/bin/ \
netns/vcmd=/usr/bin/
endef endef
define fpm-distributed-rpm = define fpm-distributed-rpm =
@ -72,7 +70,7 @@ fpm -s dir -t rpm -n core-distributed \
--description "Common Open Research Emulator Distributed Package" \ --description "Common Open Research Emulator Distributed Package" \
--url https://github.com/coreemu/core \ --url https://github.com/coreemu/core \
--vendor "$(PACKAGE_VENDOR)" \ --vendor "$(PACKAGE_VENDOR)" \
-p core_distributed_VERSION_ARCH.rpm \ -p core-distributed_VERSION_ARCH.rpm \
-v $(PACKAGE_VERSION) \ -v $(PACKAGE_VERSION) \
-d "ethtool" \ -d "ethtool" \
-d "procps-ng" \ -d "procps-ng" \
@ -83,12 +81,75 @@ fpm -s dir -t rpm -n core-distributed \
-d "net-tools" \ -d "net-tools" \
-d "openssh-server" \ -d "openssh-server" \
-d "xterm" \ -d "xterm" \
-C $(DESTDIR) netns/vnoded=/usr/bin/ \
netns/vcmd=/usr/bin/
endef endef
.PHONY: fpm-distributed define fpm-rpm =
fpm-distributed: clean-local-fpm fpm -s dir -t rpm -n core \
$(MAKE) -C netns install DESTDIR=$(DESTDIR) -m "$(PACKAGE_MAINTAINERS)" \
--license "BSD" \
--description "core vnoded/vcmd and system dependencies" \
--url https://github.com/coreemu/core \
--vendor "$(PACKAGE_VENDOR)" \
-p core_VERSION_ARCH.rpm \
-v $(PACKAGE_VERSION) \
--rpm-init package/core-daemon \
--after-install package/after-install.sh \
--after-remove package/after-remove.sh \
-d "ethtool" \
-d "tk" \
-d "procps-ng" \
-d "bash >= 3.0" \
-d "ebtables" \
-d "iproute" \
-d "libev" \
-d "net-tools" \
-d "nftables" \
netns/vnoded=/usr/bin/ \
netns/vcmd=/usr/bin/ \
package/etc/core.conf=/etc/core/ \
package/etc/logging.conf=/etc/core/ \
package/examples=/opt/core/ \
daemon/dist/core-$(PACKAGE_VERSION)-py3-none-any.whl=/opt/core/
endef
define fpm-deb =
fpm -s dir -t deb -n core \
-m "$(PACKAGE_MAINTAINERS)" \
--license "BSD" \
--description "core vnoded/vcmd and system dependencies" \
--url https://github.com/coreemu/core \
--vendor "$(PACKAGE_VENDOR)" \
-p core_VERSION_ARCH.deb \
-v $(PACKAGE_VERSION) \
--deb-systemd package/core-daemon.service \
--deb-no-default-config-files \
--after-install package/after-install.sh \
--after-remove package/after-remove.sh \
-d "ethtool" \
-d "tk" \
-d "libtk-img" \
-d "procps" \
-d "libc6 >= 2.14" \
-d "bash >= 3.0" \
-d "ebtables" \
-d "iproute2" \
-d "libev4" \
-d "nftables" \
netns/vnoded=/usr/bin/ \
netns/vcmd=/usr/bin/ \
package/etc/core.conf=/etc/core/ \
package/etc/logging.conf=/etc/core/ \
package/examples=/opt/core/ \
daemon/dist/core-$(PACKAGE_VERSION)-py3-none-any.whl=/opt/core/
endef
.PHONY: fpm
fpm: clean-local-fpm
cd daemon && poetry build -f wheel
$(call fpm-deb)
$(call fpm-rpm)
$(call fpm-distributed-deb) $(call fpm-distributed-deb)
$(call fpm-distributed-rpm) $(call fpm-distributed-rpm)
@ -115,7 +176,6 @@ $(info creating file $1 from $1.in)
-e 's,[@]CORE_STATE_DIR[@],$(CORE_STATE_DIR),g' \ -e 's,[@]CORE_STATE_DIR[@],$(CORE_STATE_DIR),g' \
-e 's,[@]CORE_DATA_DIR[@],$(CORE_DATA_DIR),g' \ -e 's,[@]CORE_DATA_DIR[@],$(CORE_DATA_DIR),g' \
-e 's,[@]CORE_CONF_DIR[@],$(CORE_CONF_DIR),g' \ -e 's,[@]CORE_CONF_DIR[@],$(CORE_CONF_DIR),g' \
-e 's,[@]CORE_GUI_CONF_DIR[@],$(CORE_GUI_CONF_DIR),g' \
< $1.in > $1 < $1.in > $1
endef endef
@ -123,7 +183,6 @@ all: change-files
.PHONY: change-files .PHONY: change-files
change-files: change-files:
$(call change-files,gui/core-gui-legacy)
$(call change-files,daemon/core/constants.py) $(call change-files,daemon/core/constants.py)
$(call change-files,netns/setup.py) $(call change-files,netns/setup.py)

View file

@ -1,5 +1,4 @@
# CORE # CORE
CORE: Common Open Research Emulator CORE: Common Open Research Emulator
Copyright (c)2005-2022 the Boeing Company. Copyright (c)2005-2022 the Boeing Company.
@ -7,7 +6,6 @@ Copyright (c)2005-2022 the Boeing Company.
See the LICENSE file included in this distribution. See the LICENSE file included in this distribution.
## About ## About
The Common Open Research Emulator (CORE) is a tool for emulating The Common Open Research Emulator (CORE) is a tool for emulating
networks on one or more machines. You can connect these emulated networks on one or more machines. You can connect these emulated
networks to live networks. CORE consists of a GUI for drawing networks to live networks. CORE consists of a GUI for drawing
@ -15,12 +13,34 @@ topologies of lightweight virtual machines, and Python modules for
scripting network emulation. scripting network emulation.
## Quick Start ## Quick Start
Requires Python 3.9+. More detailed instructions and install options can be found
[here](https://coreemu.github.io/core/install.html).
The following should get you up and running on Ubuntu 18+ and CentOS 7+ ### Package Install
from a clean install, it will prompt you for sudo password. This would Grab the latest deb/rpm from [releases](https://github.com/coreemu/core/releases).
This will install vnoded/vcmd, system dependencies, and CORE within a python
virtual environment at `/opt/core/venv`.
```shell
sudo <yum/apt> install -y ./<package>
```
Then install OSPF MDR from source:
```shell
git clone https://github.com/USNavalResearchLaboratory/ospf-mdr.git
cd 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)
sudo make install
```
### Script Install
The following should get you up and running on Ubuntu 22.04. This would
install CORE into a python3 virtual environment and install install CORE into a python3 virtual environment and install
[OSPF MDR](https://github.com/USNavalResearchLaboratory/ospf-mdr) from source. [OSPF MDR](https://github.com/USNavalResearchLaboratory/ospf-mdr) from source.
For more detailed installation see [here](https://coreemu.github.io/core/install.html).
```shell ```shell
git clone https://github.com/coreemu/core.git git clone https://github.com/coreemu/core.git
@ -36,7 +56,6 @@ inv install -p /usr
``` ```
## Documentation & Support ## Documentation & Support
We are leveraging GitHub hosted documentation and Discord for persistent We are leveraging GitHub hosted documentation and Discord for persistent
chat rooms. This allows for more dynamic conversations and the chat rooms. This allows for more dynamic conversations and the
capability to respond faster. Feel free to join us at the link below. capability to respond faster. Feel free to join us at the link below.

View file

@ -1,9 +1,5 @@
#!/bin/sh #!/bin/sh
# #
# (c)2010-2012 the Boeing Company
#
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
#
# Bootstrap the autoconf system. # Bootstrap the autoconf system.
# #

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 8.2.0) AC_INIT(core, 9.0.0)
# autoconf and automake initialization # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) AC_CONFIG_SRCDIR([netns/version.h.in])
@ -30,25 +30,14 @@ AC_SUBST(CORE_CONF_DIR)
AC_SUBST(CORE_DATA_DIR) AC_SUBST(CORE_DATA_DIR)
AC_SUBST(CORE_STATE_DIR) AC_SUBST(CORE_STATE_DIR)
# CORE GUI configuration files and preferences in CORE_GUI_CONF_DIR # documentation option
# scenario files in ~/.core/configs/
AC_ARG_WITH([guiconfdir],
[AS_HELP_STRING([--with-guiconfdir=dir],
[specify GUI configuration directory])],
[CORE_GUI_CONF_DIR="$with_guiconfdir"],
[CORE_GUI_CONF_DIR="\$\${HOME}/.core"])
AC_SUBST(CORE_GUI_CONF_DIR)
AC_ARG_ENABLE([gui],
[AS_HELP_STRING([--enable-gui[=ARG]],
[build and install the GUI (default is yes)])],
[], [enable_gui=yes])
AC_SUBST(enable_gui)
AC_ARG_ENABLE([docs], AC_ARG_ENABLE([docs],
[AS_HELP_STRING([--enable-docs[=ARG]], [AS_HELP_STRING([--enable-docs[=ARG]],
[build python documentation (default is no)])], [build python documentation (default is no)])],
[], [enable_docs=no]) [], [enable_docs=no])
AC_SUBST(enable_docs) AC_SUBST(enable_docs)
# python option
AC_ARG_ENABLE([python], AC_ARG_ENABLE([python],
[AS_HELP_STRING([--enable-python[=ARG]], [AS_HELP_STRING([--enable-python[=ARG]],
[build and install the python bindings (default is yes)])], [build and install the python bindings (default is yes)])],
@ -94,28 +83,7 @@ if test "x$enable_daemon" = "xyes"; then
want_python=yes want_python=yes
want_linux_netns=yes want_linux_netns=yes
# Checks for libraries. AM_PATH_PYTHON(3.9)
AC_CHECK_LIB([netgraph], [NgMkSockNode])
# Checks for header files.
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h stdint.h stdlib.h string.h sys/ioctl.h sys/mount.h sys/socket.h sys/time.h termios.h unistd.h])
# Checks for typedefs, structures, and compiler characteristics.
AC_C_INLINE
AC_TYPE_INT32_T
AC_TYPE_PID_T
AC_TYPE_SIZE_T
AC_TYPE_SSIZE_T
AC_TYPE_UINT32_T
AC_TYPE_UINT8_T
# Checks for library functions.
AC_FUNC_FORK
AC_FUNC_MALLOC
AC_FUNC_REALLOC
AC_CHECK_FUNCS([atexit dup2 gettimeofday memset socket strerror uname])
AM_PATH_PYTHON(3.6)
AS_IF([$PYTHON -m grpc_tools.protoc -h &> /dev/null], [], [AC_MSG_ERROR([please install python grpcio-tools])]) AS_IF([$PYTHON -m grpc_tools.protoc -h &> /dev/null], [], [AC_MSG_ERROR([please install python grpcio-tools])])
AC_CHECK_PROG(sysctl_path, sysctl, $as_dir, no, $SEARCHPATH) AC_CHECK_PROG(sysctl_path, sysctl, $as_dir, no, $SEARCHPATH)
@ -171,6 +139,25 @@ fi
if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; then if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; then
want_linux_netns=yes want_linux_netns=yes
# Checks for header files.
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h stdint.h stdlib.h string.h sys/ioctl.h sys/mount.h sys/socket.h sys/time.h termios.h unistd.h])
# Checks for typedefs, structures, and compiler characteristics.
AC_C_INLINE
AC_TYPE_INT32_T
AC_TYPE_PID_T
AC_TYPE_SIZE_T
AC_TYPE_SSIZE_T
AC_TYPE_UINT32_T
AC_TYPE_UINT8_T
# Checks for library functions.
AC_FUNC_FORK
AC_FUNC_MALLOC
AC_FUNC_REALLOC
AC_CHECK_FUNCS([atexit dup2 gettimeofday memset socket strerror uname])
PKG_CHECK_MODULES(libev, libev, PKG_CHECK_MODULES(libev, libev,
AC_MSG_RESULT([found libev using pkgconfig OK]) AC_MSG_RESULT([found libev using pkgconfig OK])
AC_SUBST(libev_CFLAGS) AC_SUBST(libev_CFLAGS)
@ -209,7 +196,6 @@ if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then
fi fi
# Variable substitutions # Variable substitutions
AM_CONDITIONAL(WANT_GUI, test x$enable_gui = xyes)
AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes) AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes)
AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes) AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes)
AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes) AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes)
@ -224,9 +210,6 @@ fi
# Output files # Output files
AC_CONFIG_FILES([Makefile AC_CONFIG_FILES([Makefile
gui/version.tcl
gui/Makefile
gui/icons/Makefile
man/Makefile man/Makefile
docs/Makefile docs/Makefile
daemon/Makefile daemon/Makefile
@ -248,17 +231,12 @@ Build:
Prefix: ${prefix} Prefix: ${prefix}
Exec Prefix: ${exec_prefix} Exec Prefix: ${exec_prefix}
GUI:
GUI path: ${CORE_LIB_DIR}
GUI config: ${CORE_GUI_CONF_DIR}
Daemon: Daemon:
Daemon path: ${bindir} Daemon path: ${bindir}
Daemon config: ${CORE_CONF_DIR} Daemon config: ${CORE_CONF_DIR}
Python: ${PYTHON} Python: ${PYTHON}
Features to build: Features to build:
Build GUI: ${enable_gui}
Build Daemon: ${enable_daemon} Build Daemon: ${enable_daemon}
Documentation: ${want_docs} Documentation: ${want_docs}

View file

@ -1,8 +1,4 @@
# CORE # CORE
# (c)2010-2012 the Boeing Company.
# See the LICENSE file included in this distribution.
#
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
# #
# Makefile for building netns components. # Makefile for building netns components.
# #
@ -25,10 +21,7 @@ DISTCLEANFILES = Makefile.in
# files to include with distribution tarball # files to include with distribution tarball
EXTRA_DIST = core \ EXTRA_DIST = core \
data \
doc/conf.py.in \ doc/conf.py.in \
examples \
scripts \
tests \ tests \
setup.cfg \ setup.cfg \
poetry.lock \ poetry.lock \

View file

@ -14,9 +14,17 @@ import grpc
from core.api.grpc import core_pb2, core_pb2_grpc, emane_pb2, wrappers from core.api.grpc import core_pb2, core_pb2_grpc, emane_pb2, wrappers
from core.api.grpc.configservices_pb2 import ( from core.api.grpc.configservices_pb2 import (
GetConfigServiceDefaultsRequest, GetConfigServiceDefaultsRequest,
GetConfigServiceRenderedRequest,
GetNodeConfigServiceRequest, GetNodeConfigServiceRequest,
) )
from core.api.grpc.core_pb2 import ExecuteScriptRequest, GetConfigRequest from core.api.grpc.core_pb2 import (
ExecuteScriptRequest,
GetConfigRequest,
GetWirelessConfigRequest,
LinkedRequest,
WirelessConfigRequest,
WirelessLinkedRequest,
)
from core.api.grpc.emane_pb2 import ( from core.api.grpc.emane_pb2 import (
EmaneLinkRequest, EmaneLinkRequest,
GetEmaneEventChannelRequest, GetEmaneEventChannelRequest,
@ -43,17 +51,19 @@ from core.api.grpc.wlan_pb2 import (
WlanConfig, WlanConfig,
WlanLinkRequest, WlanLinkRequest,
) )
from core.api.grpc.wrappers import LinkOptions
from core.emulator.data import IpPrefixes from core.emulator.data import IpPrefixes
from core.errors import CoreError from core.errors import CoreError
from core.utils import SetQueue
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class MoveNodesStreamer: class MoveNodesStreamer:
def __init__(self, session_id: int = None, source: str = None) -> None: def __init__(self, session_id: int, source: str = None) -> None:
self.session_id = session_id self.session_id: int = session_id
self.source = source self.source: Optional[str] = source
self.queue: Queue = Queue() self.queue: SetQueue = SetQueue()
def send_position(self, node_id: int, x: float, y: float) -> None: def send_position(self, node_id: int, x: float, y: float) -> None:
position = wrappers.Position(x=x, y=y) position = wrappers.Position(x=x, y=y)
@ -563,23 +573,6 @@ class CoreGrpcClient:
response = self.stub.GetNodeTerminal(request) response = self.stub.GetNodeTerminal(request)
return response.terminal return response.terminal
def get_node_links(self, session_id: int, node_id: int) -> List[wrappers.Link]:
"""
Get current links for a node.
:param session_id: session id
:param node_id: node id
:return: list of links
:raises grpc.RpcError: when session or node doesn't exist
"""
request = core_pb2.GetNodeLinksRequest(session_id=session_id, node_id=node_id)
response = self.stub.GetNodeLinks(request)
links = []
for link_proto in response.links:
link = wrappers.Link.from_proto(link_proto)
links.append(link)
return links
def add_link( def add_link(
self, session_id: int, link: wrappers.Link, source: str = None self, session_id: int, link: wrappers.Link, source: str = None
) -> Tuple[bool, wrappers.Interface, wrappers.Interface]: ) -> Tuple[bool, wrappers.Interface, wrappers.Interface]:
@ -741,9 +734,9 @@ class CoreGrpcClient:
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
defaults = [] defaults = []
for node_type in service_defaults: for model in service_defaults:
services = service_defaults[node_type] services = service_defaults[model]
default = ServiceDefaults(node_type=node_type, services=services) default = ServiceDefaults(model=model, services=services)
defaults.append(default) defaults.append(default)
request = SetServiceDefaultsRequest(session_id=session_id, defaults=defaults) request = SetServiceDefaultsRequest(session_id=session_id, defaults=defaults)
response = self.stub.SetServiceDefaults(request) response = self.stub.SetServiceDefaults(request)
@ -987,6 +980,23 @@ class CoreGrpcClient:
response = self.stub.GetNodeConfigService(request) response = self.stub.GetNodeConfigService(request)
return dict(response.config) return dict(response.config)
def get_config_service_rendered(
self, session_id: int, node_id: int, name: str
) -> Dict[str, str]:
"""
Retrieve the rendered config service files for a node.
:param session_id: id of session
:param node_id: id of node
:param name: name of service
:return: dict mapping names of files to rendered data
"""
request = GetConfigServiceRenderedRequest(
session_id=session_id, node_id=node_id, name=name
)
response = self.stub.GetConfigServiceRendered(request)
return dict(response.rendered)
def get_emane_event_channel( def get_emane_event_channel(
self, session_id: int, nem_id: int self, session_id: int, nem_id: int
) -> wrappers.EmaneEventChannel: ) -> wrappers.EmaneEventChannel:
@ -1049,6 +1059,81 @@ class CoreGrpcClient:
""" """
self.stub.EmanePathlosses(streamer.iter()) self.stub.EmanePathlosses(streamer.iter())
def linked(
self,
session_id: int,
node1_id: int,
node2_id: int,
iface1_id: int,
iface2_id: int,
linked: bool,
) -> None:
"""
Link or unlink an existing core wired link.
:param session_id: session containing the link
:param node1_id: first node in link
:param node2_id: second node in link
:param iface1_id: node1 interface
:param iface2_id: node2 interface
:param linked: True to connect link, False to disconnect
:return: nothing
"""
request = LinkedRequest(
session_id=session_id,
node1_id=node1_id,
node2_id=node2_id,
iface1_id=iface1_id,
iface2_id=iface2_id,
linked=linked,
)
self.stub.Linked(request)
def wireless_linked(
self,
session_id: int,
wireless_id: int,
node1_id: int,
node2_id: int,
linked: bool,
) -> None:
request = WirelessLinkedRequest(
session_id=session_id,
wireless_id=wireless_id,
node1_id=node1_id,
node2_id=node2_id,
linked=linked,
)
self.stub.WirelessLinked(request)
def wireless_config(
self,
session_id: int,
wireless_id: int,
node1_id: int,
node2_id: int,
options1: LinkOptions,
options2: LinkOptions = None,
) -> None:
if options2 is None:
options2 = options1
request = WirelessConfigRequest(
session_id=session_id,
wireless_id=wireless_id,
node1_id=node1_id,
node2_id=node2_id,
options1=options1.to_proto(),
options2=options2.to_proto(),
)
self.stub.WirelessConfig(request)
def get_wireless_config(
self, session_id: int, node_id: int
) -> Dict[str, wrappers.ConfigOption]:
request = GetWirelessConfigRequest(session_id=session_id, node_id=node_id)
response = self.stub.GetWirelessConfig(request)
return wrappers.ConfigOption.from_dict(response.config)
def connect(self) -> None: def connect(self) -> None:
""" """
Open connection to server, must be closed manually. Open connection to server, must be closed manually.

View file

@ -3,7 +3,7 @@ from queue import Empty, Queue
from typing import Iterable, Optional from typing import Iterable, Optional
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.api.grpc.grpcutils import convert_link from core.api.grpc.grpcutils import convert_link_data
from core.emulator.data import ( from core.emulator.data import (
ConfigData, ConfigData,
EventData, EventData,
@ -33,7 +33,7 @@ def handle_node_event(node_data: NodeData) -> core_pb2.Event:
node_proto = core_pb2.Node( node_proto = core_pb2.Node(
id=node.id, id=node.id,
name=node.name, name=node.name,
model=node.type, model=node.model,
icon=node.icon, icon=node.icon,
position=position, position=position,
geo=geo, geo=geo,
@ -51,7 +51,7 @@ def handle_link_event(link_data: LinkData) -> core_pb2.Event:
:param link_data: link data :param link_data: link data
:return: link event that has message type and link information :return: link event that has message type and link information
""" """
link = convert_link(link_data) link = convert_link_data(link_data)
message_type = link_data.message_type.value message_type = link_data.message_type.value
link_event = core_pb2.LinkEvent(message_type=message_type, link=link) link_event = core_pb2.LinkEvent(message_type=message_type, link=link)
return core_pb2.Event(link_event=link_event, source=link_data.source) return core_pb2.Event(link_event=link_event, source=link_data.source)

View file

@ -1,7 +1,7 @@
import logging import logging
import time import time
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Tuple, Type, Union from typing import Any, Dict, List, Optional, Tuple, Type, Union
import grpc import grpc
from grpc import ServicerContext from grpc import ServicerContext
@ -17,17 +17,26 @@ from core.api.grpc.services_pb2 import (
ServiceDefaults, ServiceDefaults,
) )
from core.config import ConfigurableOptions from core.config import ConfigurableOptions
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet, EmaneOptions
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions from core.emulator.data import InterfaceData, LinkData, LinkOptions
from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.enumerations import LinkTypes, NodeTypes
from core.emulator.links import CoreLink
from core.emulator.session import Session from core.emulator.session import Session
from core.errors import CoreError from core.errors import CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.base import (
from core.nodes.docker import DockerNode CoreNode,
CoreNodeBase,
CoreNodeOptions,
NodeBase,
NodeOptions,
Position,
)
from core.nodes.docker import DockerNode, DockerOptions
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode from core.nodes.lxd import LxcNode
from core.nodes.network import CtrlNet, PtpNet, WlanNode from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode
from core.nodes.wireless import WirelessNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -53,34 +62,33 @@ class CpuUsage:
return (total_diff - idle_diff) / total_diff return (total_diff - idle_diff) / total_diff
def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOptions]: def add_node_data(
_class: Type[NodeBase], node_proto: core_pb2.Node
) -> Tuple[Position, NodeOptions]:
""" """
Convert node protobuf message to data for creating a node. Convert node protobuf message to data for creating a node.
:param _class: node class to create options from
:param node_proto: node proto message :param node_proto: node proto message
:return: node type, id, and options :return: node type, id, and options
""" """
_id = node_proto.id options = _class.create_options()
_type = NodeTypes(node_proto.type) options.icon = node_proto.icon
options = NodeOptions( options.canvas = node_proto.canvas
name=node_proto.name, if isinstance(options, CoreNodeOptions):
model=node_proto.model, options.model = node_proto.model
icon=node_proto.icon, options.services = node_proto.services
image=node_proto.image, options.config_services = node_proto.config_services
services=node_proto.services, if isinstance(options, EmaneOptions):
config_services=node_proto.config_services, options.emane_model = node_proto.emane
canvas=node_proto.canvas, if isinstance(options, DockerOptions):
) options.image = node_proto.image
if node_proto.emane: position = Position()
options.emane = node_proto.emane position.set(node_proto.position.x, node_proto.position.y)
if node_proto.server:
options.server = node_proto.server
position = node_proto.position
options.set_position(position.x, position.y)
if node_proto.HasField("geo"): if node_proto.HasField("geo"):
geo = node_proto.geo geo = node_proto.geo
options.set_location(geo.lat, geo.lon, geo.alt) position.set_geo(geo.lon, geo.lat, geo.alt)
return _type, _id, options return position, options
def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData: def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
@ -110,7 +118,7 @@ def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
def add_link_data( def add_link_data(
link_proto: core_pb2.Link link_proto: core_pb2.Link
) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]: ) -> Tuple[InterfaceData, InterfaceData, LinkOptions]:
""" """
Convert link proto to link interfaces and options data. Convert link proto to link interfaces and options data.
@ -119,7 +127,6 @@ def add_link_data(
""" """
iface1_data = link_iface(link_proto.iface1) iface1_data = link_iface(link_proto.iface1)
iface2_data = link_iface(link_proto.iface2) iface2_data = link_iface(link_proto.iface2)
link_type = LinkTypes(link_proto.type)
options = LinkOptions() options = LinkOptions()
options_proto = link_proto.options options_proto = link_proto.options
if options_proto: if options_proto:
@ -134,7 +141,7 @@ def add_link_data(
options.buffer = options_proto.buffer options.buffer = options_proto.buffer
options.unidirectional = options_proto.unidirectional options.unidirectional = options_proto.unidirectional
options.key = options_proto.key options.key = options_proto.key
return iface1_data, iface2_data, options, link_type return iface1_data, iface2_data, options
def create_nodes( def create_nodes(
@ -149,9 +156,17 @@ def create_nodes(
""" """
funcs = [] funcs = []
for node_proto in node_protos: for node_proto in node_protos:
_type, _id, options = add_node_data(node_proto) _type = NodeTypes(node_proto.type)
_class = session.get_node_class(_type) _class = session.get_node_class(_type)
args = (_class, _id, options) position, options = add_node_data(_class, node_proto)
args = (
_class,
node_proto.id or None,
node_proto.name or None,
node_proto.server or None,
position,
options,
)
funcs.append((session.add_node, args, {})) funcs.append((session.add_node, args, {}))
start = time.monotonic() start = time.monotonic()
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
@ -174,8 +189,8 @@ def create_links(
for link_proto in link_protos: for link_proto in link_protos:
node1_id = link_proto.node1_id node1_id = link_proto.node1_id
node2_id = link_proto.node2_id node2_id = link_proto.node2_id
iface1, iface2, options, link_type = add_link_data(link_proto) iface1, iface2, options = add_link_data(link_proto)
args = (node1_id, node2_id, iface1, iface2, options, link_type) args = (node1_id, node2_id, iface1, iface2, options)
funcs.append((session.add_link, args, {})) funcs.append((session.add_link, args, {}))
start = time.monotonic() start = time.monotonic()
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
@ -198,8 +213,8 @@ def edit_links(
for link_proto in link_protos: for link_proto in link_protos:
node1_id = link_proto.node1_id node1_id = link_proto.node1_id
node2_id = link_proto.node2_id node2_id = link_proto.node2_id
iface1, iface2, options, link_type = add_link_data(link_proto) iface1, iface2, options = add_link_data(link_proto)
args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type) args = (node1_id, node2_id, iface1.id, iface2.id, options)
funcs.append((session.update_link, args, {})) funcs.append((session.update_link, args, {}))
start = time.monotonic() start = time.monotonic()
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
@ -220,6 +235,22 @@ def convert_value(value: Any) -> str:
return value return value
def convert_session_options(session: Session) -> Dict[str, common_pb2.ConfigOption]:
config_options = {}
for option in session.options.options:
value = session.options.get(option.id)
config_option = common_pb2.ConfigOption(
label=option.label,
name=option.id,
value=value,
type=option.type.value,
select=option.options,
group="Options",
)
config_options[option.id] = config_option
return config_options
def get_config_options( def get_config_options(
config: Dict[str, str], config: Dict[str, str],
configurable_options: Union[ConfigurableOptions, Type[ConfigurableOptions]], configurable_options: Union[ConfigurableOptions, Type[ConfigurableOptions]],
@ -270,7 +301,6 @@ def get_node_proto(
lat=node.position.lat, lon=node.position.lon, alt=node.position.alt lat=node.position.lat, lon=node.position.lon, alt=node.position.alt
) )
services = [x.name for x in node.services] services = [x.name for x in node.services]
model = node.type
node_dir = None node_dir = None
config_services = [] config_services = []
if isinstance(node, CoreNodeBase): if isinstance(node, CoreNodeBase):
@ -281,7 +311,7 @@ def get_node_proto(
channel = str(node.ctrlchnlname) channel = str(node.ctrlchnlname)
emane_model = None emane_model = None
if isinstance(node, EmaneNet): if isinstance(node, EmaneNet):
emane_model = node.model.name emane_model = node.wireless_model.name
image = None image = None
if isinstance(node, (DockerNode, LxcNode)): if isinstance(node, (DockerNode, LxcNode)):
image = node.image image = node.image
@ -291,6 +321,21 @@ def get_node_proto(
) )
if wlan_config: if wlan_config:
wlan_config = get_config_options(wlan_config, BasicRangeModel) wlan_config = get_config_options(wlan_config, BasicRangeModel)
# check for wireless config
wireless_config = None
if isinstance(node, WirelessNode):
configs = node.get_config()
wireless_config = {}
for config in configs.values():
config_option = common_pb2.ConfigOption(
label=config.label,
name=config.id,
value=config.default,
type=config.type.value,
select=config.options,
group=config.group,
)
wireless_config[config.id] = config_option
# check for mobility config # check for mobility config
mobility_config = session.mobility.get_configs( mobility_config = session.mobility.get_configs(
node.id, config_type=Ns2ScriptedMobility.name node.id, config_type=Ns2ScriptedMobility.name
@ -325,7 +370,7 @@ def get_node_proto(
id=node.id, id=node.id,
name=node.name, name=node.name,
emane=emane_model, emane=emane_model,
model=model, model=node.model,
type=node_type.value, type=node_type.value,
position=position, position=position,
geo=geo, geo=geo,
@ -337,6 +382,7 @@ def get_node_proto(
channel=channel, channel=channel,
canvas=node.canvas, canvas=node.canvas,
wlan_config=wlan_config, wlan_config=wlan_config,
wireless_config=wireless_config,
mobility_config=mobility_config, mobility_config=mobility_config,
service_configs=service_configs, service_configs=service_configs,
config_service_configs=config_service_configs, config_service_configs=config_service_configs,
@ -344,61 +390,84 @@ def get_node_proto(
) )
def get_links(node: NodeBase): def get_links(session: Session, node: NodeBase) -> List[core_pb2.Link]:
""" """
Retrieve a list of links for grpc to use. Retrieve a list of links for grpc to use.
:param session: session to get links for node
:param node: node to get links from :param node: node to get links from
:return: protobuf links :return: protobuf links
""" """
link_protos = []
for core_link in session.link_manager.node_links(node):
link_protos.extend(convert_core_link(core_link))
if isinstance(node, (WlanNode, EmaneNet)):
for link_data in node.links():
link_protos.append(convert_link_data(link_data))
return link_protos
def convert_iface(iface: CoreInterface) -> core_pb2.Interface:
"""
Convert interface to protobuf.
:param iface: interface to convert
:return: protobuf interface
"""
if isinstance(iface.node, CoreNetwork):
return core_pb2.Interface(id=iface.id)
else:
ip4 = iface.get_ip4()
ip4_mask = ip4.prefixlen if ip4 else None
ip4 = str(ip4.ip) if ip4 else None
ip6 = iface.get_ip6()
ip6_mask = ip6.prefixlen if ip6 else None
ip6 = str(ip6.ip) if ip6 else None
mac = str(iface.mac) if iface.mac else None
return core_pb2.Interface(
id=iface.id,
name=iface.name,
mac=mac,
ip4=ip4,
ip4_mask=ip4_mask,
ip6=ip6,
ip6_mask=ip6_mask,
)
def convert_core_link(core_link: CoreLink) -> List[core_pb2.Link]:
"""
Convert core link to protobuf data.
:param core_link: core link to convert
:return: protobuf link data
"""
links = [] links = []
for link in node.links(): node1, iface1 = core_link.node1, core_link.iface1
link_proto = convert_link(link) node2, iface2 = core_link.node2, core_link.iface2
links.append(link_proto) unidirectional = core_link.is_unidirectional()
link = convert_link(node1, iface1, node2, iface2, iface1.options, unidirectional)
links.append(link)
if unidirectional:
link = convert_link(
node2, iface2, node1, iface1, iface2.options, unidirectional
)
links.append(link)
return links return links
def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface: def convert_link_data(link_data: LinkData) -> core_pb2.Link:
return core_pb2.Interface(
id=iface_data.id,
name=iface_data.name,
mac=iface_data.mac,
ip4=iface_data.ip4,
ip4_mask=iface_data.ip4_mask,
ip6=iface_data.ip6,
ip6_mask=iface_data.ip6_mask,
)
def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions:
return core_pb2.LinkOptions(
jitter=options_data.jitter,
key=options_data.key,
mburst=options_data.mburst,
mer=options_data.mer,
loss=options_data.loss,
bandwidth=options_data.bandwidth,
burst=options_data.burst,
delay=options_data.delay,
dup=options_data.dup,
buffer=options_data.buffer,
unidirectional=options_data.unidirectional,
)
def convert_link(link_data: LinkData) -> core_pb2.Link:
""" """
Convert link_data into core protobuf link. Convert link_data into core protobuf link.
:param link_data: link to convert :param link_data: link to convert
:return: core protobuf Link :return: core protobuf Link
""" """
iface1 = None iface1 = None
if link_data.iface1 is not None: if link_data.iface1 is not None:
iface1 = convert_iface(link_data.iface1) iface1 = convert_iface_data(link_data.iface1)
iface2 = None iface2 = None
if link_data.iface2 is not None: if link_data.iface2 is not None:
iface2 = convert_iface(link_data.iface2) iface2 = convert_iface_data(link_data.iface2)
options = convert_link_options(link_data.options) options = convert_link_options(link_data.options)
return core_pb2.Link( return core_pb2.Link(
type=link_data.type.value, type=link_data.type.value,
@ -413,6 +482,123 @@ def convert_link(link_data: LinkData) -> core_pb2.Link:
) )
def convert_iface_data(iface_data: InterfaceData) -> core_pb2.Interface:
"""
Convert interface data to protobuf.
:param iface_data: interface data to convert
:return: interface protobuf
"""
return core_pb2.Interface(
id=iface_data.id,
name=iface_data.name,
mac=iface_data.mac,
ip4=iface_data.ip4,
ip4_mask=iface_data.ip4_mask,
ip6=iface_data.ip6,
ip6_mask=iface_data.ip6_mask,
)
def convert_link_options(options: LinkOptions) -> core_pb2.LinkOptions:
"""
Convert link options to protobuf.
:param options: link options to convert
:return: link options protobuf
"""
return core_pb2.LinkOptions(
jitter=options.jitter,
key=options.key,
mburst=options.mburst,
mer=options.mer,
loss=options.loss,
bandwidth=options.bandwidth,
burst=options.burst,
delay=options.delay,
dup=options.dup,
buffer=options.buffer,
unidirectional=options.unidirectional,
)
def convert_options_proto(options: core_pb2.LinkOptions) -> LinkOptions:
return LinkOptions(
delay=options.delay,
bandwidth=options.bandwidth,
loss=options.loss,
dup=options.dup,
jitter=options.jitter,
mer=options.mer,
burst=options.burst,
mburst=options.mburst,
buffer=options.buffer,
unidirectional=options.unidirectional,
key=options.key,
)
def convert_link(
node1: NodeBase,
iface1: Optional[CoreInterface],
node2: NodeBase,
iface2: Optional[CoreInterface],
options: LinkOptions,
unidirectional: bool,
) -> core_pb2.Link:
"""
Convert link objects to link protobuf.
:param node1: first node in link
:param iface1: node1 interface
:param node2: second node in link
:param iface2: node2 interface
:param options: link options
:param unidirectional: if this link is considered unidirectional
:return: protobuf link
"""
if iface1 is not None:
iface1 = convert_iface(iface1)
if iface2 is not None:
iface2 = convert_iface(iface2)
is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet))
is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet))
if not (is_node1_wireless or is_node2_wireless):
options = convert_link_options(options)
options.unidirectional = unidirectional
else:
options = None
return core_pb2.Link(
type=LinkTypes.WIRED.value,
node1_id=node1.id,
node2_id=node2.id,
iface1=iface1,
iface2=iface2,
options=options,
network_id=None,
label=None,
color=None,
)
def parse_proc_net_dev(lines: List[str]) -> Dict[str, Any]:
"""
Parse lines of output from /proc/net/dev.
:param lines: lines of /proc/net/dev
:return: parsed device to tx/rx values
"""
stats = {}
for line in lines[2:]:
line = line.strip()
if not line:
continue
line = line.split()
line[0] = line[0].strip(":")
stats[line[0]] = {"rx": float(line[1]), "tx": float(line[9])}
return stats
def get_net_stats() -> Dict[str, Dict]: def get_net_stats() -> Dict[str, Dict]:
""" """
Retrieve status about the current interfaces in the system Retrieve status about the current interfaces in the system
@ -420,18 +606,8 @@ def get_net_stats() -> Dict[str, Dict]:
:return: send and receive status of the interfaces in the system :return: send and receive status of the interfaces in the system
""" """
with open("/proc/net/dev", "r") as f: with open("/proc/net/dev", "r") as f:
data = f.readlines()[2:] lines = f.readlines()[2:]
return parse_proc_net_dev(lines)
stats = {}
for line in data:
line = line.strip()
if not line:
continue
line = line.split()
line[0] = line[0].strip(":")
stats[line[0]] = {"rx": float(line[1]), "tx": float(line[9])}
return stats
def session_location(session: Session, location: core_pb2.SessionLocation) -> None: def session_location(session: Session, location: core_pb2.SessionLocation) -> None:
@ -490,39 +666,14 @@ def get_service_configuration(service: CoreService) -> NodeServiceData:
) )
def iface_to_data(iface: CoreInterface) -> InterfaceData: def iface_to_proto(session: Session, iface: CoreInterface) -> core_pb2.Interface:
ip4 = iface.get_ip4()
ip4_addr = str(ip4.ip) if ip4 else None
ip4_mask = ip4.prefixlen if ip4 else None
ip6 = iface.get_ip6()
ip6_addr = str(ip6.ip) if ip6 else None
ip6_mask = ip6.prefixlen if ip6 else None
return InterfaceData(
id=iface.node_id,
name=iface.name,
mac=str(iface.mac),
ip4=ip4_addr,
ip4_mask=ip4_mask,
ip6=ip6_addr,
ip6_mask=ip6_mask,
)
def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
""" """
Convenience for converting a core interface to the protobuf representation. Convenience for converting a core interface to the protobuf representation.
:param node_id: id of node to convert interface for :param session: session interface belongs to
:param iface: interface to convert :param iface: interface to convert
:return: interface proto :return: interface proto
""" """
if iface.node and iface.node.id == node_id:
_id = iface.node_id
else:
_id = iface.net_id
net_id = iface.net.id if iface.net else None
node_id = iface.node.id if iface.node else None
net2_id = iface.othernet.id if iface.othernet else None
ip4_net = iface.get_ip4() ip4_net = iface.get_ip4()
ip4 = str(ip4_net.ip) if ip4_net else None ip4 = str(ip4_net.ip) if ip4_net else None
ip4_mask = ip4_net.prefixlen if ip4_net else None ip4_mask = ip4_net.prefixlen if ip4_net else None
@ -530,11 +681,13 @@ def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
ip6 = str(ip6_net.ip) if ip6_net else None ip6 = str(ip6_net.ip) if ip6_net else None
ip6_mask = ip6_net.prefixlen if ip6_net else None ip6_mask = ip6_net.prefixlen if ip6_net else None
mac = str(iface.mac) if iface.mac else None mac = str(iface.mac) if iface.mac else None
nem_id = None
nem_port = None
if isinstance(iface.net, EmaneNet):
nem_id = session.emane.get_nem_id(iface)
nem_port = session.emane.get_nem_port(iface)
return core_pb2.Interface( return core_pb2.Interface(
id=_id, id=iface.id,
net_id=net_id,
net2_id=net2_id,
node_id=node_id,
name=iface.name, name=iface.name,
mac=mac, mac=mac,
mtu=iface.mtu, mtu=iface.mtu,
@ -543,6 +696,8 @@ def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
ip4_mask=ip4_mask, ip4_mask=ip4_mask,
ip6=ip6, ip6=ip6,
ip6_mask=ip6_mask, ip6_mask=ip6_mask,
nem_id=nem_id,
nem_port=nem_port,
) )
@ -574,6 +729,12 @@ def get_nem_id(
def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]: def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]:
"""
Get emane model configuration protobuf data.
:param session: session to get emane model configuration for
:return: dict of emane model protobuf configurations
"""
configs = {} configs = {}
for _id, model_configs in session.emane.node_configs.items(): for _id, model_configs in session.emane.node_configs.items():
for model_name in model_configs: for model_name in model_configs:
@ -591,6 +752,12 @@ def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneCo
def get_hooks(session: Session) -> List[core_pb2.Hook]: def get_hooks(session: Session) -> List[core_pb2.Hook]:
"""
Retrieve hook protobuf data for a session.
:param session: session to get hooks for
:return: list of hook protobufs
"""
hooks = [] hooks = []
for state in session.hooks: for state in session.hooks:
state_hooks = session.hooks[state] state_hooks = session.hooks[state]
@ -601,9 +768,15 @@ def get_hooks(session: Session) -> List[core_pb2.Hook]:
def get_default_services(session: Session) -> List[ServiceDefaults]: def get_default_services(session: Session) -> List[ServiceDefaults]:
"""
Retrieve the default service sets for a given session.
:param session: session to get default service sets for
:return: list of default service sets
"""
default_services = [] default_services = []
for name, services in session.services.default_services.items(): for model, services in session.services.default_services.items():
default_service = ServiceDefaults(node_type=name, services=services) default_service = ServiceDefaults(model=model, services=services)
default_services.append(default_service) default_services.append(default_service)
return default_services return default_services
@ -611,6 +784,14 @@ def get_default_services(session: Session) -> List[ServiceDefaults]:
def get_mobility_node( def get_mobility_node(
session: Session, node_id: int, context: ServicerContext session: Session, node_id: int, context: ServicerContext
) -> Union[WlanNode, EmaneNet]: ) -> Union[WlanNode, EmaneNet]:
"""
Get mobility node.
:param session: session to get node from
:param node_id: id of node to get
:param context: grpc context
:return: wlan or emane node
"""
try: try:
return session.get_node(node_id, WlanNode) return session.get_node(node_id, WlanNode)
except CoreError: except CoreError:
@ -621,17 +802,26 @@ def get_mobility_node(
def convert_session(session: Session) -> wrappers.Session: def convert_session(session: Session) -> wrappers.Session:
links = [] """
nodes = [] Convert session to its wrapped version.
:param session: session to convert
:return: wrapped session data
"""
emane_configs = get_emane_model_configs_dict(session) emane_configs = get_emane_model_configs_dict(session)
nodes = []
links = []
for _id in session.nodes: for _id in session.nodes:
node = session.nodes[_id] node = session.nodes[_id]
if not isinstance(node, (PtpNet, CtrlNet)): if not isinstance(node, (PtpNet, CtrlNet)):
node_emane_configs = emane_configs.get(node.id, []) node_emane_configs = emane_configs.get(node.id, [])
node_proto = get_node_proto(session, node, node_emane_configs) node_proto = get_node_proto(session, node, node_emane_configs)
nodes.append(node_proto) nodes.append(node_proto)
node_links = get_links(node) if isinstance(node, (WlanNode, EmaneNet)):
links.extend(node_links) for link_data in node.links():
links.append(convert_link_data(link_data))
for core_link in session.link_manager.links():
links.extend(convert_core_link(core_link))
default_services = get_default_services(session) default_services = get_default_services(session)
x, y, z = session.location.refxyz x, y, z = session.location.refxyz
lat, lon, alt = session.location.refgeo lat, lon, alt = session.location.refgeo
@ -640,7 +830,7 @@ def convert_session(session: Session) -> wrappers.Session:
) )
hooks = get_hooks(session) hooks = get_hooks(session)
session_file = str(session.file_path) if session.file_path else None session_file = str(session.file_path) if session.file_path else None
options = get_config_options(session.options.get_configs(), session.options) options = convert_session_options(session)
servers = [ servers = [
core_pb2.Server(name=x.name, host=x.host) core_pb2.Server(name=x.name, host=x.host)
for x in session.distributed.servers.values() for x in session.distributed.servers.values()
@ -665,6 +855,15 @@ def convert_session(session: Session) -> wrappers.Session:
def configure_node( def configure_node(
session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext
) -> None: ) -> None:
"""
Configure a node using all provided protobuf data.
:param session: session for node
:param node: node protobuf data
:param core_node: session node
:param context: grpc context
:return: nothing
"""
for emane_config in node.emane_configs: for emane_config in node.emane_configs:
_id = utils.iface_config_id(node.id, emane_config.iface_id) _id = utils.iface_config_id(node.id, emane_config.iface_id)
config = {k: v.value for k, v in emane_config.config.items()} config = {k: v.value for k, v in emane_config.config.items()}
@ -675,6 +874,9 @@ def configure_node(
if node.mobility_config: if node.mobility_config:
config = {k: v.value for k, v in node.mobility_config.items()} config = {k: v.value for k, v in node.mobility_config.items()}
session.mobility.set_model_config(node.id, Ns2ScriptedMobility.name, config) session.mobility.set_model_config(node.id, Ns2ScriptedMobility.name, config)
if isinstance(core_node, WirelessNode) and node.wireless_config:
config = {k: v.value for k, v in node.wireless_config.items()}
core_node.set_config(config)
for service_name, service_config in node.service_configs.items(): for service_name, service_config in node.service_configs.items():
data = service_config.data data = service_config.data
config = ServiceConfig( config = ServiceConfig(

View file

@ -1,7 +1,8 @@
import atexit
import logging import logging
import os import os
import re import re
import signal
import sys
import tempfile import tempfile
import time import time
from concurrent import futures from concurrent import futures
@ -23,10 +24,22 @@ from core.api.grpc.configservices_pb2 import (
ConfigService, ConfigService,
GetConfigServiceDefaultsRequest, GetConfigServiceDefaultsRequest,
GetConfigServiceDefaultsResponse, GetConfigServiceDefaultsResponse,
GetConfigServiceRenderedRequest,
GetConfigServiceRenderedResponse,
GetNodeConfigServiceRequest, GetNodeConfigServiceRequest,
GetNodeConfigServiceResponse, GetNodeConfigServiceResponse,
) )
from core.api.grpc.core_pb2 import ExecuteScriptResponse from core.api.grpc.core_pb2 import (
ExecuteScriptResponse,
GetWirelessConfigRequest,
GetWirelessConfigResponse,
LinkedRequest,
LinkedResponse,
WirelessConfigRequest,
WirelessConfigResponse,
WirelessLinkedRequest,
WirelessLinkedResponse,
)
from core.api.grpc.emane_pb2 import ( from core.api.grpc.emane_pb2 import (
EmaneLinkRequest, EmaneLinkRequest,
EmaneLinkResponse, EmaneLinkResponse,
@ -79,19 +92,20 @@ from core.emulator.data import InterfaceData, LinkData, LinkOptions
from core.emulator.enumerations import ( from core.emulator.enumerations import (
EventTypes, EventTypes,
ExceptionLevels, ExceptionLevels,
LinkTypes,
MessageFlags, MessageFlags,
NodeTypes,
) )
from core.emulator.session import NT, Session from core.emulator.session import NT, Session
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode, NodeBase from core.nodes.base import CoreNode, NodeBase
from core.nodes.network import WlanNode from core.nodes.network import CoreNetwork, WlanNode
from core.nodes.wireless import WirelessNode
from core.services.coreservices import ServiceManager from core.services.coreservices import ServiceManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ONE_DAY_IN_SECONDS: int = 60 * 60 * 24 _ONE_DAY_IN_SECONDS: int = 60 * 60 * 24
_INTERFACE_REGEX: Pattern = re.compile(r"veth(?P<node>[0-9a-fA-F]+)") _INTERFACE_REGEX: Pattern = re.compile(r"beth(?P<node>[0-9a-fA-F]+)")
_MAX_WORKERS = 1000 _MAX_WORKERS = 1000
@ -107,11 +121,20 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
self.coreemu: CoreEmu = coreemu self.coreemu: CoreEmu = coreemu
self.running: bool = True self.running: bool = True
self.server: Optional[grpc.Server] = None self.server: Optional[grpc.Server] = None
atexit.register(self._exit_handler) # catch signals
signal.signal(signal.SIGHUP, self._signal_handler)
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
signal.signal(signal.SIGUSR1, self._signal_handler)
signal.signal(signal.SIGUSR2, self._signal_handler)
def _exit_handler(self) -> None: def _signal_handler(self, signal_number: int, _) -> None:
logger.debug("catching exit, stop running") logger.info("caught signal: %s", signal_number)
self.coreemu.shutdown()
self.running = False self.running = False
if self.server:
self.server.stop(None)
sys.exit(signal_number)
def _is_running(self, context) -> bool: def _is_running(self, context) -> bool:
return self.running and context.is_active() return self.running and context.is_active()
@ -248,18 +271,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
# clear previous state and setup for creation # clear previous state and setup for creation
session.clear() session.clear()
session.directory.mkdir(exist_ok=True)
if request.definition: if request.definition:
state = EventTypes.DEFINITION_STATE state = EventTypes.DEFINITION_STATE
else: else:
state = EventTypes.CONFIGURATION_STATE state = EventTypes.CONFIGURATION_STATE
session.directory.mkdir(exist_ok=True)
session.set_state(state) session.set_state(state)
session.user = request.session.user if request.session.user:
session.set_user(request.session.user)
# session options # session options
session.options.config_reset()
for option in request.session.options.values(): for option in request.session.options.values():
session.options.set_config(option.name, option.value) session.options.set(option.name, option.value)
session.metadata = dict(request.session.metadata) session.metadata = dict(request.session.metadata)
# add servers # add servers
@ -378,11 +401,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
self, request: core_pb2.GetSessionsRequest, context: ServicerContext self, request: core_pb2.GetSessionsRequest, context: ServicerContext
) -> core_pb2.GetSessionsResponse: ) -> core_pb2.GetSessionsResponse:
""" """
Delete the session Get all currently known session overviews.
:param request: get-session request :param request: get sessions request
:param context: context object :param context: context object
:return: a delete-session response :return: a get sessions response
""" """
logger.debug("get sessions: %s", request) logger.debug("get sessions: %s", request)
sessions = [] sessions = []
@ -469,7 +492,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
while self._is_running(context): while self._is_running(context):
now = time.monotonic() now = time.monotonic()
stats = get_net_stats() stats = get_net_stats()
# calculate average # calculate average
if last_check is not None: if last_check is not None:
interval = now - last_check interval = now - last_check
@ -486,7 +508,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
(current_rxtx["tx"] - previous_rxtx["tx"]) * 8.0 / interval (current_rxtx["tx"] - previous_rxtx["tx"]) * 8.0 / interval
) )
throughput = rx_kbps + tx_kbps throughput = rx_kbps + tx_kbps
if key.startswith("veth"): if key.startswith("beth"):
key = key.split(".") key = key.split(".")
node_id = _INTERFACE_REGEX.search(key[0]).group("node") node_id = _INTERFACE_REGEX.search(key[0]).group("node")
node_id = int(node_id, base=16) node_id = int(node_id, base=16)
@ -512,7 +534,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
bridge_throughput.throughput = throughput bridge_throughput.throughput = throughput
except ValueError: except ValueError:
pass pass
yield throughputs_event yield throughputs_event
last_check = now last_check = now
@ -540,9 +561,17 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logger.debug("add node: %s", request) logger.debug("add node: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
_type, _id, options = grpcutils.add_node_data(request.node) _type = NodeTypes(request.node.type)
_class = session.get_node_class(_type) _class = session.get_node_class(_type)
node = session.add_node(_class, _id, options) position, options = grpcutils.add_node_data(_class, request.node)
node = session.add_node(
_class,
request.node.id or None,
request.node.name or None,
request.node.server or None,
position,
options,
)
grpcutils.configure_node(session, request.node, node, context) grpcutils.configure_node(session, request.node, node, context)
source = request.source if request.source else None source = request.source if request.source else None
session.broadcast_node(node, MessageFlags.ADD, source) session.broadcast_node(node, MessageFlags.ADD, source)
@ -564,12 +593,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
ifaces = [] ifaces = []
for iface_id in node.ifaces: for iface_id in node.ifaces:
iface = node.ifaces[iface_id] iface = node.ifaces[iface_id]
iface_proto = grpcutils.iface_to_proto(request.node_id, iface) iface_proto = grpcutils.iface_to_proto(session, iface)
ifaces.append(iface_proto) ifaces.append(iface_proto)
emane_configs = grpcutils.get_emane_model_configs_dict(session) emane_configs = grpcutils.get_emane_model_configs_dict(session)
node_emane_configs = emane_configs.get(node.id, []) node_emane_configs = emane_configs.get(node.id, [])
node_proto = grpcutils.get_node_proto(session, node, node_emane_configs) node_proto = grpcutils.get_node_proto(session, node, node_emane_configs)
links = get_links(node) links = get_links(session, node)
return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces, links=links) return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces, links=links)
def MoveNode( def MoveNode(
@ -705,18 +734,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
node2_id = request.link.node2_id node2_id = request.link.node2_id
self.get_node(session, node1_id, context, NodeBase) self.get_node(session, node1_id, context, NodeBase)
self.get_node(session, node2_id, context, NodeBase) self.get_node(session, node2_id, context, NodeBase)
iface1_data, iface2_data, options, link_type = grpcutils.add_link_data( iface1_data, iface2_data, options = grpcutils.add_link_data(request.link)
request.link
)
node1_iface, node2_iface = session.add_link( node1_iface, node2_iface = session.add_link(
node1_id, node2_id, iface1_data, iface2_data, options, link_type node1_id, node2_id, iface1_data, iface2_data, options
) )
iface1_data = None iface1_data = None
if node1_iface: if node1_iface:
iface1_data = grpcutils.iface_to_data(node1_iface) if isinstance(node1_iface.node, CoreNetwork):
iface1_data = InterfaceData(id=node1_iface.id)
else:
iface1_data = node1_iface.get_data()
iface2_data = None iface2_data = None
if node2_iface: if node2_iface:
iface2_data = grpcutils.iface_to_data(node2_iface) if isinstance(node2_iface.node, CoreNetwork):
iface2_data = InterfaceData(id=node2_iface.id)
else:
iface2_data = node2_iface.get_data()
source = request.source if request.source else None source = request.source if request.source else None
link_data = LinkData( link_data = LinkData(
message_type=MessageFlags.ADD, message_type=MessageFlags.ADD,
@ -731,9 +764,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
iface1_proto = None iface1_proto = None
iface2_proto = None iface2_proto = None
if node1_iface: if node1_iface:
iface1_proto = grpcutils.iface_to_proto(node1_id, node1_iface) iface1_proto = grpcutils.iface_to_proto(session, node1_iface)
if node2_iface: if node2_iface:
iface2_proto = grpcutils.iface_to_proto(node2_id, node2_iface) iface2_proto = grpcutils.iface_to_proto(session, node2_iface)
return core_pb2.AddLinkResponse( return core_pb2.AddLinkResponse(
result=True, iface1=iface1_proto, iface2=iface2_proto result=True, iface1=iface1_proto, iface2=iface2_proto
) )
@ -912,7 +945,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
session.services.default_services.clear() session.services.default_services.clear()
for service_defaults in request.defaults: for service_defaults in request.defaults:
session.services.default_services[ session.services.default_services[
service_defaults.node_type service_defaults.model
] = service_defaults.services ] = service_defaults.services
return SetServiceDefaultsResponse(result=True) return SetServiceDefaultsResponse(result=True)
@ -1163,7 +1196,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
self, request: core_pb2.GetInterfacesRequest, context: ServicerContext self, request: core_pb2.GetInterfacesRequest, context: ServicerContext
) -> core_pb2.GetInterfacesResponse: ) -> core_pb2.GetInterfacesResponse:
""" """
Retrieve all the interfaces of the system including bridges, virtual ethernet, and loopback Retrieve all the interfaces of the system including bridges, virtual ethernet,
and loopback.
:param request: get-interfaces request :param request: get-interfaces request
:param context: context object :param context: context object
@ -1188,32 +1222,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logger.debug("emane link: %s", request) logger.debug("emane link: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
nem1 = request.nem1 flag = MessageFlags.ADD if request.linked else MessageFlags.DELETE
iface1 = session.emane.get_iface(nem1) link = session.emane.get_nem_link(request.nem1, request.nem2, flag)
if not iface1: if link:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found")
node1 = iface1.node
nem2 = request.nem2
iface2 = session.emane.get_iface(nem2)
if not iface2:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found")
node2 = iface2.node
if iface1.net == iface2.net:
if request.linked:
flag = MessageFlags.ADD
else:
flag = MessageFlags.DELETE
color = session.get_link_color(iface1.net.id)
link = LinkData(
message_type=flag,
type=LinkTypes.WIRELESS,
node1_id=node1.id,
node2_id=node2.id,
network_id=iface1.net.id,
color=color,
)
session.broadcast_link(link) session.broadcast_link(link)
return EmaneLinkResponse(result=True) return EmaneLinkResponse(result=True)
else: else:
@ -1240,6 +1251,27 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
config = {x.id: x.default for x in service.default_configs} config = {x.id: x.default for x in service.default_configs}
return GetNodeConfigServiceResponse(config=config) return GetNodeConfigServiceResponse(config=config)
def GetConfigServiceRendered(
self, request: GetConfigServiceRenderedRequest, context: ServicerContext
) -> GetConfigServiceRenderedResponse:
"""
Retrieves the rendered file data for a given config service on a node.
:param request: config service render request
:param context: grpc context
:return: rendered config service files
"""
session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context, CoreNode)
self.validate_service(request.name, context)
service = node.config_services.get(request.name)
if not service:
context.abort(
grpc.StatusCode.NOT_FOUND, f"unknown node service {request.name}"
)
rendered = service.get_rendered_templates()
return GetConfigServiceRenderedResponse(rendered=rendered)
def GetConfigServiceDefaults( def GetConfigServiceDefaults(
self, request: GetConfigServiceDefaultsRequest, context: ServicerContext self, request: GetConfigServiceDefaultsRequest, context: ServicerContext
) -> GetConfigServiceDefaultsResponse: ) -> GetConfigServiceDefaultsResponse:
@ -1299,18 +1331,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
) -> WlanLinkResponse: ) -> WlanLinkResponse:
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
wlan = self.get_node(session, request.wlan, context, WlanNode) wlan = self.get_node(session, request.wlan, context, WlanNode)
if not isinstance(wlan.model, BasicRangeModel): if not isinstance(wlan.wireless_model, BasicRangeModel):
context.abort( context.abort(
grpc.StatusCode.NOT_FOUND, grpc.StatusCode.NOT_FOUND,
f"wlan node {request.wlan} does not using BasicRangeModel", f"wlan node {request.wlan} is not using BasicRangeModel",
) )
node1 = self.get_node(session, request.node1_id, context, CoreNode) node1 = self.get_node(session, request.node1_id, context, CoreNode)
node2 = self.get_node(session, request.node2_id, context, CoreNode) node2 = self.get_node(session, request.node2_id, context, CoreNode)
node1_iface, node2_iface = None, None node1_iface, node2_iface = None, None
for net, iface1, iface2 in node1.commonnets(node2): for iface in node1.get_ifaces(control=False):
if net == wlan: if iface.net == wlan:
node1_iface = iface1 node1_iface = iface
node2_iface = iface2 break
for iface in node2.get_ifaces(control=False):
if iface.net == wlan:
node2_iface = iface
break break
result = False result = False
if node1_iface and node2_iface: if node1_iface and node2_iface:
@ -1318,7 +1353,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
wlan.link(node1_iface, node2_iface) wlan.link(node1_iface, node2_iface)
else: else:
wlan.unlink(node1_iface, node2_iface) wlan.unlink(node1_iface, node2_iface)
wlan.model.sendlinkmsg(node1_iface, node2_iface, unlink=not request.linked) wlan.wireless_model.sendlinkmsg(
node1_iface, node2_iface, unlink=not request.linked
)
result = True result = True
return WlanLinkResponse(result=result) return WlanLinkResponse(result=result)
@ -1335,3 +1372,60 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context) nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context)
session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2) session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
return EmanePathlossesResponse() return EmanePathlossesResponse()
def Linked(
self, request: LinkedRequest, context: ServicerContext
) -> LinkedResponse:
session = self.get_session(request.session_id, context)
session.linked(
request.node1_id,
request.node2_id,
request.iface1_id,
request.iface2_id,
request.linked,
)
return LinkedResponse()
def WirelessLinked(
self, request: WirelessLinkedRequest, context: ServicerContext
) -> WirelessLinkedResponse:
session = self.get_session(request.session_id, context)
wireless = self.get_node(session, request.wireless_id, context, WirelessNode)
wireless.link_control(request.node1_id, request.node2_id, request.linked)
return WirelessLinkedResponse()
def WirelessConfig(
self, request: WirelessConfigRequest, context: ServicerContext
) -> WirelessConfigResponse:
session = self.get_session(request.session_id, context)
wireless = self.get_node(session, request.wireless_id, context, WirelessNode)
options1 = request.options1
options2 = options1
if request.HasField("options2"):
options2 = request.options2
options1 = grpcutils.convert_options_proto(options1)
options2 = grpcutils.convert_options_proto(options2)
wireless.link_config(request.node1_id, request.node2_id, options1, options2)
return WirelessConfigResponse()
def GetWirelessConfig(
self, request: GetWirelessConfigRequest, context: ServicerContext
) -> GetWirelessConfigResponse:
session = self.get_session(request.session_id, context)
try:
wireless = session.get_node(request.node_id, WirelessNode)
configs = wireless.get_config()
except CoreError:
configs = {x.id: x for x in WirelessNode.options}
config_options = {}
for config in configs.values():
config_option = common_pb2.ConfigOption(
label=config.label,
name=config.id,
value=config.default,
type=config.type.value,
select=config.options,
group=config.group,
)
config_options[config.id] = config_option
return GetWirelessConfigResponse(config=config_options)

View file

@ -67,6 +67,7 @@ class NodeType(Enum):
CONTROL_NET = 13 CONTROL_NET = 13
DOCKER = 15 DOCKER = 15
LXC = 16 LXC = 16
WIRELESS = 17
class LinkType(Enum): class LinkType(Enum):
@ -209,12 +210,12 @@ class Service:
@dataclass @dataclass
class ServiceDefault: class ServiceDefault:
node_type: str model: str
services: List[str] services: List[str]
@classmethod @classmethod
def from_proto(cls, proto: services_pb2.ServiceDefaults) -> "ServiceDefault": def from_proto(cls, proto: services_pb2.ServiceDefaults) -> "ServiceDefault":
return ServiceDefault(node_type=proto.node_type, services=list(proto.services)) return ServiceDefault(model=proto.model, services=list(proto.services))
@dataclass @dataclass
@ -480,6 +481,8 @@ class Interface:
mtu: int = None mtu: int = None
node_id: int = None node_id: int = None
net2_id: int = None net2_id: int = None
nem_id: int = None
nem_port: int = None
@classmethod @classmethod
def from_proto(cls, proto: core_pb2.Interface) -> "Interface": def from_proto(cls, proto: core_pb2.Interface) -> "Interface":
@ -496,6 +499,8 @@ class Interface:
mtu=proto.mtu, mtu=proto.mtu,
node_id=proto.node_id, node_id=proto.node_id,
net2_id=proto.net2_id, net2_id=proto.net2_id,
nem_id=proto.nem_id,
nem_port=proto.nem_port,
) )
def to_proto(self) -> core_pb2.Interface: def to_proto(self) -> core_pb2.Interface:
@ -736,6 +741,7 @@ class Node:
Tuple[str, Optional[int]], Dict[str, ConfigOption] Tuple[str, Optional[int]], Dict[str, ConfigOption]
] = field(default_factory=dict, repr=False) ] = field(default_factory=dict, repr=False)
wlan_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) wlan_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False)
wireless_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False)
mobility_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) mobility_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False)
service_configs: Dict[str, NodeServiceData] = field( service_configs: Dict[str, NodeServiceData] = field(
default_factory=dict, repr=False default_factory=dict, repr=False
@ -770,7 +776,7 @@ class Node:
id=proto.id, id=proto.id,
name=proto.name, name=proto.name,
type=NodeType(proto.type), type=NodeType(proto.type),
model=proto.model, model=proto.model or None,
position=Position.from_proto(proto.position), position=Position.from_proto(proto.position),
services=set(proto.services), services=set(proto.services),
config_services=set(proto.config_services), config_services=set(proto.config_services),
@ -788,6 +794,7 @@ class Node:
service_file_configs=service_file_configs, service_file_configs=service_file_configs,
config_service_configs=config_service_configs, config_service_configs=config_service_configs,
emane_model_configs=emane_configs, emane_model_configs=emane_configs,
wireless_config=ConfigOption.from_dict(proto.wireless_config),
) )
def to_proto(self) -> core_pb2.Node: def to_proto(self) -> core_pb2.Node:
@ -839,6 +846,7 @@ class Node:
service_configs=service_configs, service_configs=service_configs,
config_service_configs=config_service_configs, config_service_configs=config_service_configs,
emane_configs=emane_configs, emane_configs=emane_configs,
wireless_config={k: v.to_proto() for k, v in self.wireless_config.items()},
) )
def set_wlan(self, config: Dict[str, str]) -> None: def set_wlan(self, config: Dict[str, str]) -> None:
@ -883,9 +891,7 @@ class Session:
def from_proto(cls, proto: core_pb2.Session) -> "Session": def from_proto(cls, proto: core_pb2.Session) -> "Session":
nodes: Dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes} nodes: Dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes}
links = [Link.from_proto(x) for x in proto.links] links = [Link.from_proto(x) for x in proto.links]
default_services = { default_services = {x.model: set(x.services) for x in proto.default_services}
x.node_type: set(x.services) for x in proto.default_services
}
hooks = {x.file: Hook.from_proto(x) for x in proto.hooks} hooks = {x.file: Hook.from_proto(x) for x in proto.hooks}
file_path = Path(proto.file) if proto.file else None file_path = Path(proto.file) if proto.file else None
options = ConfigOption.from_dict(proto.options) options = ConfigOption.from_dict(proto.options)
@ -913,9 +919,9 @@ class Session:
options = {k: v.to_proto() for k, v in self.options.items()} options = {k: v.to_proto() for k, v in self.options.items()}
servers = [x.to_proto() for x in self.servers] servers = [x.to_proto() for x in self.servers]
default_services = [] default_services = []
for node_type, services in self.default_services.items(): for model, services in self.default_services.items():
default_service = services_pb2.ServiceDefaults( default_service = services_pb2.ServiceDefaults(
node_type=node_type, services=services model=model, services=services
) )
default_services.append(default_service) default_services.append(default_service)
file = str(self.file) if self.file else None file = str(self.file) if self.file else None
@ -1102,7 +1108,6 @@ class ConfigEvent:
data_types=list(proto.data_types), data_types=list(proto.data_types),
data_values=proto.data_values, data_values=proto.data_values,
captions=proto.captions, captions=proto.captions,
bitmap=proto.bitmap,
possible_values=proto.possible_values, possible_values=proto.possible_values,
groups=proto.groups, groups=proto.groups,
iface_id=proto.iface_id, iface_id=proto.iface_id,
@ -1194,13 +1199,13 @@ class EmanePathlossesRequest:
) )
@dataclass @dataclass(frozen=True)
class MoveNodesRequest: class MoveNodesRequest:
session_id: int session_id: int
node_id: int node_id: int
source: str = None source: str = field(compare=False, default=None)
position: Position = None position: Position = field(compare=False, default=None)
geo: Geo = None geo: Geo = field(compare=False, default=None)
def to_proto(self) -> core_pb2.MoveNodesRequest: def to_proto(self) -> core_pb2.MoveNodesRequest:
position = self.position.to_proto() if self.position else None position = self.position.to_proto() if self.position else None

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,60 +0,0 @@
"""
Defines core server for handling TCP connections.
"""
import socketserver
from core.emulator.coreemu import CoreEmu
class CoreServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
"""
TCP server class, manages sessions and spawns request handlers for
incoming connections.
"""
daemon_threads = True
allow_reuse_address = True
def __init__(self, server_address, handler_class, config=None):
"""
Server class initialization takes configuration data and calls
the socketserver constructor.
:param tuple[str, int] server_address: server host and port to use
:param class handler_class: request handler
:param dict config: configuration setting
"""
self.coreemu = CoreEmu(config)
self.config = config
socketserver.TCPServer.__init__(self, server_address, handler_class)
class CoreUdpServer(socketserver.ThreadingMixIn, socketserver.UDPServer):
"""
UDP server class, manages sessions and spawns request handlers for
incoming connections.
"""
daemon_threads = True
allow_reuse_address = True
def __init__(self, server_address, handler_class, mainserver):
"""
Server class initialization takes configuration data and calls
the SocketServer constructor
:param server_address:
:param class handler_class: request handler
:param mainserver:
"""
self.mainserver = mainserver
socketserver.UDPServer.__init__(self, server_address, handler_class)
def start(self):
"""
Thread target to run concurrently with the TCP server.
:return: nothing
"""
self.serve_forever()

View file

@ -1,178 +0,0 @@
"""
Converts CORE data objects into legacy API messages.
"""
import logging
from collections import OrderedDict
from typing import Dict, List
from core.api.tlv import coreapi, structutils
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):
"""
Convenience method for converting NodeData to a packed TLV message.
:param core.emulator.data.NodeData node_data: node data to convert
:return: packed node message
"""
node = node_data.node
services = None
if node.services is not None:
services = "|".join([x.name for x in node.services])
server = None
if node.server is not None:
server = node.server.name
tlv_data = structutils.pack_values(
coreapi.CoreNodeTlv,
[
(NodeTlvs.NUMBER, node.id),
(NodeTlvs.TYPE, node.apitype.value),
(NodeTlvs.NAME, node.name),
(NodeTlvs.MODEL, node.type),
(NodeTlvs.EMULATION_SERVER, server),
(NodeTlvs.X_POSITION, int(node.position.x)),
(NodeTlvs.Y_POSITION, int(node.position.y)),
(NodeTlvs.CANVAS, node.canvas),
(NodeTlvs.SERVICES, services),
(NodeTlvs.LATITUDE, str(node.position.lat)),
(NodeTlvs.LONGITUDE, str(node.position.lon)),
(NodeTlvs.ALTITUDE, str(node.position.alt)),
(NodeTlvs.ICON, node.icon),
],
)
return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data)
def convert_config(config_data):
"""
Convenience method for converting ConfigData to a packed TLV message.
:param core.emulator.data.ConfigData config_data: config data to convert
:return: packed message
"""
session = None
if config_data.session is not None:
session = str(config_data.session)
tlv_data = structutils.pack_values(
coreapi.CoreConfigTlv,
[
(ConfigTlvs.NODE, config_data.node),
(ConfigTlvs.OBJECT, config_data.object),
(ConfigTlvs.TYPE, config_data.type),
(ConfigTlvs.DATA_TYPES, config_data.data_types),
(ConfigTlvs.VALUES, config_data.data_values),
(ConfigTlvs.CAPTIONS, config_data.captions),
(ConfigTlvs.BITMAP, config_data.bitmap),
(ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values),
(ConfigTlvs.GROUPS, config_data.groups),
(ConfigTlvs.SESSION, session),
(ConfigTlvs.IFACE_ID, config_data.iface_id),
(ConfigTlvs.NETWORK_ID, config_data.network_id),
(ConfigTlvs.OPAQUE, config_data.opaque),
],
)
return coreapi.CoreConfMessage.pack(config_data.message_type, tlv_data)
class ConfigShim:
"""
Provides helper methods for converting newer configuration values into TLV
compatible formats.
"""
@classmethod
def str_to_dict(cls, key_values: str) -> Dict[str, str]:
"""
Converts a TLV key/value string into an ordered mapping.
:param key_values:
:return: ordered mapping of key/value pairs
"""
key_values = key_values.split("|")
values = OrderedDict()
for key_value in key_values:
key, value = key_value.split("=", 1)
values[key] = value
return values
@classmethod
def groups_to_str(cls, config_groups: List[ConfigGroup]) -> str:
"""
Converts configuration groups to a TLV formatted string.
:param config_groups: configuration groups to format
:return: TLV configuration group string
"""
group_strings = []
for config_group in config_groups:
group_string = (
f"{config_group.name}:{config_group.start}-{config_group.stop}"
)
group_strings.append(group_string)
return "|".join(group_strings)
@classmethod
def config_data(
cls,
flags: int,
node_id: int,
type_flags: int,
configurable_options: ConfigurableOptions,
config: Dict[str, str],
) -> ConfigData:
"""
Convert this class to a Config API message. Some TLVs are defined
by the class, but node number, conf type flags, and values must
be passed in.
:param flags: message flags
:param node_id: node id
:param type_flags: type flags
:param configurable_options: options to create config data for
:param config: configuration values for options
:return: configuration data object
"""
key_values = None
captions = None
data_types = []
possible_values = []
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
else:
captions += f"|{configuration.label}"
data_types.append(configuration.type.value)
options = ",".join(configuration.options)
possible_values.append(options)
_id = configuration.id
config_value = config.get(_id, configuration.default)
key_value = f"{_id}={config_value}"
if not key_values:
key_values = key_value
else:
key_values += f"|{key_value}"
groups_str = cls.groups_to_str(configurable_options.config_groups())
return ConfigData(
message_type=flags,
node=node_id,
object=configurable_options.name,
type=type_flags,
data_types=tuple(data_types),
data_values=key_values,
captions=captions,
possible_values="|".join(possible_values),
bitmap=configurable_options.bitmap,
groups=groups_str,
)

View file

@ -1,212 +0,0 @@
"""
Enumerations specific to the CORE TLV API.
"""
from enum import Enum
CORE_API_PORT = 4038
class MessageTypes(Enum):
"""
CORE message types.
"""
NODE = 0x01
LINK = 0x02
EXECUTE = 0x03
REGISTER = 0x04
CONFIG = 0x05
FILE = 0x06
INTERFACE = 0x07
EVENT = 0x08
SESSION = 0x09
EXCEPTION = 0x0A
class NodeTlvs(Enum):
"""
Node type, length, value enumerations.
"""
NUMBER = 0x01
TYPE = 0x02
NAME = 0x03
IP_ADDRESS = 0x04
MAC_ADDRESS = 0x05
IP6_ADDRESS = 0x06
MODEL = 0x07
EMULATION_SERVER = 0x08
SESSION = 0x0A
X_POSITION = 0x20
Y_POSITION = 0x21
CANVAS = 0x22
EMULATION_ID = 0x23
NETWORK_ID = 0x24
SERVICES = 0x25
LATITUDE = 0x30
LONGITUDE = 0x31
ALTITUDE = 0x32
ICON = 0x42
OPAQUE = 0x50
class LinkTlvs(Enum):
"""
Link type, length, value enumerations.
"""
N1_NUMBER = 0x01
N2_NUMBER = 0x02
DELAY = 0x03
BANDWIDTH = 0x04
LOSS = 0x05
DUP = 0x06
JITTER = 0x07
MER = 0x08
BURST = 0x09
SESSION = 0x0A
MBURST = 0x10
TYPE = 0x20
GUI_ATTRIBUTES = 0x21
UNIDIRECTIONAL = 0x22
EMULATION_ID = 0x23
NETWORK_ID = 0x24
KEY = 0x25
IFACE1_NUMBER = 0x30
IFACE1_IP4 = 0x31
IFACE1_IP4_MASK = 0x32
IFACE1_MAC = 0x33
IFACE1_IP6 = 0x34
IFACE1_IP6_MASK = 0x35
IFACE2_NUMBER = 0x36
IFACE2_IP4 = 0x37
IFACE2_IP4_MASK = 0x38
IFACE2_MAC = 0x39
IFACE2_IP6 = 0x40
IFACE2_IP6_MASK = 0x41
IFACE1_NAME = 0x42
IFACE2_NAME = 0x43
OPAQUE = 0x50
class ExecuteTlvs(Enum):
"""
Execute type, length, value enumerations.
"""
NODE = 0x01
NUMBER = 0x02
TIME = 0x03
COMMAND = 0x04
RESULT = 0x05
STATUS = 0x06
SESSION = 0x0A
class ConfigTlvs(Enum):
"""
Configuration type, length, value enumerations.
"""
NODE = 0x01
OBJECT = 0x02
TYPE = 0x03
DATA_TYPES = 0x04
VALUES = 0x05
CAPTIONS = 0x06
BITMAP = 0x07
POSSIBLE_VALUES = 0x08
GROUPS = 0x09
SESSION = 0x0A
IFACE_ID = 0x0B
NETWORK_ID = 0x24
OPAQUE = 0x50
class ConfigFlags(Enum):
"""
Configuration flags.
"""
NONE = 0x00
REQUEST = 0x01
UPDATE = 0x02
RESET = 0x03
class FileTlvs(Enum):
"""
File type, length, value enumerations.
"""
NODE = 0x01
NAME = 0x02
MODE = 0x03
NUMBER = 0x04
TYPE = 0x05
SOURCE_NAME = 0x06
SESSION = 0x0A
DATA = 0x10
COMPRESSED_DATA = 0x11
class InterfaceTlvs(Enum):
"""
Interface type, length, value enumerations.
"""
NODE = 0x01
NUMBER = 0x02
NAME = 0x03
IP_ADDRESS = 0x04
MASK = 0x05
MAC_ADDRESS = 0x06
IP6_ADDRESS = 0x07
IP6_MASK = 0x08
TYPE = 0x09
SESSION = 0x0A
STATE = 0x0B
EMULATION_ID = 0x23
NETWORK_ID = 0x24
class EventTlvs(Enum):
"""
Event type, length, value enumerations.
"""
NODE = 0x01
TYPE = 0x02
NAME = 0x03
DATA = 0x04
TIME = 0x05
SESSION = 0x0A
class SessionTlvs(Enum):
"""
Session type, length, value enumerations.
"""
NUMBER = 0x01
NAME = 0x02
FILE = 0x03
NODE_COUNT = 0x04
DATE = 0x05
THUMB = 0x06
USER = 0x07
OPAQUE = 0x0A
class ExceptionTlvs(Enum):
"""
Exception type, length, value enumerations.
"""
NODE = 0x01
SESSION = 0x02
LEVEL = 0x03
SOURCE = 0x04
DATE = 0x05
TEXT = 0x06
OPAQUE = 0x0A

View file

@ -1,45 +0,0 @@
"""
Utilities for working with python struct data.
"""
import logging
logger = logging.getLogger(__name__)
def pack_values(clazz, packers):
"""
Pack values for a given legacy class.
:param class clazz: class that will provide a pack method
:param list packers: a list of tuples that are used to pack values and transform them
:return: packed data string of all values
"""
# iterate through tuples of values to pack
logger.debug("packing: %s", packers)
data = b""
for packer in packers:
# check if a transformer was provided for valid values
transformer = None
if len(packer) == 2:
tlv_type, value = packer
elif len(packer) == 3:
tlv_type, value, transformer = packer
else:
raise RuntimeError("packer had more than 3 arguments")
# only pack actual values and avoid packing empty strings
# protobuf defaults to empty strings and does no imply a value to set
if value is None or (isinstance(value, str) and not value):
continue
# transform values as needed
if transformer:
value = transformer(value)
# pack and add to existing data
logger.debug("packing: %s - %s type(%s)", tlv_type, value, type(value))
data += clazz.pack(tlv_type.value, value)
return data

View file

@ -44,6 +44,7 @@ class Configuration:
label: str = None label: str = None
default: str = "" default: str = ""
options: List[str] = field(default_factory=list) options: List[str] = field(default_factory=list)
group: str = "Configuration"
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.label = self.label if self.label else self.id self.label = self.label if self.label else self.id
@ -78,6 +79,7 @@ class ConfigBool(Configuration):
""" """
type: ConfigDataTypes = ConfigDataTypes.BOOL type: ConfigDataTypes = ConfigDataTypes.BOOL
value: bool = False
@dataclass @dataclass
@ -87,6 +89,7 @@ class ConfigFloat(Configuration):
""" """
type: ConfigDataTypes = ConfigDataTypes.FLOAT type: ConfigDataTypes = ConfigDataTypes.FLOAT
value: float = 0.0
@dataclass @dataclass
@ -96,6 +99,7 @@ class ConfigInt(Configuration):
""" """
type: ConfigDataTypes = ConfigDataTypes.INT32 type: ConfigDataTypes = ConfigDataTypes.INT32
value: int = 0
@dataclass @dataclass
@ -105,6 +109,7 @@ class ConfigString(Configuration):
""" """
type: ConfigDataTypes = ConfigDataTypes.STRING type: ConfigDataTypes = ConfigDataTypes.STRING
value: str = ""
class ConfigurableOptions: class ConfigurableOptions:
@ -113,7 +118,6 @@ class ConfigurableOptions:
""" """
name: Optional[str] = None name: Optional[str] = None
bitmap: Optional[str] = None
options: List[Configuration] = [] options: List[Configuration] = []
@classmethod @classmethod

View file

@ -331,6 +331,33 @@ class ConfigService(abc.ABC):
templates[file] = template templates[file] = template
return templates return templates
def get_rendered_templates(self) -> Dict[str, str]:
templates = {}
data = self.data()
for file in sorted(self.files):
rendered = self._get_rendered_template(file, data)
templates[file] = rendered
return templates
def _get_rendered_template(self, file: str, data: Dict[str, Any]) -> str:
file_path = Path(file)
template_path = get_template_path(file_path)
if file in self.custom_templates:
text = self.custom_templates[file]
rendered = self.render_text(text, data)
elif self.templates.has_template(template_path):
rendered = self.render_template(template_path, data)
else:
try:
text = self.get_text_template(file)
except Exception as e:
raise ConfigServiceTemplateError(
f"node({self.node.name}) service({self.name}) file({file}) "
f"failure getting template: {e}"
)
rendered = self.render_text(text, data)
return rendered
def create_files(self) -> None: def create_files(self) -> None:
""" """
Creates service files inside associated node. Creates service files inside associated node.
@ -342,22 +369,8 @@ class ConfigService(abc.ABC):
logger.debug( logger.debug(
"node(%s) service(%s) template(%s)", self.node.name, self.name, file "node(%s) service(%s) template(%s)", self.node.name, self.name, file
) )
rendered = self._get_rendered_template(file, data)
file_path = Path(file) file_path = Path(file)
template_path = get_template_path(file_path)
if file in self.custom_templates:
text = self.custom_templates[file]
rendered = self.render_text(text, data)
elif self.templates.has_template(template_path):
rendered = self.render_template(template_path, data)
else:
try:
text = self.get_text_template(file)
except Exception as e:
raise ConfigServiceTemplateError(
f"node({self.node.name}) service({self.name}) file({file}) "
f"failure getting template: {e}"
)
rendered = self.render_text(text, data)
self.node.create_file(file_path, rendered) self.node.create_file(file_path, rendered)
def run_startup(self, wait: bool) -> None: def run_startup(self, wait: bool) -> None:
@ -459,7 +472,7 @@ class ConfigService(abc.ABC):
except Exception: except Exception:
raise CoreError( raise CoreError(
f"node({self.node.name}) service({self.name}) file({template_path})" f"node({self.node.name}) service({self.name}) file({template_path})"
f"{exceptions.text_error_template().render_template()}" f"{exceptions.text_error_template().render_unicode()}"
) )
def _define_config(self, configs: List[Configuration]) -> None: def _define_config(self, configs: List[Configuration]) -> None:

View file

@ -4,14 +4,26 @@ from typing import Any, Dict, List
from core.config import Configuration from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.nodes.base import CoreNodeBase from core.nodes.base import CoreNodeBase, NodeBase
from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.network import WlanNode from core.nodes.network import PtpNet, WlanNode
from core.nodes.physical import Rj45Node
from core.nodes.wireless import WirelessNode
GROUP: str = "FRR" GROUP: str = "FRR"
FRR_STATE_DIR: str = "/var/run/frr" FRR_STATE_DIR: str = "/var/run/frr"
def is_wireless(node: NodeBase) -> bool:
"""
Check if the node is a wireless type node.
:param node: node to check type for
:return: True if wireless type, False otherwise
"""
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
def has_mtu_mismatch(iface: CoreInterface) -> bool: def has_mtu_mismatch(iface: CoreInterface) -> bool:
""" """
Helper to detect MTU mismatch and add the appropriate FRR Helper to detect MTU mismatch and add the appropriate FRR
@ -53,6 +65,20 @@ def get_router_id(node: CoreNodeBase) -> str:
return "0.0.0.0" return "0.0.0.0"
def rj45_check(iface: CoreInterface) -> bool:
"""
Helper to detect whether interface is connected an external RJ45
link.
"""
if iface.net:
for peer_iface in iface.net.get_ifaces():
if peer_iface == iface:
continue
if isinstance(peer_iface.node, Rj45Node):
return True
return False
class FRRZebra(ConfigService): class FRRZebra(ConfigService):
name: str = "FRRzebra" name: str = "FRRzebra"
group: str = GROUP group: str = GROUP
@ -74,10 +100,10 @@ class FRRZebra(ConfigService):
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
frr_conf = self.files[0] frr_conf = self.files[0]
frr_bin_search = self.node.session.options.get_config( frr_bin_search = self.node.session.options.get(
"frr_bin_search", default="/usr/local/bin /usr/bin /usr/lib/frr" "frr_bin_search", default="/usr/local/bin /usr/bin /usr/lib/frr"
).strip('"') ).strip('"')
frr_sbin_search = self.node.session.options.get_config( frr_sbin_search = self.node.session.options.get(
"frr_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/frr" "frr_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/frr"
).strip('"') ).strip('"')
@ -158,7 +184,7 @@ class FRROspfv2(FrrService, ConfigService):
addresses = [] addresses = []
for iface in self.node.get_ifaces(control=False): for iface in self.node.get_ifaces(control=False):
for ip4 in iface.ip4s: for ip4 in iface.ip4s:
addresses.append(str(ip4.ip)) addresses.append(str(ip4))
data = dict(router_id=router_id, addresses=addresses) data = dict(router_id=router_id, addresses=addresses)
text = """ text = """
router ospf router ospf
@ -166,15 +192,31 @@ class FRROspfv2(FrrService, ConfigService):
% for addr in addresses: % for addr in addresses:
network ${addr} area 0 network ${addr} area 0
% endfor % endfor
ospf opaque-lsa
! !
""" """
return self.render_text(text, data) return self.render_text(text, data)
def frr_iface_config(self, iface: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
if has_mtu_mismatch(iface): has_mtu = has_mtu_mismatch(iface)
return "ip ospf mtu-ignore" has_rj45 = rj45_check(iface)
else: is_ptp = isinstance(iface.net, PtpNet)
return "" data = dict(has_mtu=has_mtu, is_ptp=is_ptp, has_rj45=has_rj45)
text = """
% if has_mtu:
ip ospf mtu-ignore
% endif
% if has_rj45:
<% return STOP_RENDERING %>
% endif
% if is_ptp:
ip ospf network point-to-point
% endif
ip ospf hello-interval 2
ip ospf dead-interval 6
ip ospf retransmit-interval 5
"""
return self.render_text(text, data)
class FRROspfv3(FrrService, ConfigService): class FRROspfv3(FrrService, ConfigService):
@ -324,7 +366,7 @@ class FRRBabel(FrrService, ConfigService):
return self.render_text(text, data) return self.render_text(text, data)
def frr_iface_config(self, iface: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
if isinstance(iface.net, (WlanNode, EmaneNet)): if is_wireless(iface.net):
text = """ text = """
babel wireless babel wireless
no babel split-horizon no babel split-horizon

View file

@ -48,6 +48,10 @@ bootdaemon()
flags="$flags -6" flags="$flags -6"
fi fi
if [ "$1" = "ospfd" ]; then
flags="$flags --apiserver"
fi
#force FRR to use CORE generated conf file #force FRR to use CORE generated conf file
flags="$flags -d -f $FRR_CONF" flags="$flags -d -f $FRR_CONF"
$FRR_SBIN_DIR/$1 $flags $FRR_SBIN_DIR/$1 $flags

View file

@ -66,7 +66,6 @@ class NrlSmf(ConfigService):
modes: Dict[str, Dict[str, str]] = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_arouted = "arouted" in self.node.config_services
has_nhdp = "NHDP" in self.node.config_services has_nhdp = "NHDP" in self.node.config_services
has_olsr = "OLSR" in self.node.config_services has_olsr = "OLSR" in self.node.config_services
ifnames = [] ifnames = []
@ -78,11 +77,7 @@ class NrlSmf(ConfigService):
ip4_prefix = f"{ip4.ip}/{24}" ip4_prefix = f"{ip4.ip}/{24}"
break break
return dict( return dict(
has_arouted=has_arouted, has_nhdp=has_nhdp, has_olsr=has_olsr, ifnames=ifnames, ip4_prefix=ip4_prefix
has_nhdp=has_nhdp,
has_olsr=has_olsr,
ifnames=ifnames,
ip4_prefix=ip4_prefix,
) )
@ -167,27 +162,3 @@ class MgenActor(ConfigService):
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [] default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {} modes: Dict[str, Dict[str, str]] = {}
class Arouted(ConfigService):
name: str = "arouted"
group: str = GROUP
directories: List[str] = []
files: List[str] = ["startarouted.sh"]
executables: List[str] = ["arouted"]
dependencies: List[str] = []
startup: List[str] = ["bash startarouted.sh"]
validate: List[str] = ["pidof arouted"]
shutdown: List[str] = ["pkill arouted"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]:
ip4_prefix = None
for iface in self.node.get_ifaces(control=False):
ip4 = iface.get_ip4()
if ip4:
ip4_prefix = f"{ip4.ip}/{24}"
break
return dict(ip4_prefix=ip4_prefix)

View file

@ -1,15 +0,0 @@
#!/bin/sh
for f in "/tmp/${node.name}_smf"; do
count=1
until [ -e "$f" ]; do
if [ $count -eq 10 ]; then
echo "ERROR: nrlmsf pipe not found: $f" >&2
exit 1
fi
sleep 0.1
count=$(($count + 1))
done
done
ip route add ${ip4_prefix} dev lo
arouted instance ${node.name}_smf tap ${node.name}_tap stability 10 2>&1 > /var/log/arouted.log &

View file

@ -1,8 +1,5 @@
<% <%
ifaces = ",".join(ifnames) ifaces = ",".join(ifnames)
arouted = ""
if has_arouted:
arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0])
if has_nhdp: if has_nhdp:
flood = "ecds" flood = "ecds"
elif has_olsr: elif has_olsr:
@ -12,4 +9,4 @@
%> %>
#!/bin/sh #!/bin/sh
# auto-generated by NrlSmf service # auto-generated by NrlSmf service
nrlsmf instance ${node.name}_smf ${ifaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 & nrlsmf instance ${node.name}_smf ${flood} ${ifaces} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &

View file

@ -5,16 +5,27 @@ from typing import Any, Dict, List
from core.config import Configuration from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.nodes.base import CoreNodeBase from core.nodes.base import CoreNodeBase, NodeBase
from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.network import PtpNet, WlanNode from core.nodes.network import PtpNet, WlanNode
from core.nodes.physical import Rj45Node from core.nodes.physical import Rj45Node
from core.nodes.wireless import WirelessNode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
GROUP: str = "Quagga" GROUP: str = "Quagga"
QUAGGA_STATE_DIR: str = "/var/run/quagga" QUAGGA_STATE_DIR: str = "/var/run/quagga"
def is_wireless(node: NodeBase) -> bool:
"""
Check if the node is a wireless type node.
:param node: node to check type for
:return: True if wireless type, False otherwise
"""
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
def has_mtu_mismatch(iface: CoreInterface) -> bool: def has_mtu_mismatch(iface: CoreInterface) -> bool:
""" """
Helper to detect MTU mismatch and add the appropriate OSPF Helper to detect MTU mismatch and add the appropriate OSPF
@ -89,10 +100,10 @@ class Zebra(ConfigService):
modes: Dict[str, Dict[str, str]] = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
quagga_bin_search = self.node.session.options.get_config( quagga_bin_search = self.node.session.options.get(
"quagga_bin_search", default="/usr/local/bin /usr/bin /usr/lib/quagga" "quagga_bin_search", default="/usr/local/bin /usr/bin /usr/lib/quagga"
).strip('"') ).strip('"')
quagga_sbin_search = self.node.session.options.get_config( quagga_sbin_search = self.node.session.options.get(
"quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga" "quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga"
).strip('"') ).strip('"')
quagga_state_dir = QUAGGA_STATE_DIR quagga_state_dir = QUAGGA_STATE_DIR
@ -265,7 +276,7 @@ class Ospfv3mdr(Ospfv3):
def quagga_iface_config(self, iface: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
config = super().quagga_iface_config(iface) config = super().quagga_iface_config(iface)
if isinstance(iface.net, (WlanNode, EmaneNet)): if is_wireless(iface.net):
config = self.clean_text( config = self.clean_text(
f""" f"""
{config} {config}
@ -295,9 +306,6 @@ class Bgp(QuaggaService, ConfigService):
ipv6_routing: bool = True ipv6_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
return ""
def quagga_iface_config(self, iface: CoreInterface) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
text = f""" text = f"""
! BGP configuration ! BGP configuration
@ -311,6 +319,9 @@ class Bgp(QuaggaService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def quagga_iface_config(self, iface: CoreInterface) -> str:
return ""
class Rip(QuaggaService, ConfigService): class Rip(QuaggaService, ConfigService):
""" """
@ -390,7 +401,7 @@ class Babel(QuaggaService, ConfigService):
return self.render_text(text, data) return self.render_text(text, data)
def quagga_iface_config(self, iface: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
if isinstance(iface.net, (WlanNode, EmaneNet)): if is_wireless(iface.net):
text = """ text = """
babel wireless babel wireless
no babel split-horizon no babel split-horizon

View file

@ -12,12 +12,12 @@ from core import utils
from core.emane.emanemodel import EmaneModel from core.emane.emanemodel import EmaneModel
from core.emane.linkmonitor import EmaneLinkMonitor from core.emane.linkmonitor import EmaneLinkMonitor
from core.emane.modelmanager import EmaneModelManager from core.emane.modelmanager import EmaneModelManager
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet, TunTap
from core.emulator.data import LinkData from core.emulator.data import LinkData
from core.emulator.enumerations import LinkTypes, MessageFlags, RegisterTlvs from core.emulator.enumerations import LinkTypes, MessageFlags, RegisterTlvs
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNetworkBase, CoreNode, NodeBase from core.nodes.base import CoreNode, NodeBase
from core.nodes.interface import CoreInterface, TunTap from core.nodes.interface import CoreInterface
from core.xml import emanexml from core.xml import emanexml
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -45,8 +45,6 @@ except ImportError:
EventServiceException = None EventServiceException = None
logger.debug("compatible emane python bindings not installed") logger.debug("compatible emane python bindings not installed")
DEFAULT_EMANE_PREFIX = "/usr"
DEFAULT_DEV = "ctrl0"
DEFAULT_LOG_LEVEL: int = 3 DEFAULT_LOG_LEVEL: int = 3
@ -133,10 +131,10 @@ class EmaneManager:
self._emane_nets: Dict[int, EmaneNet] = {} self._emane_nets: Dict[int, EmaneNet] = {}
self._emane_node_lock: threading.Lock = threading.Lock() self._emane_node_lock: threading.Lock = threading.Lock()
# port numbers are allocated from these counters # port numbers are allocated from these counters
self.platformport: int = self.session.options.get_config_int( self.platformport: int = self.session.options.get_int(
"emane_platform_port", 8100 "emane_platform_port", 8100
) )
self.transformport: int = self.session.options.get_config_int( self.transformport: int = self.session.options.get_int(
"emane_transform_port", 8200 "emane_transform_port", 8200
) )
self.doeventloop: bool = False self.doeventloop: bool = False
@ -153,7 +151,7 @@ class EmaneManager:
self.nem_service: Dict[int, EmaneEventService] = {} self.nem_service: Dict[int, EmaneEventService] = {}
def next_nem_id(self, iface: CoreInterface) -> int: def next_nem_id(self, iface: CoreInterface) -> int:
nem_id = self.session.options.get_config_int("nem_id_start") nem_id = self.session.options.get_int("nem_id_start")
while nem_id in self.nems_to_ifaces: while nem_id in self.nems_to_ifaces:
nem_id += 1 nem_id += 1
self.nems_to_ifaces[nem_id] = iface self.nems_to_ifaces[nem_id] = iface
@ -223,12 +221,10 @@ class EmaneManager:
:param iface: interface running emane :param iface: interface running emane
:return: net, node, or interface model configuration :return: net, node, or interface model configuration
""" """
model_name = emane_net.model.name model_name = emane_net.wireless_model.name
config = None
# try to retrieve interface specific configuration # try to retrieve interface specific configuration
if iface.node_id is not None: key = utils.iface_config_id(iface.node.id, iface.id)
key = utils.iface_config_id(iface.node.id, iface.node_id) config = self.get_config(key, model_name, default=False)
config = self.get_config(key, model_name, default=False)
# attempt to retrieve node specific config, when iface config is not present # attempt to retrieve node specific config, when iface config is not present
if not config: if not config:
config = self.get_config(iface.node.id, model_name, default=False) config = self.get_config(iface.node.id, model_name, default=False)
@ -239,7 +235,7 @@ class EmaneManager:
config = self.get_config(emane_net.id, model_name, default=False) config = self.get_config(emane_net.id, model_name, default=False)
# return default config values, when a config is not present # return default config values, when a config is not present
if not config: if not config:
config = emane_net.model.default_values() config = emane_net.wireless_model.default_values()
return config return config
def config_reset(self, node_id: int = None) -> None: def config_reset(self, node_id: int = None) -> None:
@ -272,7 +268,8 @@ class EmaneManager:
nodes = set() nodes = set()
for emane_net in self._emane_nets.values(): for emane_net in self._emane_nets.values():
for iface in emane_net.get_ifaces(): for iface in emane_net.get_ifaces():
nodes.add(iface.node) if isinstance(iface.node, CoreNode):
nodes.add(iface.node)
return nodes return nodes
def setup(self) -> EmaneState: def setup(self) -> EmaneState:
@ -323,7 +320,7 @@ class EmaneManager:
for emane_net, iface in self.get_ifaces(): for emane_net, iface in self.get_ifaces():
self.start_iface(emane_net, iface) self.start_iface(emane_net, iface)
def start_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None: def start_iface(self, emane_net: EmaneNet, iface: TunTap) -> None:
nem_id = self.next_nem_id(iface) nem_id = self.next_nem_id(iface)
nem_port = self.get_nem_port(iface) nem_port = self.get_nem_port(iface)
logger.info( logger.info(
@ -338,10 +335,10 @@ class EmaneManager:
self.start_daemon(iface) self.start_daemon(iface)
self.install_iface(iface, config) self.install_iface(iface, config)
def get_ifaces(self) -> List[Tuple[EmaneNet, CoreInterface]]: def get_ifaces(self) -> List[Tuple[EmaneNet, TunTap]]:
ifaces = [] ifaces = []
for emane_net in self._emane_nets.values(): for emane_net in self._emane_nets.values():
if not emane_net.model: if not emane_net.wireless_model:
logger.error("emane net(%s) has no model", emane_net.name) logger.error("emane net(%s) has no model", emane_net.name)
continue continue
for iface in emane_net.get_ifaces(): for iface in emane_net.get_ifaces():
@ -352,8 +349,9 @@ class EmaneManager:
iface.name, iface.name,
) )
continue continue
ifaces.append((emane_net, iface)) if isinstance(iface, TunTap):
return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].node_id)) ifaces.append((emane_net, iface))
return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].id))
def setup_control_channels( def setup_control_channels(
self, nem_id: int, iface: CoreInterface, config: Dict[str, str] self, nem_id: int, iface: CoreInterface, config: Dict[str, str]
@ -384,6 +382,8 @@ class EmaneManager:
service = EmaneEventService( service = EmaneEventService(
self, event_net.brname, eventgroup, int(eventport) self, event_net.brname, eventgroup, int(eventport)
) )
if self.doeventmonitor():
service.start()
self.services[event_net.brname] = service self.services[event_net.brname] = service
self.nem_service[nem_id] = service self.nem_service[nem_id] = service
except EventServiceException: except EventServiceException:
@ -484,7 +484,7 @@ class EmaneManager:
logger.exception("error writing to emane nem file") logger.exception("error writing to emane nem file")
def links_enabled(self) -> bool: def links_enabled(self) -> bool:
return self.session.options.get_config_int("link_enabled") == 1 return self.session.options.get_int("link_enabled") == 1
def poststartup(self) -> None: def poststartup(self) -> None:
""" """
@ -498,7 +498,7 @@ class EmaneManager:
"post startup for emane node: %s - %s", emane_net.id, emane_net.name "post startup for emane node: %s - %s", emane_net.id, emane_net.name
) )
for iface in emane_net.get_ifaces(): for iface in emane_net.get_ifaces():
emane_net.model.post_startup(iface) emane_net.wireless_model.post_startup(iface)
if events_enabled: if events_enabled:
iface.setposition() iface.setposition()
@ -550,9 +550,11 @@ class EmaneManager:
emane_net = self._emane_nets[node_id] emane_net = self._emane_nets[node_id]
logger.debug("checking emane model for node: %s", node_id) logger.debug("checking emane model for node: %s", node_id)
# skip nodes that already have a model set # skip nodes that already have a model set
if emane_net.model: if emane_net.wireless_model:
logger.debug( logger.debug(
"node(%s) already has model(%s)", emane_net.id, emane_net.model.name "node(%s) already has model(%s)",
emane_net.id,
emane_net.wireless_model.name,
) )
continue continue
# set model configured for node, due to legacy messaging configuration # set model configured for node, due to legacy messaging configuration
@ -602,8 +604,8 @@ class EmaneManager:
""" """
node = iface.node node = iface.node
loglevel = str(DEFAULT_LOG_LEVEL) loglevel = str(DEFAULT_LOG_LEVEL)
cfgloglevel = self.session.options.get_config_int("emane_log_level") cfgloglevel = self.session.options.get_int("emane_log_level", 2)
realtime = self.session.options.get_config_bool("emane_realtime", default=True) realtime = self.session.options.get_bool("emane_realtime", True)
if cfgloglevel: if cfgloglevel:
logger.info("setting user-defined emane log level: %d", cfgloglevel) logger.info("setting user-defined emane log level: %d", cfgloglevel)
loglevel = str(cfgloglevel) loglevel = str(cfgloglevel)
@ -622,9 +624,9 @@ class EmaneManager:
args = f"{emanecmd} -f {log_file} {platform_xml}" args = f"{emanecmd} -f {log_file} {platform_xml}"
node.host_cmd(args, cwd=self.session.directory) node.host_cmd(args, cwd=self.session.directory)
def install_iface(self, iface: CoreInterface, config: Dict[str, str]) -> None: def install_iface(self, iface: TunTap, config: Dict[str, str]) -> None:
external = config.get("external", "0") external = config.get("external", "0")
if isinstance(iface, TunTap) and external == "0": if external == "0":
iface.set_ips() iface.set_ips()
# at this point we register location handlers for generating # at this point we register location handlers for generating
# EMANE location events # EMANE location events
@ -636,20 +638,13 @@ class EmaneManager:
""" """
Returns boolean whether or not EMANE events will be monitored. Returns boolean whether or not EMANE events will be monitored.
""" """
# this support must be explicitly turned on; by default, CORE will return self.session.options.get_bool("emane_event_monitor", False)
# generate the EMANE events when nodes are moved
return self.session.options.get_config_bool("emane_event_monitor")
def genlocationevents(self) -> bool: def genlocationevents(self) -> bool:
""" """
Returns boolean whether or not EMANE events will be generated. Returns boolean whether or not EMANE events will be generated.
""" """
# By default, CORE generates EMANE location events when nodes return self.session.options.get_bool("emane_event_generate", True)
# are moved; this can be explicitly disabled in core.conf
tmp = self.session.options.get_config_bool("emane_event_generate")
if tmp is None:
tmp = not self.doeventmonitor()
return tmp
def handlelocationevent(self, rxnemid: int, eid: int, data: str) -> None: def handlelocationevent(self, rxnemid: int, eid: int, data: str) -> None:
""" """
@ -732,9 +727,6 @@ class EmaneManager:
self.session.broadcast_node(node) self.session.broadcast_node(node)
return True return True
def is_emane_net(self, net: Optional[CoreNetworkBase]) -> bool:
return isinstance(net, EmaneNet)
def emanerunning(self, node: CoreNode) -> bool: def emanerunning(self, node: CoreNode) -> bool:
""" """
Return True if an EMANE process associated with the given node is running, Return True if an EMANE process associated with the given node is running,

View file

@ -190,9 +190,9 @@ class EmaneLinkMonitor:
def start(self) -> None: def start(self) -> None:
options = self.emane_manager.session.options options = self.emane_manager.session.options
self.loss_threshold = options.get_config_int("loss_threshold") self.loss_threshold = options.get_int("loss_threshold")
self.link_interval = options.get_config_int("link_interval") self.link_interval = options.get_int("link_interval")
self.link_timeout = options.get_config_int("link_timeout") self.link_timeout = options.get_int("link_timeout")
self.initialize() self.initialize()
if not self.clients: if not self.clients:
logger.info("no valid emane models to monitor links") logger.info("no valid emane models to monitor links")

View file

@ -4,19 +4,15 @@ share the same MAC+PHY model.
""" """
import logging import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Type import time
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type, Union
from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.data import InterfaceData, LinkData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.enumerations import ( from core.emulator.enumerations import EventTypes, MessageFlags, RegisterTlvs
EventTypes, from core.errors import CoreCommandError, CoreError
LinkTypes, from core.nodes.base import CoreNetworkBase, CoreNode, NodeOptions
MessageFlags,
NodeTypes,
RegisterTlvs,
)
from core.errors import CoreError
from core.nodes.base import CoreNetworkBase, CoreNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -24,10 +20,7 @@ logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emane.emanemodel import EmaneModel from core.emane.emanemodel import EmaneModel
from core.emulator.session import Session from core.emulator.session import Session
from core.location.mobility import WirelessModel, WayPointMobility from core.location.mobility import WayPointMobility
OptionalEmaneModel = Optional[EmaneModel]
WirelessModelType = Type[WirelessModel]
try: try:
from emane.events import LocationEvent from emane.events import LocationEvent
@ -39,6 +32,120 @@ except ImportError:
logger.debug("compatible emane python bindings not installed") logger.debug("compatible emane python bindings not installed")
class TunTap(CoreInterface):
"""
TUN/TAP virtual device in TAP mode
"""
def __init__(
self,
_id: int,
name: str,
localname: str,
use_ovs: bool,
node: CoreNode = None,
server: "DistributedServer" = None,
) -> None:
super().__init__(_id, name, localname, use_ovs, node=node, server=server)
self.node: CoreNode = node
def startup(self) -> None:
"""
Startup logic for a tunnel tap.
:return: nothing
"""
self.up = True
def shutdown(self) -> None:
"""
Shutdown functionality for a tunnel tap.
:return: nothing
"""
if not self.up:
return
self.up = False
def waitfor(
self, func: Callable[[], int], attempts: int = 10, maxretrydelay: float = 0.25
) -> bool:
"""
Wait for func() to return zero with exponential backoff.
:param func: function to wait for a result of zero
:param attempts: number of attempts to wait for a zero result
:param maxretrydelay: maximum retry delay
:return: True if wait succeeded, False otherwise
"""
delay = 0.01
result = False
for i in range(1, attempts + 1):
r = func()
if r == 0:
result = True
break
msg = f"attempt {i} failed with nonzero exit status {r}"
if i < attempts + 1:
msg += ", retrying..."
logger.info(msg)
time.sleep(delay)
delay += delay
if delay > maxretrydelay:
delay = maxretrydelay
else:
msg += ", giving up"
logger.info(msg)
return result
def nodedevexists(self) -> int:
"""
Checks if device exists.
:return: 0 if device exists, 1 otherwise
"""
try:
self.node.node_net_client.device_show(self.name)
return 0
except CoreCommandError:
return 1
def waitfordevicenode(self) -> None:
"""
Check for presence of a node device - tap device may not appear right away waits.
:return: nothing
"""
logger.debug("waiting for device node: %s", self.name)
count = 0
while True:
result = self.waitfor(self.nodedevexists)
if result:
break
should_retry = count < 5
is_emane_running = self.node.session.emane.emanerunning(self.node)
if all([should_retry, is_emane_running]):
count += 1
else:
raise RuntimeError("node device failed to exist")
def set_ips(self) -> None:
"""
Set interface ip addresses.
:return: nothing
"""
self.waitfordevicenode()
for ip in self.ips():
self.node.node_net_client.create_address(self.name, str(ip))
@dataclass
class EmaneOptions(NodeOptions):
emane_model: str = None
"""name of emane model to associate an emane network to"""
class EmaneNet(CoreNetworkBase): class EmaneNet(CoreNetworkBase):
""" """
EMANE node contains NEM configuration and causes connected nodes EMANE node contains NEM configuration and causes connected nodes
@ -46,22 +153,26 @@ class EmaneNet(CoreNetworkBase):
Emane controller object that exists in a session. Emane controller object that exists in a session.
""" """
apitype: NodeTypes = NodeTypes.EMANE
linktype: LinkTypes = LinkTypes.WIRED
type: str = "wlan"
has_custom_iface: bool = True
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
server: DistributedServer = None, server: DistributedServer = None,
options: EmaneOptions = None,
) -> None: ) -> None:
super().__init__(session, _id, name, server) options = options or EmaneOptions()
super().__init__(session, _id, name, server, options)
self.conf: str = "" self.conf: str = ""
self.model: "OptionalEmaneModel" = None
self.mobility: Optional[WayPointMobility] = None self.mobility: Optional[WayPointMobility] = None
model_class = self.session.emane.get_model(options.emane_model)
self.wireless_model: Optional["EmaneModel"] = model_class(self.session, self.id)
if self.session.state == EventTypes.RUNTIME_STATE:
self.session.emane.add_node(self)
@classmethod
def create_options(cls) -> EmaneOptions:
return EmaneOptions()
def linkconfig( def linkconfig(
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
@ -69,18 +180,15 @@ class EmaneNet(CoreNetworkBase):
""" """
The CommEffect model supports link configuration. The CommEffect model supports link configuration.
""" """
if not self.model: if not self.wireless_model:
return return
self.model.linkconfig(iface, options, iface2) self.wireless_model.linkconfig(iface, options, iface2)
def config(self, conf: str) -> None:
self.conf = conf
def startup(self) -> None: def startup(self) -> None:
pass self.up = True
def shutdown(self) -> None: def shutdown(self) -> None:
pass self.up = False
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass pass
@ -88,30 +196,37 @@ class EmaneNet(CoreNetworkBase):
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass pass
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
raise CoreError("emane networks cannot be linked to other networks")
def updatemodel(self, config: Dict[str, str]) -> None: def updatemodel(self, config: Dict[str, str]) -> None:
if not self.model: """
raise CoreError(f"no model set to update for node({self.name})") Update configuration for the current model.
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: :param config: configuration to update model with
:return: nothing
"""
if not self.wireless_model:
raise CoreError(f"no model set to update for node({self.name})")
logger.info(
"node(%s) updating model(%s): %s", self.id, self.wireless_model.name, config
)
self.wireless_model.update_config(config)
def setmodel(
self,
model: Union[Type["EmaneModel"], Type["WayPointMobility"]],
config: Dict[str, str],
) -> None:
""" """
set the EmaneModel associated with this node set the EmaneModel associated with this node
""" """
if model.config_type == RegisterTlvs.WIRELESS: if model.config_type == RegisterTlvs.WIRELESS:
# EmaneModel really uses values from ConfigurableManager self.wireless_model = model(session=self.session, _id=self.id)
# when buildnemxml() is called, not during init() self.wireless_model.update_config(config)
self.model = model(session=self.session, _id=self.id)
self.model.update_config(config)
elif model.config_type == RegisterTlvs.MOBILITY: elif model.config_type == RegisterTlvs.MOBILITY:
self.mobility = model(session=self.session, _id=self.id) self.mobility = model(session=self.session, _id=self.id)
self.mobility.update_config(config) self.mobility.update_config(config)
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
links = super().links(flags) links = []
emane_manager = self.session.emane emane_manager = self.session.emane
# gather current emane links # gather current emane links
nem_ids = set() nem_ids = set()
@ -132,22 +247,44 @@ class EmaneNet(CoreNetworkBase):
# ignore incomplete links # ignore incomplete links
if (nem2, nem1) not in emane_links: if (nem2, nem1) not in emane_links:
continue continue
link = emane_manager.get_nem_link(nem1, nem2) link = emane_manager.get_nem_link(nem1, nem2, flags)
if link: if link:
links.append(link) links.append(link)
return links return links
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface: def create_tuntap(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
# TUN/TAP is not ready for addressing yet; the device may """
# take some time to appear, and installing it into a Create a tuntap interface for the provided node.
# namespace after it has been bound removes addressing;
# save addresses with the interface now :param node: node to create tuntap interface for
iface_id = node.newtuntap(iface_data.id, iface_data.name) :param iface_data: interface data to create interface with
node.attachnet(iface_id, self) :return: created tuntap interface
iface = node.get_iface(iface_id) """
iface.set_mac(iface_data.mac) with node.lock:
for ip in iface_data.get_ips(): if iface_data.id is not None and iface_data.id in node.ifaces:
iface.add_ip(ip) raise CoreError(
f"node({self.id}) interface({iface_data.id}) already exists"
)
iface_id = (
iface_data.id if iface_data.id is not None else node.next_iface_id()
)
name = iface_data.name if iface_data.name is not None else f"eth{iface_id}"
session_id = self.session.short_session_id()
localname = f"tap{node.id}.{iface_id}.{session_id}"
iface = TunTap(iface_id, name, localname, self.session.use_ovs(), node=node)
if iface_data.mac:
iface.set_mac(iface_data.mac)
for ip in iface_data.get_ips():
iface.add_ip(ip)
node.ifaces[iface_id] = iface
self.attach(iface)
if self.up:
iface.startup()
if self.session.state == EventTypes.RUNTIME_STATE: if self.session.state == EventTypes.RUNTIME_STATE:
self.session.emane.start_iface(self, iface) self.session.emane.start_iface(self, iface)
return iface return iface
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
raise CoreError(
f"emane network({self.name}) do not support adopting interfaces"
)

View file

@ -1,8 +1,5 @@
import atexit
import logging import logging
import os import os
import signal
import sys
from pathlib import Path from pathlib import Path
from typing import Dict, List, Type from typing import Dict, List, Type
@ -18,25 +15,6 @@ logger = logging.getLogger(__name__)
DEFAULT_EMANE_PREFIX: str = "/usr" DEFAULT_EMANE_PREFIX: str = "/usr"
def signal_handler(signal_number: int, _) -> None:
"""
Handle signals and force an exit with cleanup.
:param signal_number: signal number
:param _: ignored
:return: nothing
"""
logger.info("caught signal: %s", signal_number)
sys.exit(signal_number)
signal.signal(signal.SIGHUP, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGUSR1, signal_handler)
signal.signal(signal.SIGUSR2, signal_handler)
class CoreEmu: class CoreEmu:
""" """
Provides logic for creating and configuring CORE sessions and the nodes within them. Provides logic for creating and configuring CORE sessions and the nodes within them.
@ -70,9 +48,6 @@ class CoreEmu:
# check executables exist on path # check executables exist on path
self._validate_env() self._validate_env()
# catch exit event
atexit.register(self.shutdown)
def _validate_env(self) -> None: def _validate_env(self) -> None:
""" """
Validates executables CORE depends on exist on path. Validates executables CORE depends on exist on path.
@ -140,10 +115,8 @@ class CoreEmu:
:return: nothing :return: nothing
""" """
logger.info("shutting down all sessions") logger.info("shutting down all sessions")
sessions = self.sessions.copy() while self.sessions:
self.sessions.clear() _, session = self.sessions.popitem()
for _id in sessions:
session = sessions[_id]
session.shutdown() session.shutdown()
def create_session(self, _id: int = None, _cls: Type[Session] = Session) -> Session: def create_session(self, _id: int = None, _cls: Type[Session] = Session) -> Session:

View file

@ -92,6 +92,10 @@ class NodeOptions:
image: str = None image: str = None
emane: str = None emane: str = None
legacy: bool = False legacy: bool = False
# src, dst
binds: List[Tuple[str, str]] = field(default_factory=list)
# src, dst, unique, delete
volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list)
def set_position(self, x: float, y: float) -> None: def set_position(self, x: float, y: float) -> None:
""" """

View file

@ -15,6 +15,7 @@ from fabric import Connection
from invoke import UnexpectedExit from invoke import UnexpectedExit
from core import utils from core import utils
from core.emulator.links import CoreLink
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import get_requirements from core.executables import get_requirements
from core.nodes.interface import GreTap from core.nodes.interface import GreTap
@ -124,9 +125,7 @@ class DistributedController:
self.session: "Session" = session self.session: "Session" = session
self.servers: Dict[str, DistributedServer] = OrderedDict() self.servers: Dict[str, DistributedServer] = OrderedDict()
self.tunnels: Dict[int, Tuple[GreTap, GreTap]] = {} self.tunnels: Dict[int, Tuple[GreTap, GreTap]] = {}
self.address: str = self.session.options.get_config( self.address: str = self.session.options.get("distributed_address")
"distributed_address", default=None
)
def add_server(self, name: str, host: str) -> None: def add_server(self, name: str, host: str) -> None:
""" """
@ -183,21 +182,36 @@ class DistributedController:
def start(self) -> None: def start(self) -> None:
""" """
Start distributed network tunnels. Start distributed network tunnels for control networks.
:return: nothing :return: nothing
""" """
mtu = self.session.options.get_config_int("mtu") mtu = self.session.options.get_int("mtu")
for node_id in self.session.nodes: for node_id in self.session.nodes:
node = self.session.nodes[node_id] node = self.session.nodes[node_id]
if not isinstance(node, CoreNetwork): if not isinstance(node, CtrlNet) or node.serverintf is not None:
continue
if isinstance(node, CtrlNet) and node.serverintf is not None:
continue continue
for name in self.servers: for name in self.servers:
server = self.servers[name] server = self.servers[name]
self.create_gre_tunnel(node, server, mtu, True) self.create_gre_tunnel(node, server, mtu, True)
def create_gre_tunnels(self, core_link: CoreLink) -> None:
"""
Creates gre tunnels for a core link with a ptp network connection.
:param core_link: core link to create gre tunnel for
:return: nothing
"""
if not self.servers:
return
if not core_link.ptp:
raise CoreError(
"attempted to create gre tunnel for core link without a ptp network"
)
mtu = self.session.options.get_int("mtu")
for server in self.servers.values():
self.create_gre_tunnel(core_link.ptp, server, mtu, True)
def create_gre_tunnel( def create_gre_tunnel(
self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool
) -> Tuple[GreTap, GreTap]: ) -> Tuple[GreTap, GreTap]:

View file

@ -20,6 +20,17 @@ class MessageFlags(Enum):
TTY = 0x40 TTY = 0x40
class ConfigFlags(Enum):
"""
Configuration flags.
"""
NONE = 0x00
REQUEST = 0x01
UPDATE = 0x02
RESET = 0x03
class NodeTypes(Enum): class NodeTypes(Enum):
""" """
Node types. Node types.
@ -38,6 +49,7 @@ class NodeTypes(Enum):
CONTROL_NET = 13 CONTROL_NET = 13
DOCKER = 15 DOCKER = 15
LXC = 16 LXC = 16
WIRELESS = 17
class LinkTypes(Enum): class LinkTypes(Enum):

View file

@ -0,0 +1,256 @@
"""
Provides functionality for maintaining information about known links
for a session.
"""
import logging
from dataclasses import dataclass
from typing import Dict, Optional, Tuple, ValuesView
from core.emulator.data import LinkData, LinkOptions
from core.emulator.enumerations import LinkTypes, MessageFlags
from core.errors import CoreError
from core.nodes.base import NodeBase
from core.nodes.interface import CoreInterface
from core.nodes.network import PtpNet
logger = logging.getLogger(__name__)
LinkKeyType = Tuple[int, Optional[int], int, Optional[int]]
def create_key(
node1: NodeBase,
iface1: Optional[CoreInterface],
node2: NodeBase,
iface2: Optional[CoreInterface],
) -> LinkKeyType:
"""
Creates a unique key for tracking links.
:param node1: first node in link
:param iface1: node1 interface
:param node2: second node in link
:param iface2: node2 interface
:return: link key
"""
iface1_id = iface1.id if iface1 else None
iface2_id = iface2.id if iface2 else None
if node1.id < node2.id:
return node1.id, iface1_id, node2.id, iface2_id
else:
return node2.id, iface2_id, node1.id, iface1_id
@dataclass
class CoreLink:
"""
Provides a core link data structure.
"""
node1: NodeBase
iface1: Optional[CoreInterface]
node2: NodeBase
iface2: Optional[CoreInterface]
ptp: PtpNet = None
label: str = None
color: str = None
def key(self) -> LinkKeyType:
"""
Retrieve the key for this link.
:return: link key
"""
return create_key(self.node1, self.iface1, self.node2, self.iface2)
def is_unidirectional(self) -> bool:
"""
Checks if this link is considered unidirectional, due to current
iface configurations.
:return: True if unidirectional, False otherwise
"""
unidirectional = False
if self.iface1 and self.iface2:
unidirectional = self.iface1.options != self.iface2.options
return unidirectional
def options(self) -> LinkOptions:
"""
Retrieve the options for this link.
:return: options for this link
"""
if self.is_unidirectional():
options = self.iface1.options
else:
if self.iface1:
options = self.iface1.options
else:
options = self.iface2.options
return options
def get_data(self, message_type: MessageFlags, source: str = None) -> LinkData:
"""
Create link data for this link.
:param message_type: link data message type
:param source: source for this data
:return: link data
"""
iface1_data = self.iface1.get_data() if self.iface1 else None
iface2_data = self.iface2.get_data() if self.iface2 else None
return LinkData(
message_type=message_type,
type=LinkTypes.WIRED,
node1_id=self.node1.id,
node2_id=self.node2.id,
iface1=iface1_data,
iface2=iface2_data,
options=self.options(),
label=self.label,
color=self.color,
source=source,
)
def get_data_unidirectional(self, source: str = None) -> LinkData:
"""
Create other unidirectional link data.
:param source: source for this data
:return: unidirectional link data
"""
iface1_data = self.iface1.get_data() if self.iface1 else None
iface2_data = self.iface2.get_data() if self.iface2 else None
return LinkData(
message_type=MessageFlags.NONE,
type=LinkTypes.WIRED,
node1_id=self.node2.id,
node2_id=self.node1.id,
iface1=iface2_data,
iface2=iface1_data,
options=self.iface2.options,
label=self.label,
color=self.color,
source=source,
)
class LinkManager:
"""
Provides core link management.
"""
def __init__(self) -> None:
"""
Create a LinkManager instance.
"""
self._links: Dict[LinkKeyType, CoreLink] = {}
self._node_links: Dict[int, Dict[LinkKeyType, CoreLink]] = {}
def add(self, core_link: CoreLink) -> None:
"""
Add a core link to be tracked.
:param core_link: link to track
:return: nothing
"""
node1, iface1 = core_link.node1, core_link.iface1
node2, iface2 = core_link.node2, core_link.iface2
if core_link.key() in self._links:
raise CoreError(
f"node1({node1.name}) iface1({iface1.id}) "
f"node2({node2.name}) iface2({iface2.id}) link already exists"
)
logger.info(
"adding link from node(%s:%s) to node(%s:%s)",
node1.name,
iface1.name if iface1 else None,
node2.name,
iface2.name if iface2 else None,
)
self._links[core_link.key()] = core_link
node1_links = self._node_links.setdefault(node1.id, {})
node1_links[core_link.key()] = core_link
node2_links = self._node_links.setdefault(node2.id, {})
node2_links[core_link.key()] = core_link
def delete(
self,
node1: NodeBase,
iface1: Optional[CoreInterface],
node2: NodeBase,
iface2: Optional[CoreInterface],
) -> CoreLink:
"""
Remove a link from being tracked.
:param node1: first node in link
:param iface1: node1 interface
:param node2: second node in link
:param iface2: node2 interface
:return: removed core link
"""
key = create_key(node1, iface1, node2, iface2)
if key not in self._links:
raise CoreError(
f"node1({node1.name}) iface1({iface1.id}) "
f"node2({node2.name}) iface2({iface2.id}) is not linked"
)
logger.info(
"deleting link from node(%s:%s) to node(%s:%s)",
node1.name,
iface1.name if iface1 else None,
node2.name,
iface2.name if iface2 else None,
)
node1_links = self._node_links[node1.id]
node1_links.pop(key)
node2_links = self._node_links[node2.id]
node2_links.pop(key)
return self._links.pop(key)
def reset(self) -> None:
"""
Resets and clears all tracking information.
:return: nothing
"""
self._links.clear()
self._node_links.clear()
def get_link(
self,
node1: NodeBase,
iface1: Optional[CoreInterface],
node2: NodeBase,
iface2: Optional[CoreInterface],
) -> Optional[CoreLink]:
"""
Retrieve a link for provided values.
:param node1: first node in link
:param iface1: interface for node1
:param node2: second node in link
:param iface2: interface for node2
:return: core link if present, None otherwise
"""
key = create_key(node1, iface1, node2, iface2)
return self._links.get(key)
def links(self) -> ValuesView[CoreLink]:
"""
Retrieve all known links
:return: iterator for all known links
"""
return self._links.values()
def node_links(self, node: NodeBase) -> ValuesView[CoreLink]:
"""
Retrieve all links for a given node.
:param node: node to get links for
:return: node links
"""
return self._node_links.get(node.id, {}).values()

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,15 @@
from typing import Any, List from typing import Dict, List, Optional
from core.config import ( from core.config import ConfigBool, ConfigInt, ConfigString, Configuration
ConfigBool, from core.errors import CoreError
ConfigInt,
ConfigString,
ConfigurableManager,
ConfigurableOptions,
Configuration,
)
from core.emulator.enumerations import RegisterTlvs
from core.plugins.sdt import Sdt from core.plugins.sdt import Sdt
class SessionConfig(ConfigurableManager, ConfigurableOptions): class SessionConfig:
""" """
Provides session configuration. Provides session configuration.
""" """
name: str = "session"
options: List[Configuration] = [ options: List[Configuration] = [
ConfigString(id="controlnet", label="Control Network"), ConfigString(id="controlnet", label="Control Network"),
ConfigString(id="controlnet0", label="Control Network 0"), ConfigString(id="controlnet0", label="Control Network 0"),
@ -42,34 +34,54 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
ConfigInt(id="link_timeout", default="4", label="EMANE Link Timeout (sec)"), ConfigInt(id="link_timeout", default="4", label="EMANE Link Timeout (sec)"),
ConfigInt(id="mtu", default="0", label="MTU for All Devices"), ConfigInt(id="mtu", default="0", label="MTU for All Devices"),
] ]
config_type: RegisterTlvs = RegisterTlvs.UTILITY
def __init__(self) -> None: def __init__(self, config: Dict[str, str] = None) -> None:
super().__init__()
self.set_configs(self.default_values())
def get_config(
self,
_id: str,
node_id: int = ConfigurableManager._default_node,
config_type: str = ConfigurableManager._default_type,
default: Any = None,
) -> str:
""" """
Retrieves a specific configuration for a node and configuration type. Create a SessionConfig instance.
:param _id: specific configuration to retrieve :param config: configuration to initialize with
:param node_id: node id to store configuration for
:param config_type: configuration type to store configuration for
:param default: default value to return when value is not found
:return: configuration value
""" """
value = super().get_config(_id, node_id, config_type, default) self._config: Dict[str, str] = {x.id: x.default for x in self.options}
if value == "": self._config.update(config or {})
value = default
return value
def get_config_bool(self, name: str, default: Any = None) -> bool: def update(self, config: Dict[str, str]) -> None:
"""
Update current configuration with provided values.
:param config: configuration to update with
:return: nothing
"""
self._config.update(config)
def set(self, name: str, value: str) -> None:
"""
Set a configuration value.
:param name: name of configuration to set
:param value: value to set
:return: nothing
"""
self._config[name] = value
def get(self, name: str, default: str = None) -> Optional[str]:
"""
Retrieve configuration value.
:param name: name of configuration to get
:param default: value to return as default
:return: return found configuration value or default
"""
return self._config.get(name, default)
def all(self) -> Dict[str, str]:
"""
Retrieve all configuration options.
:return: configuration value dict
"""
return self._config
def get_bool(self, name: str, default: bool = None) -> bool:
""" """
Get configuration value as a boolean. Get configuration value as a boolean.
@ -77,12 +89,15 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
:param default: default value if not found :param default: default value if not found
:return: boolean for configuration value :return: boolean for configuration value
""" """
value = self.get_config(name) value = self._config.get(name)
if value is None and default is None:
raise CoreError(f"missing session options for {name}")
if value is None: if value is None:
return default return default
return value.lower() == "true" else:
return value.lower() == "true"
def get_config_int(self, name: str, default: Any = None) -> int: def get_int(self, name: str, default: int = None) -> int:
""" """
Get configuration value as int. Get configuration value as int.
@ -90,17 +105,10 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
:param default: default value if not found :param default: default value if not found
:return: int for configuration value :return: int for configuration value
""" """
value = self.get_config(name, default=default) value = self._config.get(name)
if value is not None: if value is None and default is None:
value = int(value) raise CoreError(f"missing session options for {name}")
return value if value is None:
return default
def config_reset(self, node_id: int = None) -> None: else:
""" return int(value)
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

@ -1,30 +1,31 @@
from typing import List from typing import List
BASH: str = "bash" BASH: str = "bash"
VNODED: str = "vnoded"
VCMD: str = "vcmd"
SYSCTL: str = "sysctl"
IP: str = "ip"
ETHTOOL: str = "ethtool" ETHTOOL: str = "ethtool"
TC: str = "tc" IP: str = "ip"
MOUNT: str = "mount" MOUNT: str = "mount"
UMOUNT: str = "umount"
OVS_VSCTL: str = "ovs-vsctl"
TEST: str = "test"
NFTABLES: str = "nft" NFTABLES: str = "nft"
OVS_VSCTL: str = "ovs-vsctl"
SYSCTL: str = "sysctl"
TC: str = "tc"
TEST: str = "test"
UMOUNT: str = "umount"
VCMD: str = "vcmd"
VNODED: str = "vnoded"
COMMON_REQUIREMENTS: List[str] = [ COMMON_REQUIREMENTS: List[str] = [
BASH, BASH,
NFTABLES,
ETHTOOL, ETHTOOL,
IP, IP,
MOUNT, MOUNT,
NFTABLES,
SYSCTL, SYSCTL,
TC, TC,
UMOUNT,
TEST, TEST,
UMOUNT,
VCMD,
VNODED,
] ]
VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD]
OVS_REQUIREMENTS: List[str] = [OVS_VSCTL] OVS_REQUIREMENTS: List[str] = [OVS_VSCTL]
@ -38,6 +39,4 @@ def get_requirements(use_ovs: bool) -> List[str]:
requirements = COMMON_REQUIREMENTS requirements = COMMON_REQUIREMENTS
if use_ovs: if use_ovs:
requirements += OVS_REQUIREMENTS requirements += OVS_REQUIREMENTS
else:
requirements += VCMD_REQUIREMENTS
return requirements return requirements

View file

@ -70,6 +70,9 @@ class CoreClient:
self.session: Optional[Session] = None self.session: Optional[Session] = None
self.user = getpass.getuser() self.user = getpass.getuser()
# menu options
self.show_throughputs: tk.BooleanVar = tk.BooleanVar(value=False)
# global service settings # global service settings
self.services: Dict[str, Set[str]] = {} self.services: Dict[str, Set[str]] = {}
self.config_services_groups: Dict[str, Set[str]] = {} self.config_services_groups: Dict[str, Set[str]] = {}
@ -242,9 +245,10 @@ class CoreClient:
logger.warning("unknown node event: %s", event) logger.warning("unknown node event: %s", event)
def enable_throughputs(self) -> None: def enable_throughputs(self) -> None:
self.handling_throughputs = self.client.throughputs( if not self.handling_throughputs:
self.session.id, self.handle_throughputs self.handling_throughputs = self.client.throughputs(
) self.session.id, self.handle_throughputs
)
def cancel_throughputs(self) -> None: def cancel_throughputs(self) -> None:
if self.handling_throughputs: if self.handling_throughputs:
@ -404,9 +408,11 @@ class CoreClient:
for edge in self.links.values(): for edge in self.links.values():
link = edge.link link = edge.link
if not definition: if not definition:
if link.iface1 and not link.iface1.mac: node1 = self.session.nodes[link.node1_id]
node2 = self.session.nodes[link.node2_id]
if nutils.is_container(node1) and link.iface1 and not link.iface1.mac:
link.iface1.mac = self.ifaces_manager.next_mac() link.iface1.mac = self.ifaces_manager.next_mac()
if link.iface2 and not link.iface2.mac: if nutils.is_container(node2) and link.iface2 and not link.iface2.mac:
link.iface2.mac = self.ifaces_manager.next_mac() link.iface2.mac = self.ifaces_manager.next_mac()
links.append(link) links.append(link)
if edge.asymmetric_link: if edge.asymmetric_link:
@ -429,13 +435,15 @@ class CoreClient:
definition, definition,
result, result,
) )
if self.show_throughputs.get():
self.enable_throughputs()
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Start Session Error", e) self.app.show_grpc_exception("Start Session Error", e)
return result, exceptions return result, exceptions
def stop_session(self, session_id: int = None) -> bool: def stop_session(self, session_id: int = None) -> bool:
if not session_id: session_id = session_id or self.session.id
session_id = self.session.id self.cancel_throughputs()
result = False result = False
try: try:
result = self.client.stop_session(session_id) result = self.client.stop_session(session_id)
@ -665,10 +673,10 @@ class CoreClient:
self.links[edge.token] = edge self.links[edge.token] = edge
src_node = edge.src.core_node src_node = edge.src.core_node
dst_node = edge.dst.core_node dst_node = edge.dst.core_node
if nutils.is_container(src_node): if edge.link.iface1:
src_iface_id = edge.link.iface1.id src_iface_id = edge.link.iface1.id
self.iface_to_edge[(src_node.id, src_iface_id)] = edge self.iface_to_edge[(src_node.id, src_iface_id)] = edge
if nutils.is_container(dst_node): if edge.link.iface2:
dst_iface_id = edge.link.iface2.id dst_iface_id = edge.link.iface2.id
self.iface_to_edge[(dst_node.id, dst_iface_id)] = edge self.iface_to_edge[(dst_node.id, dst_iface_id)] = edge
@ -741,6 +749,9 @@ class CoreClient:
configs.append(config) configs.append(config)
return configs return configs
def get_config_service_rendered(self, node_id: int, name: str) -> Dict[str, str]:
return self.client.get_config_service_rendered(self.session.id, node_id, name)
def get_config_service_configs_proto( def get_config_service_configs_proto(
self self
) -> List[configservices_pb2.ConfigServiceConfig]: ) -> List[configservices_pb2.ConfigServiceConfig]:
@ -774,6 +785,9 @@ class CoreClient:
) )
return config return config
def get_wireless_config(self, node_id: int) -> Dict[str, ConfigOption]:
return self.client.get_wireless_config(self.session.id, node_id)
def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]: def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]:
config = self.client.get_mobility_config(self.session.id, node_id) config = self.client.get_mobility_config(self.session.id, node_id)
logger.debug( logger.debug(

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -34,10 +34,10 @@ class ConfigServiceConfigDialog(Dialog):
self.core: "CoreClient" = app.core self.core: "CoreClient" = app.core
self.node: Node = node self.node: Node = node
self.service_name: str = service_name self.service_name: str = service_name
self.radiovar: tk.IntVar = tk.IntVar() self.radiovar: tk.IntVar = tk.IntVar(value=2)
self.radiovar.set(2)
self.directories: List[str] = [] self.directories: List[str] = []
self.templates: List[str] = [] self.templates: List[str] = []
self.rendered: Dict[str, str] = {}
self.dependencies: List[str] = [] self.dependencies: List[str] = []
self.executables: List[str] = [] self.executables: List[str] = []
self.startup_commands: List[str] = [] self.startup_commands: List[str] = []
@ -48,10 +48,9 @@ class ConfigServiceConfigDialog(Dialog):
self.default_shutdown: List[str] = [] self.default_shutdown: List[str] = []
self.validation_mode: Optional[ServiceValidationMode] = None self.validation_mode: Optional[ServiceValidationMode] = None
self.validation_time: Optional[int] = None self.validation_time: Optional[int] = None
self.validation_period: tk.StringVar = tk.StringVar() self.validation_period: tk.DoubleVar = tk.DoubleVar()
self.modes: List[str] = [] self.modes: List[str] = []
self.mode_configs: Dict[str, Dict[str, str]] = {} self.mode_configs: Dict[str, Dict[str, str]] = {}
self.notebook: Optional[ttk.Notebook] = None self.notebook: Optional[ttk.Notebook] = None
self.templates_combobox: Optional[ttk.Combobox] = None self.templates_combobox: Optional[ttk.Combobox] = None
self.modes_combobox: Optional[ttk.Combobox] = None self.modes_combobox: Optional[ttk.Combobox] = None
@ -61,6 +60,7 @@ class ConfigServiceConfigDialog(Dialog):
self.validation_time_entry: Optional[ttk.Entry] = None self.validation_time_entry: Optional[ttk.Entry] = None
self.validation_mode_entry: Optional[ttk.Entry] = None self.validation_mode_entry: Optional[ttk.Entry] = None
self.template_text: Optional[CodeText] = None self.template_text: Optional[CodeText] = None
self.rendered_text: Optional[CodeText] = None
self.validation_period_entry: Optional[ttk.Entry] = None self.validation_period_entry: Optional[ttk.Entry] = None
self.original_service_files: Dict[str, str] = {} self.original_service_files: Dict[str, str] = {}
self.temp_service_files: Dict[str, str] = {} self.temp_service_files: Dict[str, str] = {}
@ -87,7 +87,6 @@ class ConfigServiceConfigDialog(Dialog):
self.validation_mode = service.validation_mode self.validation_mode = service.validation_mode
self.validation_time = service.validation_timer self.validation_time = service.validation_timer
self.validation_period.set(service.validation_period) self.validation_period.set(service.validation_period)
defaults = self.core.client.get_config_service_defaults(self.service_name) defaults = self.core.client.get_config_service_defaults(self.service_name)
self.original_service_files = defaults.templates self.original_service_files = defaults.templates
self.temp_service_files = dict(self.original_service_files) self.temp_service_files = dict(self.original_service_files)
@ -95,6 +94,9 @@ class ConfigServiceConfigDialog(Dialog):
self.mode_configs = defaults.modes self.mode_configs = defaults.modes
self.config = ConfigOption.from_dict(defaults.config) self.config = ConfigOption.from_dict(defaults.config)
self.default_config = {x.name: x.value for x in self.config.values()} self.default_config = {x.name: x.value for x in self.config.values()}
self.rendered = self.core.get_config_service_rendered(
self.node.id, self.service_name
)
service_config = self.node.config_service_configs.get(self.service_name) service_config = self.node.config_service_configs.get(self.service_name)
if service_config: if service_config:
for key, value in service_config.config.items(): for key, value in service_config.config.items():
@ -110,7 +112,6 @@ class ConfigServiceConfigDialog(Dialog):
def draw(self) -> None: def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
# draw notebook # draw notebook
self.notebook = ttk.Notebook(self.top) self.notebook = ttk.Notebook(self.top)
self.notebook.grid(sticky=tk.NSEW, pady=PADY) self.notebook.grid(sticky=tk.NSEW, pady=PADY)
@ -125,6 +126,7 @@ class ConfigServiceConfigDialog(Dialog):
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky=tk.NSEW) tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1) tab.columnconfigure(0, weight=1)
tab.rowconfigure(2, weight=1)
self.notebook.add(tab, text="Directories/Files") self.notebook.add(tab, text="Directories/Files")
label = ttk.Label( label = ttk.Label(
@ -137,33 +139,54 @@ class ConfigServiceConfigDialog(Dialog):
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Directories") label = ttk.Label(frame, text="Directories")
label.grid(row=0, column=0, sticky=tk.W, padx=PADX) label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
directories_combobox = ttk.Combobox( state = "readonly" if self.directories else tk.DISABLED
frame, values=self.directories, state="readonly" directories_combobox = ttk.Combobox(frame, values=self.directories, state=state)
)
directories_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY) directories_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
if self.directories: if self.directories:
directories_combobox.current(0) directories_combobox.current(0)
label = ttk.Label(frame, text="Files")
label = ttk.Label(frame, text="Templates")
label.grid(row=1, column=0, sticky=tk.W, padx=PADX) label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
state = "readonly" if self.templates else tk.DISABLED
self.templates_combobox = ttk.Combobox( self.templates_combobox = ttk.Combobox(
frame, values=self.templates, state="readonly" frame, values=self.templates, state=state
) )
self.templates_combobox.bind( self.templates_combobox.bind(
"<<ComboboxSelected>>", self.handle_template_changed "<<ComboboxSelected>>", self.handle_template_changed
) )
self.templates_combobox.grid(row=1, column=1, sticky=tk.EW, pady=PADY) self.templates_combobox.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
# draw file template tab
self.template_text = CodeText(tab) notebook = ttk.Notebook(tab)
notebook.rowconfigure(0, weight=1)
notebook.columnconfigure(0, weight=1)
notebook.grid(sticky=tk.NSEW, pady=PADY)
# draw rendered file tab
rendered_tab = ttk.Frame(notebook, padding=FRAME_PAD)
rendered_tab.grid(sticky=tk.NSEW)
rendered_tab.rowconfigure(0, weight=1)
rendered_tab.columnconfigure(0, weight=1)
notebook.add(rendered_tab, text="Rendered")
self.rendered_text = CodeText(rendered_tab)
self.rendered_text.grid(sticky=tk.NSEW)
self.rendered_text.text.bind("<FocusOut>", self.update_template_file_data)
# draw template file tab
template_tab = ttk.Frame(notebook, padding=FRAME_PAD)
template_tab.grid(sticky=tk.NSEW)
template_tab.rowconfigure(0, weight=1)
template_tab.columnconfigure(0, weight=1)
notebook.add(template_tab, text="Template")
self.template_text = CodeText(template_tab)
self.template_text.grid(sticky=tk.NSEW) self.template_text.grid(sticky=tk.NSEW)
tab.rowconfigure(self.template_text.grid_info()["row"], weight=1) self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
if self.templates: if self.templates:
self.templates_combobox.current(0) self.templates_combobox.current(0)
self.template_text.text.delete(1.0, "end") template_name = self.templates[0]
self.template_text.text.insert( temp_data = self.temp_service_files[template_name]
"end", self.temp_service_files[self.templates[0]] self.template_text.set_text(temp_data)
) rendered_data = self.rendered[template_name]
self.template_text.text.bind("<FocusOut>", self.update_template_file_data) self.rendered_text.set_text(rendered_data)
else:
self.template_text.text.configure(state=tk.DISABLED)
self.rendered_text.text.configure(state=tk.DISABLED)
def draw_tab_config(self) -> None: def draw_tab_config(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
@ -243,7 +266,7 @@ class ConfigServiceConfigDialog(Dialog):
label = ttk.Label(frame, text="Validation Time") label = ttk.Label(frame, text="Validation Time")
label.grid(row=0, column=0, sticky=tk.W, padx=PADX) label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
self.validation_time_entry = ttk.Entry(frame) self.validation_time_entry = ttk.Entry(frame)
self.validation_time_entry.insert("end", self.validation_time) self.validation_time_entry.insert("end", str(self.validation_time))
self.validation_time_entry.config(state=tk.DISABLED) self.validation_time_entry.config(state=tk.DISABLED)
self.validation_time_entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY) self.validation_time_entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
@ -323,9 +346,11 @@ class ConfigServiceConfigDialog(Dialog):
self.destroy() self.destroy()
def handle_template_changed(self, event: tk.Event) -> None: def handle_template_changed(self, event: tk.Event) -> None:
template = self.templates_combobox.get() template_name = self.templates_combobox.get()
self.template_text.text.delete(1.0, "end") temp_data = self.temp_service_files[template_name]
self.template_text.text.insert("end", self.temp_service_files[template]) self.template_text.set_text(temp_data)
rendered = self.rendered[template_name]
self.rendered_text.set_text(rendered)
def handle_mode_changed(self, event: tk.Event) -> None: def handle_mode_changed(self, event: tk.Event) -> None:
mode = self.modes_combobox.get() mode = self.modes_combobox.get()
@ -333,10 +358,13 @@ class ConfigServiceConfigDialog(Dialog):
logger.info("mode config: %s", config) logger.info("mode config: %s", config)
self.config_frame.set_values(config) self.config_frame.set_values(config)
def update_template_file_data(self, event: tk.Event) -> None: def update_template_file_data(self, _event: tk.Event) -> None:
scrolledtext = event.widget
template = self.templates_combobox.get() template = self.templates_combobox.get()
self.temp_service_files[template] = scrolledtext.get(1.0, "end") self.temp_service_files[template] = self.rendered_text.get_text()
if self.rendered[template] != self.temp_service_files[template]:
self.modified_files.add(template)
return
self.temp_service_files[template] = self.template_text.get_text()
if self.temp_service_files[template] != self.original_service_files[template]: if self.temp_service_files[template] != self.original_service_files[template]:
self.modified_files.add(template) self.modified_files.add(template)
else: else:
@ -351,14 +379,24 @@ class ConfigServiceConfigDialog(Dialog):
return has_custom_templates or has_custom_config return has_custom_templates or has_custom_config
def click_defaults(self) -> None: def click_defaults(self) -> None:
# clear all saved state data
self.modified_files.clear()
self.node.config_service_configs.pop(self.service_name, None) self.node.config_service_configs.pop(self.service_name, None)
self.temp_service_files = dict(self.original_service_files)
# reset session definition and retrieve default rendered templates
self.core.start_session(definition=True)
self.rendered = self.core.get_config_service_rendered(
self.node.id, self.service_name
)
logger.info( logger.info(
"cleared config service config: %s", self.node.config_service_configs "cleared config service config: %s", self.node.config_service_configs
) )
self.temp_service_files = dict(self.original_service_files) # reset current selected file data and config data, if present
filename = self.templates_combobox.get() template_name = self.templates_combobox.get()
self.template_text.text.delete(1.0, "end") temp_data = self.temp_service_files[template_name]
self.template_text.text.insert("end", self.temp_service_files[filename]) self.template_text.set_text(temp_data)
rendered_data = self.rendered[template_name]
self.rendered_text.set_text(rendered_data)
if self.config_frame: if self.config_frame:
logger.info("resetting defaults: %s", self.default_config) logger.info("resetting defaults: %s", self.default_config)
self.config_frame.set_values(self.default_config) self.config_frame.set_values(self.default_config)

View file

@ -23,7 +23,7 @@ class ServicesSelectDialog(Dialog):
def __init__( def __init__(
self, master: tk.BaseWidget, app: "Application", current_services: Set[str] self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
) -> None: ) -> None:
super().__init__(app, "Node Services", master=master) super().__init__(app, "Node Config Services", master=master)
self.groups: Optional[ListboxScroll] = None self.groups: Optional[ListboxScroll] = None
self.services: Optional[CheckboxList] = None self.services: Optional[CheckboxList] = None
self.current: Optional[ListboxScroll] = None self.current: Optional[ListboxScroll] = None
@ -45,7 +45,7 @@ class ServicesSelectDialog(Dialog):
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
self.groups = ListboxScroll(label_frame) self.groups = ListboxScroll(label_frame)
self.groups.grid(sticky=tk.NSEW) self.groups.grid(sticky=tk.NSEW)
for group in sorted(self.app.core.services): for group in sorted(self.app.core.config_services_groups):
self.groups.listbox.insert(tk.END, group) self.groups.listbox.insert(tk.END, group)
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change) self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
self.groups.listbox.selection_set(0) self.groups.listbox.selection_set(0)
@ -86,7 +86,7 @@ class ServicesSelectDialog(Dialog):
index = selection[0] index = selection[0]
group = self.groups.listbox.get(index) group = self.groups.listbox.get(index)
self.services.clear() self.services.clear()
for name in sorted(self.app.core.services[group]): for name in sorted(self.app.core.config_services_groups[group]):
checked = name in self.current_services checked = name in self.current_services
self.services.add(name, checked) self.services.add(name, checked)
@ -147,7 +147,7 @@ class CustomNodesDialog(Dialog):
frame, text="Icon", compound=tk.LEFT, command=self.click_icon frame, text="Icon", compound=tk.LEFT, command=self.click_icon
) )
self.image_button.grid(sticky=tk.EW, pady=PADY) self.image_button.grid(sticky=tk.EW, pady=PADY)
button = ttk.Button(frame, text="Services", command=self.click_services) button = ttk.Button(frame, text="Config Services", command=self.click_services)
button.grid(sticky=tk.EW) button.grid(sticky=tk.EW)
def draw_node_buttons(self) -> None: def draw_node_buttons(self) -> None:

View file

@ -230,13 +230,8 @@ class NodeConfigDialog(Dialog):
if nutils.is_model(self.node): if nutils.is_model(self.node):
label = ttk.Label(frame, text="Type") label = ttk.Label(frame, text="Type")
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY) label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
combobox = ttk.Combobox( entry = ttk.Entry(frame, textvariable=self.type, state=tk.DISABLED)
frame, entry.grid(row=row, column=1, sticky=tk.EW)
textvariable=self.type,
values=list(nutils.NODE_MODELS),
state=combo_state,
)
combobox.grid(row=row, column=1, sticky=tk.EW)
row += 1 row += 1
# container image field # container image field
@ -275,7 +270,7 @@ class NodeConfigDialog(Dialog):
ifaces_scroll.listbox.bind("<<ListboxSelect>>", self.iface_select) ifaces_scroll.listbox.bind("<<ListboxSelect>>", self.iface_select)
# interfaces # interfaces
if self.canvas_node.ifaces: if nutils.is_container(self.node):
self.draw_ifaces() self.draw_ifaces()
self.draw_spacer() self.draw_spacer()

View file

@ -0,0 +1,55 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional
import grpc
from core.api.grpc.wrappers import ConfigOption, Node
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class WirelessConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
super().__init__(app, f"Wireless Configuration - {canvas_node.core_node.name}")
self.node: Node = canvas_node.core_node
self.config_frame: Optional[ConfigFrame] = None
self.config: Dict[str, ConfigOption] = {}
try:
config = self.node.wireless_config
if not config:
config = self.app.core.get_wireless_config(self.node.id)
self.config: Dict[str, ConfigOption] = config
self.draw()
except grpc.RpcError as e:
self.app.show_grpc_exception("Wireless Config Error", e)
self.has_error: bool = 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.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)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
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.node.wireless_config = self.config
self.destroy()

View file

@ -416,6 +416,8 @@ class Edge:
self.src_label2 = None self.src_label2 = None
self.dst_label = None self.dst_label = None
self.dst_label2 = None self.dst_label2 = None
if self.dst:
self.arc_common_edges()
def hide(self) -> None: def hide(self) -> None:
self.hidden = True self.hidden = True
@ -507,6 +509,7 @@ class CanvasWirelessEdge(Edge):
if self.src.hidden or self.dst.hidden: if self.src.hidden or self.dst.hidden:
self.hide() self.hide()
self.set_binding() self.set_binding()
self.arc_common_edges()
def set_binding(self) -> None: def set_binding(self) -> None:
self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info) self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
@ -758,6 +761,4 @@ class CanvasEdge(Edge):
self.src.delete_antenna() self.src.delete_antenna()
self.app.core.deleted_canvas_edges([self]) self.app.core.deleted_canvas_edges([self])
super().delete() super().delete()
if self.dst:
self.arc_common_edges()
self.manager.edges.pop(self.token, None) self.manager.edges.pop(self.token, None)

View file

@ -16,6 +16,7 @@ from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
from core.gui.dialogs.nodeconfig import NodeConfigDialog from core.gui.dialogs.nodeconfig import NodeConfigDialog
from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog
from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog
from core.gui.dialogs.wirelessconfig import WirelessConfigDialog
from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog
from core.gui.frames.node import NodeInfoFrame from core.gui.frames.node import NodeInfoFrame
from core.gui.graph import tags from core.gui.graph import tags
@ -219,6 +220,7 @@ class CanvasNode:
# clear existing menu # clear existing menu
self.context.delete(0, tk.END) self.context.delete(0, tk.END)
is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_wlan = self.core_node.type == NodeType.WIRELESS_LAN
is_wireless = self.core_node.type == NodeType.WIRELESS
is_emane = self.core_node.type == NodeType.EMANE is_emane = self.core_node.type == NodeType.EMANE
is_mobility = is_wlan or is_emane is_mobility = is_wlan or is_emane
if self.app.core.is_runtime(): if self.app.core.is_runtime():
@ -231,6 +233,10 @@ class CanvasNode:
self.context.add_command( self.context.add_command(
label="WLAN Config", command=self.show_wlan_config label="WLAN Config", command=self.show_wlan_config
) )
if is_wireless:
self.context.add_command(
label="Wireless Config", command=self.show_wireless_config
)
if is_mobility and self.core_node.id in self.app.core.mobility_players: if is_mobility and self.core_node.id in self.app.core.mobility_players:
self.context.add_command( self.context.add_command(
label="Mobility Player", command=self.show_mobility_player label="Mobility Player", command=self.show_mobility_player
@ -268,6 +274,10 @@ class CanvasNode:
self.context.add_command( self.context.add_command(
label="WLAN Config", command=self.show_wlan_config label="WLAN Config", command=self.show_wlan_config
) )
if is_wireless:
self.context.add_command(
label="Wireless Config", command=self.show_wireless_config
)
if is_mobility: if is_mobility:
self.context.add_command( self.context.add_command(
label="Mobility Config", command=self.show_mobility_config label="Mobility Config", command=self.show_mobility_config
@ -298,7 +308,10 @@ class CanvasNode:
other_iface = edge.other_iface(self) other_iface = edge.other_iface(self)
label = other_node.core_node.name label = other_node.core_node.name
if other_iface: if other_iface:
label = f"{label}:{other_iface.name}" iface_label = other_iface.id
if other_iface.name:
iface_label = other_iface.name
label = f"{label}:{iface_label}"
func_unlink = functools.partial(self.click_unlink, edge) func_unlink = functools.partial(self.click_unlink, edge)
unlink_menu.add_command(label=label, command=func_unlink) unlink_menu.add_command(label=label, command=func_unlink)
themes.style_menu(unlink_menu) themes.style_menu(unlink_menu)
@ -343,6 +356,10 @@ class CanvasNode:
dialog = NodeConfigDialog(self.app, self) dialog = NodeConfigDialog(self.app, self)
dialog.show() dialog.show()
def show_wireless_config(self) -> None:
dialog = WirelessConfigDialog(self.app, self)
dialog.show()
def show_wlan_config(self) -> None: def show_wlan_config(self) -> None:
dialog = WlanConfigDialog(self.app, self) dialog = WlanConfigDialog(self.app, self)
if not dialog.has_error: if not dialog.has_error:

View file

@ -53,6 +53,7 @@ class ImageEnum(Enum):
LINK = "link" LINK = "link"
HUB = "hub" HUB = "hub"
WLAN = "wlan" WLAN = "wlan"
WIRELESS = "wireless"
EMANE = "emane" EMANE = "emane"
RJ45 = "rj45" RJ45 = "rj45"
TUNNEL = "tunnel" TUNNEL = "tunnel"
@ -92,14 +93,15 @@ TYPE_MAP: Dict[Tuple[NodeType, str], ImageEnum] = {
(NodeType.DEFAULT, "host"): ImageEnum.HOST, (NodeType.DEFAULT, "host"): ImageEnum.HOST,
(NodeType.DEFAULT, "mdr"): ImageEnum.MDR, (NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
(NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER, (NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
(NodeType.HUB, ""): ImageEnum.HUB, (NodeType.HUB, None): ImageEnum.HUB,
(NodeType.SWITCH, ""): ImageEnum.SWITCH, (NodeType.SWITCH, None): ImageEnum.SWITCH,
(NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN, (NodeType.WIRELESS_LAN, None): ImageEnum.WLAN,
(NodeType.EMANE, ""): ImageEnum.EMANE, (NodeType.WIRELESS, None): ImageEnum.WIRELESS,
(NodeType.RJ45, ""): ImageEnum.RJ45, (NodeType.EMANE, None): ImageEnum.EMANE,
(NodeType.TUNNEL, ""): ImageEnum.TUNNEL, (NodeType.RJ45, None): ImageEnum.RJ45,
(NodeType.DOCKER, ""): ImageEnum.DOCKER, (NodeType.TUNNEL, None): ImageEnum.TUNNEL,
(NodeType.LXC, ""): ImageEnum.LXC, (NodeType.DOCKER, None): ImageEnum.DOCKER,
(NodeType.LXC, None): ImageEnum.LXC,
} }

View file

@ -241,10 +241,10 @@ class InterfaceManager:
dst_node = edge.dst.core_node dst_node = edge.dst.core_node
self.determine_subnets(edge.src, edge.dst) self.determine_subnets(edge.src, edge.dst)
src_iface = None src_iface = None
if nutils.is_container(src_node): if nutils.is_iface_node(src_node):
src_iface = self.create_iface(edge.src, edge.linked_wireless) src_iface = self.create_iface(edge.src, edge.linked_wireless)
dst_iface = None dst_iface = None
if nutils.is_container(dst_node): if nutils.is_iface_node(dst_node):
dst_iface = self.create_iface(edge.dst, edge.linked_wireless) dst_iface = self.create_iface(edge.dst, edge.linked_wireless)
link = Link( link = Link(
type=LinkType.WIRED, type=LinkType.WIRED,
@ -258,22 +258,26 @@ class InterfaceManager:
def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface: def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface:
node = canvas_node.core_node node = canvas_node.core_node
ip4, ip6 = self.get_ips(node) if nutils.is_bridge(node):
if wireless_link: iface_id = canvas_node.next_iface_id()
ip4_mask = WIRELESS_IP4_MASK iface = Interface(id=iface_id)
ip6_mask = WIRELESS_IP6_MASK
else: else:
ip4_mask = IP4_MASK ip4, ip6 = self.get_ips(node)
ip6_mask = IP6_MASK if wireless_link:
iface_id = canvas_node.next_iface_id() ip4_mask = WIRELESS_IP4_MASK
name = f"eth{iface_id}" ip6_mask = WIRELESS_IP6_MASK
iface = Interface( else:
id=iface_id, ip4_mask = IP4_MASK
name=name, ip6_mask = IP6_MASK
ip4=ip4, iface_id = canvas_node.next_iface_id()
ip4_mask=ip4_mask, name = f"eth{iface_id}"
ip6=ip6, iface = Interface(
ip6_mask=ip6_mask, id=iface_id,
) name=name,
ip4=ip4,
ip4_mask=ip4_mask,
ip6=ip6,
ip6_mask=ip6_mask,
)
logger.info("create node(%s) interface(%s)", node.name, iface) logger.info("create node(%s) interface(%s)", node.name, iface)
return iface return iface

View file

@ -235,7 +235,11 @@ class Menubar(tk.Menu):
menu.add_command( menu.add_command(
label="Configure Throughput", command=self.click_config_throughput label="Configure Throughput", command=self.click_config_throughput
) )
menu.add_checkbutton(label="Enable Throughput?", command=self.click_throughput) menu.add_checkbutton(
label="Enable Throughput?",
command=self.click_throughput,
variable=self.core.show_throughputs,
)
widget_menu.add_cascade(label="Throughput", menu=menu) widget_menu.add_cascade(label="Throughput", menu=menu)
def draw_widgets_menu(self) -> None: def draw_widgets_menu(self) -> None:
@ -393,7 +397,7 @@ class Menubar(tk.Menu):
dialog.show() dialog.show()
def click_throughput(self) -> None: def click_throughput(self) -> None:
if not self.core.handling_throughputs: if self.core.show_throughputs.get():
self.core.enable_throughputs() self.core.enable_throughputs()
else: else:
self.core.cancel_throughputs() self.core.cancel_throughputs()

View file

@ -18,12 +18,16 @@ NETWORK_NODES: List["NodeDraw"] = []
NODE_ICONS = {} NODE_ICONS = {}
CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC}
IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC} IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC}
WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} WIRELESS_NODES: Set[NodeType] = {
NodeType.WIRELESS_LAN,
NodeType.EMANE,
NodeType.WIRELESS,
}
RJ45_NODES: Set[NodeType] = {NodeType.RJ45} RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH} BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH}
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET} IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET}
MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"} NODE_MODELS: Set[str] = {"router", "PC", "mdr", "prouter"}
ROUTER_NODES: Set[str] = {"router", "mdr"} ROUTER_NODES: Set[str] = {"router", "mdr"}
ANTENNA_ICON: Optional[PhotoImage] = None ANTENNA_ICON: Optional[PhotoImage] = None
@ -46,6 +50,7 @@ def setup() -> None:
(ImageEnum.HUB, NodeType.HUB, "Hub"), (ImageEnum.HUB, NodeType.HUB, "Hub"),
(ImageEnum.SWITCH, NodeType.SWITCH, "Switch"), (ImageEnum.SWITCH, NodeType.SWITCH, "Switch"),
(ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"), (ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"),
(ImageEnum.WIRELESS, NodeType.WIRELESS, "Wireless"),
(ImageEnum.EMANE, NodeType.EMANE, "EMANE"), (ImageEnum.EMANE, NodeType.EMANE, "EMANE"),
(ImageEnum.RJ45, NodeType.RJ45, "RJ45"), (ImageEnum.RJ45, NodeType.RJ45, "RJ45"),
(ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"), (ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"),
@ -97,6 +102,10 @@ def is_custom(node: Node) -> bool:
return is_model(node) and node.model not in NODE_MODELS return is_model(node) and node.model not in NODE_MODELS
def is_iface_node(node: Node) -> bool:
return is_container(node) or is_bridge(node)
def get_custom_services(gui_config: GuiConfig, name: str) -> List[str]: def get_custom_services(gui_config: GuiConfig, name: str) -> List[str]:
for custom_node in gui_config.nodes: for custom_node in gui_config.nodes:
if custom_node.name == name: if custom_node.name == name:
@ -114,7 +123,7 @@ def _get_custom_file(config: GuiConfig, name: str) -> Optional[str]:
def get_icon(node: Node, app: "Application") -> PhotoImage: def get_icon(node: Node, app: "Application") -> PhotoImage:
scale = app.app_scale scale = app.app_scale
image = None image = None
# node icon was overriden with a specific value # node icon was overridden with a specific value
if node.icon: if node.icon:
try: try:
image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale) image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale)

View file

@ -257,6 +257,13 @@ class CodeText(ttk.Frame):
yscrollbar.grid(row=0, column=1, sticky=tk.NS) yscrollbar.grid(row=0, column=1, sticky=tk.NS)
self.text.configure(yscrollcommand=yscrollbar.set) self.text.configure(yscrollcommand=yscrollbar.set)
def get_text(self) -> str:
return self.text.get(1.0, tk.END)
def set_text(self, text: str) -> None:
self.text.delete(1.0, tk.END)
self.text.insert(tk.END, text.rstrip())
class Spinbox(ttk.Entry): class Spinbox(ttk.Entry):
def __init__(self, master: tk.BaseWidget = None, **kwargs: Any) -> None: def __init__(self, master: tk.BaseWidget = None, **kwargs: Any) -> None:

View file

@ -225,7 +225,6 @@ class WirelessModel(ConfigurableOptions):
""" """
config_type: RegisterTlvs = RegisterTlvs.WIRELESS config_type: RegisterTlvs = RegisterTlvs.WIRELESS
bitmap: str = None
position_callback: Callable[[CoreInterface], None] = None position_callback: Callable[[CoreInterface], None] = None
def __init__(self, session: "Session", _id: int) -> None: def __init__(self, session: "Session", _id: int) -> None:
@ -321,7 +320,8 @@ class BasicRangeModel(WirelessModel):
loss=self.loss, loss=self.loss,
jitter=self.jitter, jitter=self.jitter,
) )
iface.config(options) iface.options.update(options)
iface.set_config()
def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]: def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]:
""" """
@ -627,7 +627,7 @@ class WayPointMobility(WirelessModel):
moved_ifaces.append(iface) moved_ifaces.append(iface)
# calculate all ranges after moving nodes; this saves calculations # calculate all ranges after moving nodes; this saves calculations
self.net.model.update(moved_ifaces) self.net.wireless_model.update(moved_ifaces)
# TODO: check session state # TODO: check session state
self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround) self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround)
@ -705,7 +705,7 @@ class WayPointMobility(WirelessModel):
x, y, z = self.initial[node.id].coords x, y, z = self.initial[node.id].coords
self.setnodeposition(node, x, y, z) self.setnodeposition(node, x, y, z)
moved_ifaces.append(iface) moved_ifaces.append(iface)
self.net.model.update(moved_ifaces) self.net.wireless_model.update(moved_ifaces)
def addwaypoint( def addwaypoint(
self, self,

View file

@ -3,8 +3,10 @@ Defines the base logic for nodes used within core.
""" """
import abc import abc
import logging import logging
import shlex
import shutil import shutil
import threading import threading
from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from threading import RLock from threading import RLock
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union
@ -13,11 +15,10 @@ import netaddr
from core import utils from core import utils
from core.configservice.dependencies import ConfigServiceDependencies from core.configservice.dependencies import ConfigServiceDependencies
from core.emulator.data import InterfaceData, LinkData from core.emulator.data import InterfaceData, LinkOptions
from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import BASH, MOUNT, TEST, VCMD, VNODED from core.executables import BASH, MOUNT, TEST, VCMD, VNODED
from core.nodes.interface import DEFAULT_MTU, CoreInterface, TunTap, Veth from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.netclient import LinuxNetClient, get_net_client from core.nodes.netclient import LinuxNetClient, get_net_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,19 +35,106 @@ if TYPE_CHECKING:
PRIVATE_DIRS: List[Path] = [Path("/var/run"), Path("/var/log")] PRIVATE_DIRS: List[Path] = [Path("/var/run"), Path("/var/log")]
@dataclass
class Position:
"""
Helper class for Cartesian coordinate position
"""
x: float = 0.0
y: float = 0.0
z: float = 0.0
lon: float = None
lat: float = None
alt: float = None
def set(self, x: float = None, y: float = None, z: float = None) -> bool:
"""
Returns True if the position has actually changed.
:param x: x position
:param y: y position
:param z: z position
:return: True if position changed, False otherwise
"""
if self.x == x and self.y == y and self.z == z:
return False
self.x = x
self.y = y
self.z = z
return True
def get(self) -> Tuple[float, float, float]:
"""
Retrieve x,y,z position.
:return: x,y,z position tuple
"""
return self.x, self.y, self.z
def has_geo(self) -> bool:
return all(x is not None for x in [self.lon, self.lat, self.alt])
def set_geo(self, lon: float, lat: float, alt: float) -> None:
"""
Set geo position lon, lat, alt.
:param lon: longitude value
:param lat: latitude value
:param alt: altitude value
:return: nothing
"""
self.lon = lon
self.lat = lat
self.alt = alt
def get_geo(self) -> Tuple[float, float, float]:
"""
Retrieve current geo position lon, lat, alt.
:return: lon, lat, alt position tuple
"""
return self.lon, self.lat, self.alt
@dataclass
class NodeOptions:
"""
Base options for configuring a node.
"""
canvas: int = None
"""id of canvas for display within gui"""
icon: str = None
"""custom icon for display, None for default"""
@dataclass
class CoreNodeOptions(NodeOptions):
model: str = "PC"
"""model is used for providing a default set of services"""
services: List[str] = field(default_factory=list)
"""services to start within node"""
config_services: List[str] = field(default_factory=list)
"""config services to start within node"""
directory: Path = None
"""directory to define node, defaults to path under the session directory"""
legacy: bool = False
"""legacy nodes default to standard services"""
class NodeBase(abc.ABC): class NodeBase(abc.ABC):
""" """
Base class for CORE nodes (nodes and networks) Base class for CORE nodes (nodes and networks)
""" """
apitype: Optional[NodeTypes] = None
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
server: "DistributedServer" = None, server: "DistributedServer" = None,
options: NodeOptions = None,
) -> None: ) -> None:
""" """
Creates a NodeBase instance. Creates a NodeBase instance.
@ -56,27 +144,29 @@ class NodeBase(abc.ABC):
:param name: object name :param name: object name
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param options: options to create node with
""" """
self.session: "Session" = session self.session: "Session" = session
if _id is None: self.id: int = _id if _id is not None else self.session.next_node_id()
_id = session.next_node_id() self.name: str = name or f"{self.__class__.__name__}{self.id}"
self.id: int = _id
if name is None:
name = f"o{self.id}"
self.name: str = name
self.server: "DistributedServer" = server self.server: "DistributedServer" = server
self.type: Optional[str] = None self.model: Optional[str] = None
self.services: CoreServices = [] self.services: CoreServices = []
self.ifaces: Dict[int, CoreInterface] = {} self.ifaces: Dict[int, CoreInterface] = {}
self.iface_id: int = 0 self.iface_id: int = 0
self.canvas: Optional[int] = None
self.icon: Optional[str] = None
self.position: Position = Position() self.position: Position = Position()
self.up: bool = False self.up: bool = False
self.lock: RLock = RLock()
self.net_client: LinuxNetClient = get_net_client( self.net_client: LinuxNetClient = get_net_client(
self.session.use_ovs(), self.host_cmd self.session.use_ovs(), self.host_cmd
) )
options = options if options else NodeOptions()
self.canvas: Optional[int] = options.canvas
self.icon: Optional[str] = options.icon
@classmethod
def create_options(cls) -> NodeOptions:
return NodeOptions()
@abc.abstractmethod @abc.abstractmethod
def startup(self) -> None: def startup(self) -> None:
@ -96,6 +186,18 @@ class NodeBase(abc.ABC):
""" """
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
"""
Adopt an interface, placing within network namespacing for containers
and setting to bridge masters for network like nodes.
:param iface: interface to adopt
:param name: proper name to use for interface
:return: nothing
"""
raise NotImplementedError
def host_cmd( def host_cmd(
self, self,
args: str, args: str,
@ -120,6 +222,19 @@ class NodeBase(abc.ABC):
else: else:
return self.server.remote_cmd(args, env, cwd, wait) return self.server.remote_cmd(args, env, cwd, wait)
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
"""
Runs a command that is in the context of a node, default is to run a standard
host command.
:param args: command to run
:param wait: True to wait for status, False otherwise
:param shell: True to use shell, False otherwise
:return: combined stdout and stderr
:raises CoreCommandError: when a non-zero exit status occurs
"""
return self.host_cmd(args, wait=wait, shell=shell)
def setposition(self, x: float = None, y: float = None, z: float = None) -> bool: def setposition(self, x: float = None, y: float = None, z: float = None) -> bool:
""" """
Set the (x,y,z) position of the object. Set the (x,y,z) position of the object.
@ -139,6 +254,71 @@ class NodeBase(abc.ABC):
""" """
return self.position.get() return self.position.get()
def create_iface(
self, iface_data: InterfaceData = None, options: LinkOptions = None
) -> CoreInterface:
"""
Creates an interface and adopts it to a node.
:param iface_data: data to create interface with
:param options: options to create interface with
:return: created interface
"""
with self.lock:
if iface_data and iface_data.id is not None:
if iface_data.id in self.ifaces:
raise CoreError(
f"node({self.id}) interface({iface_data.id}) already exists"
)
iface_id = iface_data.id
else:
iface_id = self.next_iface_id()
mtu = DEFAULT_MTU
if iface_data and iface_data.mtu is not None:
mtu = iface_data.mtu
unique_name = f"{self.id}.{iface_id}.{self.session.short_session_id()}"
name = f"veth{unique_name}"
localname = f"beth{unique_name}"
iface = CoreInterface(
iface_id,
name,
localname,
self.session.use_ovs(),
mtu,
self,
self.server,
)
if iface_data:
if iface_data.mac:
iface.set_mac(iface_data.mac)
for ip in iface_data.get_ips():
iface.add_ip(ip)
if iface_data.name:
name = iface_data.name
if options:
iface.options.update(options)
self.ifaces[iface_id] = iface
if self.up:
iface.startup()
self.adopt_iface(iface, name)
else:
iface.name = name
return iface
def delete_iface(self, iface_id: int) -> CoreInterface:
"""
Delete an interface.
:param iface_id: interface id to delete
:return: the removed interface
"""
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)
logger.info("node(%s) removing interface(%s)", self.name, iface.name)
iface.shutdown()
return iface
def get_iface(self, iface_id: int) -> CoreInterface: def get_iface(self, iface_id: int) -> CoreInterface:
""" """
Retrieve interface based on id. Retrieve interface based on id.
@ -191,15 +371,6 @@ class NodeBase(abc.ABC):
self.iface_id += 1 self.iface_id += 1
return iface_id return iface_id
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
"""
Build link data for this node.
:param flags: message flags
:return: list of link data
"""
return []
class CoreNodeBase(NodeBase): class CoreNodeBase(NodeBase):
""" """
@ -212,6 +383,7 @@ class CoreNodeBase(NodeBase):
_id: int = None, _id: int = None,
name: str = None, name: str = None,
server: "DistributedServer" = None, server: "DistributedServer" = None,
options: NodeOptions = None,
) -> None: ) -> None:
""" """
Create a CoreNodeBase instance. Create a CoreNodeBase instance.
@ -222,19 +394,11 @@ class CoreNodeBase(NodeBase):
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server, options)
self.config_services: Dict[str, "ConfigService"] = {} self.config_services: Dict[str, "ConfigService"] = {}
self.directory: Optional[Path] = None self.directory: Optional[Path] = None
self.tmpnodedir: bool = False self.tmpnodedir: bool = False
@abc.abstractmethod
def startup(self) -> None:
raise NotImplementedError
@abc.abstractmethod
def shutdown(self) -> None:
raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def create_dir(self, dir_path: Path) -> None: def create_dir(self, dir_path: Path) -> None:
""" """
@ -270,19 +434,6 @@ class CoreNodeBase(NodeBase):
""" """
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
"""
Runs a command within a node container.
:param args: command to run
:param wait: True to wait for status, False otherwise
:param shell: True to use shell, False otherwise
:return: combined stdout and stderr
:raises CoreCommandError: when a non-zero exit status occurs
"""
raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def termcmdstring(self, sh: str) -> str: def termcmdstring(self, sh: str) -> str:
""" """
@ -293,19 +444,6 @@ class CoreNodeBase(NodeBase):
""" """
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod
def new_iface(
self, net: "CoreNetworkBase", iface_data: InterfaceData
) -> CoreInterface:
"""
Create a new interface.
:param net: network to associate with
:param iface_data: interface data for new interface
:return: interface index
"""
raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def path_exists(self, path: str) -> bool: def path_exists(self, path: str) -> bool:
""" """
@ -318,7 +456,7 @@ class CoreNodeBase(NodeBase):
def host_path(self, path: Path, is_dir: bool = False) -> Path: def host_path(self, path: Path, is_dir: bool = False) -> Path:
""" """
Return the name of a node"s file on the host filesystem. Return the name of a node's file on the host filesystem.
:param path: path to translate to host path :param path: path to translate to host path
:param is_dir: True if path is a directory path, False otherwise :param is_dir: True if path is a directory path, False otherwise
@ -387,60 +525,12 @@ class CoreNodeBase(NodeBase):
:return: nothing :return: nothing
""" """
preserve = self.session.options.get_config("preservedir") == "1" preserve = self.session.options.get_int("preservedir") == 1
if preserve: if preserve:
return return
if self.tmpnodedir: if self.tmpnodedir:
self.host_cmd(f"rm -rf {self.directory}") self.host_cmd(f"rm -rf {self.directory}")
def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
"""
Add network interface to node and set the network interface index if successful.
:param iface: network interface to add
:param iface_id: interface id
:return: nothing
"""
if iface_id in self.ifaces:
raise CoreError(f"interface({iface_id}) already exists")
self.ifaces[iface_id] = iface
iface.node_id = iface_id
def delete_iface(self, iface_id: int) -> None:
"""
Delete a network interface
:param iface_id: interface index to delete
:return: nothing
"""
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)
logger.info("node(%s) removing interface(%s)", self.name, iface.name)
iface.detachnet()
iface.shutdown()
def attachnet(self, iface_id: int, net: "CoreNetworkBase") -> None:
"""
Attach a network.
:param iface_id: interface of index to attach
:param net: network to attach
:return: nothing
"""
iface = self.get_iface(iface_id)
iface.attachnet(net)
def detachnet(self, iface_id: int) -> None:
"""
Detach network interface.
:param iface_id: interface id to detach
:return: nothing
"""
iface = self.get_iface(iface_id)
iface.detachnet()
def setposition(self, x: float = None, y: float = None, z: float = None) -> None: def setposition(self, x: float = None, y: float = None, z: float = None) -> None:
""" """
Set position. Set position.
@ -455,40 +545,19 @@ class CoreNodeBase(NodeBase):
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.setposition() iface.setposition()
def commonnets(
self, node: "CoreNodeBase", want_ctrl: bool = False
) -> List[Tuple["CoreNetworkBase", CoreInterface, CoreInterface]]:
"""
Given another node or net object, return common networks between
this node and that object. A list of tuples is returned, with each tuple
consisting of (network, interface1, interface2).
:param node: node to get common network with
:param want_ctrl: flag set to determine if control network are wanted
:return: tuples of common networks
"""
common = []
for iface1 in self.get_ifaces(control=want_ctrl):
for iface2 in node.get_ifaces():
if iface1.net == iface2.net:
common.append((iface1.net, iface1, iface2))
return common
class CoreNode(CoreNodeBase): class CoreNode(CoreNodeBase):
""" """
Provides standard core node logic. Provides standard core node logic.
""" """
apitype: NodeTypes = NodeTypes.DEFAULT
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
directory: Path = None,
server: "DistributedServer" = None, server: "DistributedServer" = None,
options: CoreNodeOptions = None,
) -> None: ) -> None:
""" """
Create a CoreNode instance. Create a CoreNode instance.
@ -496,19 +565,37 @@ class CoreNode(CoreNodeBase):
:param session: core session instance :param session: core session instance
:param _id: object id :param _id: object id
:param name: object name :param name: object name
:param directory: node directory
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param options: options to create node with
""" """
super().__init__(session, _id, name, server) options = options or CoreNodeOptions()
self.directory: Optional[Path] = directory super().__init__(session, _id, name, server, options)
self.directory: Optional[Path] = options.directory
self.ctrlchnlname: Path = self.session.directory / self.name self.ctrlchnlname: Path = self.session.directory / self.name
self.pid: Optional[int] = None self.pid: Optional[int] = None
self.lock: RLock = RLock()
self._mounts: List[Tuple[Path, Path]] = [] self._mounts: List[Tuple[Path, Path]] = []
self.node_net_client: LinuxNetClient = self.create_node_net_client( self.node_net_client: LinuxNetClient = self.create_node_net_client(
self.session.use_ovs() self.session.use_ovs()
) )
options = options or CoreNodeOptions()
self.model: Optional[str] = options.model
# setup services
if options.legacy or options.services:
logger.debug("set node type: %s", self.model)
self.session.services.add_services(self, self.model, options.services)
# add config services
config_services = options.config_services
if not options.legacy and not config_services and not options.services:
config_services = self.session.services.default_services.get(self.model, [])
logger.info("setting node config services: %s", config_services)
for name in config_services:
service_class = self.session.service_manager.get_service(name)
self.add_config_service(service_class)
@classmethod
def create_options(cls) -> CoreNodeOptions:
return CoreNodeOptions()
def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient:
""" """
@ -585,6 +672,10 @@ class CoreNode(CoreNodeBase):
self._mounts = [] self._mounts = []
# shutdown all interfaces # shutdown all interfaces
for iface in self.get_ifaces(): for iface in self.get_ifaces():
try:
self.node_net_client.device_flush(iface.name)
except CoreCommandError:
pass
iface.shutdown() iface.shutdown()
# kill node process if present # kill node process if present
try: try:
@ -604,7 +695,7 @@ class CoreNode(CoreNodeBase):
finally: finally:
self.rmnodedir() self.rmnodedir()
def _create_cmd(self, args: str, shell: bool = False) -> str: def create_cmd(self, args: str, shell: bool = False) -> str:
""" """
Create command used to run commands within the context of a node. Create command used to run commands within the context of a node.
@ -613,7 +704,7 @@ class CoreNode(CoreNodeBase):
:return: node command :return: node command
""" """
if shell: if shell:
args = f'{BASH} -c "{args}"' args = f"{BASH} -c {shlex.quote(args)}"
return f"{VCMD} -c {self.ctrlchnlname} -- {args}" return f"{VCMD} -c {self.ctrlchnlname} -- {args}"
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
@ -627,7 +718,7 @@ class CoreNode(CoreNodeBase):
:return: combined stdout and stderr :return: combined stdout and stderr
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
args = self._create_cmd(args, shell) args = self.create_cmd(args, shell)
if self.server is None: if self.server is None:
return utils.cmd(args, wait=wait, shell=shell) return utils.cmd(args, wait=wait, shell=shell)
else: else:
@ -653,7 +744,7 @@ class CoreNode(CoreNodeBase):
:param sh: shell to execute command in :param sh: shell to execute command in
:return: str :return: str
""" """
terminal = self._create_cmd(sh) terminal = self.create_cmd(sh)
if self.server is None: if self.server is None:
return terminal return terminal
else: else:
@ -691,150 +782,6 @@ class CoreNode(CoreNodeBase):
self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}") self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}")
self._mounts.append((src_path, target_path)) self._mounts.append((src_path, target_path))
def next_iface_id(self) -> int:
"""
Retrieve a new interface index.
:return: new interface index
"""
with self.lock:
return super().next_iface_id()
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:
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}"
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:
"""
Create a new tunnel tap.
:param iface_id: interface id
:param ifname: interface name
:return: interface index
"""
with self.lock:
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, name, localname, node=self)
if self.up:
tuntap.startup()
try:
self.add_iface(tuntap, iface_id)
except CoreError as e:
tuntap.shutdown()
raise e
return iface_id
def set_mac(self, iface_id: int, mac: str) -> None:
"""
Set hardware address for an interface.
:param iface_id: id of interface to set hardware address for
:param mac: mac address to set
:return: nothing
:raises CoreCommandError: when a non-zero exit status occurs
"""
iface = self.get_iface(iface_id)
iface.set_mac(mac)
if self.up:
self.node_net_client.device_mac(iface.name, str(iface.mac))
def add_ip(self, iface_id: int, ip: str) -> None:
"""
Add an ip address to an interface in the format "10.0.0.1/24".
:param iface_id: id of interface to add address to
:param ip: address to add to interface
:return: nothing
:raises CoreError: when ip address provided is invalid
:raises CoreCommandError: when a non-zero exit status occurs
"""
iface = self.get_iface(iface_id)
iface.add_ip(ip)
if self.up:
# ipv4 check
broadcast = None
if netaddr.valid_ipv4(ip):
broadcast = "+"
self.node_net_client.create_address(iface.name, ip, broadcast)
def remove_ip(self, iface_id: int, ip: str) -> None:
"""
Remove an ip address from an interface in the format "10.0.0.1/24".
:param iface_id: id of interface to delete address from
:param ip: ip address to remove from interface
:return: nothing
:raises CoreError: when ip address provided is invalid
:raises CoreCommandError: when a non-zero exit status occurs
"""
iface = self.get_iface(iface_id)
iface.remove_ip(ip)
if self.up:
self.node_net_client.delete_address(iface.name, ip)
def ifup(self, iface_id: int) -> None:
"""
Bring an interface up.
:param iface_id: index of interface to bring up
:return: nothing
"""
if self.up:
iface = self.get_iface(iface_id)
self.node_net_client.device_up(iface.name)
def new_iface(
self, net: "CoreNetworkBase", iface_data: InterfaceData
) -> CoreInterface:
"""
Create a new network interface.
:param net: network to associate with
:param iface_data: interface data for new interface
:return: interface index
"""
with self.lock:
if net.has_custom_iface:
return net.custom_iface(self, iface_data)
else:
iface_id = iface_data.id
if iface_id is not None and iface_id in self.ifaces:
raise CoreError(
f"node({self.name}) already has interface({iface_id})"
)
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)
for ip in iface_data.get_ips():
self.add_ip(iface_id, ip)
self.ifup(iface_id)
return self.get_iface(iface_id)
def _find_parent_path(self, path: Path) -> Optional[Path]: def _find_parent_path(self, path: Path) -> Optional[Path]:
""" """
Check if there is a mounted parent directory created for this node. Check if there is a mounted parent directory created for this node.
@ -910,21 +857,62 @@ class CoreNode(CoreNodeBase):
if mode is not None: if mode is not None:
self.host_cmd(f"chmod {mode:o} {host_path}") self.host_cmd(f"chmod {mode:o} {host_path}")
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
"""
Adopt interface to the network namespace of the node and setting
the proper name provided.
:param iface: interface to adopt
:param name: proper name for interface
:return: nothing
"""
# TODO: container, checksums off (container only?)
# TODO: container, get flow id (container only?)
# validate iface belongs to node and get id
iface_id = self.get_iface_id(iface)
if iface_id == -1:
raise CoreError(f"adopting unknown iface({iface.name})")
# add iface to container namespace
self.net_client.device_ns(iface.name, str(self.pid))
# use default iface name for container, if a unique name was not provided
if iface.name == name:
name = f"eth{iface_id}"
self.node_net_client.device_name(iface.name, name)
iface.name = name
# turn checksums off
self.node_net_client.checksums_off(iface.name)
# retrieve flow id for container
iface.flow_id = self.node_net_client.get_ifindex(iface.name)
logger.debug("interface flow index: %s - %s", iface.name, iface.flow_id)
# set mac address
if iface.mac:
self.node_net_client.device_mac(iface.name, str(iface.mac))
logger.debug("interface mac: %s - %s", iface.name, iface.mac)
# set all addresses
for ip in iface.ips():
# ipv4 check
broadcast = None
if netaddr.valid_ipv4(ip):
broadcast = "+"
self.node_net_client.create_address(iface.name, str(ip), broadcast)
# configure iface options
iface.set_config()
# set iface up
self.node_net_client.device_up(iface.name)
class CoreNetworkBase(NodeBase): class CoreNetworkBase(NodeBase):
""" """
Base class for networks Base class for networks
""" """
linktype: LinkTypes = LinkTypes.WIRED
has_custom_iface: bool = False
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
_id: int, _id: int,
name: str, name: str,
server: "DistributedServer" = None, server: "DistributedServer" = None,
options: NodeOptions = None,
) -> None: ) -> None:
""" """
Create a CoreNetworkBase instance. Create a CoreNetworkBase instance.
@ -934,64 +922,15 @@ class CoreNetworkBase(NodeBase):
:param name: object name :param name: object name
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param options: options to create node with
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server, options)
self.mtu: int = DEFAULT_MTU mtu = self.session.options.get_int("mtu")
self.mtu: int = mtu if mtu > 0 else DEFAULT_MTU
self.brname: Optional[str] = None self.brname: Optional[str] = None
self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {} self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {}
self.linked_lock: threading.Lock = threading.Lock() self.linked_lock: threading.Lock = threading.Lock()
@abc.abstractmethod
def startup(self) -> None:
"""
Each object implements its own startup method.
:return: nothing
"""
raise NotImplementedError
@abc.abstractmethod
def shutdown(self) -> None:
"""
Each object implements its own shutdown method.
:return: nothing
"""
raise NotImplementedError
@abc.abstractmethod
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
"""
Link network to another.
:param net: network to link with
:return: created interface
"""
raise NotImplementedError
@abc.abstractmethod
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
"""
Defines custom logic for creating an interface, if required.
:param node: node to create interface for
:param iface_data: data for creating interface
:return: created interface
"""
raise NotImplementedError
def get_linked_iface(self, net: "CoreNetworkBase") -> Optional[CoreInterface]:
"""
Return the interface that links this net with another net.
:param net: interface to get link for
:return: interface the provided network is linked to
"""
for iface in self.get_ifaces():
if iface.othernet == net:
return iface
return None
def attach(self, iface: CoreInterface) -> None: def attach(self, iface: CoreInterface) -> None:
""" """
Attach network interface. Attach network interface.
@ -999,9 +938,10 @@ class CoreNetworkBase(NodeBase):
:param iface: network interface to attach :param iface: network interface to attach
:return: nothing :return: nothing
""" """
i = self.next_iface_id() iface_id = self.next_iface_id()
self.ifaces[i] = iface self.ifaces[iface_id] = iface
iface.net_id = i iface.net = self
iface.net_id = iface_id
with self.linked_lock: with self.linked_lock:
self.linked[iface] = {} self.linked[iface] = {}
@ -1013,118 +953,7 @@ class CoreNetworkBase(NodeBase):
:return: nothing :return: nothing
""" """
del self.ifaces[iface.net_id] del self.ifaces[iface.net_id]
iface.net = None
iface.net_id = None iface.net_id = None
with self.linked_lock: with self.linked_lock:
del self.linked[iface] del self.linked[iface]
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
"""
Build link data objects for this network. Each link object describes a link
between this network and a node.
:param flags: message type
:return: list of link data
"""
all_links = []
# build a link message from this network node to each node having a
# connected interface
for iface in self.get_ifaces():
unidirectional = 0
linked_node = iface.node
if linked_node is None:
# two layer-2 switches/hubs linked together
if not iface.othernet:
continue
linked_node = iface.othernet
if linked_node.id == self.id:
continue
if iface.local_options != iface.options:
unidirectional = 1
iface_data = iface.get_data()
link_data = LinkData(
message_type=flags,
type=self.linktype,
node1_id=self.id,
node2_id=linked_node.id,
iface2=iface_data,
options=iface.local_options,
)
link_data.options.unidirectional = unidirectional
all_links.append(link_data)
if unidirectional:
link_data = LinkData(
message_type=MessageFlags.NONE,
type=self.linktype,
node1_id=linked_node.id,
node2_id=self.id,
options=iface.options,
)
link_data.options.unidirectional = unidirectional
all_links.append(link_data)
return all_links
class Position:
"""
Helper class for Cartesian coordinate position
"""
def __init__(self, x: float = None, y: float = None, z: float = None) -> None:
"""
Creates a Position instance.
:param x: x position
:param y: y position
:param z: z position
"""
self.x: float = x
self.y: float = y
self.z: float = z
self.lon: Optional[float] = None
self.lat: Optional[float] = None
self.alt: Optional[float] = None
def set(self, x: float = None, y: float = None, z: float = None) -> bool:
"""
Returns True if the position has actually changed.
:param x: x position
:param y: y position
:param z: z position
:return: True if position changed, False otherwise
"""
if self.x == x and self.y == y and self.z == z:
return False
self.x = x
self.y = y
self.z = z
return True
def get(self) -> Tuple[float, float, float]:
"""
Retrieve x,y,z position.
:return: x,y,z position tuple
"""
return self.x, self.y, self.z
def set_geo(self, lon: float, lat: float, alt: float) -> None:
"""
Set geo position lon, lat, alt.
:param lon: longitude value
:param lat: latitude value
:param alt: altitude value
:return: nothing
"""
self.lon = lon
self.lat = lat
self.alt = alt
def get_geo(self) -> Tuple[float, float, float]:
"""
Retrieve current geo position lon, lat, alt.
:return: lon, lat, alt position tuple
"""
return self.lon, self.lat, self.alt

View file

@ -1,112 +1,114 @@
import json import json
import logging import logging
import shlex
from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Callable, Dict, Optional from typing import TYPE_CHECKING, Dict, List, Tuple
from core import utils
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError, CoreError
from core.errors import CoreCommandError from core.executables import BASH
from core.nodes.base import CoreNode from core.nodes.base import CoreNode, CoreNodeOptions
from core.nodes.netclient import LinuxNetClient, get_net_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
DOCKER: str = "docker"
class DockerClient:
def __init__(self, name: str, image: str, run: Callable[..., str]) -> None:
self.name: str = name
self.image: str = image
self.run: Callable[..., str] = run
self.pid: Optional[str] = None
def create_container(self) -> str: @dataclass
self.run( class DockerOptions(CoreNodeOptions):
f"docker run -td --init --net=none --hostname {self.name} " image: str = "ubuntu"
f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 " """image used when creating container"""
f"--privileged {self.image} /bin/bash" binds: List[Tuple[str, str]] = field(default_factory=list)
) """bind mount source and destinations to setup within container"""
self.pid = self.get_pid() volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list)
return self.pid """
volume mount source, destination, unique, delete to setup within container
def get_info(self) -> Dict: unique is True for node unique volume naming
args = f"docker inspect {self.name}" delete is True for deleting volume mount during shutdown
output = self.run(args) """
data = json.loads(output)
if not data:
raise CoreCommandError(1, args, f"docker({self.name}) not present")
return data[0]
def is_alive(self) -> bool:
try:
data = self.get_info()
return data["State"]["Running"]
except CoreCommandError:
return False
def stop_container(self) -> None: @dataclass
self.run(f"docker rm -f {self.name}") class DockerVolume:
src: str
def check_cmd(self, cmd: str, wait: bool = True, shell: bool = False) -> str: """volume mount name"""
logger.info("docker cmd output: %s", cmd) dst: str
return utils.cmd(f"docker exec {self.name} {cmd}", wait=wait, shell=shell) """volume mount destination directory"""
unique: bool = True
def create_ns_cmd(self, cmd: str) -> str: """True to create a node unique prefixed name for this volume"""
return f"nsenter -t {self.pid} -a {cmd}" delete: bool = True
"""True to delete the volume during shutdown"""
def get_pid(self) -> str: path: str = None
args = f"docker inspect -f '{{{{.State.Pid}}}}' {self.name}" """path to the volume on the host"""
output = self.run(args)
self.pid = output
logger.debug("node(%s) pid: %s", self.name, self.pid)
return output
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)
class DockerNode(CoreNode): class DockerNode(CoreNode):
apitype = NodeTypes.DOCKER """
Provides logic for creating a Docker based node.
"""
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
directory: str = None,
server: DistributedServer = None, server: DistributedServer = None,
image: str = None, options: DockerOptions = None,
) -> None: ) -> None:
""" """
Create a DockerNode instance. Create a DockerNode instance.
:param session: core session instance :param session: core session instance
:param _id: object id :param _id: node id
:param name: object name :param name: node name
:param directory: node directory
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param image: image to start container with :param options: options for creating node
""" """
if image is None: options = options or DockerOptions()
image = "ubuntu" super().__init__(session, _id, name, server, options)
self.image: str = image self.image: str = options.image
super().__init__(session, _id, name, directory, server) self.binds: List[Tuple[str, str]] = options.binds
self.volumes: Dict[str, DockerVolume] = {}
for src, dst, unique, delete in options.volumes:
src_name = self._unique_name(src) if unique else src
self.volumes[src] = DockerVolume(src_name, dst, unique, delete)
def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: @classmethod
def create_options(cls) -> DockerOptions:
""" """
Create node network client for running network commands within the nodes Return default creation options, which can be used during node creation.
container.
:param use_ovs: True for OVS bridges, False for Linux bridges :return: docker options
:return:node network client
""" """
return get_net_client(use_ovs, self.nsenter_cmd) return DockerOptions()
def create_cmd(self, args: str, shell: bool = False) -> str:
"""
Create command used to run commands within the context of a node.
:param args: command arguments
:param shell: True to run shell like, False otherwise
:return: node command
"""
if shell:
args = f"{BASH} -c {shlex.quote(args)}"
return f"nsenter -t {self.pid} -m -u -i -p -n {args}"
def _unique_name(self, name: str) -> str:
"""
Creates a session/node unique prefixed name for the provided input.
:param name: name to make unique
:return: unique session/node prefixed name
"""
return f"{self.session.id}.{self.id}.{name}"
def alive(self) -> bool: def alive(self) -> bool:
""" """
@ -114,22 +116,52 @@ class DockerNode(CoreNode):
:return: True if node is alive, False otherwise :return: True if node is alive, False otherwise
""" """
return self.client.is_alive() try:
running = self.host_cmd(
f"{DOCKER} inspect -f '{{{{.State.Running}}}}' {self.name}"
)
return json.loads(running)
except CoreCommandError:
return False
def startup(self) -> None: def startup(self) -> None:
""" """
Start a new namespace node by invoking the vnoded process that Create a docker container instance for the specified image.
allocates a new namespace. Bring up the loopback device and set
the hostname.
:return: nothing :return: nothing
""" """
with self.lock: with self.lock:
if self.up: if self.up:
raise ValueError("starting a node that is already up") raise CoreError(f"starting node({self.name}) that is already up")
self.makenodedir() self.makenodedir()
self.client = DockerClient(self.name, self.image, self.host_cmd) binds = ""
self.pid = self.client.create_container() for src, dst in self.binds:
binds += f"--mount type=bind,source={src},target={dst} "
volumes = ""
for volume in self.volumes.values():
volumes += (
f"--mount type=volume," f"source={volume.src},target={volume.dst} "
)
hostname = self.name.replace("_", "-")
self.host_cmd(
f"{DOCKER} run -td --init --net=none --hostname {hostname} "
f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 "
f"{binds} {volumes} "
f"--privileged {self.image} tail -f /dev/null"
)
self.pid = self.host_cmd(
f"{DOCKER} inspect -f '{{{{.State.Pid}}}}' {self.name}"
)
for src, dst in self.binds:
link_path = self.host_path(Path(dst), True)
self.host_cmd(f"ln -s {src} {link_path}")
for volume in self.volumes.values():
volume.path = self.host_cmd(
f"{DOCKER} volume inspect -f '{{{{.Mountpoint}}}}' {volume.src}"
)
link_path = self.host_path(Path(volume.dst), True)
self.host_cmd(f"ln -s {volume.path} {link_path}")
logger.debug("node(%s) pid: %s", self.name, self.pid)
self.up = True self.up = True
def shutdown(self) -> None: def shutdown(self) -> None:
@ -141,20 +173,14 @@ class DockerNode(CoreNode):
# nothing to do if node is not up # nothing to do if node is not up
if not self.up: if not self.up:
return return
with self.lock: with self.lock:
self.ifaces.clear() self.ifaces.clear()
self.client.stop_container() self.host_cmd(f"{DOCKER} rm -f {self.name}")
for volume in self.volumes.values():
if volume.delete:
self.host_cmd(f"{DOCKER} volume rm {volume.src}")
self.up = False self.up = False
def nsenter_cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
if self.server is None:
args = self.client.create_ns_cmd(args)
return utils.cmd(args, wait=wait, shell=shell)
else:
args = self.client.create_ns_cmd(args)
return self.server.remote_cmd(args, wait=wait)
def termcmdstring(self, sh: str = "/bin/sh") -> str: def termcmdstring(self, sh: str = "/bin/sh") -> str:
""" """
Create a terminal command string. Create a terminal command string.
@ -162,7 +188,11 @@ class DockerNode(CoreNode):
:param sh: shell to execute command in :param sh: shell to execute command in
:return: str :return: str
""" """
return f"docker exec -it {self.name} bash" terminal = f"{DOCKER} exec -it {self.name} {sh}"
if self.server is None:
return terminal
else:
return f"ssh -X -f {self.server.host} xterm -e {terminal}"
def create_dir(self, dir_path: Path) -> None: def create_dir(self, dir_path: Path) -> None:
""" """
@ -172,8 +202,7 @@ class DockerNode(CoreNode):
:return: nothing :return: nothing
""" """
logger.debug("creating node dir: %s", dir_path) logger.debug("creating node dir: %s", dir_path)
args = f"mkdir -p {dir_path}" self.cmd(f"mkdir -p {dir_path}")
self.cmd(args)
def mount(self, src_path: str, target_path: str) -> None: def mount(self, src_path: str, target_path: str) -> None:
""" """
@ -206,7 +235,7 @@ class DockerNode(CoreNode):
self.cmd(f"mkdir -m {0o755:o} -p {directory}") self.cmd(f"mkdir -m {0o755:o} -p {directory}")
if self.server is not None: if self.server is not None:
self.server.remote_put(temp_path, temp_path) self.server.remote_put(temp_path, temp_path)
self.client.copy_file(temp_path, file_path) self.host_cmd(f"{DOCKER} cp {temp_path} {self.name}:{file_path}")
self.cmd(f"chmod {mode:o} {file_path}") self.cmd(f"chmod {mode:o} {file_path}")
if self.server is not None: if self.server is not None:
self.host_cmd(f"rm -f {temp_path}") self.host_cmd(f"rm -f {temp_path}")
@ -231,6 +260,6 @@ class DockerNode(CoreNode):
temp_path = Path(temp.name) temp_path = Path(temp.name)
src_path = temp_path src_path = temp_path
self.server.remote_put(src_path, temp_path) self.server.remote_put(src_path, temp_path)
self.client.copy_file(src_path, dst_path) self.host_cmd(f"{DOCKER} cp {src_path} {self.name}:{dst_path}")
if mode is not None: if mode is not None:
self.cmd(f"chmod {mode:o} {dst_path}") self.cmd(f"chmod {mode:o} {dst_path}")

View file

@ -4,7 +4,6 @@ virtual ethernet classes that implement the interfaces available under Linux.
import logging import logging
import math import math
import time
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Callable, Dict, List, Optional from typing import TYPE_CHECKING, Callable, Dict, List, Optional
@ -20,11 +19,12 @@ from core.nodes.netclient import LinuxNetClient, get_net_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.distributed import DistributedServer
from core.emulator.session import Session from core.emulator.session import Session
from core.nodes.base import CoreNetworkBase, CoreNode from core.emulator.distributed import DistributedServer
from core.nodes.base import CoreNetworkBase, CoreNode, NodeBase
DEFAULT_MTU: int = 1500 DEFAULT_MTU: int = 1500
IFACE_NAME_LENGTH: int = 15
def tc_clear_cmd(name: str) -> str: def tc_clear_cmd(name: str) -> str:
@ -78,35 +78,42 @@ class CoreInterface:
def __init__( def __init__(
self, self,
session: "Session", _id: int,
name: str, name: str,
localname: str, localname: str,
use_ovs: bool,
mtu: int = DEFAULT_MTU, mtu: int = DEFAULT_MTU,
node: "NodeBase" = None,
server: "DistributedServer" = None, server: "DistributedServer" = None,
node: "CoreNode" = None,
) -> None: ) -> None:
""" """
Creates a CoreInterface instance. Creates a CoreInterface instance.
:param session: core session instance :param _id: interface id for associated node
:param name: interface name :param name: interface name
:param localname: interface local name :param localname: interface local name
:param use_ovs: True to use ovs, False otherwise
:param mtu: mtu value :param mtu: mtu value
:param node: node associated with this interface
: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: if len(name) >= IFACE_NAME_LENGTH:
raise CoreError(f"interface name ({name}) too long, max 16") raise CoreError(
if len(localname) >= 16: f"interface name ({name}) too long, max {IFACE_NAME_LENGTH}"
raise CoreError(f"interface local name ({localname}) too long, max 16") )
self.session: "Session" = session if len(localname) >= IFACE_NAME_LENGTH:
self.node: Optional["CoreNode"] = node raise CoreError(
f"interface local name ({localname}) too long, max {IFACE_NAME_LENGTH}"
)
self.id: int = _id
self.node: Optional["NodeBase"] = node
# id of interface for network, used by wlan/emane
self.net_id: Optional[int] = None
self.name: str = name self.name: str = name
self.localname: str = localname self.localname: str = localname
self.up: bool = False self.up: bool = False
self.mtu: int = mtu self.mtu: int = mtu
self.net: Optional[CoreNetworkBase] = None self.net: Optional[CoreNetworkBase] = None
self.othernet: Optional[CoreNetworkBase] = None
self.ip4s: List[netaddr.IPNetwork] = [] self.ip4s: List[netaddr.IPNetwork] = []
self.ip6s: List[netaddr.IPNetwork] = [] self.ip6s: List[netaddr.IPNetwork] = []
self.mac: Optional[netaddr.EUI] = None self.mac: Optional[netaddr.EUI] = None
@ -114,20 +121,12 @@ class CoreInterface:
self.poshook: Callable[[CoreInterface], None] = lambda x: None self.poshook: Callable[[CoreInterface], None] = lambda x: None
# used with EMANE # used with EMANE
self.transport_type: TransportType = TransportType.VIRTUAL self.transport_type: TransportType = TransportType.VIRTUAL
# id of interface for node
self.node_id: Optional[int] = None
# id of interface for network
self.net_id: Optional[int] = None
# id used to find flow data # id used to find flow data
self.flow_id: Optional[int] = None self.flow_id: Optional[int] = None
self.server: Optional["DistributedServer"] = server self.server: Optional["DistributedServer"] = server
self.net_client: LinuxNetClient = get_net_client( self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd)
self.session.use_ovs(), self.host_cmd
)
self.control: bool = False self.control: bool = False
# configuration data # configuration data
self.has_local_netem: bool = False
self.local_options: LinkOptions = LinkOptions()
self.has_netem: bool = False self.has_netem: bool = False
self.options: LinkOptions = LinkOptions() self.options: LinkOptions = LinkOptions()
@ -161,7 +160,13 @@ class CoreInterface:
:return: nothing :return: nothing
""" """
pass 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.name)
self.net_client.device_up(self.localname)
self.up = True
def shutdown(self) -> None: def shutdown(self) -> None:
""" """
@ -169,29 +174,14 @@ class CoreInterface:
:return: nothing :return: nothing
""" """
pass if not self.up:
return
def attachnet(self, net: "CoreNetworkBase") -> None: if self.localname:
""" try:
Attach network. self.net_client.delete_device(self.localname)
except CoreCommandError:
:param net: network to attach pass
:return: nothing self.up = False
"""
if self.net:
self.detachnet()
self.net = None
net.attach(self)
self.net = net
def detachnet(self) -> None:
"""
Detach from a network.
:return: nothing
"""
if self.net is not None:
self.net.detach(self)
def add_ip(self, ip: str) -> None: def add_ip(self, ip: str) -> None:
""" """
@ -303,41 +293,24 @@ class CoreInterface:
""" """
return self.transport_type == TransportType.VIRTUAL return self.transport_type == TransportType.VIRTUAL
def config(self, options: LinkOptions, use_local: bool = True) -> None: def set_config(self) -> None:
"""
Configure interface using tc based on existing state and provided
link options.
:param options: options to configure with
:param use_local: True to use localname for device, False for name
:return: nothing
"""
# determine name, options, and if anything has changed
name = self.localname if use_local else self.name
current_options = self.local_options if use_local else self.options
changed = current_options.update(options)
# nothing more to do when nothing has changed or not up
if not changed or not self.up:
return
# clear current settings # clear current settings
if current_options.is_clear(): if self.options.is_clear():
clear_local_netem = use_local and self.has_local_netem if self.has_netem:
clear_netem = not use_local and self.has_netem cmd = tc_clear_cmd(self.name)
if clear_local_netem or clear_netem: if self.node:
cmd = tc_clear_cmd(name) self.node.cmd(cmd)
self.host_cmd(cmd)
if use_local:
self.has_local_netem = False
else: else:
self.has_netem = False self.host_cmd(cmd)
self.has_netem = False
# set updated settings # set updated settings
else: else:
cmd = tc_cmd(name, current_options, self.mtu) cmd = tc_cmd(self.name, self.options, self.mtu)
self.host_cmd(cmd) if self.node:
if use_local: self.node.cmd(cmd)
self.has_local_netem = True
else: else:
self.has_netem = True self.host_cmd(cmd)
self.has_netem = True
def get_data(self) -> InterfaceData: def get_data(self) -> InterfaceData:
""" """
@ -345,231 +318,22 @@ class CoreInterface:
:return: interface data :return: interface data
""" """
if self.node:
iface_id = self.node.get_iface_id(self)
else:
iface_id = self.othernet.get_iface_id(self)
data = InterfaceData(
id=iface_id, name=self.name, mac=str(self.mac) if self.mac else None
)
ip4 = self.get_ip4() ip4 = self.get_ip4()
if ip4: ip4_addr = str(ip4.ip) if ip4 else None
data.ip4 = str(ip4.ip) ip4_mask = ip4.prefixlen if ip4 else None
data.ip4_mask = ip4.prefixlen
ip6 = self.get_ip6() ip6 = self.get_ip6()
if ip6: ip6_addr = str(ip6.ip) if ip6 else None
data.ip6 = str(ip6.ip) ip6_mask = ip6.prefixlen if ip6 else None
data.ip6_mask = ip6.prefixlen mac = str(self.mac) if self.mac else None
return data return InterfaceData(
id=self.id,
name=self.name,
class Veth(CoreInterface): mac=mac,
""" ip4=ip4_addr,
Provides virtual ethernet functionality for core nodes. ip4_mask=ip4_mask,
""" ip6=ip6_addr,
ip6_mask=ip6_mask,
def adopt_node(self, iface_id: int, name: str, start: bool) -> None: )
"""
Adopt this interface to the provided node, configuring and associating
with the node as needed.
:param iface_id: interface id for node
:param name: name of interface fo rnode
:param start: True to start interface, False otherwise
:return: nothing
"""
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:
"""
Interface startup logic.
:return: nothing
: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
def shutdown(self) -> None:
"""
Interface shutdown logic.
:return: nothing
"""
if not self.up:
return
if self.node:
try:
self.node.node_net_client.device_flush(self.name)
except CoreCommandError:
pass
if self.localname:
try:
self.net_client.delete_device(self.localname)
except CoreCommandError:
pass
self.up = False
class TunTap(CoreInterface):
"""
TUN/TAP virtual device in TAP mode
"""
def startup(self) -> None:
"""
Startup logic for a tunnel tap.
:return: nothing
"""
# TODO: more sophisticated TAP creation here
# Debian does not support -p (tap) option, RedHat does.
# For now, this is disabled to allow the TAP to be created by another
# system (e.g. EMANE"s emanetransportd)
# check_call(["tunctl", "-t", self.name])
# self.install()
self.up = True
def shutdown(self) -> None:
"""
Shutdown functionality for a tunnel tap.
:return: nothing
"""
if not self.up:
return
try:
self.node.node_net_client.device_flush(self.name)
except CoreCommandError:
logger.exception("error shutting down tunnel tap")
self.up = False
def waitfor(
self, func: Callable[[], int], attempts: int = 10, maxretrydelay: float = 0.25
) -> bool:
"""
Wait for func() to return zero with exponential backoff.
:param func: function to wait for a result of zero
:param attempts: number of attempts to wait for a zero result
:param maxretrydelay: maximum retry delay
:return: True if wait succeeded, False otherwise
"""
delay = 0.01
result = False
for i in range(1, attempts + 1):
r = func()
if r == 0:
result = True
break
msg = f"attempt {i} failed with nonzero exit status {r}"
if i < attempts + 1:
msg += ", retrying..."
logger.info(msg)
time.sleep(delay)
delay += delay
if delay > maxretrydelay:
delay = maxretrydelay
else:
msg += ", giving up"
logger.info(msg)
return result
def waitfordevicelocal(self) -> None:
"""
Check for presence of a local device - tap device may not
appear right away waits
:return: wait for device local response
"""
logger.debug("waiting for device local: %s", self.localname)
def localdevexists():
try:
self.net_client.device_show(self.localname)
return 0
except CoreCommandError:
return 1
self.waitfor(localdevexists)
def waitfordevicenode(self) -> None:
"""
Check for presence of a node device - tap device may not appear right away waits.
:return: nothing
"""
logger.debug("waiting for device node: %s", self.name)
def nodedevexists():
try:
self.node.node_net_client.device_show(self.name)
return 0
except CoreCommandError:
return 1
count = 0
while True:
result = self.waitfor(nodedevexists)
if result:
break
# TODO: emane specific code
# check if this is an EMANE interface; if so, continue
# waiting if EMANE is still running
should_retry = count < 5
is_emane = self.session.emane.is_emane_net(self.net)
is_emane_running = self.session.emane.emanerunning(self.node)
if all([should_retry, is_emane, is_emane_running]):
count += 1
else:
raise RuntimeError("node device failed to exist")
def install(self) -> None:
"""
Install this TAP into its namespace. This is not done from the
startup() method but called at a later time when a userspace
program (running on the host) has had a chance to open the socket
end of the TAP.
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
self.waitfordevicelocal()
netns = str(self.node.pid)
self.net_client.device_ns(self.localname, netns)
self.node.node_net_client.device_name(self.localname, self.name)
self.node.node_net_client.device_up(self.name)
def set_ips(self) -> None:
"""
Set interface ip addresses.
:return: nothing
"""
self.waitfordevicenode()
for ip in self.ips():
self.node.node_net_client.create_address(self.name, str(ip))
class GreTap(CoreInterface): class GreTap(CoreInterface):
@ -594,7 +358,7 @@ class GreTap(CoreInterface):
""" """
Creates a GreTap instance. Creates a GreTap instance.
:param session: core session instance :param session: session for this gre tap
:param remoteip: remote address :param remoteip: remote address
:param key: gre tap key :param key: gre tap key
:param node: related core node :param node: related core node
@ -612,7 +376,7 @@ class GreTap(CoreInterface):
sessionid = session.short_session_id() sessionid = session.short_session_id()
localname = f"gt.{self.id}.{sessionid}" localname = f"gt.{self.id}.{sessionid}"
name = f"{localname}p" name = f"{localname}p"
super().__init__(session, name, localname, mtu, server, node) super().__init__(0, name, localname, session.use_ovs(), mtu, node, server)
self.transport_type: TransportType = TransportType.RAW self.transport_type: TransportType = TransportType.RAW
self.remote_ip: str = remoteip self.remote_ip: str = remoteip
self.ttl: int = ttl self.ttl: int = ttl

View file

@ -1,15 +1,17 @@
import json import json
import logging import logging
import shlex
import time import time
from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Callable, Dict, Optional from typing import TYPE_CHECKING, Dict, List, Tuple
from core import utils from core.emulator.data import InterfaceData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.enumerations import NodeTypes
from core.errors import CoreCommandError from core.errors import CoreCommandError
from core.nodes.base import CoreNode from core.executables import BASH
from core.nodes.base import CoreNode, CoreNodeOptions
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -18,65 +20,29 @@ if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
class LxdClient: @dataclass
def __init__(self, name: str, image: str, run: Callable[..., str]) -> None: class LxcOptions(CoreNodeOptions):
self.name: str = name image: str = "ubuntu"
self.image: str = image """image used when creating container"""
self.run: Callable[..., str] = run binds: List[Tuple[str, str]] = field(default_factory=list)
self.pid: Optional[int] = None """bind mount source and destinations to setup within container"""
volumes: List[Tuple[str, str, bool, bool]] = field(default_factory=list)
"""
volume mount source, destination, unique, delete to setup within container
def create_container(self) -> int: unique is True for node unique volume naming
self.run(f"lxc launch {self.image} {self.name}") delete is True for deleting volume mount during shutdown
data = self.get_info() """
self.pid = data["state"]["pid"]
return self.pid
def get_info(self) -> Dict:
args = f"lxc list {self.name} --format json"
output = self.run(args)
data = json.loads(output)
if not data:
raise CoreCommandError(1, args, f"LXC({self.name}) not present")
return data[0]
def is_alive(self) -> bool:
try:
data = self.get_info()
return data["state"]["status"] == "Running"
except CoreCommandError:
return False
def stop_container(self) -> None:
self.run(f"lxc delete --force {self.name}")
def create_cmd(self, cmd: str) -> str:
return f"lxc exec -nT {self.name} -- {cmd}"
def create_ns_cmd(self, cmd: str) -> str:
return f"nsenter -t {self.pid} -m -u -i -p -n {cmd}"
def check_cmd(self, cmd: str, wait: bool = True, shell: bool = False) -> str:
args = self.create_cmd(cmd)
return utils.cmd(args, wait=wait, shell=shell)
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)
class LxcNode(CoreNode): class LxcNode(CoreNode):
apitype = NodeTypes.LXC
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
directory: str = None,
server: DistributedServer = None, server: DistributedServer = None,
image: str = None, options: LxcOptions = None,
) -> None: ) -> None:
""" """
Create a LxcNode instance. Create a LxcNode instance.
@ -84,15 +50,37 @@ class LxcNode(CoreNode):
:param session: core session instance :param session: core session instance
:param _id: object id :param _id: object id
:param name: object name :param name: object name
:param directory: node directory
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param image: image to start container with :param options: option to create node with
""" """
if image is None: options = options or LxcOptions()
image = "ubuntu" super().__init__(session, _id, name, server, options)
self.image: str = image self.image: str = options.image
super().__init__(session, _id, name, directory, server)
@classmethod
def create_options(cls) -> LxcOptions:
return LxcOptions()
def create_cmd(self, args: str, shell: bool = False) -> str:
"""
Create command used to run commands within the context of a node.
:param args: command arguments
:param shell: True to run shell like, False otherwise
:return: node command
"""
if shell:
args = f"{BASH} -c {shlex.quote(args)}"
return f"nsenter -t {self.pid} -m -u -i -p -n {args}"
def _get_info(self) -> Dict:
args = f"lxc list {self.name} --format json"
output = self.host_cmd(args)
data = json.loads(output)
if not data:
raise CoreCommandError(1, args, f"LXC({self.name}) not present")
return data[0]
def alive(self) -> bool: def alive(self) -> bool:
""" """
@ -100,7 +88,11 @@ class LxcNode(CoreNode):
:return: True if node is alive, False otherwise :return: True if node is alive, False otherwise
""" """
return self.client.is_alive() try:
data = self._get_info()
return data["state"]["status"] == "Running"
except CoreCommandError:
return False
def startup(self) -> None: def startup(self) -> None:
""" """
@ -112,8 +104,9 @@ class LxcNode(CoreNode):
if self.up: if self.up:
raise ValueError("starting a node that is already up") raise ValueError("starting a node that is already up")
self.makenodedir() self.makenodedir()
self.client = LxdClient(self.name, self.image, self.host_cmd) self.host_cmd(f"lxc launch {self.image} {self.name}")
self.pid = self.client.create_container() data = self._get_info()
self.pid = data["state"]["pid"]
self.up = True self.up = True
def shutdown(self) -> None: def shutdown(self) -> None:
@ -125,10 +118,9 @@ class LxcNode(CoreNode):
# nothing to do if node is not up # nothing to do if node is not up
if not self.up: if not self.up:
return return
with self.lock: with self.lock:
self.ifaces.clear() self.ifaces.clear()
self.client.stop_container() self.host_cmd(f"lxc delete --force {self.name}")
self.up = False self.up = False
def termcmdstring(self, sh: str = "/bin/sh") -> str: def termcmdstring(self, sh: str = "/bin/sh") -> str:
@ -138,7 +130,11 @@ class LxcNode(CoreNode):
:param sh: shell to execute command in :param sh: shell to execute command in
:return: str :return: str
""" """
return f"lxc exec {self.name} -- {sh}" terminal = f"lxc exec {self.name} -- {sh}"
if self.server is None:
return terminal
else:
return f"ssh -X -f {self.server.host} xterm -e {terminal}"
def create_dir(self, dir_path: Path) -> None: def create_dir(self, dir_path: Path) -> None:
""" """
@ -182,7 +178,9 @@ class LxcNode(CoreNode):
self.cmd(f"mkdir -m {0o755:o} -p {directory}") self.cmd(f"mkdir -m {0o755:o} -p {directory}")
if self.server is not None: if self.server is not None:
self.server.remote_put(temp_path, temp_path) self.server.remote_put(temp_path, temp_path)
self.client.copy_file(temp_path, file_path) if not str(file_path).startswith("/"):
file_path = Path("/root/") / file_path
self.host_cmd(f"lxc file push {temp_path} {self.name}/{file_path}")
self.cmd(f"chmod {mode:o} {file_path}") self.cmd(f"chmod {mode:o} {file_path}")
if self.server is not None: if self.server is not None:
self.host_cmd(f"rm -f {temp_path}") self.host_cmd(f"rm -f {temp_path}")
@ -208,11 +206,16 @@ class LxcNode(CoreNode):
temp_path = Path(temp.name) temp_path = Path(temp.name)
src_path = temp_path src_path = temp_path
self.server.remote_put(src_path, temp_path) self.server.remote_put(src_path, temp_path)
self.client.copy_file(src_path, dst_path) if not str(dst_path).startswith("/"):
dst_path = Path("/root/") / dst_path
self.host_cmd(f"lxc file push {src_path} {self.name}/{dst_path}")
if mode is not None: if mode is not None:
self.cmd(f"chmod {mode:o} {dst_path}") self.cmd(f"chmod {mode:o} {dst_path}")
def add_iface(self, iface: CoreInterface, iface_id: int) -> None: def create_iface(
super().add_iface(iface, iface_id) self, iface_data: InterfaceData = None, options: LinkOptions = None
) -> CoreInterface:
iface = super().create_iface(iface_data, options)
# adding small delay to allow time for adding addresses to work correctly # adding small delay to allow time for adding addresses to work correctly
time.sleep(0.5) time.sleep(0.5)
return iface

View file

@ -28,6 +28,7 @@ class LinuxNetClient:
:param name: name for hostname :param name: name for hostname
:return: nothing :return: nothing
""" """
name = name.replace("_", "-")
self.run(f"hostname {name}") self.run(f"hostname {name}")
def create_route(self, route: str, device: str) -> None: def create_route(self, route: str, device: str) -> None:

View file

@ -4,26 +4,19 @@ Defines network nodes used within core.
import logging import logging
import threading import threading
from collections import OrderedDict from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from queue import Queue
from typing import TYPE_CHECKING, Dict, List, Optional, Type from typing import TYPE_CHECKING, Dict, List, Optional, Type
import netaddr import netaddr
from core import utils from core import utils
from core.emulator.data import InterfaceData, LinkData from core.emulator.data import InterfaceData, LinkData
from core.emulator.enumerations import ( from core.emulator.enumerations import MessageFlags, NetworkPolicy, RegisterTlvs
LinkTypes,
MessageFlags,
NetworkPolicy,
NodeTypes,
RegisterTlvs,
)
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import NFTABLES from core.executables import NFTABLES
from core.nodes.base import CoreNetworkBase, CoreNode from core.nodes.base import CoreNetworkBase, NodeOptions
from core.nodes.interface import CoreInterface, GreTap, Veth from core.nodes.interface import CoreInterface, GreTap
from core.nodes.netclient import get_net_client from core.nodes.netclient import get_net_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,27 +26,9 @@ if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
from core.location.mobility import WirelessModel, WayPointMobility from core.location.mobility import WirelessModel, WayPointMobility
WirelessModelType = Type[WirelessModel]
LEARNING_DISABLED: int = 0 LEARNING_DISABLED: int = 0
class SetQueue(Queue):
"""
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: class NftablesQueue:
""" """
Helper class for queuing up nftables commands into rate-limited Helper class for queuing up nftables commands into rate-limited
@ -78,7 +53,7 @@ class NftablesQueue:
# list of pending nftables commands # list of pending nftables commands
self.cmds: List[str] = [] self.cmds: List[str] = []
# list of WLANs requiring update # list of WLANs requiring update
self.updates: SetQueue = SetQueue() self.updates: utils.SetQueue = utils.SetQueue()
def start(self) -> None: def start(self) -> None:
""" """
@ -206,6 +181,12 @@ class NftablesQueue:
nft_queue: NftablesQueue = NftablesQueue() nft_queue: NftablesQueue = NftablesQueue()
@dataclass
class NetworkOptions(NodeOptions):
policy: NetworkPolicy = None
"""allows overriding the network policy, otherwise uses class defined default"""
class CoreNetwork(CoreNetworkBase): class CoreNetwork(CoreNetworkBase):
""" """
Provides linux bridge network functionality for core nodes. Provides linux bridge network functionality for core nodes.
@ -219,28 +200,29 @@ class CoreNetwork(CoreNetworkBase):
_id: int = None, _id: int = None,
name: str = None, name: str = None,
server: "DistributedServer" = None, server: "DistributedServer" = None,
policy: NetworkPolicy = None, options: NetworkOptions = None,
) -> None: ) -> None:
""" """
Creates a LxBrNet instance. Creates a CoreNetwork instance.
:param session: core session instance :param session: core session instance
:param _id: object id :param _id: object id
:param name: object name :param name: object name
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param policy: network policy :param options: options to create node with
""" """
super().__init__(session, _id, name, server) options = options or NetworkOptions()
if name is None: super().__init__(session, _id, name, server, options)
name = str(self.id) self.policy: NetworkPolicy = options.policy if options.policy else self.policy
if policy is not None:
self.policy: NetworkPolicy = policy
self.name: Optional[str] = name
sessionid = self.session.short_session_id() sessionid = self.session.short_session_id()
self.brname: str = f"b.{self.id}.{sessionid}" self.brname: str = f"b.{self.id}.{sessionid}"
self.has_nftables_chain: bool = False self.has_nftables_chain: bool = False
@classmethod
def create_options(cls) -> NetworkOptions:
return NetworkOptions()
def host_cmd( def host_cmd(
self, self,
args: str, args: str,
@ -280,6 +262,17 @@ class CoreNetwork(CoreNetworkBase):
self.up = True self.up = True
nft_queue.start() nft_queue.start()
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
"""
Adopt interface and set it to use this bridge as master.
:param iface: interface to adpopt
:param name: formal name for interface
:return: nothing
"""
iface.net_client.set_iface_master(self.brname, iface.name)
iface.set_config()
def shutdown(self) -> None: def shutdown(self) -> None:
""" """
Linux bridge shutdown logic. Linux bridge shutdown logic.
@ -309,9 +302,9 @@ class CoreNetwork(CoreNetworkBase):
:param iface: network interface to attach :param iface: network interface to attach
:return: nothing :return: nothing
""" """
super().attach(iface)
if self.up: if self.up:
iface.net_client.set_iface_master(self.brname, iface.localname) iface.net_client.set_iface_master(self.brname, iface.localname)
super().attach(iface)
def detach(self, iface: CoreInterface) -> None: def detach(self, iface: CoreInterface) -> None:
""" """
@ -320,9 +313,9 @@ class CoreNetwork(CoreNetworkBase):
:param iface: network interface to detach :param iface: network interface to detach
:return: nothing :return: nothing
""" """
super().detach(iface)
if self.up: if self.up:
iface.net_client.delete_iface(self.brname, iface.localname) iface.net_client.delete_iface(self.brname, iface.localname)
super().detach(iface)
def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool: def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool:
""" """
@ -378,67 +371,6 @@ class CoreNetwork(CoreNetworkBase):
self.linked[iface1][iface2] = True self.linked[iface1][iface2] = True
nft_queue.update(self) nft_queue.update(self)
def linknet(self, net: CoreNetworkBase) -> CoreInterface:
"""
Link this bridge with another by creating a veth pair and installing
each device into each bridge.
:param net: network to link with
:return: created interface
"""
sessionid = self.session.short_session_id()
try:
_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}"
name = f"veth{net_id}.{_id}.{sessionid}"
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] = {}
iface.net = self
iface.othernet = net
return iface
def get_linked_iface(self, net: CoreNetworkBase) -> Optional[CoreInterface]:
"""
Return the interface of that links this net with another net
(that were linked using linknet()).
:param net: interface to get link for
:return: interface the provided network is linked to
"""
for iface in self.get_ifaces():
if iface.othernet == net:
return iface
return None
def add_ips(self, ips: List[str]) -> None:
"""
Add ip addresses on the bridge in the format "10.0.0.1/24".
:param ips: ip address to add
:return: nothing
"""
if not self.up:
return
for ip in ips:
self.net_client.create_address(self.brname, ip)
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
raise CoreError(f"{type(self).__name__} does not support, custom interfaces")
class GreTapBridge(CoreNetwork): class GreTapBridge(CoreNetwork):
""" """
@ -558,6 +490,20 @@ class GreTapBridge(CoreNetwork):
self.add_ips(ips) self.add_ips(ips)
@dataclass
class CtrlNetOptions(NetworkOptions):
prefix: str = None
"""ip4 network prefix to use for generating an address"""
updown_script: str = None
"""script to execute during startup and shutdown"""
serverintf: str = None
"""used to associate an interface with the control network bridge"""
assign_address: bool = True
"""used to determine if a specific address should be assign using hostid"""
hostid: int = None
"""used with assign address to """
class CtrlNet(CoreNetwork): class CtrlNet(CoreNetwork):
""" """
Control network functionality. Control network functionality.
@ -576,36 +522,32 @@ class CtrlNet(CoreNetwork):
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
prefix: str,
_id: int = None, _id: int = None,
name: str = None, name: str = None,
hostid: int = None,
server: "DistributedServer" = None, server: "DistributedServer" = None,
assign_address: bool = True, options: CtrlNetOptions = None,
updown_script: str = None,
serverintf: str = None,
) -> None: ) -> None:
""" """
Creates a CtrlNet instance. Creates a CtrlNet instance.
:param session: core session instance :param session: core session instance
:param _id: node id :param _id: node id
:param name: node namee :param name: node name
:param prefix: control network ipv4 prefix
:param hostid: host id
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param assign_address: assigned address :param options: node options for creation
:param updown_script: updown script
:param serverintf: server interface
:return:
""" """
self.prefix: netaddr.IPNetwork = netaddr.IPNetwork(prefix).cidr options = options or CtrlNetOptions()
self.hostid: Optional[int] = hostid super().__init__(session, _id, name, server, options)
self.assign_address: bool = assign_address self.prefix: netaddr.IPNetwork = netaddr.IPNetwork(options.prefix).cidr
self.updown_script: Optional[str] = updown_script self.hostid: Optional[int] = options.hostid
self.serverintf: Optional[str] = serverintf self.assign_address: bool = options.assign_address
super().__init__(session, _id, name, server) self.updown_script: Optional[str] = options.updown_script
self.serverintf: Optional[str] = options.serverintf
@classmethod
def create_options(cls) -> CtrlNetOptions:
return CtrlNetOptions()
def add_addresses(self, index: int) -> None: def add_addresses(self, index: int) -> None:
""" """
@ -686,15 +628,6 @@ class CtrlNet(CoreNetwork):
super().shutdown() super().shutdown()
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
"""
Do not include CtrlNet in link messages describing this session.
:param flags: message flags
:return: list of link data
"""
return []
class PtpNet(CoreNetwork): class PtpNet(CoreNetwork):
""" """
@ -714,59 +647,13 @@ class PtpNet(CoreNetwork):
raise CoreError("ptp links support at most 2 network interfaces") raise CoreError("ptp links support at most 2 network interfaces")
super().attach(iface) super().attach(iface)
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
"""
Build CORE API TLVs for a point-to-point link. One Link message
describes this network.
:param flags: message flags
:return: list of link data
"""
all_links = []
if len(self.ifaces) != 2:
return all_links
ifaces = self.get_ifaces()
iface1 = ifaces[0]
iface2 = ifaces[1]
unidirectional = 0 if iface1.local_options == iface2.local_options else 1
iface1_data = iface1.get_data()
iface2_data = iface2.get_data()
link_data = LinkData(
message_type=flags,
type=self.linktype,
node1_id=iface1.node.id,
node2_id=iface2.node.id,
iface1=iface1_data,
iface2=iface2_data,
options=iface1.local_options,
)
link_data.options.unidirectional = unidirectional
all_links.append(link_data)
# build a 2nd link message for the upstream link parameters
# (swap if1 and if2)
if unidirectional:
link_data = LinkData(
message_type=MessageFlags.NONE,
type=self.linktype,
node1_id=iface2.node.id,
node2_id=iface1.node.id,
iface1=InterfaceData(id=iface2_data.id),
iface2=InterfaceData(id=iface1_data.id),
options=iface2.local_options,
)
link_data.options.unidirectional = unidirectional
all_links.append(link_data)
return all_links
class SwitchNode(CoreNetwork): class SwitchNode(CoreNetwork):
""" """
Provides switch functionality within a core node. Provides switch functionality within a core node.
""" """
apitype: NodeTypes = NodeTypes.SWITCH
policy: NetworkPolicy = NetworkPolicy.ACCEPT policy: NetworkPolicy = NetworkPolicy.ACCEPT
type: str = "lanswitch"
class HubNode(CoreNetwork): class HubNode(CoreNetwork):
@ -775,9 +662,7 @@ class HubNode(CoreNetwork):
ports by turning off MAC address learning. ports by turning off MAC address learning.
""" """
apitype: NodeTypes = NodeTypes.HUB
policy: NetworkPolicy = NetworkPolicy.ACCEPT policy: NetworkPolicy = NetworkPolicy.ACCEPT
type: str = "hub"
def startup(self) -> None: def startup(self) -> None:
""" """
@ -794,10 +679,7 @@ class WlanNode(CoreNetwork):
Provides wireless lan functionality within a core node. Provides wireless lan functionality within a core node.
""" """
apitype: NodeTypes = NodeTypes.WIRELESS_LAN
linktype: LinkTypes = LinkTypes.WIRED
policy: NetworkPolicy = NetworkPolicy.DROP policy: NetworkPolicy = NetworkPolicy.DROP
type: str = "wlan"
def __init__( def __init__(
self, self,
@ -805,7 +687,7 @@ class WlanNode(CoreNetwork):
_id: int = None, _id: int = None,
name: str = None, name: str = None,
server: "DistributedServer" = None, server: "DistributedServer" = None,
policy: NetworkPolicy = None, options: NetworkOptions = None,
) -> None: ) -> None:
""" """
Create a WlanNode instance. Create a WlanNode instance.
@ -815,11 +697,11 @@ class WlanNode(CoreNetwork):
:param name: node name :param name: node name
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param policy: wlan policy :param options: options to create node with
""" """
super().__init__(session, _id, name, server, policy) super().__init__(session, _id, name, server, options)
# wireless and mobility models (BasicRangeModel, Ns2WaypointMobility) # wireless and mobility models (BasicRangeModel, Ns2WaypointMobility)
self.model: Optional[WirelessModel] = None self.wireless_model: Optional[WirelessModel] = None
self.mobility: Optional[WayPointMobility] = None self.mobility: Optional[WayPointMobility] = None
def startup(self) -> None: def startup(self) -> None:
@ -839,27 +721,27 @@ class WlanNode(CoreNetwork):
:return: nothing :return: nothing
""" """
super().attach(iface) super().attach(iface)
if self.model: if self.wireless_model:
iface.poshook = self.model.position_callback iface.poshook = self.wireless_model.position_callback
iface.setposition() iface.setposition()
def setmodel(self, model: "WirelessModelType", config: Dict[str, str]): def setmodel(self, wireless_model: Type["WirelessModel"], config: Dict[str, str]):
""" """
Sets the mobility and wireless model. Sets the mobility and wireless model.
:param model: wireless model to set to :param wireless_model: wireless model to set to
:param config: configuration for model being set :param config: configuration for model being set
:return: nothing :return: nothing
""" """
logger.debug("node(%s) setting model: %s", self.name, model.name) logger.debug("node(%s) setting model: %s", self.name, wireless_model.name)
if model.config_type == RegisterTlvs.WIRELESS: if wireless_model.config_type == RegisterTlvs.WIRELESS:
self.model = model(session=self.session, _id=self.id) self.wireless_model = wireless_model(session=self.session, _id=self.id)
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.poshook = self.model.position_callback iface.poshook = self.wireless_model.position_callback
iface.setposition() iface.setposition()
self.updatemodel(config) self.updatemodel(config)
elif model.config_type == RegisterTlvs.MOBILITY: elif wireless_model.config_type == RegisterTlvs.MOBILITY:
self.mobility = model(session=self.session, _id=self.id) self.mobility = wireless_model(session=self.session, _id=self.id)
self.mobility.update_config(config) self.mobility.update_config(config)
def update_mobility(self, config: Dict[str, str]) -> None: def update_mobility(self, config: Dict[str, str]) -> None:
@ -868,12 +750,12 @@ class WlanNode(CoreNetwork):
self.mobility.update_config(config) self.mobility.update_config(config)
def updatemodel(self, config: Dict[str, str]) -> None: def updatemodel(self, config: Dict[str, str]) -> None:
if not self.model: if not self.wireless_model:
raise CoreError(f"no model set to update for node({self.name})") raise CoreError(f"no model set to update for node({self.name})")
logger.debug( logger.debug(
"node(%s) updating model(%s): %s", self.id, self.model.name, config "node(%s) updating model(%s): %s", self.id, self.wireless_model.name, config
) )
self.model.update_config(config) self.wireless_model.update_config(config)
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.setposition() iface.setposition()
@ -884,10 +766,10 @@ class WlanNode(CoreNetwork):
:param flags: message flags :param flags: message flags
:return: list of link data :return: list of link data
""" """
links = super().links(flags) if self.wireless_model:
if self.model: return self.wireless_model.links(flags)
links.extend(self.model.links(flags)) else:
return links return []
class TunnelNode(GreTapBridge): class TunnelNode(GreTapBridge):
@ -895,6 +777,4 @@ class TunnelNode(GreTapBridge):
Provides tunnel functionality in a core node. Provides tunnel functionality in a core node.
""" """
apitype: NodeTypes = NodeTypes.TUNNEL
policy: NetworkPolicy = NetworkPolicy.ACCEPT policy: NetworkPolicy = NetworkPolicy.ACCEPT
type: str = "tunnel"

View file

@ -3,17 +3,18 @@ PhysicalNode class for including real systems in the emulated network.
""" """
import logging import logging
import threading
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Tuple from typing import TYPE_CHECKING, List, Optional, Tuple
from core.emulator.data import InterfaceData import netaddr
from core.emulator.data import InterfaceData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.enumerations import NodeTypes, TransportType from core.emulator.enumerations import TransportType
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import MOUNT, TEST, UMOUNT from core.executables import BASH, TEST, UMOUNT
from core.nodes.base import CoreNetworkBase, CoreNodeBase from core.nodes.base import CoreNode, CoreNodeBase, CoreNodeOptions, NodeOptions
from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.interface import CoreInterface
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,201 +22,19 @@ if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
class PhysicalNode(CoreNodeBase):
def __init__(
self,
session: "Session",
_id: int = None,
name: 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.directory: Optional[Path] = directory
self.lock: threading.RLock = threading.RLock()
self._mounts: List[Tuple[Path, Path]] = []
def startup(self) -> None:
with self.lock:
self.makenodedir()
self.up = True
def shutdown(self) -> None:
if not self.up:
return
with self.lock:
while self._mounts:
_, 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:
"""
Determines if a file or directory path exists.
:param path: path to file or directory
:return: True if path exists, False otherwise
"""
try:
self.host_cmd(f"{TEST} -e {path}")
return True
except CoreCommandError:
return False
def termcmdstring(self, sh: str = "/bin/sh") -> str:
"""
Create a terminal command string.
:param sh: shell to execute command in
:return: str
"""
return sh
def set_mac(self, iface_id: int, mac: str) -> None:
"""
Set mac address for an interface.
:param iface_id: index of interface to set hardware address for
:param mac: mac address to set
:return: nothing
:raises CoreCommandError: when a non-zero exit status occurs
"""
iface = self.get_iface(iface_id)
iface.set_mac(mac)
if self.up:
self.net_client.device_mac(iface.name, str(iface.mac))
def add_ip(self, iface_id: int, ip: str) -> None:
"""
Add an ip address to an interface in the format "10.0.0.1/24".
:param iface_id: id of interface to add address to
:param ip: address to add to interface
:return: nothing
:raises CoreError: when ip address provided is invalid
:raises CoreCommandError: when a non-zero exit status occurs
"""
iface = self.get_iface(iface_id)
iface.add_ip(ip)
if self.up:
self.net_client.create_address(iface.name, ip)
def remove_ip(self, iface_id: int, ip: str) -> None:
"""
Remove an ip address from an interface in the format "10.0.0.1/24".
:param iface_id: id of interface to delete address from
:param ip: ip address to remove from interface
:return: nothing
:raises CoreError: when ip address provided is invalid
:raises CoreCommandError: when a non-zero exit status occurs
"""
iface = self.get_iface(iface_id)
iface.remove_ip(ip)
if self.up:
self.net_client.delete_address(iface.name, ip)
def adopt_iface(
self, iface: CoreInterface, iface_id: int, mac: str, ips: List[str]
) -> None:
"""
When a link message is received linking this node to another part of
the emulation, no new interface is created; instead, adopt the
GreTap interface as the node interface.
"""
iface.name = f"gt{iface_id}"
iface.node = self
self.add_iface(iface, iface_id)
# use a more reasonable name, e.g. "gt0" instead of "gt.56286.150"
if self.up:
self.net_client.device_down(iface.localname)
self.net_client.device_name(iface.localname, iface.name)
iface.localname = iface.name
if mac:
self.set_mac(iface_id, mac)
for ip in ips:
self.add_ip(iface_id, ip)
if self.up:
self.net_client.device_up(iface.localname)
def next_iface_id(self) -> int:
with self.lock:
while self.iface_id in self.ifaces:
self.iface_id += 1
iface_id = self.iface_id
self.iface_id += 1
return iface_id
def new_iface(
self, net: CoreNetworkBase, iface_data: InterfaceData
) -> CoreInterface:
logger.info("creating interface")
ips = iface_data.get_ips()
iface_id = iface_data.id
if iface_id is None:
iface_id = self.next_iface_id()
name = iface_data.name
if name is None:
name = f"gt{iface_id}"
_, 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
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, 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_path: Path) -> None:
logger.info("unmounting '%s'", target_path)
try:
self.host_cmd(f"{UMOUNT} -l {target_path}", cwd=self.directory)
except CoreCommandError:
logger.exception("unmounting failed for %s", target_path)
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
return self.host_cmd(args, wait=wait)
def create_dir(self, dir_path: Path) -> None:
raise CoreError("physical node does not support creating directories")
def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
raise CoreError("physical node does not support creating files")
def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
raise CoreError("physical node does not support copying files")
class Rj45Node(CoreNodeBase): class Rj45Node(CoreNodeBase):
""" """
RJ45Node is a physical interface on the host linked to the emulated RJ45Node is a physical interface on the host linked to the emulated
network. network.
""" """
apitype: NodeTypes = NodeTypes.RJ45
type: str = "rj45"
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
mtu: int = DEFAULT_MTU,
server: DistributedServer = None, server: DistributedServer = None,
options: NodeOptions = None,
) -> None: ) -> None:
""" """
Create an RJ45Node instance. Create an RJ45Node instance.
@ -223,17 +42,15 @@ class Rj45Node(CoreNodeBase):
:param session: core session instance :param session: core session instance
:param _id: node id :param _id: node id
:param name: node name :param name: node name
:param mtu: rj45 mtu
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
:param options: option to create node with
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server, options)
self.iface: CoreInterface = CoreInterface( self.iface: CoreInterface = CoreInterface(
session, name, name, mtu, server, self self.iface_id, name, name, session.use_ovs(), node=self, server=server
) )
self.iface.transport_type = TransportType.RAW self.iface.transport_type = TransportType.RAW
self.lock: threading.RLock = threading.RLock()
self.iface_id: Optional[int] = None
self.old_up: bool = False self.old_up: bool = False
self.old_addrs: List[Tuple[str, Optional[str]]] = [] self.old_addrs: List[Tuple[str, Optional[str]]] = []
@ -245,7 +62,7 @@ class Rj45Node(CoreNodeBase):
:raises CoreCommandError: when there is a command exception :raises CoreCommandError: when there is a command exception
""" """
# interface will also be marked up during net.attach() # interface will also be marked up during net.attach()
self.savestate() self.save_state()
self.net_client.device_up(self.iface.localname) self.net_client.device_up(self.iface.localname)
self.up = True self.up = True
@ -266,7 +83,7 @@ class Rj45Node(CoreNodeBase):
except CoreCommandError: except CoreCommandError:
pass pass
self.up = False self.up = False
self.restorestate() self.restore_state()
def path_exists(self, path: str) -> bool: def path_exists(self, path: str) -> bool:
""" """
@ -281,33 +98,28 @@ class Rj45Node(CoreNodeBase):
except CoreCommandError: except CoreCommandError:
return False return False
def new_iface( def create_iface(
self, net: CoreNetworkBase, iface_data: InterfaceData self, iface_data: InterfaceData = None, options: LinkOptions = None
) -> CoreInterface: ) -> CoreInterface:
"""
This is called when linking with another node. Since this node
represents an interface, we do not create another object here,
but attach ourselves to the given network.
:param net: new network instance
:param iface_data: interface data for new interface
:return: interface index
:raises ValueError: when an interface has already been created, one max
"""
with self.lock: with self.lock:
iface_id = iface_data.id if self.iface.id in self.ifaces:
if iface_id is None:
iface_id = 0
if self.iface.net is not None:
raise CoreError( raise CoreError(
f"RJ45({self.name}) nodes support at most 1 network interface" f"rj45({self.name}) nodes support at most 1 network interface"
) )
self.ifaces[iface_id] = self.iface if iface_data and iface_data.mtu is not None:
self.iface_id = iface_id self.iface.mtu = iface_data.mtu
self.iface.attachnet(net) self.iface.ip4s.clear()
self.iface.ip6s.clear()
for ip in iface_data.get_ips(): for ip in iface_data.get_ips():
self.add_ip(ip) self.iface.add_ip(ip)
return self.iface self.ifaces[self.iface.id] = self.iface
if self.up:
for ip in self.iface.ips():
self.net_client.create_address(self.iface.name, str(ip))
return self.iface
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
raise CoreError(f"rj45({self.name}) does not support adopt interface")
def delete_iface(self, iface_id: int) -> None: def delete_iface(self, iface_id: int) -> None:
""" """
@ -318,16 +130,10 @@ class Rj45Node(CoreNodeBase):
""" """
self.get_iface(iface_id) self.get_iface(iface_id)
self.ifaces.pop(iface_id) self.ifaces.pop(iface_id)
if self.iface.net is None:
raise CoreError(
f"RJ45({self.name}) is not currently connected to a network"
)
self.iface.detachnet()
self.iface.net = None
self.shutdown() self.shutdown()
def get_iface(self, iface_id: int) -> CoreInterface: def get_iface(self, iface_id: int) -> CoreInterface:
if iface_id != self.iface_id or iface_id not in self.ifaces: if iface_id not in self.ifaces:
raise CoreError(f"node({self.name}) interface({iface_id}) does not exist") raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
return self.iface return self.iface
@ -341,42 +147,17 @@ class Rj45Node(CoreNodeBase):
""" """
if iface is not self.iface: if iface is not self.iface:
raise CoreError(f"node({self.name}) does not have interface({iface.name})") raise CoreError(f"node({self.name}) does not have interface({iface.name})")
return self.iface_id return self.iface.id
def add_ip(self, ip: str) -> None: def save_state(self) -> None:
"""
Add an ip address to an interface in the format "10.0.0.1/24".
:param ip: address to add to interface
:return: nothing
:raises CoreError: when ip address provided is invalid
:raises CoreCommandError: when a non-zero exit status occurs
"""
self.iface.add_ip(ip)
if self.up:
self.net_client.create_address(self.name, ip)
def remove_ip(self, ip: str) -> None:
"""
Remove an ip address from an interface in the format "10.0.0.1/24".
:param ip: ip address to remove from interface
:return: nothing
:raises CoreError: when ip address provided is invalid
:raises CoreCommandError: when a non-zero exit status occurs
"""
self.iface.remove_ip(ip)
if self.up:
self.net_client.delete_address(self.name, ip)
def savestate(self) -> None:
""" """
Save the addresses and other interface state before using the Save the addresses and other interface state before using the
interface for emulation purposes. TODO: save/restore the PROMISC flag interface for emulation purposes.
:return: nothing :return: nothing
:raises CoreCommandError: when there is a command exception :raises CoreCommandError: when there is a command exception
""" """
# TODO: save/restore the PROMISC flag
self.old_up = False self.old_up = False
self.old_addrs: List[Tuple[str, Optional[str]]] = [] self.old_addrs: List[Tuple[str, Optional[str]]] = []
localname = self.iface.localname localname = self.iface.localname
@ -397,7 +178,7 @@ class Rj45Node(CoreNodeBase):
self.old_addrs.append((items[1], None)) self.old_addrs.append((items[1], None))
logger.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up) logger.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up)
def restorestate(self) -> None: def restore_state(self) -> None:
""" """
Restore the addresses and other interface state after using it. Restore the addresses and other interface state after using it.
@ -437,3 +218,69 @@ class Rj45Node(CoreNodeBase):
def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None: def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
raise CoreError("rj45 does not support copying files") raise CoreError("rj45 does not support copying files")
class PhysicalNode(CoreNode):
def __init__(
self,
session: "Session",
_id: int = None,
name: str = None,
server: DistributedServer = None,
options: CoreNodeOptions = None,
) -> None:
if not self.server:
raise CoreError("physical nodes must be assigned to a remote server")
super().__init__(session, _id, name, server, options)
def startup(self) -> None:
with self.lock:
self.makenodedir()
self.up = True
def shutdown(self) -> None:
if not self.up:
return
with self.lock:
while self._mounts:
_, target_path = self._mounts.pop(-1)
self.umount(target_path)
for iface in self.get_ifaces():
iface.shutdown()
self.rmnodedir()
def create_cmd(self, args: str, shell: bool = False) -> str:
if shell:
args = f'{BASH} -c "{args}"'
return args
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
# validate iface belongs to node and get id
iface_id = self.get_iface_id(iface)
if iface_id == -1:
raise CoreError(f"adopting unknown iface({iface.name})")
# turn checksums off
self.node_net_client.checksums_off(iface.name)
# retrieve flow id for container
iface.flow_id = self.node_net_client.get_ifindex(iface.name)
logger.debug("interface flow index: %s - %s", iface.name, iface.flow_id)
if iface.mac:
self.net_client.device_mac(iface.name, str(iface.mac))
# set all addresses
for ip in iface.ips():
# ipv4 check
broadcast = None
if netaddr.valid_ipv4(ip):
broadcast = "+"
self.node_net_client.create_address(iface.name, str(ip), broadcast)
# configure iface options
iface.set_config()
# set iface up
self.net_client.device_up(iface.name)
def umount(self, target_path: Path) -> None:
logger.info("unmounting '%s'", target_path)
try:
self.host_cmd(f"{UMOUNT} -l {target_path}", cwd=self.directory)
except CoreCommandError:
logger.exception("unmounting failed for %s", target_path)

View file

@ -0,0 +1,345 @@
"""
Defines a wireless node that allows programmatic link connectivity and
configuration between pairs of nodes.
"""
import copy
import logging
import math
import secrets
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
from core.config import ConfigBool, ConfigFloat, ConfigInt, Configuration
from core.emulator.data import LinkData, LinkOptions
from core.emulator.enumerations import LinkTypes, MessageFlags
from core.errors import CoreError
from core.executables import NFTABLES
from core.nodes.base import CoreNetworkBase, NodeOptions
from core.nodes.interface import CoreInterface
if TYPE_CHECKING:
from core.emulator.session import Session
from core.emulator.distributed import DistributedServer
logger = logging.getLogger(__name__)
CONFIG_ENABLED: bool = True
CONFIG_RANGE: float = 400.0
CONFIG_LOSS_RANGE: float = 300.0
CONFIG_LOSS_FACTOR: float = 1.0
CONFIG_LOSS: float = 0.0
CONFIG_DELAY: int = 5000
CONFIG_BANDWIDTH: int = 54_000_000
CONFIG_JITTER: int = 0
KEY_ENABLED: str = "movement"
KEY_RANGE: str = "max-range"
KEY_BANDWIDTH: str = "bandwidth"
KEY_DELAY: str = "delay"
KEY_JITTER: str = "jitter"
KEY_LOSS_RANGE: str = "loss-range"
KEY_LOSS_FACTOR: str = "loss-factor"
KEY_LOSS: str = "loss"
def calc_distance(
point1: Tuple[float, float, float], point2: Tuple[float, float, float]
) -> float:
a = point1[0] - point2[0]
b = point1[1] - point2[1]
c = 0
if point1[2] is not None and point2[2] is not None:
c = point1[2] - point2[2]
return math.hypot(math.hypot(a, b), c)
def get_key(node1_id: int, node2_id: int) -> Tuple[int, int]:
return (node1_id, node2_id) if node1_id < node2_id else (node2_id, node1_id)
@dataclass
class WirelessLink:
bridge1: str
bridge2: str
iface: CoreInterface
linked: bool
label: str = None
class WirelessNode(CoreNetworkBase):
options: List[Configuration] = [
ConfigBool(
id=KEY_ENABLED, default="1" if CONFIG_ENABLED else "0", label="Enabled?"
),
ConfigFloat(
id=KEY_RANGE, default=str(CONFIG_RANGE), label="Max Range (pixels)"
),
ConfigInt(
id=KEY_BANDWIDTH, default=str(CONFIG_BANDWIDTH), label="Bandwidth (bps)"
),
ConfigInt(id=KEY_DELAY, default=str(CONFIG_DELAY), label="Delay (usec)"),
ConfigInt(id=KEY_JITTER, default=str(CONFIG_JITTER), label="Jitter (usec)"),
ConfigFloat(
id=KEY_LOSS_RANGE,
default=str(CONFIG_LOSS_RANGE),
label="Loss Start Range (pixels)",
),
ConfigFloat(
id=KEY_LOSS_FACTOR, default=str(CONFIG_LOSS_FACTOR), label="Loss Factor"
),
ConfigFloat(id=KEY_LOSS, default=str(CONFIG_LOSS), label="Loss Initial"),
]
devices: Set[str] = set()
@classmethod
def add_device(cls) -> str:
while True:
name = f"we{secrets.token_hex(6)}"
if name not in cls.devices:
cls.devices.add(name)
break
return name
@classmethod
def delete_device(cls, name: str) -> None:
cls.devices.discard(name)
def __init__(
self,
session: "Session",
_id: int,
name: str,
server: "DistributedServer" = None,
options: NodeOptions = None,
):
super().__init__(session, _id, name, server, options)
self.bridges: Dict[int, Tuple[CoreInterface, str]] = {}
self.links: Dict[Tuple[int, int], WirelessLink] = {}
self.position_enabled: bool = CONFIG_ENABLED
self.bandwidth: int = CONFIG_BANDWIDTH
self.delay: int = CONFIG_DELAY
self.jitter: int = CONFIG_JITTER
self.max_range: float = CONFIG_RANGE
self.loss_initial: float = CONFIG_LOSS
self.loss_range: float = CONFIG_LOSS_RANGE
self.loss_factor: float = CONFIG_LOSS_FACTOR
def startup(self) -> None:
if self.up:
return
self.up = True
def shutdown(self) -> None:
while self.bridges:
_, (_, bridge_name) = self.bridges.popitem()
self.net_client.delete_bridge(bridge_name)
self.host_cmd(f"{NFTABLES} delete table bridge {bridge_name}")
while self.links:
_, link = self.links.popitem()
link.iface.shutdown()
self.up = False
def attach(self, iface: CoreInterface) -> None:
super().attach(iface)
logging.info("attaching node(%s) iface(%s)", iface.node.name, iface.name)
if self.up:
# create node unique bridge
bridge_name = f"wb{iface.node.id}.{self.id}.{self.session.id}"
self.net_client.create_bridge(bridge_name)
# setup initial bridge rules
self.host_cmd(f'{NFTABLES} "add table bridge {bridge_name}"')
self.host_cmd(
f"{NFTABLES} "
f"'add chain bridge {bridge_name} forward {{type filter hook "
f"forward priority -1; policy drop;}}'"
)
self.host_cmd(
f"{NFTABLES} "
f"'add rule bridge {bridge_name} forward "
f"ibriport != {bridge_name} accept'"
)
# associate node iface with bridge
iface.net_client.set_iface_master(bridge_name, iface.localname)
# assign position callback, when enabled
if self.position_enabled:
iface.poshook = self.position_callback
# save created bridge
self.bridges[iface.node.id] = (iface, bridge_name)
def post_startup(self) -> None:
routes = {}
for node_id, (iface, bridge_name) in self.bridges.items():
for onode_id, (oiface, obridge_name) in self.bridges.items():
if node_id == onode_id:
continue
if node_id < onode_id:
node1, node2 = iface.node, oiface.node
bridge1, bridge2 = bridge_name, obridge_name
else:
node1, node2 = oiface.node, iface.node
bridge1, bridge2 = obridge_name, bridge_name
key = (node1.id, node2.id)
if key in self.links:
continue
# create node to node link
name1 = self.add_device()
name2 = self.add_device()
link_iface = CoreInterface(0, name1, name2, self.session.use_ovs())
link_iface.startup()
link = WirelessLink(bridge1, bridge2, link_iface, False)
self.links[key] = link
# track bridge routes
node1_routes = routes.setdefault(node1.id, set())
node1_routes.add(name1)
node2_routes = routes.setdefault(node2.id, set())
node2_routes.add(name2)
if self.position_enabled:
link.linked = True
# assign ifaces to respective bridges
self.net_client.set_iface_master(bridge1, link_iface.name)
self.net_client.set_iface_master(bridge2, link_iface.localname)
# calculate link data
self.calc_link(iface, oiface)
for node_id, ifaces in routes.items():
iface, bridge_name = self.bridges[node_id]
ifaces = ",".join(ifaces)
# out routes
self.host_cmd(
f"{NFTABLES} "
f'"add rule bridge {bridge_name} forward '
f"iif {iface.localname} oif {{{ifaces}}} "
f'accept"'
)
# in routes
self.host_cmd(
f"{NFTABLES} "
f'"add rule bridge {bridge_name} forward '
f"iif {{{ifaces}}} oif {iface.localname} "
f'accept"'
)
def link_control(self, node1_id: int, node2_id: int, linked: bool) -> None:
key = get_key(node1_id, node2_id)
link = self.links.get(key)
if not link:
raise CoreError(f"invalid node links node1({node1_id}) node2({node2_id})")
bridge1, bridge2 = link.bridge1, link.bridge2
iface = link.iface
if not link.linked and linked:
link.linked = True
self.net_client.set_iface_master(bridge1, iface.name)
self.net_client.set_iface_master(bridge2, iface.localname)
self.send_link(key[0], key[1], MessageFlags.ADD, link.label)
elif link.linked and not linked:
link.linked = False
self.net_client.delete_iface(bridge1, iface.name)
self.net_client.delete_iface(bridge2, iface.localname)
self.send_link(key[0], key[1], MessageFlags.DELETE, link.label)
def link_config(
self, node1_id: int, node2_id: int, options1: LinkOptions, options2: LinkOptions
) -> None:
key = get_key(node1_id, node2_id)
link = self.links.get(key)
if not link:
raise CoreError(f"invalid node links node1({node1_id}) node2({node2_id})")
iface = link.iface
has_netem = iface.has_netem
iface.options.update(options1)
iface.set_config()
name, localname = iface.name, iface.localname
iface.name, iface.localname = localname, name
iface.options.update(options2)
iface.has_netem = has_netem
iface.set_config()
iface.name, iface.localname = name, localname
if options1 == options2:
link.label = f"{options1.loss:.2f}%/{options1.delay}us"
else:
link.label = (
f"({options1.loss:.2f}%/{options1.delay}us) "
f"({options2.loss:.2f}%/{options2.delay}us)"
)
self.send_link(key[0], key[1], MessageFlags.NONE, link.label)
def send_link(
self,
node1_id: int,
node2_id: int,
message_type: MessageFlags,
label: str = None,
) -> None:
"""
Broadcasts out a wireless link/unlink message.
:param node1_id: first node in link
:param node2_id: second node in link
:param message_type: type of link message to send
:param label: label to display for link
:return: nothing
"""
color = self.session.get_link_color(self.id)
link_data = LinkData(
message_type=message_type,
type=LinkTypes.WIRELESS,
node1_id=node1_id,
node2_id=node2_id,
network_id=self.id,
color=color,
label=label,
)
self.session.broadcast_link(link_data)
def position_callback(self, iface: CoreInterface) -> None:
for oiface, bridge_name in self.bridges.values():
if iface == oiface:
continue
self.calc_link(iface, oiface)
def calc_link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
key = get_key(iface1.node.id, iface2.node.id)
link = self.links.get(key)
point1 = iface1.node.position.get()
point2 = iface2.node.position.get()
distance = calc_distance(point1, point2)
if distance >= self.max_range:
if link.linked:
self.link_control(iface1.node.id, iface2.node.id, False)
else:
if not link.linked:
self.link_control(iface1.node.id, iface2.node.id, True)
loss_distance = max(distance - self.loss_range, 0.0)
max_distance = max(self.max_range - self.loss_range, 0.0)
loss = min((loss_distance / max_distance) * 100.0 * self.loss_factor, 100.0)
loss = max(self.loss_initial, loss)
options = LinkOptions(
loss=loss,
delay=self.delay,
bandwidth=self.bandwidth,
jitter=self.jitter,
)
self.link_config(iface1.node.id, iface2.node.id, options, options)
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
raise CoreError(f"{type(self)} does not support adopt interface")
def get_config(self) -> Dict[str, Configuration]:
config = {x.id: x for x in copy.copy(self.options)}
config[KEY_ENABLED].default = "1" if self.position_enabled else "0"
config[KEY_RANGE].default = str(self.max_range)
config[KEY_LOSS_RANGE].default = str(self.loss_range)
config[KEY_LOSS_FACTOR].default = str(self.loss_factor)
config[KEY_LOSS].default = str(self.loss_initial)
config[KEY_BANDWIDTH].default = str(self.bandwidth)
config[KEY_DELAY].default = str(self.delay)
config[KEY_JITTER].default = str(self.jitter)
return config
def set_config(self, config: Dict[str, str]) -> None:
logger.info("wireless config: %s", config)
self.position_enabled = config[KEY_ENABLED] == "1"
self.max_range = float(config[KEY_RANGE])
self.loss_range = float(config[KEY_LOSS_RANGE])
self.loss_factor = float(config[KEY_LOSS_FACTOR])
self.loss_initial = float(config[KEY_LOSS])
self.bandwidth = int(config[KEY_BANDWIDTH])
self.delay = int(config[KEY_DELAY])
self.jitter = int(config[KEY_JITTER])

450
daemon/core/player.py Normal file
View file

@ -0,0 +1,450 @@
import ast
import csv
import enum
import logging
import sched
from pathlib import Path
from threading import Thread
from typing import IO, Callable, Dict, Optional
import grpc
from core.api.grpc.client import CoreGrpcClient, MoveNodesStreamer
from core.api.grpc.wrappers import LinkOptions
logger = logging.getLogger(__name__)
@enum.unique
class PlayerEvents(enum.Enum):
"""
Provides event types for processing file events.
"""
XY = enum.auto()
GEO = enum.auto()
CMD = enum.auto()
WLINK = enum.auto()
WILINK = enum.auto()
WICONFIG = enum.auto()
@classmethod
def get(cls, value: str) -> Optional["PlayerEvents"]:
"""
Retrieves a valid event type from read input.
:param value: value to get event type for
:return: valid event type, None otherwise
"""
event = None
try:
event = cls[value]
except KeyError:
pass
return event
class CorePlayerWriter:
"""
Provides conveniences for programatically creating a core file for playback.
"""
def __init__(self, file_path: str):
"""
Create a CorePlayerWriter instance.
:param file_path: path to create core file
"""
self._time: float = 0.0
self._file_path: str = file_path
self._file: Optional[IO] = None
self._csv_file: Optional[csv.writer] = None
def open(self) -> None:
"""
Opens the provided file path for writing and csv creation.
:return: nothing
"""
logger.info("core player write file(%s)", self._file_path)
self._file = open(self._file_path, "w", newline="")
self._csv_file = csv.writer(self._file, quoting=csv.QUOTE_MINIMAL)
def close(self) -> None:
"""
Closes the file being written to.
:return: nothing
"""
if self._file:
self._file.close()
def update(self, delay: float) -> None:
"""
Update and move the current play time forward by delay amount.
:param delay: amount to move time forward by
:return: nothing
"""
self._time += delay
def write_xy(self, node_id: int, x: float, y: float) -> None:
"""
Write a node xy movement event.
:param node_id: id of node to move
:param x: x position
:param y: y position
:return: nothing
"""
self._csv_file.writerow([self._time, PlayerEvents.XY.name, node_id, x, y])
def write_geo(self, node_id: int, lon: float, lat: float, alt: float) -> None:
"""
Write a node geo movement event.
:param node_id: id of node to move
:param lon: longitude position
:param lat: latitude position
:param alt: altitude position
:return: nothing
"""
self._csv_file.writerow(
[self._time, PlayerEvents.GEO.name, node_id, lon, lat, alt]
)
def write_cmd(self, node_id: int, wait: bool, shell: bool, cmd: str) -> None:
"""
Write a node command event.
:param node_id: id of node to run command on
:param wait: should command wait for successful execution
:param shell: should command run under shell context
:param cmd: command to run
:return: nothing
"""
self._csv_file.writerow(
[self._time, PlayerEvents.CMD.name, node_id, wait, shell, f"'{cmd}'"]
)
def write_wlan_link(
self, wireless_id: int, node1_id: int, node2_id: int, linked: bool
) -> None:
"""
Write a wlan link event.
:param wireless_id: id of wlan network for link
:param node1_id: first node connected to wlan
:param node2_id: second node connected to wlan
:param linked: True if nodes are linked, False otherwise
:return: nothing
"""
self._csv_file.writerow(
[
self._time,
PlayerEvents.WLINK.name,
wireless_id,
node1_id,
node2_id,
linked,
]
)
def write_wireless_link(
self, wireless_id: int, node1_id: int, node2_id: int, linked: bool
) -> None:
"""
Write a wireless link event.
:param wireless_id: id of wireless network for link
:param node1_id: first node connected to wireless
:param node2_id: second node connected to wireless
:param linked: True if nodes are linked, False otherwise
:return: nothing
"""
self._csv_file.writerow(
[
self._time,
PlayerEvents.WILINK.name,
wireless_id,
node1_id,
node2_id,
linked,
]
)
def write_wireless_config(
self,
wireless_id: int,
node1_id: int,
node2_id: int,
loss1: float,
delay1: int,
loss2: float = None,
delay2: float = None,
) -> None:
"""
Write a wireless link config event.
:param wireless_id: id of wireless network for link
:param node1_id: first node connected to wireless
:param node2_id: second node connected to wireless
:param loss1: loss for the first interface
:param delay1: delay for the first interface
:param loss2: loss for the second interface, defaults to first interface loss
:param delay2: delay for second interface, defaults to first interface delay
:return: nothing
"""
loss2 = loss2 if loss2 is not None else loss1
delay2 = delay2 if delay2 is not None else delay1
self._csv_file.writerow(
[
self._time,
PlayerEvents.WICONFIG.name,
wireless_id,
node1_id,
node2_id,
loss1,
delay1,
loss2,
delay2,
]
)
class CorePlayer:
"""
Provides core player functionality for reading a file with timed events
and playing them out.
"""
def __init__(self, file_path: Path):
"""
Creates a CorePlayer instance.
:param file_path: file to play path
"""
self.file_path: Path = file_path
self.core: CoreGrpcClient = CoreGrpcClient()
self.session_id: Optional[int] = None
self.node_streamer: Optional[MoveNodesStreamer] = None
self.node_streamer_thread: Optional[Thread] = None
self.scheduler: sched.scheduler = sched.scheduler()
self.handlers: Dict[PlayerEvents, Callable] = {
PlayerEvents.XY: self.handle_xy,
PlayerEvents.GEO: self.handle_geo,
PlayerEvents.CMD: self.handle_cmd,
PlayerEvents.WLINK: self.handle_wlink,
PlayerEvents.WILINK: self.handle_wireless_link,
PlayerEvents.WICONFIG: self.handle_wireless_config,
}
def init(self, session_id: Optional[int]) -> bool:
"""
Initialize core connections, settings to or retrieving session to use.
Also setup node streamer for xy/geo movements.
:param session_id: session id to use, None for default session
:return: True if init was successful, False otherwise
"""
self.core.connect()
try:
if session_id is None:
sessions = self.core.get_sessions()
if len(sessions):
session_id = sessions[0].id
if session_id is None:
logger.error("no core sessions found")
return False
self.session_id = session_id
logger.info("playing to session(%s)", self.session_id)
self.node_streamer = MoveNodesStreamer(self.session_id)
self.node_streamer_thread = Thread(
target=self.core.move_nodes, args=(self.node_streamer,), daemon=True
)
self.node_streamer_thread.start()
except grpc.RpcError as e:
logger.error("core is not running: %s", e.details())
return False
return True
def start(self) -> None:
"""
Starts playing file, reading the csv data line by line, then handling
each line event type. Delay is tracked and calculated, while processing,
to ensure we wait for the event time to be active.
:return: nothing
"""
current_time = 0.0
with self.file_path.open("r", newline="") as f:
for row in csv.reader(f):
# determine delay
input_time = float(row[0])
delay = input_time - current_time
current_time = input_time
# determine event
event_value = row[1]
event = PlayerEvents.get(event_value)
if not event:
logger.error("unknown event type: %s", ",".join(row))
continue
# get args and event functions
args = tuple(ast.literal_eval(x) for x in row[2:])
event_func = self.handlers.get(event)
if not event_func:
logger.error("unknown event type handler: %s", ",".join(row))
continue
logger.info(
"processing line time(%s) event(%s) args(%s)",
input_time,
event.name,
args,
)
# schedule and run event
self.scheduler.enter(delay, 1, event_func, argument=args)
self.scheduler.run()
self.stop()
def stop(self) -> None:
"""
Stop and cleanup playback.
:return: nothing
"""
logger.info("stopping playback, cleaning up")
self.node_streamer.stop()
self.node_streamer_thread.join()
self.node_streamer_thread = None
def handle_xy(self, node_id: int, x: float, y: float) -> None:
"""
Handle node xy movement event.
:param node_id: id of node to move
:param x: x position
:param y: y position
:return: nothing
"""
logger.debug("handling xy node(%s) x(%s) y(%s)", node_id, x, y)
self.node_streamer.send_position(node_id, x, y)
def handle_geo(self, node_id: int, lon: float, lat: float, alt: float) -> None:
"""
Handle node geo movement event.
:param node_id: id of node to move
:param lon: longitude position
:param lat: latitude position
:param alt: altitude position
:return: nothing
"""
logger.debug(
"handling geo node(%s) lon(%s) lat(%s) alt(%s)", node_id, lon, lat, alt
)
self.node_streamer.send_geo(node_id, lon, lat, alt)
def handle_cmd(self, node_id: int, wait: bool, shell: bool, cmd: str) -> None:
"""
Handle node command event.
:param node_id: id of node to run command
:param wait: True to wait for successful command, False otherwise
:param shell: True to run command in shell context, False otherwise
:param cmd: command to run
:return: nothing
"""
logger.debug(
"handling cmd node(%s) wait(%s) shell(%s) cmd(%s)",
node_id,
wait,
shell,
cmd,
)
status, output = self.core.node_command(
self.session_id, node_id, cmd, wait, shell
)
logger.info("cmd result(%s): %s", status, output)
def handle_wlink(
self, net_id: int, node1_id: int, node2_id: int, linked: bool
) -> None:
"""
Handle wlan link event.
:param net_id: id of wlan network
:param node1_id: first node in link
:param node2_id: second node in link
:param linked: True if linked, Flase otherwise
:return: nothing
"""
logger.debug(
"handling wlink node1(%s) node2(%s) net(%s) linked(%s)",
node1_id,
node2_id,
net_id,
linked,
)
self.core.wlan_link(self.session_id, net_id, node1_id, node2_id, linked)
def handle_wireless_link(
self, wireless_id: int, node1_id: int, node2_id: int, linked: bool
) -> None:
"""
Handle wireless link event.
:param wireless_id: id of wireless network
:param node1_id: first node in link
:param node2_id: second node in link
:param linked: True if linked, Flase otherwise
:return: nothing
"""
logger.debug(
"handling link wireless(%s) node1(%s) node2(%s) linked(%s)",
wireless_id,
node1_id,
node2_id,
linked,
)
self.core.wireless_linked(
self.session_id, wireless_id, node1_id, node2_id, linked
)
def handle_wireless_config(
self,
wireless_id: int,
node1_id: int,
node2_id: int,
loss1: float,
delay1: int,
loss2: float,
delay2: int,
) -> None:
"""
Handle wireless config event.
:param wireless_id: id of wireless network
:param node1_id: first node in link
:param node2_id: second node in link
:param loss1: first interface loss
:param delay1: first interface delay
:param loss2: second interface loss
:param delay2: second interface delay
:return: nothing
"""
logger.debug(
"handling config wireless(%s) node1(%s) node2(%s) "
"options1(%s/%s) options2(%s/%s)",
wireless_id,
node1_id,
node2_id,
loss1,
delay1,
loss2,
delay2,
)
options1 = LinkOptions(loss=loss1, delay=delay1)
options2 = LinkOptions(loss=loss2, delay=delay2)
self.core.wireless_config(
self.session_id, wireless_id, node1_id, node2_id, options1, options2
)

View file

@ -4,22 +4,46 @@ sdt.py: Scripted Display Tool (SDT3D) helper
import logging import logging
import socket import socket
from typing import IO, TYPE_CHECKING, Dict, List, Optional, Set, Tuple from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
from urllib.parse import urlparse from urllib.parse import urlparse
from core.constants import CORE_CONF_DIR, CORE_DATA_DIR from core.constants import CORE_CONF_DIR
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData, NodeData from core.emulator.data import LinkData, NodeData
from core.emulator.enumerations import EventTypes, MessageFlags from core.emulator.enumerations import EventTypes, MessageFlags
from core.errors import CoreError from core.errors import CoreError
from core.nodes.base import CoreNetworkBase, NodeBase from core.nodes.base import CoreNode, NodeBase
from core.nodes.network import WlanNode from core.nodes.network import HubNode, SwitchNode, TunnelNode, WlanNode
from core.nodes.physical import Rj45Node
from core.nodes.wireless import WirelessNode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
LOCAL_ICONS_PATH: Path = Path(__file__).parent.parent / "gui" / "data" / "icons"
CORE_LAYER: str = "CORE"
NODE_LAYER: str = "CORE::Nodes"
LINK_LAYER: str = "CORE::Links"
WIRED_LINK_LAYER: str = f"{LINK_LAYER}::wired"
CORE_LAYERS: List[str] = [CORE_LAYER, LINK_LAYER, NODE_LAYER, WIRED_LINK_LAYER]
DEFAULT_LINK_COLOR: str = "red"
NODE_TYPES: Dict[Type[NodeBase], str] = {
HubNode: "hub",
SwitchNode: "lanswitch",
TunnelNode: "tunnel",
WlanNode: "wlan",
EmaneNet: "emane",
WirelessNode: "wireless",
Rj45Node: "rj45",
}
def is_wireless(node: NodeBase) -> bool:
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
def get_link_id(node1_id: int, node2_id: int, network_id: int) -> str: def get_link_id(node1_id: int, node2_id: int, network_id: int) -> str:
link_id = f"{node1_id}-{node2_id}" link_id = f"{node1_id}-{node2_id}"
@ -28,14 +52,6 @@ def get_link_id(node1_id: int, node2_id: int, network_id: int) -> str:
return link_id return link_id
CORE_LAYER: str = "CORE"
NODE_LAYER: str = "CORE::Nodes"
LINK_LAYER: str = "CORE::Links"
WIRED_LINK_LAYER: str = f"{LINK_LAYER}::wired"
CORE_LAYERS: List[str] = [CORE_LAYER, LINK_LAYER, NODE_LAYER, WIRED_LINK_LAYER]
DEFAULT_LINK_COLOR: str = "red"
class Sdt: class Sdt:
""" """
Helper class for exporting session objects to NRL"s SDT3D. Helper class for exporting session objects to NRL"s SDT3D.
@ -48,16 +64,18 @@ class Sdt:
DEFAULT_ALT: int = 2500 DEFAULT_ALT: int = 2500
# TODO: read in user"s nodes.conf here; below are default node types from the GUI # TODO: read in user"s nodes.conf here; below are default node types from the GUI
DEFAULT_SPRITES: Dict[str, str] = [ DEFAULT_SPRITES: Dict[str, str] = [
("router", "router.gif"), ("router", "router.png"),
("host", "host.gif"), ("host", "host.png"),
("PC", "pc.gif"), ("PC", "pc.png"),
("mdr", "mdr.gif"), ("mdr", "mdr.png"),
("prouter", "router_green.gif"), ("prouter", "prouter.png"),
("hub", "hub.gif"), ("hub", "hub.png"),
("lanswitch", "lanswitch.gif"), ("lanswitch", "lanswitch.png"),
("wlan", "wlan.gif"), ("wlan", "wlan.png"),
("rj45", "rj45.gif"), ("emane", "emane.png"),
("tunnel", "tunnel.gif"), ("wireless", "wireless.png"),
("rj45", "rj45.png"),
("tunnel", "tunnel.png"),
] ]
def __init__(self, session: "Session") -> None: def __init__(self, session: "Session") -> None:
@ -67,7 +85,7 @@ class Sdt:
:param session: session this manager is tied to :param session: session this manager is tied to
""" """
self.session: "Session" = session self.session: "Session" = session
self.sock: Optional[IO] = None self.sock: Optional[socket.socket] = None
self.connected: bool = False self.connected: bool = False
self.url: str = self.DEFAULT_SDT_URL self.url: str = self.DEFAULT_SDT_URL
self.address: Optional[Tuple[Optional[str], Optional[int]]] = None self.address: Optional[Tuple[Optional[str], Optional[int]]] = None
@ -83,7 +101,7 @@ class Sdt:
:return: True if enabled, False otherwise :return: True if enabled, False otherwise
""" """
return self.session.options.get_config("enablesdt") == "1" return self.session.options.get_int("enablesdt") == 1
def seturl(self) -> None: def seturl(self) -> None:
""" """
@ -92,7 +110,7 @@ class Sdt:
:return: nothing :return: nothing
""" """
url = self.session.options.get_config("stdurl", default=self.DEFAULT_SDT_URL) url = self.session.options.get("stdurl", self.DEFAULT_SDT_URL)
self.url = urlparse(url) self.url = urlparse(url)
self.address = (self.url.hostname, self.url.port) self.address = (self.url.hostname, self.url.port)
self.protocol = self.url.scheme self.protocol = self.url.scheme
@ -140,7 +158,7 @@ class Sdt:
:return: initialize command status :return: initialize command status
""" """
if not self.cmd(f'path "{CORE_DATA_DIR}/icons/normal"'): if not self.cmd(f'path "{LOCAL_ICONS_PATH.absolute()}"'):
return False return False
# send node type to icon mappings # send node type to icon mappings
for node_type, icon in self.DEFAULT_SPRITES: for node_type, icon in self.DEFAULT_SPRITES:
@ -162,7 +180,6 @@ class Sdt:
logger.error("error closing socket") logger.error("error closing socket")
finally: finally:
self.sock = None self.sock = None
self.connected = False self.connected = False
def shutdown(self) -> None: def shutdown(self) -> None:
@ -190,7 +207,6 @@ class Sdt:
""" """
if self.sock is None: if self.sock is None:
return False return False
try: try:
cmd = f"{cmdstr}\n".encode() cmd = f"{cmdstr}\n".encode()
logger.debug("sdt cmd: %s", cmd) logger.debug("sdt cmd: %s", cmd)
@ -210,26 +226,23 @@ class Sdt:
:return: nothing :return: nothing
""" """
nets = []
# create layers
for layer in CORE_LAYERS: for layer in CORE_LAYERS:
self.cmd(f"layer {layer}") self.cmd(f"layer {layer}")
with self.session.nodes_lock: with self.session.nodes_lock:
for node_id in self.session.nodes: nets = []
node = self.session.nodes[node_id] for node in self.session.nodes.values():
if isinstance(node, CoreNetworkBase): if isinstance(node, (EmaneNet, WlanNode)):
nets.append(node) nets.append(node)
if not isinstance(node, NodeBase): if not isinstance(node, NodeBase):
continue continue
self.add_node(node) self.add_node(node)
for link in self.session.link_manager.links():
if is_wireless(link.node1) or is_wireless(link.node2):
continue
link_data = link.get_data(MessageFlags.ADD)
self.handle_link_update(link_data)
for net in nets: for net in nets:
all_links = net.links(flags=MessageFlags.ADD) for link_data in net.links(MessageFlags.ADD):
for link_data in all_links:
is_wireless = isinstance(net, (WlanNode, EmaneNet))
if is_wireless and link_data.node1_id == net.id:
continue
self.handle_link_update(link_data) self.handle_link_update(link_data)
def get_node_position(self, node: NodeBase) -> Optional[str]: def get_node_position(self, node: NodeBase) -> Optional[str]:
@ -258,13 +271,14 @@ class Sdt:
pos = self.get_node_position(node) pos = self.get_node_position(node)
if not pos: if not pos:
return return
node_type = node.type if isinstance(node, CoreNode):
if node_type is None: node_type = node.model
node_type = type(node).type else:
node_type = NODE_TYPES.get(type(node), "PC")
icon = node.icon icon = node.icon
if icon: if icon:
node_type = node.name node_type = node.name
icon = icon.replace("$CORE_DATA_DIR", str(CORE_DATA_DIR)) icon = icon.replace("$CORE_DATA_DIR", str(LOCAL_ICONS_PATH.absolute()))
icon = icon.replace("$CORE_CONF_DIR", str(CORE_CONF_DIR)) icon = icon.replace("$CORE_CONF_DIR", str(CORE_CONF_DIR))
self.cmd(f"sprite {node_type} image {icon}") self.cmd(f"sprite {node_type} image {icon}")
self.cmd( self.cmd(
@ -341,7 +355,7 @@ class Sdt:
result = False result = False
try: try:
node = self.session.get_node(node_id, NodeBase) node = self.session.get_node(node_id, NodeBase)
result = isinstance(node, (WlanNode, EmaneNet)) result = isinstance(node, (WlanNode, EmaneNet, WirelessNode))
except CoreError: except CoreError:
pass pass
return result return result

104
daemon/core/scripts/cleanup.py Executable file
View file

@ -0,0 +1,104 @@
import argparse
import os
import subprocess
import sys
import time
def check_root() -> None:
if os.geteuid() != 0:
print("permission denied, run this script as root")
sys.exit(1)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="helps cleanup lingering core processes and files",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-d", "--daemon", action="store_true", help="also kill core-daemon"
)
return parser.parse_args()
def cleanup_daemon() -> None:
print("killing core-daemon process ... ", end="")
result = subprocess.call("pkill -9 core-daemon", shell=True)
if result:
print("not found")
else:
print("done")
def cleanup_nodes() -> None:
print("killing vnoded processes ... ", end="")
result = subprocess.call("pkill -KILL vnoded", shell=True)
if result:
print("none found")
else:
time.sleep(1)
print("done")
def cleanup_emane() -> None:
print("killing emane processes ... ", end="")
result = subprocess.call("pkill emane", shell=True)
if result:
print("none found")
else:
print("done")
def cleanup_sessions() -> None:
print("removing session directories ... ", end="")
result = subprocess.call("rm -rf /tmp/pycore*", shell=True)
if result:
print("none found")
else:
print("done")
def cleanup_interfaces() -> None:
print("cleaning up devices")
output = subprocess.check_output("ip -o -br link show", shell=True)
lines = output.decode().strip().split("\n")
for line in lines:
values = line.split()
name = values[0]
if (
name.startswith("veth")
or name.startswith("beth")
or name.startswith("gt.")
or name.startswith("b.")
or name.startswith("ctrl")
):
result = subprocess.call(f"ip link delete {name}", shell=True)
if result:
print(f"failed to remove {name}")
else:
print(f"removed {name}")
if name.startswith("b."):
result = subprocess.call(
f"nft delete table bridge {name}",
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
shell=True,
)
if not result:
print(f"cleared nft rules for {name}")
def main() -> None:
check_root()
args = parse_args()
if args.daemon:
cleanup_daemon()
cleanup_nodes()
cleanup_emane()
cleanup_interfaces()
cleanup_sessions()
if __name__ == "__main__":
main()

View file

@ -1,4 +1,3 @@
#!/usr/bin/env python3
import json import json
import sys import sys
from argparse import ( from argparse import (
@ -28,11 +27,13 @@ from core.api.grpc.wrappers import (
Position, Position,
) )
NODE_TYPES = [x for x in NodeType if x != NodeType.PEER_TO_PEER] NODE_TYPES = [x.name for x in NodeType if x != NodeType.PEER_TO_PEER]
def protobuf_to_json(message: Any) -> Dict[str, Any]: def protobuf_to_json(message: Any) -> Dict[str, Any]:
return MessageToDict(message, including_default_value_fields=True, preserving_proto_field_name=True) return MessageToDict(
message, including_default_value_fields=True, preserving_proto_field_name=True
)
def print_json(data: Any) -> None: def print_json(data: Any) -> None:
@ -122,18 +123,15 @@ def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int:
return sessions[0].id return sessions[0].id
def create_iface(iface_id: int, mac: str, ip4_net: IPNetwork, ip6_net: IPNetwork) -> Interface: def create_iface(
iface_id: int, mac: str, ip4_net: IPNetwork, ip6_net: IPNetwork
) -> Interface:
ip4 = str(ip4_net.ip) if ip4_net else None ip4 = str(ip4_net.ip) if ip4_net else None
ip4_mask = ip4_net.prefixlen if ip4_net else None ip4_mask = ip4_net.prefixlen if ip4_net else None
ip6 = str(ip6_net.ip) if ip6_net else None ip6 = str(ip6_net.ip) if ip6_net else None
ip6_mask = ip6_net.prefixlen if ip6_net else None ip6_mask = ip6_net.prefixlen if ip6_net else None
return Interface( return Interface(
id=iface_id, id=iface_id, mac=mac, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask
mac=mac,
ip4=ip4,
ip4_mask=ip4_mask,
ip6=ip6,
ip6_mask=ip6_mask,
) )
@ -216,12 +214,14 @@ def query_session(core: CoreGrpcClient, args: Namespace) -> None:
for node in session.nodes.values(): for node in session.nodes.values():
xy_pos = f"{int(node.position.x)},{int(node.position.y)}" xy_pos = f"{int(node.position.x)},{int(node.position.y)}"
geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}" geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}"
print(f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}") print(
f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}"
)
print("\nLinks") print("\nLinks")
for link in session.links: for link in session.links:
n1 = session.nodes[link.node1_id].name n1 = session.nodes[link.node1_id].name
n2 = session.nodes[link.node2_id].name n2 = session.nodes[link.node2_id].name
print(f"Node | ", end="") print("Node | ", end="")
print_iface_header() print_iface_header()
print(f"{n1:<6} | ", end="") print(f"{n1:<6} | ", end="")
if link.iface1: if link.iface1:
@ -248,7 +248,9 @@ def query_node(core: CoreGrpcClient, args: Namespace) -> None:
print("ID | Name | Type | XY | Geo") print("ID | Name | Type | XY | Geo")
xy_pos = f"{int(node.position.x)},{int(node.position.y)}" xy_pos = f"{int(node.position.x)},{int(node.position.y)}"
geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}" geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}"
print(f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}") print(
f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}"
)
if ifaces: if ifaces:
print("Interfaces") print("Interfaces")
print("Connected To | ", end="") print("Connected To | ", end="")
@ -348,10 +350,14 @@ def add_link(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session) session_id = get_current_session(core, args.session)
iface1 = None iface1 = None
if args.iface1_id is not None: if args.iface1_id is not None:
iface1 = create_iface(args.iface1_id, args.iface1_mac, args.iface1_ip4, args.iface1_ip6) iface1 = create_iface(
args.iface1_id, args.iface1_mac, args.iface1_ip4, args.iface1_ip6
)
iface2 = None iface2 = None
if args.iface2_id is not None: if args.iface2_id is not None:
iface2 = create_iface(args.iface2_id, args.iface2_mac, args.iface2_ip4, args.iface2_ip6) iface2 = create_iface(
args.iface2_id, args.iface2_mac, args.iface2_ip4, args.iface2_ip6
)
options = LinkOptions( options = LinkOptions(
bandwidth=args.bandwidth, bandwidth=args.bandwidth,
loss=args.loss, loss=args.loss,
@ -432,13 +438,17 @@ def setup_node_parser(parent) -> None:
add_parser.add_argument( add_parser.add_argument(
"-t", "--type", choices=NODE_TYPES, default="DEFAULT", help="type of node" "-t", "--type", choices=NODE_TYPES, default="DEFAULT", help="type of node"
) )
add_parser.add_argument("-m", "--model", help="used to determine services, optional") add_parser.add_argument(
"-m", "--model", help="used to determine services, optional"
)
group = add_parser.add_mutually_exclusive_group(required=True) group = add_parser.add_mutually_exclusive_group(required=True)
group.add_argument("-p", "--pos", type=position_type, help="x,y position") group.add_argument("-p", "--pos", type=position_type, help="x,y position")
group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position") group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position")
add_parser.add_argument("-ic", "--icon", help="icon to use, optional") add_parser.add_argument("-ic", "--icon", help="icon to use, optional")
add_parser.add_argument("-im", "--image", help="container image, optional") add_parser.add_argument("-im", "--image", help="container image, optional")
add_parser.add_argument("-e", "--emane", help="emane model, only required for emane nodes") add_parser.add_argument(
"-e", "--emane", help="emane model, only required for emane nodes"
)
add_parser.set_defaults(func=add_node) add_parser.set_defaults(func=add_node)
edit_parser = subparsers.add_parser("edit", help="edit a node") edit_parser = subparsers.add_parser("edit", help="edit a node")
@ -449,7 +459,9 @@ def setup_node_parser(parent) -> None:
move_parser = subparsers.add_parser("move", help="move a node") move_parser = subparsers.add_parser("move", help="move a node")
move_parser.formatter_class = ArgumentDefaultsHelpFormatter move_parser.formatter_class = ArgumentDefaultsHelpFormatter
move_parser.add_argument("-i", "--id", type=int, help="id to use, optional", required=True) move_parser.add_argument(
"-i", "--id", type=int, help="id to use, optional", required=True
)
group = move_parser.add_mutually_exclusive_group(required=True) group = move_parser.add_mutually_exclusive_group(required=True)
group.add_argument("-p", "--pos", type=position_type, help="x,y position") group.add_argument("-p", "--pos", type=position_type, help="x,y position")
group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position") group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position")
@ -474,19 +486,33 @@ def setup_link_parser(parent) -> None:
add_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True) add_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True)
add_parser.add_argument("-n2", "--node2", type=int, help="node2 id", required=True) add_parser.add_argument("-n2", "--node2", type=int, help="node2 id", required=True)
add_parser.add_argument("-i1-i", "--iface1-id", type=int, help="node1 interface id") add_parser.add_argument("-i1-i", "--iface1-id", type=int, help="node1 interface id")
add_parser.add_argument("-i1-m", "--iface1-mac", type=mac_type, help="node1 interface mac") add_parser.add_argument(
add_parser.add_argument("-i1-4", "--iface1-ip4", type=ip4_type, help="node1 interface ip4") "-i1-m", "--iface1-mac", type=mac_type, help="node1 interface mac"
add_parser.add_argument("-i1-6", "--iface1-ip6", type=ip6_type, help="node1 interface ip6") )
add_parser.add_argument(
"-i1-4", "--iface1-ip4", type=ip4_type, help="node1 interface ip4"
)
add_parser.add_argument(
"-i1-6", "--iface1-ip6", type=ip6_type, help="node1 interface ip6"
)
add_parser.add_argument("-i2-i", "--iface2-id", type=int, help="node2 interface id") add_parser.add_argument("-i2-i", "--iface2-id", type=int, help="node2 interface id")
add_parser.add_argument("-i2-m", "--iface2-mac", type=mac_type, help="node2 interface mac") add_parser.add_argument(
add_parser.add_argument("-i2-4", "--iface2-ip4", type=ip4_type, help="node2 interface ip4") "-i2-m", "--iface2-mac", type=mac_type, help="node2 interface mac"
add_parser.add_argument("-i2-6", "--iface2-ip6", type=ip6_type, help="node2 interface ip6") )
add_parser.add_argument(
"-i2-4", "--iface2-ip4", type=ip4_type, help="node2 interface ip4"
)
add_parser.add_argument(
"-i2-6", "--iface2-ip6", type=ip6_type, help="node2 interface ip6"
)
add_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)") add_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)")
add_parser.add_argument("-l", "--loss", type=float, help="loss (%%)") add_parser.add_argument("-l", "--loss", type=float, help="loss (%%)")
add_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)") add_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)")
add_parser.add_argument("-de", "--delay", type=int, help="delay (us)") add_parser.add_argument("-de", "--delay", type=int, help="delay (us)")
add_parser.add_argument("-du", "--duplicate", type=int, help="duplicate (%%)") add_parser.add_argument("-du", "--duplicate", type=int, help="duplicate (%%)")
add_parser.add_argument("-u", "--uni", action="store_true", help="is link unidirectional?") add_parser.add_argument(
"-u", "--uni", action="store_true", help="is link unidirectional?"
)
add_parser.set_defaults(func=add_link) add_parser.set_defaults(func=add_link)
edit_parser = subparsers.add_parser("edit", help="edit a link") edit_parser = subparsers.add_parser("edit", help="edit a link")
@ -507,8 +533,12 @@ def setup_link_parser(parent) -> None:
delete_parser = subparsers.add_parser("delete", help="delete a link") delete_parser = subparsers.add_parser("delete", help="delete a link")
delete_parser.formatter_class = ArgumentDefaultsHelpFormatter delete_parser.formatter_class = ArgumentDefaultsHelpFormatter
delete_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True) delete_parser.add_argument(
delete_parser.add_argument("-n2", "--node2", type=int, help="node1 id", required=True) "-n1", "--node1", type=int, help="node1 id", required=True
)
delete_parser.add_argument(
"-n2", "--node2", type=int, help="node1 id", required=True
)
delete_parser.add_argument("-i1", "--iface1", type=int, help="node1 interface id") delete_parser.add_argument("-i1", "--iface1", type=int, help="node1 interface id")
delete_parser.add_argument("-i2", "--iface2", type=int, help="node2 interface id") delete_parser.add_argument("-i2", "--iface2", type=int, help="node2 interface id")
delete_parser.set_defaults(func=delete_link) delete_parser.set_defaults(func=delete_link)
@ -526,20 +556,28 @@ def setup_query_parser(parent) -> None:
session_parser = subparsers.add_parser("session", help="query session") session_parser = subparsers.add_parser("session", help="query session")
session_parser.formatter_class = ArgumentDefaultsHelpFormatter session_parser.formatter_class = ArgumentDefaultsHelpFormatter
session_parser.add_argument("-i", "--id", type=int, help="session to query", required=True) session_parser.add_argument(
"-i", "--id", type=int, help="session to query", required=True
)
session_parser.set_defaults(func=query_session) session_parser.set_defaults(func=query_session)
node_parser = subparsers.add_parser("node", help="query node") node_parser = subparsers.add_parser("node", help="query node")
node_parser.formatter_class = ArgumentDefaultsHelpFormatter node_parser.formatter_class = ArgumentDefaultsHelpFormatter
node_parser.add_argument("-i", "--id", type=int, help="session to query", required=True) node_parser.add_argument(
node_parser.add_argument("-n", "--node", type=int, help="node to query", required=True) "-i", "--id", type=int, help="session to query", required=True
)
node_parser.add_argument(
"-n", "--node", type=int, help="node to query", required=True
)
node_parser.set_defaults(func=query_node) node_parser.set_defaults(func=query_node)
def setup_xml_parser(parent) -> None: def setup_xml_parser(parent) -> None:
parser = parent.add_parser("xml", help="open session xml") parser = parent.add_parser("xml", help="open session xml")
parser.formatter_class = ArgumentDefaultsHelpFormatter parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-f", "--file", type=file_type, help="xml file to open", required=True) parser.add_argument(
"-f", "--file", type=file_type, help="xml file to open", required=True
)
parser.add_argument("-s", "--start", action="store_true", help="start the session?") parser.add_argument("-s", "--start", action="store_true", help="start the session?")
parser.set_defaults(func=open_xml) parser.set_defaults(func=open_xml)

View file

@ -1,4 +1,3 @@
#!/usr/bin/env python3
""" """
core-daemon: the CORE daemon is a server process that receives CORE API core-daemon: the CORE daemon is a server process that receives CORE API
messages and instantiates emulated nodes and networks within the kernel. Various messages and instantiates emulated nodes and networks within the kernel. Various
@ -8,19 +7,15 @@ message handlers are defined and some support for sending messages.
import argparse import argparse
import logging import logging
import os import os
import sys
import threading
import time import time
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path from pathlib import Path
from core import constants from core import constants
from core.api.grpc.server import CoreGrpcServer from core.api.grpc.server import CoreGrpcServer
from core.api.tlv.corehandlers import CoreHandler, CoreUdpHandler
from core.api.tlv.coreserver import CoreServer, CoreUdpServer
from core.api.tlv.enumerations import CORE_API_PORT
from core.constants import CORE_CONF_DIR, COREDPY_VERSION from core.constants import CORE_CONF_DIR, COREDPY_VERSION
from core.utils import close_onexec, load_logging_config from core.emulator.coreemu import CoreEmu
from core.utils import load_logging_config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,20 +29,6 @@ def banner():
logger.info("CORE daemon v.%s started %s", constants.COREDPY_VERSION, time.ctime()) logger.info("CORE daemon v.%s started %s", constants.COREDPY_VERSION, time.ctime())
def start_udp(mainserver, server_address):
"""
Start a thread running a UDP server on the same host,port for
connectionless requests.
:param CoreServer mainserver: main core tcp server to piggy back off of
:param server_address:
:return: CoreUdpServer
"""
mainserver.udpserver = CoreUdpServer(server_address, CoreUdpHandler, mainserver)
mainserver.udpthread = threading.Thread(target=mainserver.udpserver.start, daemon=True)
mainserver.udpthread.start()
def cored(cfg): def cored(cfg):
""" """
Start the CoreServer object and enter the server loop. Start the CoreServer object and enter the server loop.
@ -55,34 +36,13 @@ def cored(cfg):
:param dict cfg: core configuration :param dict cfg: core configuration
:return: nothing :return: nothing
""" """
host = cfg["listenaddr"]
port = int(cfg["port"])
if host == "" or host is None:
host = "localhost"
try:
address = (host, port)
server = CoreServer(address, CoreHandler, cfg)
except:
logger.exception("error starting main server on: %s:%s", host, port)
sys.exit(1)
# initialize grpc api # initialize grpc api
grpc_server = CoreGrpcServer(server.coreemu) coreemu = CoreEmu(cfg)
grpc_server = CoreGrpcServer(coreemu)
address_config = cfg["grpcaddress"] address_config = cfg["grpcaddress"]
port_config = cfg["grpcport"] port_config = cfg["grpcport"]
grpc_address = f"{address_config}:{port_config}" grpc_address = f"{address_config}:{port_config}"
grpc_thread = threading.Thread(target=grpc_server.listen, args=(grpc_address,), daemon=True) grpc_server.listen(grpc_address)
grpc_thread.start()
# start udp server
start_udp(server, address)
# close handlers
close_onexec(server.fileno())
logger.info("CORE TLV API TCP/UDP listening on: %s:%s", host, port)
server.serve_forever()
def get_merged_config(filename): def get_merged_config(filename):
@ -98,49 +58,55 @@ def get_merged_config(filename):
default_grpc_port = "50051" default_grpc_port = "50051"
default_address = "localhost" default_address = "localhost"
defaults = { defaults = {
"port": str(CORE_API_PORT),
"listenaddr": default_address,
"grpcport": default_grpc_port, "grpcport": default_grpc_port,
"grpcaddress": default_address, "grpcaddress": default_address,
"logfile": default_log "logfile": default_log,
} }
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=f"CORE daemon v.{COREDPY_VERSION} instantiates Linux network namespace nodes.") description=f"CORE daemon v.{COREDPY_VERSION} instantiates Linux network namespace nodes."
parser.add_argument("-f", "--configfile", dest="configfile", )
help=f"read config from specified file; default = {filename}") parser.add_argument(
parser.add_argument("-p", "--port", dest="port", type=int, "-f",
help=f"port number to listen on; default = {CORE_API_PORT}") "--configfile",
parser.add_argument("--ovs", action="store_true", help="enable experimental ovs mode, default is false") dest="configfile",
parser.add_argument("--grpc-port", dest="grpcport", help=f"read config from specified file; default = {filename}",
help=f"grpc port to listen on; default {default_grpc_port}") )
parser.add_argument("--grpc-address", dest="grpcaddress", parser.add_argument(
help=f"grpc address to listen on; default {default_address}") "--ovs",
parser.add_argument("-l", "--logfile", help=f"core logging configuration; default {default_log}") action="store_true",
help="enable experimental ovs mode, default is false",
)
parser.add_argument(
"--grpc-port",
dest="grpcport",
help=f"grpc port to listen on; default {default_grpc_port}",
)
parser.add_argument(
"--grpc-address",
dest="grpcaddress",
help=f"grpc address to listen on; default {default_address}",
)
parser.add_argument(
"-l", "--logfile", help=f"core logging configuration; default {default_log}"
)
# parse command line options # parse command line options
args = parser.parse_args() args = parser.parse_args()
# convert ovs to internal format # convert ovs to internal format
args.ovs = "1" if args.ovs else "0" args.ovs = "1" if args.ovs else "0"
# read the config file # read the config file
if args.configfile is not None: if args.configfile is not None:
filename = args.configfile filename = args.configfile
del args.configfile del args.configfile
cfg = ConfigParser(defaults) cfg = ConfigParser(defaults)
cfg.read(filename) cfg.read(filename)
section = "core-daemon" section = "core-daemon"
if not cfg.has_section(section): if not cfg.has_section(section):
cfg.add_section(section) cfg.add_section(section)
# merge argparse with configparser # merge argparse with configparser
for opt in vars(args): for opt in vars(args):
val = getattr(args, opt) val = getattr(args, opt)
if val is not None: if val is not None:
cfg.set(section, opt, str(val)) cfg.set(section, opt, str(val))
return dict(cfg.items(section)) return dict(cfg.items(section))

View file

@ -1,4 +1,3 @@
#!/usr/bin/env python3
import argparse import argparse
import logging import logging
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
@ -9,12 +8,19 @@ from core.gui.app import Application
def main() -> None: def main() -> None:
# parse flags # parse flags
parser = argparse.ArgumentParser(description=f"CORE Python GUI") parser = argparse.ArgumentParser(description="CORE Python GUI")
parser.add_argument("-l", "--level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], default="INFO", parser.add_argument(
help="logging level") "-l",
"--level",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
default="INFO",
help="logging level",
)
parser.add_argument("-p", "--proxy", action="store_true", help="enable proxy") parser.add_argument("-p", "--proxy", action="store_true", help="enable proxy")
parser.add_argument("-s", "--session", type=int, help="session id to join") parser.add_argument("-s", "--session", type=int, help="session id to join")
parser.add_argument("--create-dir", action="store_true", help="create gui directory and exit") parser.add_argument(
"--create-dir", action="store_true", help="create gui directory and exit"
)
args = parser.parse_args() args = parser.parse_args()
# check home directory exists and create if necessary # check home directory exists and create if necessary
@ -25,9 +31,13 @@ def main() -> None:
# setup logging # setup logging
log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s"
stream_handler = logging.StreamHandler() stream_handler = logging.StreamHandler()
file_handler = TimedRotatingFileHandler(filename=appconfig.LOG_PATH, when="D", backupCount=5) file_handler = TimedRotatingFileHandler(
filename=appconfig.LOG_PATH, when="D", backupCount=5
)
log_level = logging.getLevelName(args.level) log_level = logging.getLevelName(args.level)
logging.basicConfig(level=log_level, format=log_format, handlers=[stream_handler, file_handler]) logging.basicConfig(
level=log_level, format=log_format, handlers=[stream_handler, file_handler]
)
logging.getLogger("PIL").setLevel(logging.ERROR) logging.getLogger("PIL").setLevel(logging.ERROR)
# start app # start app

51
daemon/core/scripts/player.py Executable file
View file

@ -0,0 +1,51 @@
import argparse
import logging
import sys
from pathlib import Path
from core.player import CorePlayer
logger = logging.getLogger(__name__)
def path_type(value: str) -> Path:
file_path = Path(value)
if not file_path.is_file():
raise argparse.ArgumentTypeError(f"file does not exist: {value}")
return file_path
def parse_args() -> argparse.Namespace:
"""
Setup and parse command line arguments.
:return: parsed arguments
"""
parser = argparse.ArgumentParser(
description="core player runs files that can move nodes and send commands",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-f", "--file", required=True, type=path_type, help="core file to play"
)
parser.add_argument(
"-s",
"--session",
type=int,
help="session to play to, first found session otherwise",
)
return parser.parse_args()
def main() -> None:
logging.basicConfig(level=logging.INFO)
args = parse_args()
player = CorePlayer(args.file)
result = player.init(args.session)
if not result:
sys.exit(1)
player.start()
if __name__ == "__main__":
main()

View file

@ -1,4 +1,3 @@
#!/usr/bin/env python3
import argparse import argparse
import enum import enum
import select import select
@ -60,15 +59,15 @@ class SdtClient:
class RouterMonitor: class RouterMonitor:
def __init__( def __init__(
self, self,
session: int, session: int,
src: str, src: str,
dst: str, dst: str,
pkt: str, pkt: str,
rate: int, rate: int,
dead: int, dead: int,
sdt_host: str, sdt_host: str,
sdt_port: int, sdt_port: int,
) -> None: ) -> None:
self.queue = Queue() self.queue = Queue()
self.core = CoreGrpcClient() self.core = CoreGrpcClient()

View file

@ -1,4 +1,3 @@
#!/usr/bin/env python3
import argparse import argparse
import re import re
from io import TextIOWrapper from io import TextIOWrapper
@ -6,9 +5,15 @@ from io import TextIOWrapper
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=f"Helps transition older CORE services to work with newer versions") description="Helps transition older CORE services to work with newer versions"
parser.add_argument("-f", "--file", dest="file", type=argparse.FileType("r"), )
help=f"service file to update") parser.add_argument(
"-f",
"--file",
dest="file",
type=argparse.FileType("r"),
help="service file to update",
)
return parser.parse_args() return parser.parse_args()
@ -20,17 +25,32 @@ def update_service(service_file: TextIOWrapper) -> None:
# rename dirs to directories # rename dirs to directories
line = re.sub(r"^(\s+)dirs", r"\1directories", line) line = re.sub(r"^(\s+)dirs", r"\1directories", line)
# fix import states for service # fix import states for service
line = re.sub(r"^.+import.+CoreService.+$", line = re.sub(
r"from core.services.coreservices import CoreService", line) r"^.+import.+CoreService.+$",
r"from core.services.coreservices import CoreService",
line,
)
# fix method signatures # fix method signatures
line = re.sub(r"def generateconfig\(cls, node, filename, services\)", line = re.sub(
r"def generate_config(cls, node, filename)", line) r"def generateconfig\(cls, node, filename, services\)",
line = re.sub(r"def getvalidate\(cls, node, services\)", r"def generate_config(cls, node, filename)",
r"def get_validate(cls, node)", line) line,
line = re.sub(r"def getstartup\(cls, node, services\)", )
r"def get_startup(cls, node)", line) line = re.sub(
line = re.sub(r"def getconfigfilenames\(cls, nodenum, services\)", r"def getvalidate\(cls, node, services\)",
r"def get_configs(cls, node)", line) r"def get_validate(cls, node)",
line,
)
line = re.sub(
r"def getstartup\(cls, node, services\)",
r"def get_startup(cls, node)",
line,
)
line = re.sub(
r"def getconfigfilenames\(cls, nodenum, services\)",
r"def get_configs(cls, node)",
line,
)
# remove unwanted lines # remove unwanted lines
if re.search(r"addservice\(", line): if re.search(r"addservice\(", line):
continue continue

View file

@ -109,114 +109,6 @@ class ServiceDependencies:
return self.boot_paths return self.boot_paths
class ServiceShim:
keys: List[str] = [
"dirs",
"files",
"startidx",
"cmdup",
"cmddown",
"cmdval",
"meta",
"starttime",
]
@classmethod
def tovaluelist(cls, node: CoreNode, service: "CoreService") -> str:
"""
Convert service properties into a string list of key=value pairs,
separated by "|".
:param node: node to get value list for
:param service: service to get value list for
:return: value list string
"""
start_time = 0
start_index = 0
valmap = [
service.dirs,
service.configs,
start_index,
service.startup,
service.shutdown,
service.validate,
service.meta,
start_time,
]
if not service.custom:
valmap[1] = service.get_configs(node)
valmap[3] = service.get_startup(node)
vals = ["%s=%s" % (x, y) for x, y in zip(cls.keys, valmap)]
return "|".join(vals)
@classmethod
def fromvaluelist(cls, service: "CoreService", values: List[str]) -> None:
"""
Convert list of values into properties for this instantiated
(customized) service.
:param service: service to get value list for
:param values: value list to set properties from
:return: nothing
"""
# TODO: support empty value? e.g. override default meta with ''
for key in cls.keys:
try:
cls.setvalue(service, key, values[cls.keys.index(key)])
except IndexError:
# old config does not need to have new keys
logger.exception("error indexing into key")
@classmethod
def setvalue(cls, service: "CoreService", key: str, value: str) -> None:
"""
Set values for this service.
:param service: service to get value list for
:param key: key to set value for
:param value: value of key to set
:return: nothing
"""
if key not in cls.keys:
raise ValueError("key `%s` not in `%s`" % (key, cls.keys))
# this handles data conversion to int, string, and tuples
if value:
if key == "startidx":
value = int(value)
elif key == "starttime":
value = float(value)
elif key == "meta":
value = str(value)
else:
value = utils.make_tuple_fromstr(value, str)
if key == "dirs":
service.dirs = value
elif key == "files":
service.configs = value
elif key == "cmdup":
service.startup = value
elif key == "cmddown":
service.shutdown = value
elif key == "cmdval":
service.validate = value
elif key == "meta":
service.meta = value
@classmethod
def servicesfromopaque(cls, opaque: str) -> List[str]:
"""
Build a list of services from an opaque data string.
:param opaque: opaque data string
:return: services
"""
servicesstring = opaque.split(":")
if servicesstring[0] != "service":
return []
return servicesstring[1].split(",")
class ServiceManager: class ServiceManager:
""" """
Manages services available for CORE nodes to use. Manages services available for CORE nodes to use.
@ -342,26 +234,6 @@ class CoreServices:
""" """
self.custom_services.clear() self.custom_services.clear()
def get_default_services(self, node_type: str) -> List[Type["CoreService"]]:
"""
Get the list of default services that should be enabled for a
node for the given node type.
:param node_type: node type to get default services for
:return: default services
"""
logger.debug("getting default services for type: %s", node_type)
results = []
defaults = self.default_services.get(node_type, [])
for name in defaults:
logger.debug("checking for service with service manager: %s", name)
service = ServiceManager.get(name)
if not service:
logger.warning("default service %s is unknown", name)
else:
results.append(service)
return results
def get_service( def get_service(
self, node_id: int, service_name: str, default_service: bool = False self, node_id: int, service_name: str, default_service: bool = False
) -> "CoreService": ) -> "CoreService":
@ -401,21 +273,21 @@ class CoreServices:
node_services[service.name] = service node_services[service.name] = service
def add_services( def add_services(
self, node: CoreNode, node_type: str, services: List[str] = None self, node: CoreNode, model: str, services: List[str] = None
) -> None: ) -> None:
""" """
Add services to a node. Add services to a node.
:param node: node to add services to :param node: node to add services to
:param node_type: node type to add services to :param model: node model type to add services for
:param services: names of services to add to node :param services: names of services to add to node
:return: nothing :return: nothing
""" """
if not services: if not services:
logger.info( logger.info(
"using default services for node(%s) type(%s)", node.name, node_type "using default services for node(%s) type(%s)", node.name, model
) )
services = self.default_services.get(node_type, []) services = self.default_services.get(model, [])
logger.info("setting services for node(%s): %s", node.name, services) logger.info("setting services for node(%s): %s", node.name, services)
for service_name in services: for service_name in services:
service = self.get_service(node.id, service_name, default_service=True) service = self.get_service(node.id, service_name, default_service=True)

View file

@ -7,15 +7,26 @@ from typing import Optional, Tuple
import netaddr import netaddr
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.nodes.base import CoreNode from core.nodes.base import CoreNode, NodeBase
from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.network import PtpNet, WlanNode from core.nodes.network import PtpNet, WlanNode
from core.nodes.physical import Rj45Node from core.nodes.physical import Rj45Node
from core.nodes.wireless import WirelessNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
FRR_STATE_DIR: str = "/var/run/frr" FRR_STATE_DIR: str = "/var/run/frr"
def is_wireless(node: NodeBase) -> bool:
"""
Check if the node is a wireless type node.
:param node: node to check type for
:return: True if wireless type, False otherwise
"""
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
class FRRZebra(CoreService): class FRRZebra(CoreService):
name: str = "FRRzebra" name: str = "FRRzebra"
group: str = "FRR" group: str = "FRR"
@ -127,11 +138,11 @@ class FRRZebra(CoreService):
""" """
Generate a shell script used to boot the FRR daemons. Generate a shell script used to boot the FRR daemons.
""" """
frr_bin_search = node.session.options.get_config( frr_bin_search = node.session.options.get(
"frr_bin_search", default='"/usr/local/bin /usr/bin /usr/lib/frr"' "frr_bin_search", '"/usr/local/bin /usr/bin /usr/lib/frr"'
) )
frr_sbin_search = node.session.options.get_config( frr_sbin_search = node.session.options.get(
"frr_sbin_search", default='"/usr/local/sbin /usr/sbin /usr/lib/frr"' "frr_sbin_search", '"/usr/local/sbin /usr/sbin /usr/lib/frr"'
) )
cfg = """\ cfg = """\
#!/bin/sh #!/bin/sh
@ -184,6 +195,10 @@ bootdaemon()
flags="$flags -6" flags="$flags -6"
fi fi
if [ "$1" = "ospfd" ]; then
flags="$flags --apiserver"
fi
#force FRR to use CORE generated conf file #force FRR to use CORE generated conf file
flags="$flags -d -f $FRR_CONF" flags="$flags -d -f $FRR_CONF"
$FRR_SBIN_DIR/$1 $flags $FRR_SBIN_DIR/$1 $flags
@ -414,12 +429,25 @@ class FRROspfv2(FrrService):
for iface in node.get_ifaces(control=False): for iface in node.get_ifaces(control=False):
for ip4 in iface.ip4s: for ip4 in iface.ip4s:
cfg += f" network {ip4} area 0\n" cfg += f" network {ip4} area 0\n"
cfg += " ospf opaque-lsa\n"
cfg += "!\n" cfg += "!\n"
return cfg return cfg
@classmethod @classmethod
def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
return cls.mtu_check(iface) cfg = cls.mtu_check(iface)
# external RJ45 connections will use default OSPF timers
if cls.rj45check(iface):
return cfg
cfg += cls.ptp_check(iface)
return (
cfg
+ """\
ip ospf hello-interval 2
ip ospf dead-interval 6
ip ospf retransmit-interval 5
"""
)
class FRROspfv3(FrrService): class FRROspfv3(FrrService):
@ -485,18 +513,6 @@ class FRROspfv3(FrrService):
@classmethod @classmethod
def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
return cls.mtu_check(iface) return cls.mtu_check(iface)
# cfg = cls.mtucheck(ifc)
# external RJ45 connections will use default OSPF timers
# if cls.rj45check(ifc):
# return cfg
# cfg += cls.ptpcheck(ifc)
# return cfg + """\
# ipv6 ospf6 hello-interval 2
# ipv6 ospf6 dead-interval 6
# ipv6 ospf6 retransmit-interval 5
# """
class FRRBgp(FrrService): class FRRBgp(FrrService):
@ -593,7 +609,7 @@ class FRRBabel(FrrService):
@classmethod @classmethod
def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
if iface.net and isinstance(iface.net, (EmaneNet, WlanNode)): if is_wireless(iface.net):
return " babel wireless\n no babel split-horizon\n" return " babel wireless\n no babel split-horizon\n"
else: else:
return " babel wired\n babel split-horizon\n" return " babel wired\n babel split-horizon\n"

View file

@ -118,12 +118,6 @@ class NrlSmf(NrlService):
ifaces = node.get_ifaces(control=False) ifaces = node.get_ifaces(control=False)
if len(ifaces) == 0: if len(ifaces) == 0:
return "" return ""
if "arouted" in servicenames:
comments += "# arouted service is enabled\n"
cmd += " tap %s_tap" % (node.name,)
cmd += " unicast %s" % cls.firstipv4prefix(node, 24)
cmd += " push lo,%s resequence on" % ifaces[0].name
if len(ifaces) > 0: if len(ifaces) > 0:
if "NHDP" in servicenames: if "NHDP" in servicenames:
comments += "# NHDP service is enabled\n" comments += "# NHDP service is enabled\n"
@ -586,46 +580,3 @@ class MgenActor(NrlService):
return "" return ""
cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n" cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n"
return cfg return cfg
class Arouted(NrlService):
"""
Adaptive Routing
"""
name: str = "arouted"
executables: Tuple[str, ...] = ("arouted",)
configs: Tuple[str, ...] = ("startarouted.sh",)
startup: Tuple[str, ...] = ("bash startarouted.sh",)
shutdown: Tuple[str, ...] = ("pkill arouted",)
validate: Tuple[str, ...] = ("pidof arouted",)
@classmethod
def generate_config(cls, node: CoreNode, filename: str) -> str:
"""
Return the Quagga.conf or quaggaboot.sh file contents.
"""
cfg = (
"""
#!/bin/sh
for f in "/tmp/%s_smf"; do
count=1
until [ -e "$f" ]; do
if [ $count -eq 10 ]; then
echo "ERROR: nrlmsf pipe not found: $f" >&2
exit 1
fi
sleep 0.1
count=$(($count + 1))
done
done
"""
% node.name
)
cfg += "ip route add %s dev lo\n" % cls.firstipv4prefix(node, 24)
cfg += "arouted instance %s_smf tap %s_tap" % (node.name, node.name)
# seconds to consider a new route valid
cfg += " stability 10"
cfg += " 2>&1 > /var/log/arouted.log &\n\n"
return cfg

View file

@ -6,16 +6,26 @@ from typing import Optional, Tuple
import netaddr import netaddr
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.enumerations import LinkTypes from core.nodes.base import CoreNode, NodeBase
from core.nodes.base import CoreNode
from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.interface import DEFAULT_MTU, CoreInterface
from core.nodes.network import PtpNet, WlanNode from core.nodes.network import PtpNet, WlanNode
from core.nodes.physical import Rj45Node from core.nodes.physical import Rj45Node
from core.nodes.wireless import WirelessNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
QUAGGA_STATE_DIR: str = "/var/run/quagga" QUAGGA_STATE_DIR: str = "/var/run/quagga"
def is_wireless(node: NodeBase) -> bool:
"""
Check if the node is a wireless type node.
:param node: node to check type for
:return: True if wireless type, False otherwise
"""
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
class Zebra(CoreService): class Zebra(CoreService):
name: str = "zebra" name: str = "zebra"
group: str = "Quagga" group: str = "Quagga"
@ -124,11 +134,11 @@ class Zebra(CoreService):
""" """
Generate a shell script used to boot the Quagga daemons. Generate a shell script used to boot the Quagga daemons.
""" """
quagga_bin_search = node.session.options.get_config( quagga_bin_search = node.session.options.get(
"quagga_bin_search", default='"/usr/local/bin /usr/bin /usr/lib/quagga"' "quagga_bin_search", '"/usr/local/bin /usr/bin /usr/lib/quagga"'
) )
quagga_sbin_search = node.session.options.get_config( quagga_sbin_search = node.session.options.get(
"quagga_sbin_search", default='"/usr/local/sbin /usr/sbin /usr/lib/quagga"' "quagga_sbin_search", '"/usr/local/sbin /usr/sbin /usr/lib/quagga"'
) )
return """\ return """\
#!/bin/sh #!/bin/sh
@ -431,7 +441,7 @@ class Ospfv3mdr(Ospfv3):
@classmethod @classmethod
def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
cfg = cls.mtu_check(iface) cfg = cls.mtu_check(iface)
if iface.net is not None and isinstance(iface.net, (WlanNode, EmaneNet)): if is_wireless(iface.net):
return ( return (
cfg cfg
+ """\ + """\
@ -542,7 +552,7 @@ class Babel(QuaggaService):
@classmethod @classmethod
def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
if iface.net and iface.net.linktype == LinkTypes.WIRELESS: if is_wireless(iface.net):
return " babel wireless\n no babel split-horizon\n" return " babel wireless\n no babel split-horizon\n"
else: else:
return " babel wired\n babel split-horizon\n" return " babel wired\n babel split-horizon\n"

View file

@ -44,9 +44,7 @@ class Ucarp(CoreService):
""" """
Returns configuration file text. Returns configuration file text.
""" """
ucarp_bin = node.session.options.get_config( ucarp_bin = node.session.options.get("ucarp_bin", "/usr/sbin/ucarp")
"ucarp_bin", default="/usr/sbin/ucarp"
)
return """\ return """\
#!/bin/sh #!/bin/sh
# Location of UCARP executable # Location of UCARP executable

View file

@ -16,7 +16,9 @@ import shlex
import shutil import shutil
import sys import sys
import threading import threading
from collections import OrderedDict
from pathlib import Path from pathlib import Path
from queue import Queue
from subprocess import PIPE, STDOUT, Popen from subprocess import PIPE, STDOUT, Popen
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@ -214,8 +216,7 @@ def cmd(
shell: bool = False, shell: bool = False,
) -> str: ) -> str:
""" """
Execute a command on the host and return a tuple containing the exit status and Execute a command on the host and returns the combined stderr stdout output.
result string. stderr output is folded into the stdout result string.
:param args: command arguments :param args: command arguments
:param env: environment to run command with :param env: environment to run command with
@ -248,6 +249,25 @@ def cmd(
raise CoreCommandError(1, input_args, "", e.strerror) raise CoreCommandError(1, input_args, "", e.strerror)
def run_cmds(args: List[str], wait: bool = True, shell: bool = False) -> List[str]:
"""
Execute a series of commands on the host and returns a list of the combined stderr
stdout output.
:param args: command arguments
:param wait: True to wait for status, False otherwise
:param shell: True to use shell, False otherwise
:return: combined stdout and stderr
:raises CoreCommandError: when there is a non-zero exit status or the file to
execute is not found
"""
outputs = []
for arg in args:
output = cmd(arg, wait=wait, shell=shell)
outputs.append(output)
return outputs
def file_munge(pathname: str, header: str, text: str) -> None: def file_munge(pathname: str, header: str, text: str) -> None:
""" """
Insert text at the end of a file, surrounded by header comments. Insert text at the end of a file, surrounded by header comments.
@ -405,6 +425,101 @@ def load_logging_config(config_path: Path) -> None:
logging.config.dictConfig(log_config) logging.config.dictConfig(log_config)
def run_cmds_threaded(
nodes: List["CoreNode"],
cmds: List[str],
wait: bool = True,
shell: bool = False,
workers: int = None,
) -> Tuple[Dict[int, List[str]], List[Exception]]:
"""
Run a set of commands in order across a provided set of nodes. Each node will
run the commands within the context of a threadpool.
:param nodes: nodes to run commands in
:param cmds: commands to run in nodes
:param wait: True to wait for status, False otherwise
:param shell: True to run shell like, False otherwise
:param workers: number of workers for threadpool, uses library default otherwise
:return: tuple including dict of node id to list of command output and a list of
exceptions if any
"""
def _node_cmds(
_target: "CoreNode", _cmds: List[str], _wait: bool, _shell: bool
) -> List[str]:
outputs = []
for _cmd in _cmds:
output = _target.cmd(_cmd, wait=_wait, shell=_shell)
outputs.append(output)
return outputs
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
futures = []
node_mappings = {}
for node in nodes:
future = executor.submit(_node_cmds, node, cmds, wait, shell)
node_mappings[future] = node
futures.append(future)
outputs = {}
exceptions = []
for future in concurrent.futures.as_completed(futures):
try:
result = future.result()
node = node_mappings[future]
outputs[node.id] = result
except Exception as e:
logger.exception("thread pool exception")
exceptions.append(e)
return outputs, exceptions
def run_cmds_mp(
nodes: List["CoreNode"],
cmds: List[str],
wait: bool = True,
shell: bool = False,
workers: int = None,
) -> Tuple[Dict[int, List[str]], List[Exception]]:
"""
Run a set of commands in order across a provided set of nodes. Each node will
run the commands within the context of a process pool. This will not work
for distributed nodes and throws an exception when encountered.
:param nodes: nodes to run commands in
:param cmds: commands to run in nodes
:param wait: True to wait for status, False otherwise
:param shell: True to run shell like, False otherwise
:param workers: number of workers for threadpool, uses library default otherwise
:return: tuple including dict of node id to list of command output and a list of
exceptions if any
:raises CoreError: when a distributed node is provided as input
"""
with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as executor:
futures = []
node_mapping = {}
for node in nodes:
node_cmds = [node.create_cmd(x) for x in cmds]
if node.server:
raise CoreError(
f"{node.name} uses a distributed server and not supported"
)
future = executor.submit(run_cmds, node_cmds, wait=wait, shell=shell)
node_mapping[future] = node
futures.append(future)
exceptions = []
outputs = {}
for future in concurrent.futures.as_completed(futures):
try:
result = future.result()
node = node_mapping[future]
outputs[node.id] = result
except Exception as e:
logger.exception("thread pool exception")
exceptions.append(e)
return outputs, exceptions
def threadpool( def threadpool(
funcs: List[Tuple[Callable, Iterable[Any], Dict[Any, Any]]], workers: int = 10 funcs: List[Tuple[Callable, Iterable[Any], Dict[Any, Any]]], workers: int = 10
) -> Tuple[List[Any], List[Exception]]: ) -> Tuple[List[Any], List[Exception]]:
@ -474,3 +589,19 @@ def parse_iface_config_id(config_id: int) -> Tuple[int, Optional[int]]:
iface_id = config_id % IFACE_CONFIG_FACTOR iface_id = config_id % IFACE_CONFIG_FACTOR
node_id = config_id // IFACE_CONFIG_FACTOR node_id = config_id // IFACE_CONFIG_FACTOR
return node_id, iface_id return node_id, iface_id
class SetQueue(Queue):
"""
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

View file

@ -1,20 +1,23 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Type, TypeVar from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar
from lxml import etree from lxml import etree
import core.nodes.base import core.nodes.base
import core.nodes.physical import core.nodes.physical
from core import utils from core import utils
from core.emane.nodes import EmaneNet from core.config import Configuration
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions from core.emane.nodes import EmaneNet, EmaneOptions
from core.emulator.data import InterfaceData, LinkOptions
from core.emulator.enumerations import EventTypes, NodeTypes from core.emulator.enumerations import EventTypes, NodeTypes
from core.errors import CoreXmlError from core.errors import CoreXmlError
from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.base import CoreNodeBase, CoreNodeOptions, NodeBase, Position
from core.nodes.docker import DockerNode from core.nodes.docker import DockerNode, DockerOptions
from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode from core.nodes.lxd import LxcNode
from core.nodes.network import CtrlNet, GreTapBridge, WlanNode from core.nodes.network import CtrlNet, GreTapBridge, PtpNet, WlanNode
from core.nodes.wireless import WirelessNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -209,7 +212,7 @@ class ServiceElement:
class DeviceElement(NodeElement): class DeviceElement(NodeElement):
def __init__(self, session: "Session", node: NodeBase) -> None: def __init__(self, session: "Session", node: NodeBase) -> None:
super().__init__(session, node, "device") super().__init__(session, node, "device")
add_attribute(self.element, "type", node.type) add_attribute(self.element, "type", node.model)
self.add_class() self.add_class()
self.add_services() self.add_services()
@ -242,21 +245,31 @@ class DeviceElement(NodeElement):
class NetworkElement(NodeElement): class NetworkElement(NodeElement):
def __init__(self, session: "Session", node: NodeBase) -> None: def __init__(self, session: "Session", node: NodeBase) -> None:
super().__init__(session, node, "network") super().__init__(session, node, "network")
if isinstance(self.node, (WlanNode, EmaneNet)): if isinstance(self.node, WlanNode):
if self.node.model: if self.node.wireless_model:
add_attribute(self.element, "model", self.node.model.name) add_attribute(self.element, "model", self.node.wireless_model.name)
if self.node.mobility:
add_attribute(self.element, "mobility", self.node.mobility.name)
if isinstance(self.node, EmaneNet):
if self.node.wireless_model:
add_attribute(self.element, "model", self.node.wireless_model.name)
if self.node.mobility: if self.node.mobility:
add_attribute(self.element, "mobility", self.node.mobility.name) add_attribute(self.element, "mobility", self.node.mobility.name)
if isinstance(self.node, GreTapBridge): if isinstance(self.node, GreTapBridge):
add_attribute(self.element, "grekey", self.node.grekey) add_attribute(self.element, "grekey", self.node.grekey)
if isinstance(self.node, WirelessNode):
config = self.node.get_config()
self.add_wireless_config(config)
self.add_type() self.add_type()
def add_type(self) -> None: def add_type(self) -> None:
if self.node.apitype: node_type = self.session.get_node_type(type(self.node))
node_type = self.node.apitype.name add_attribute(self.element, "type", node_type.name)
else:
node_type = self.node.__class__.__name__ def add_wireless_config(self, config: Dict[str, Configuration]) -> None:
add_attribute(self.element, "type", node_type) wireless_element = etree.SubElement(self.element, "wireless")
for config_item in config.values():
add_configuration(wireless_element, config_item.id, config_item.default)
class CoreXmlWriter: class CoreXmlWriter:
@ -269,8 +282,8 @@ class CoreXmlWriter:
def write_session(self) -> None: def write_session(self) -> None:
# generate xml content # generate xml content
links = self.write_nodes() self.write_nodes()
self.write_links(links) self.write_links()
self.write_mobility_configs() self.write_mobility_configs()
self.write_emane_configs() self.write_emane_configs()
self.write_service_configs() self.write_service_configs()
@ -334,16 +347,9 @@ class CoreXmlWriter:
def write_session_options(self) -> None: def write_session_options(self) -> None:
option_elements = etree.Element("session_options") option_elements = etree.Element("session_options")
options_config = self.session.options.get_configs() for option in self.session.options.options:
if not options_config: value = self.session.options.get(option.id)
return add_configuration(option_elements, option.id, value)
default_options = self.session.options.default_values()
for _id in default_options:
default_value = default_options[_id]
value = options_config.get(_id, default_value)
add_configuration(option_elements, _id, value)
if option_elements.getchildren(): if option_elements.getchildren():
self.scenario.append(option_elements) self.scenario.append(option_elements)
@ -439,52 +445,48 @@ class CoreXmlWriter:
self.scenario.append(service_configurations) self.scenario.append(service_configurations)
def write_default_services(self) -> None: def write_default_services(self) -> None:
node_types = etree.Element("default_services") models = etree.Element("default_services")
for node_type in self.session.services.default_services: for model in self.session.services.default_services:
services = self.session.services.default_services[node_type] services = self.session.services.default_services[model]
node_type = etree.SubElement(node_types, "node", type=node_type) model = etree.SubElement(models, "node", type=model)
for service in services: for service in services:
etree.SubElement(node_type, "service", name=service) etree.SubElement(model, "service", name=service)
if models.getchildren():
self.scenario.append(models)
if node_types.getchildren(): def write_nodes(self) -> None:
self.scenario.append(node_types) for node in self.session.nodes.values():
def write_nodes(self) -> List[LinkData]:
links = []
for node_id in self.session.nodes:
node = self.session.nodes[node_id]
# network node # network node
is_network_or_rj45 = isinstance( is_network_or_rj45 = isinstance(
node, (core.nodes.base.CoreNetworkBase, core.nodes.physical.Rj45Node) node, (core.nodes.base.CoreNetworkBase, core.nodes.physical.Rj45Node)
) )
is_controlnet = isinstance(node, CtrlNet) is_controlnet = isinstance(node, CtrlNet)
if is_network_or_rj45 and not is_controlnet: is_ptp = isinstance(node, PtpNet)
if is_network_or_rj45 and not (is_controlnet or is_ptp):
self.write_network(node) self.write_network(node)
# device node # device node
elif isinstance(node, core.nodes.base.CoreNodeBase): elif isinstance(node, core.nodes.base.CoreNodeBase):
self.write_device(node) self.write_device(node)
# add known links
links.extend(node.links())
return links
def write_network(self, node: NodeBase) -> None: def write_network(self, node: NodeBase) -> None:
# ignore p2p and other nodes that are not part of the api
if not node.apitype:
return
network = NetworkElement(self.session, node) network = NetworkElement(self.session, node)
self.networks.append(network.element) self.networks.append(network.element)
def write_links(self, links: List[LinkData]) -> None: def write_links(self) -> None:
link_elements = etree.Element("links") link_elements = etree.Element("links")
# add link data for core_link in self.session.link_manager.links():
for link_data in links: node1, iface1 = core_link.node1, core_link.iface1
# skip basic range links node2, iface2 = core_link.node2, core_link.iface2
if link_data.iface1 is None and link_data.iface2 is None: unidirectional = core_link.is_unidirectional()
continue link_element = self.create_link_element(
link_element = self.create_link_element(link_data) node1, iface1, node2, iface2, core_link.options(), unidirectional
)
link_elements.append(link_element) link_elements.append(link_element)
if unidirectional:
link_element = self.create_link_element(
node2, iface2, node1, iface1, iface2.options, unidirectional
)
link_elements.append(link_element)
if link_elements.getchildren(): if link_elements.getchildren():
self.scenario.append(link_elements) self.scenario.append(link_elements)
@ -493,67 +495,71 @@ class CoreXmlWriter:
self.devices.append(device.element) self.devices.append(device.element)
def create_iface_element( def create_iface_element(
self, element_name: str, node_id: int, iface_data: InterfaceData self, element_name: str, iface: CoreInterface
) -> etree.Element: ) -> etree.Element:
iface_element = etree.Element(element_name) iface_element = etree.Element(element_name)
node = self.session.get_node(node_id, NodeBase) # check if interface if connected to emane
if isinstance(node, CoreNodeBase): if isinstance(iface.node, CoreNodeBase) and isinstance(iface.net, EmaneNet):
iface = node.get_iface(iface_data.id) nem_id = self.session.emane.get_nem_id(iface)
# check if emane interface add_attribute(iface_element, "nem", nem_id)
if isinstance(iface.net, EmaneNet): ip4 = iface.get_ip4()
nem_id = self.session.emane.get_nem_id(iface) ip4_mask = None
add_attribute(iface_element, "nem", nem_id) if ip4:
add_attribute(iface_element, "id", iface_data.id) ip4_mask = ip4.prefixlen
add_attribute(iface_element, "name", iface_data.name) ip4 = str(ip4.ip)
add_attribute(iface_element, "mac", iface_data.mac) ip6 = iface.get_ip6()
add_attribute(iface_element, "ip4", iface_data.ip4) ip6_mask = None
add_attribute(iface_element, "ip4_mask", iface_data.ip4_mask) if ip6:
add_attribute(iface_element, "ip6", iface_data.ip6) ip6_mask = ip6.prefixlen
add_attribute(iface_element, "ip6_mask", iface_data.ip6_mask) ip6 = str(ip6.ip)
add_attribute(iface_element, "id", iface.id)
add_attribute(iface_element, "name", iface.name)
add_attribute(iface_element, "mac", iface.mac)
add_attribute(iface_element, "ip4", ip4)
add_attribute(iface_element, "ip4_mask", ip4_mask)
add_attribute(iface_element, "ip6", ip6)
add_attribute(iface_element, "ip6_mask", ip6_mask)
return iface_element return iface_element
def create_link_element(self, link_data: LinkData) -> etree.Element: def create_link_element(
self,
node1: NodeBase,
iface1: Optional[CoreInterface],
node2: NodeBase,
iface2: Optional[CoreInterface],
options: LinkOptions,
unidirectional: bool,
) -> etree.Element:
link_element = etree.Element("link") link_element = etree.Element("link")
add_attribute(link_element, "node1", link_data.node1_id) add_attribute(link_element, "node1", node1.id)
add_attribute(link_element, "node2", link_data.node2_id) add_attribute(link_element, "node2", node2.id)
# check for interface one # check for interface one
if link_data.iface1 is not None: if iface1 is not None:
iface1 = self.create_iface_element( iface1 = self.create_iface_element("iface1", iface1)
"iface1", link_data.node1_id, link_data.iface1
)
link_element.append(iface1) link_element.append(iface1)
# check for interface two # check for interface two
if link_data.iface2 is not None: if iface2 is not None:
iface2 = self.create_iface_element( iface2 = self.create_iface_element("iface2", iface2)
"iface2", link_data.node2_id, link_data.iface2
)
link_element.append(iface2) link_element.append(iface2)
# check for options, don't write for emane/wlan links # check for options, don't write for emane/wlan links
node1 = self.session.get_node(link_data.node1_id, NodeBase) is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet, WirelessNode))
node2 = self.session.get_node(link_data.node2_id, NodeBase) is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet, WirelessNode))
is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet)) if not (is_node1_wireless or is_node2_wireless):
is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet)) unidirectional = 1 if unidirectional else 0
if not any([is_node1_wireless, is_node2_wireless]): options_element = etree.Element("options")
options_data = link_data.options add_attribute(options_element, "delay", options.delay)
options = etree.Element("options") add_attribute(options_element, "bandwidth", options.bandwidth)
add_attribute(options, "delay", options_data.delay) add_attribute(options_element, "loss", options.loss)
add_attribute(options, "bandwidth", options_data.bandwidth) add_attribute(options_element, "dup", options.dup)
add_attribute(options, "loss", options_data.loss) add_attribute(options_element, "jitter", options.jitter)
add_attribute(options, "dup", options_data.dup) add_attribute(options_element, "mer", options.mer)
add_attribute(options, "jitter", options_data.jitter) add_attribute(options_element, "burst", options.burst)
add_attribute(options, "mer", options_data.mer) add_attribute(options_element, "mburst", options.mburst)
add_attribute(options, "burst", options_data.burst) add_attribute(options_element, "unidirectional", unidirectional)
add_attribute(options, "mburst", options_data.mburst) add_attribute(options_element, "key", options.key)
add_attribute(options, "unidirectional", options_data.unidirectional) add_attribute(options_element, "buffer", options.buffer)
add_attribute(options, "network_id", link_data.network_id) if options_element.items():
add_attribute(options, "key", options_data.key) link_element.append(options_element)
add_attribute(options, "buffer", options_data.buffer)
if options.items():
link_element.append(options)
return link_element return link_element
@ -586,14 +592,12 @@ class CoreXmlReader:
return return
for node in default_services.iterchildren(): for node in default_services.iterchildren():
node_type = node.get("type") model = node.get("type")
services = [] services = []
for service in node.iterchildren(): for service in node.iterchildren():
services.append(service.get("name")) services.append(service.get("name"))
logger.info( logger.info("reading default services for nodes(%s): %s", model, services)
"reading default services for nodes(%s): %s", node_type, services self.session.services.default_services[model] = services
)
self.session.services.default_services[node_type] = services
def read_session_metadata(self) -> None: def read_session_metadata(self) -> None:
session_metadata = self.scenario.find("session_metadata") session_metadata = self.scenario.find("session_metadata")
@ -618,8 +622,7 @@ class CoreXmlReader:
value = configuration.get("value") value = configuration.get("value")
xml_config[name] = value xml_config[name] = value
logger.info("reading session options: %s", xml_config) logger.info("reading session options: %s", xml_config)
config = self.session.options.get_configs() self.session.options.update(xml_config)
config.update(xml_config)
def read_session_hooks(self) -> None: def read_session_hooks(self) -> None:
session_hooks = self.scenario.find("session_hooks") session_hooks = self.scenario.find("session_hooks")
@ -799,71 +802,85 @@ class CoreXmlReader:
clazz = device_element.get("class") clazz = device_element.get("class")
image = device_element.get("image") image = device_element.get("image")
server = device_element.get("server") server = device_element.get("server")
options = NodeOptions( canvas = get_int(device_element, "canvas")
name=name, model=model, image=image, icon=icon, server=server
)
node_type = NodeTypes.DEFAULT node_type = NodeTypes.DEFAULT
if clazz == "docker": if clazz == "docker":
node_type = NodeTypes.DOCKER node_type = NodeTypes.DOCKER
elif clazz == "lxc": elif clazz == "lxc":
node_type = NodeTypes.LXC node_type = NodeTypes.LXC
_class = self.session.get_node_class(node_type) _class = self.session.get_node_class(node_type)
options = _class.create_options()
service_elements = device_element.find("services") options.icon = icon
if service_elements is not None: options.canvas = canvas
options.services = [x.get("name") for x in service_elements.iterchildren()] # check for special options
if isinstance(options, CoreNodeOptions):
config_service_elements = device_element.find("configservices") options.model = model
if config_service_elements is not None: service_elements = device_element.find("services")
options.config_services = [ if service_elements is not None:
x.get("name") for x in config_service_elements.iterchildren() options.services.extend(
] x.get("name") for x in service_elements.iterchildren()
)
config_service_elements = device_element.find("configservices")
if config_service_elements is not None:
options.config_services.extend(
x.get("name") for x in config_service_elements.iterchildren()
)
if isinstance(options, DockerOptions):
options.image = image
# get position information
position_element = device_element.find("position") position_element = device_element.find("position")
position = None
if position_element is not None: if position_element is not None:
position = Position()
x = get_float(position_element, "x") x = get_float(position_element, "x")
y = get_float(position_element, "y") y = get_float(position_element, "y")
if all([x, y]): if all([x, y]):
options.set_position(x, y) position.set(x, y)
lat = get_float(position_element, "lat") lat = get_float(position_element, "lat")
lon = get_float(position_element, "lon") lon = get_float(position_element, "lon")
alt = get_float(position_element, "alt") alt = get_float(position_element, "alt")
if all([lat, lon, alt]): if all([lat, lon, alt]):
options.set_location(lat, lon, alt) position.set_geo(lon, lat, alt)
logger.info("reading node id(%s) model(%s) name(%s)", node_id, model, name) logger.info("reading node id(%s) model(%s) name(%s)", node_id, model, name)
self.session.add_node(_class, node_id, options) self.session.add_node(_class, node_id, name, server, position, options)
def read_network(self, network_element: etree.Element) -> None: def read_network(self, network_element: etree.Element) -> None:
node_id = get_int(network_element, "id") node_id = get_int(network_element, "id")
name = network_element.get("name") name = network_element.get("name")
server = network_element.get("server")
node_type = NodeTypes[network_element.get("type")] node_type = NodeTypes[network_element.get("type")]
_class = self.session.get_node_class(node_type) _class = self.session.get_node_class(node_type)
icon = network_element.get("icon") options = _class.create_options()
server = network_element.get("server") options.canvas = get_int(network_element, "canvas")
options = NodeOptions(name=name, icon=icon, server=server) options.icon = network_element.get("icon")
if node_type == NodeTypes.EMANE: if isinstance(options, EmaneOptions):
model = network_element.get("model") options.emane_model = network_element.get("model")
options.emane = model
position_element = network_element.find("position") position_element = network_element.find("position")
position = None
if position_element is not None: if position_element is not None:
position = Position()
x = get_float(position_element, "x") x = get_float(position_element, "x")
y = get_float(position_element, "y") y = get_float(position_element, "y")
if all([x, y]): if all([x, y]):
options.set_position(x, y) position.set(x, y)
lat = get_float(position_element, "lat") lat = get_float(position_element, "lat")
lon = get_float(position_element, "lon") lon = get_float(position_element, "lon")
alt = get_float(position_element, "alt") alt = get_float(position_element, "alt")
if all([lat, lon, alt]): if all([lat, lon, alt]):
options.set_location(lat, lon, alt) position.set_geo(lon, lat, alt)
logger.info( logger.info(
"reading node id(%s) node_type(%s) name(%s)", node_id, node_type, name "reading node id(%s) node_type(%s) name(%s)", node_id, node_type, name
) )
self.session.add_node(_class, node_id, options) node = self.session.add_node(_class, node_id, name, server, position, options)
if isinstance(node, WirelessNode):
wireless_element = network_element.find("wireless")
if wireless_element:
config = {}
for config_element in wireless_element.iterchildren():
name = config_element.get("name")
value = config_element.get("value")
config[name] = value
node.set_config(config)
def read_configservice_configs(self) -> None: def read_configservice_configs(self) -> None:
configservice_configs = self.scenario.find("configservice_configurations") configservice_configs = self.scenario.find("configservice_configurations")

View file

@ -162,12 +162,14 @@ def build_platform_xml(
""" """
# create top level platform element # create top level platform element
platform_element = etree.Element("platform") platform_element = etree.Element("platform")
for configuration in emane_net.model.platform_config: for configuration in emane_net.wireless_model.platform_config:
name = configuration.id name = configuration.id
value = config[configuration.id] value = config[configuration.id]
add_param(platform_element, name, value) add_param(platform_element, name, value)
add_param( add_param(
platform_element, emane_net.model.platform_controlport, f"0.0.0.0:{nem_port}" platform_element,
emane_net.wireless_model.platform_controlport,
f"0.0.0.0:{nem_port}",
) )
# build nem xml # build nem xml
@ -177,7 +179,7 @@ def build_platform_xml(
) )
# create model based xml files # create model based xml files
emane_net.model.build_xml_files(config, iface) emane_net.wireless_model.build_xml_files(config, iface)
# check if this is an external transport # check if this is an external transport
if is_external(config): if is_external(config):

View file

@ -1,8 +1,4 @@
# CORE # CORE
# (c)2012 the Boeing Company.
# See the LICENSE file included in this distribution.
#
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
# #
# Builds html and pdf documentation using Sphinx. # Builds html and pdf documentation using Sphinx.
# #

1068
daemon/poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -58,3 +58,13 @@ message GetNodeConfigServiceRequest {
message GetNodeConfigServiceResponse { message GetNodeConfigServiceResponse {
map<string, string> config = 1; map<string, string> config = 1;
} }
message GetConfigServiceRenderedRequest {
int32 session_id = 1;
int32 node_id = 2;
string name = 3;
}
message GetConfigServiceRenderedResponse {
map<string, string> rendered = 1;
}

View file

@ -61,6 +61,8 @@ service CoreApi {
} }
rpc DeleteLink (DeleteLinkRequest) returns (DeleteLinkResponse) { rpc DeleteLink (DeleteLinkRequest) returns (DeleteLinkResponse) {
} }
rpc Linked (LinkedRequest) returns (LinkedResponse) {
}
// mobility rpc // mobility rpc
rpc GetMobilityConfig (mobility.GetMobilityConfigRequest) returns (mobility.GetMobilityConfigResponse) { rpc GetMobilityConfig (mobility.GetMobilityConfigRequest) returns (mobility.GetMobilityConfigResponse) {
@ -89,6 +91,8 @@ service CoreApi {
} }
rpc ConfigServiceAction (services.ServiceActionRequest) returns (services.ServiceActionResponse) { rpc ConfigServiceAction (services.ServiceActionRequest) returns (services.ServiceActionResponse) {
} }
rpc GetConfigServiceRendered (configservices.GetConfigServiceRenderedRequest) returns (configservices.GetConfigServiceRenderedResponse) {
}
// wlan rpc // wlan rpc
rpc GetWlanConfig (wlan.GetWlanConfigRequest) returns (wlan.GetWlanConfigResponse) { rpc GetWlanConfig (wlan.GetWlanConfigRequest) returns (wlan.GetWlanConfigResponse) {
@ -98,6 +102,14 @@ service CoreApi {
rpc WlanLink (wlan.WlanLinkRequest) returns (wlan.WlanLinkResponse) { rpc WlanLink (wlan.WlanLinkRequest) returns (wlan.WlanLinkResponse) {
} }
// wireless rpc
rpc WirelessLinked (WirelessLinkedRequest) returns (WirelessLinkedResponse) {
}
rpc WirelessConfig (WirelessConfigRequest) returns (WirelessConfigResponse) {
}
rpc GetWirelessConfig (GetWirelessConfigRequest) returns (GetWirelessConfigResponse) {
}
// emane rpc // emane rpc
rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) { rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) {
} }
@ -280,12 +292,11 @@ message ConfigEvent {
repeated int32 data_types = 5; repeated int32 data_types = 5;
string data_values = 6; string data_values = 6;
string captions = 7; string captions = 7;
string bitmap = 8; string possible_values = 8;
string possible_values = 9; string groups = 9;
string groups = 10; int32 iface_id = 10;
int32 iface_id = 11; int32 network_id = 11;
int32 network_id = 12; string opaque = 12;
string opaque = 13;
} }
message ExceptionEvent { message ExceptionEvent {
@ -615,6 +626,7 @@ message Node {
map<string, services.NodeServiceConfig> service_configs = 18; map<string, services.NodeServiceConfig> service_configs = 18;
map<string, configservices.ConfigServiceConfig> config_service_configs= 19; map<string, configservices.ConfigServiceConfig> config_service_configs= 19;
repeated emane.NodeEmaneConfig emane_configs = 20; repeated emane.NodeEmaneConfig emane_configs = 20;
map<string, common.ConfigOption> wireless_config = 21;
} }
message Link { message Link {
@ -656,6 +668,8 @@ message Interface {
int32 mtu = 10; int32 mtu = 10;
int32 node_id = 11; int32 node_id = 11;
int32 net2_id = 12; int32 net2_id = 12;
int32 nem_id = 13;
int32 nem_port = 14;
} }
message SessionLocation { message SessionLocation {
@ -684,3 +698,47 @@ message Server {
string name = 1; string name = 1;
string host = 2; string host = 2;
} }
message LinkedRequest {
int32 session_id = 1;
int32 node1_id = 2;
int32 node2_id = 3;
int32 iface1_id = 4;
int32 iface2_id = 5;
bool linked = 6;
}
message LinkedResponse {
}
message WirelessLinkedRequest {
int32 session_id = 1;
int32 wireless_id = 2;
int32 node1_id = 3;
int32 node2_id = 4;
bool linked = 5;
}
message WirelessLinkedResponse {
}
message WirelessConfigRequest {
int32 session_id = 1;
int32 wireless_id = 2;
int32 node1_id = 3;
int32 node2_id = 4;
LinkOptions options1 = 5;
LinkOptions options2 = 6;
}
message WirelessConfigResponse {
}
message GetWirelessConfigRequest {
int32 session_id = 1;
int32 node_id = 2;
}
message GetWirelessConfigResponse {
map<string, common.ConfigOption> config = 1;
}

View file

@ -37,7 +37,7 @@ message ServiceAction {
} }
message ServiceDefaults { message ServiceDefaults {
string node_type = 1; string model = 1;
repeated string services = 2; repeated string services = 2;
} }

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "core" name = "core"
version = "8.2.0" version = "9.0.0"
description = "CORE Common Open Research Emulator" description = "CORE Common Open Research Emulator"
authors = ["Boeing Research and Technology"] authors = ["Boeing Research and Technology"]
license = "BSD-2-Clause" license = "BSD-2-Clause"
@ -14,29 +14,38 @@ include = [
] ]
exclude = ["core/constants.py.in"] exclude = ["core/constants.py.in"]
[tool.poetry.scripts]
core-daemon = "core.scripts.daemon:main"
core-cli = "core.scripts.cli:main"
core-gui = "core.scripts.gui:main"
core-player = "core.scripts.player:main"
core-route-monitor = "core.scripts.routemonitor:main"
core-service-update = "core.scripts.serviceupdate:main"
core-cleanup = "core.scripts.cleanup:main"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.6" python = "^3.9"
dataclasses = { version = "*", python = "~3.6" } fabric = "2.7.1"
fabric = "2.5.0" grpcio = "1.49.1"
grpcio = "1.27.2"
invoke = "1.4.1" invoke = "1.4.1"
lxml = "4.6.5" lxml = "4.9.1"
mako = "1.1.3"
netaddr = "0.7.19" netaddr = "0.7.19"
pillow = "8.3.2" protobuf = "3.19.5"
protobuf = "3.19.4" pyproj = "3.3.1"
pyproj = "2.6.1.post1"
pyyaml = "5.4" pyyaml = "5.4"
Pillow = "9.2.0"
Mako = "1.2.3"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
black = "==19.3b0" black = "==19.3b0"
flake8 = "3.8.2" flake8 = "3.8.2"
grpcio-tools = "1.27.2" grpcio-tools = "1.43.0"
isort = "4.3.21" isort = "4.3.21"
mock = "4.0.2" mock = "4.0.2"
pre-commit = "2.1.1" pre-commit = "2.1.1"
pytest = "5.4.3"
[tool.poetry.group.dev.dependencies]
pytest = "6.2.5"
[tool.isort] [tool.isort]
skip_glob = "*_pb2*.py,doc,build" skip_glob = "*_pb2*.py,doc,build"

View file

@ -1,71 +0,0 @@
#!/bin/sh
if [ "z$1" = "z-h" -o "z$1" = "z--help" ]; then
echo "usage: $0 [-d [-l]]"
echo -n " Clean up all CORE namespaces processes, bridges, interfaces, "
echo "and session\n directories. Options:"
echo " -h show this help message and exit"
echo " -d also kill the Python daemon"
echo " -l remove the core-daemon.log file"
exit 0
fi
if [ `id -u` != 0 ]; then
echo "Permission denied. Re-run this script as root."
exit 1
fi
PATH="/sbin:/bin:/usr/sbin:/usr/bin"
export PATH
if [ "z$1" = "z-d" ]; then
pypids=`pidof python3 python`
for p in $pypids; do
grep -q core-daemon /proc/$p/cmdline
if [ $? = 0 ]; then
echo "cleaning up core-daemon process: $p"
kill -9 $p
fi
done
fi
if [ "z$2" = "z-l" ]; then
rm -f /var/log/core-daemon.log
fi
kaopts="-v"
killall --help 2>&1 | grep -q namespace
if [ $? = 0 ]; then
kaopts="$kaopts --ns 0"
fi
vnodedpids=`pidof vnoded`
if [ "z$vnodedpids" != "z" ]; then
echo "cleaning up old vnoded processes: $vnodedpids"
killall $kaopts -KILL vnoded
# pause for 1 second for interfaces to disappear
sleep 1
fi
killall -q emane
killall -q emanetransportd
killall -q emaneeventservice
if [ -d /sys/class/net ]; then
ifcommand="ls -1 /sys/class/net"
else
ifcommand="ip -o link show | sed -r -e 's/[0-9]+: ([^[:space:]]+): .*/\1/'"
fi
eval "$ifcommand" | awk '
/^veth[0-9]+\./ {print "removing interface " $1; system("ip link del " $1);}
/tmp\./ {print "removing interface " $1; system("ip link del " $1);}
/gt\./ {print "removing interface " $1; system("ip link del " $1);}
/b\./ {print "removing bridge " $1; system("ip link set " $1 " down; ip link del " $1);}
/ctrl[0-9]+\./ {print "removing bridge " $1; system("ip link set " $1 " down; ip link del " $1);}
'
nft list ruleset | awk '
$3 ~ /^b\./ {print "removing nftables " $3; system("nft delete table bridge " $3);}
'
rm -rf /tmp/pycore*

View file

@ -1,70 +0,0 @@
#!/usr/bin/env python3
import argparse
import re
import sys
from pathlib import Path
from core import utils
from core.api.grpc.client import CoreGrpcClient
from core.errors import CoreCommandError
if __name__ == "__main__":
# parse flags
parser = argparse.ArgumentParser(description="Converts CORE imn files to xml")
parser.add_argument("-f", "--file", dest="file", help="imn file to convert")
parser.add_argument(
"-d", "--dest", dest="dest", default=None, help="destination for xml file, defaults to same location as imn"
)
args = parser.parse_args()
# validate provided file exists
imn_file = Path(args.file)
if not imn_file.exists():
print(f"{args.file} does not exist")
sys.exit(1)
# validate destination
if args.dest is not None:
dest = Path(args.dest)
if not dest.exists() or not dest.is_dir():
print(f"{dest.resolve()} does not exist or is not a directory")
sys.exit(1)
xml_file = Path(dest, imn_file.with_suffix(".xml").name)
else:
xml_file = Path(imn_file.with_suffix(".xml").name)
# validate xml file
if xml_file.exists():
print(f"{xml_file.resolve()} already exists")
sys.exit(1)
# run provided imn using core-gui batch mode
try:
print(f"running {imn_file.resolve()} in batch mode")
output = utils.cmd(f"core-gui --batch {imn_file.resolve()}")
last_line = output.split("\n")[-1].strip()
# check for active session
if last_line == "Another session is active.":
print("need to restart core-daemon or shutdown previous batch session")
sys.exit(1)
# parse session id
m = re.search(r"Session id is (\d+)\.", last_line)
if not m:
print(f"failed to find session id: {output}")
sys.exit(1)
session_id = int(m.group(1))
print(f"created session {session_id}")
# save xml and delete session
client = CoreGrpcClient()
with client.context_connect():
print(f"saving xml {xml_file.resolve()}")
client.save_xml(session_id, str(xml_file))
print(f"deleting session {session_id}")
client.delete_session(session_id)
except CoreCommandError as e:
print(f"core-gui batch failed for {imn_file.resolve()}: {e}")
sys.exit(1)

View file

@ -1,247 +0,0 @@
#!/usr/bin/env python3
"""
core-manage: Helper tool to add, remove, or check for services, models, and
node types in a CORE installation.
"""
import ast
import optparse
import os
import re
import sys
from core import services
from core.constants import CORE_CONF_DIR
class FileUpdater:
"""
Helper class for changing configuration files.
"""
actions = ("add", "remove", "check")
targets = ("service", "model", "nodetype")
def __init__(self, action, target, data, options):
"""
"""
self.action = action
self.target = target
self.data = data
self.options = options
self.verbose = options.verbose
self.search, self.filename = self.get_filename(target)
def process(self):
""" Invoke update_file() using a helper method depending on target.
"""
if self.verbose:
txt = "Updating"
if self.action == "check":
txt = "Checking"
sys.stdout.write(f"{txt} file: {self.filename}\n")
if self.target == "service":
r = self.update_file(fn=self.update_services)
elif self.target == "model":
r = self.update_file(fn=self.update_emane_models)
elif self.target == "nodetype":
r = self.update_nodes_conf()
if self.verbose:
txt = ""
if not r:
txt = "NOT "
if self.action == "check":
sys.stdout.write(f"String {txt} found.\n")
else:
sys.stdout.write(f"File {txt} updated.\n")
return r
def update_services(self, line):
""" Modify the __init__.py file having this format:
__all__ = ["quagga", "nrl", "xorp", "bird", ]
Returns True or False when "check" is the action, a modified line
otherwise.
"""
line = line.strip("\n")
key, valstr = line.split("= ")
vals = ast.literal_eval(valstr)
r = self.update_keyvals(key, vals)
if self.action == "check":
return r
valstr = str(r)
return "= ".join([key, valstr]) + "\n"
def update_emane_models(self, line):
""" Modify the core.conf file having this format:
emane_models = RfPipe, Ieee80211abg, CommEffect, Bypass
Returns True or False when "check" is the action, a modified line
otherwise.
"""
line = line.strip("\n")
key, valstr = line.split("= ")
vals = valstr.split(", ")
r = self.update_keyvals(key, vals)
if self.action == "check":
return r
valstr = ", ".join(r)
return "= ".join([key, valstr]) + "\n"
def update_keyvals(self, key, vals):
""" Perform self.action on (key, vals).
Returns True or False when "check" is the action, a modified line
otherwise.
"""
if self.action == "check":
if self.data in vals:
return True
else:
return False
elif self.action == "add":
if self.data not in vals:
vals.append(self.data)
elif self.action == "remove":
try:
vals.remove(self.data)
except ValueError:
pass
return vals
def get_filename(self, target):
""" Return search string and filename based on target.
"""
if target == "service":
filename = os.path.abspath(services.__file__)
search = "__all__ ="
elif target == "model":
filename = os.path.join(CORE_CONF_DIR, "core.conf")
search = "emane_models ="
elif target == "nodetype":
if self.options.userpath is None:
raise ValueError("missing user path")
filename = os.path.join(self.options.userpath, "nodes.conf")
search = self.data
else:
raise ValueError("unknown target")
if not os.path.exists(filename):
raise ValueError(f"file {filename} does not exist")
return search, filename
def update_file(self, fn=None):
""" Open a file and search for self.search, invoking the supplied
function on the matching line. Write file changes if necessary.
Returns True if the file has changed (or action is "check" and the
search string is found), False otherwise.
"""
changed = False
output = "" # this accumulates output, assumes input is small
with open(self.filename, "r") as f:
for line in f:
if line[:len(self.search)] == self.search:
r = fn(line) # line may be modified by fn() here
if self.action == "check":
return r
else:
if line != r:
changed = True
line = r
output += line
if changed:
with open(self.filename, "w") as f:
f.write(output)
return changed
def update_nodes_conf(self):
""" Add/remove/check entries from nodes.conf. This file
contains a Tcl-formatted array of node types. The array index must be
properly set for new entries. Uses self.{action, filename, search,
data} variables as input and returns the same value as update_file().
"""
changed = False
output = "" # this accumulates output, assumes input is small
with open(self.filename, "r") as f:
for line in f:
# make sure data is not added twice
if line.find(self.search) >= 0:
if self.action == "check":
return True
elif self.action == "add":
return False
elif self.action == "remove":
changed = True
continue
else:
output += line
if self.action == "add":
index = int(re.match("^\d+", line).group(0))
output += str(index + 1) + " " + self.data + "\n"
changed = True
if changed:
with open(self.filename, "w") as f:
f.write(output)
return changed
def main():
actions = ", ".join(FileUpdater.actions)
targets = ", ".join(FileUpdater.targets)
usagestr = "usage: %prog [-h] [options] <action> <target> <string>\n"
usagestr += "\nHelper tool to add, remove, or check for "
usagestr += "services, models, and node types\nin a CORE installation.\n"
usagestr += "\nExamples:\n %prog add service newrouting"
usagestr += "\n %prog -v check model RfPipe"
usagestr += "\n %prog --userpath=\"$HOME/.core\" add nodetype \"{ftp ftp.gif ftp.gif {DefaultRoute FTP} netns {FTP server} }\" \n"
usagestr += f"\nArguments:\n <action> should be one of: {actions}"
usagestr += f"\n <target> should be one of: {targets}"
usagestr += f"\n <string> is the text to {actions}"
parser = optparse.OptionParser(usage=usagestr)
parser.set_defaults(userpath=None, verbose=False, )
parser.add_option("--userpath", dest="userpath", type="string",
help="use the specified user path (e.g. \"$HOME/.core" \
"\") to access nodes.conf")
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
help="be verbose when performing action")
def usage(msg=None, err=0):
sys.stdout.write("\n")
if msg:
sys.stdout.write(msg + "\n\n")
parser.print_help()
sys.exit(err)
(options, args) = parser.parse_args()
if len(args) != 3:
usage("Missing required arguments!", 1)
action = args[0]
if action not in FileUpdater.actions:
usage(f"invalid action {action}", 1)
target = args[1]
if target not in FileUpdater.targets:
usage(f"invalid target {target}", 1)
if target == "nodetype" and not options.userpath:
usage(f"user path option required for this target ({target})")
data = args[2]
try:
up = FileUpdater(action, target, data, options)
r = up.process()
except Exception as e:
sys.stderr.write(f"Exception: {e}\n")
sys.exit(1)
if not r:
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()

View file

@ -1,279 +0,0 @@
#!/usr/bin/env python3
"""
coresendmsg: utility for generating CORE messages
"""
import optparse
import os
import socket
import sys
from core.api.tlv import coreapi
from core.api.tlv.enumerations import CORE_API_PORT, MessageTypes, SessionTlvs
from core.emulator.enumerations import MessageFlags
def print_available_tlvs(t, tlv_class):
"""
Print a TLV list.
"""
print(f"TLVs available for {t} message:")
for tlv in sorted([tlv for tlv in tlv_class.tlv_type_map], key=lambda x: x.name):
print(tlv.name.lower())
def print_examples(name):
"""
Print example usage of this script.
"""
examples = [
("node number=3 x_position=125 y_position=525",
"move node number 3 to x,y=(125,525)"),
("node number=4 icon=/usr/local/share/core/icons/normal/router_red.gif",
"change node number 4\"s icon to red"),
("node flags=add number=5 type=0 name=\"n5\" x_position=500 y_position=500",
"add a new router node n5"),
("link n1_number=2 n2_number=3 delay=15000",
"set a 15ms delay on the link between n2 and n3"),
("link n1_number=2 n2_number=3 gui_attributes=\"color=blue\"",
"change the color of the link between n2 and n3"),
("link flags=add n1_number=4 n2_number=5 interface1_ip4=\"10.0.3.2\" "
"interface1_ip4_mask=24 interface2_ip4=\"10.0.3.1\" interface2_ip4_mask=24",
"link node n5 with n4 using the given interface addresses"),
("execute flags=string,text node=1 number=1000 command=\"uname -a\" -l",
"run a command on node 1 and wait for the result"),
("execute node=2 number=1001 command=\"killall ospfd\"",
"run a command on node 2 and ignore the result"),
("file flags=add node=1 name=\"/var/log/test.log\" data=\"hello world.\"",
"write a test.log file on node 1 with the given contents"),
("file flags=add node=2 name=\"test.log\" source_name=\"./test.log\"",
"move a test.log file from host to node 2"),
]
print(f"Example {name} invocations:")
for cmd, descr in examples:
print(f" {name} {cmd}\n\t\t{descr}")
def receive_message(sock):
"""
Retrieve a message from a socket and return the CoreMessage object or
None upon disconnect. Socket data beyond the first message is dropped.
"""
try:
# large receive buffer used for UDP sockets, instead of just receiving
# the 4-byte header
data = sock.recv(4096)
msghdr = data[:coreapi.CoreMessage.header_len]
except KeyboardInterrupt:
print("CTRL+C pressed")
sys.exit(1)
if len(msghdr) == 0:
return None
msgdata = None
msgtype, msgflags, msglen = coreapi.CoreMessage.unpack_header(msghdr)
if msglen:
msgdata = data[coreapi.CoreMessage.header_len:]
try:
msgcls = coreapi.CLASS_MAP[msgtype]
except KeyError:
msg = coreapi.CoreMessage(msgflags, msghdr, msgdata)
msg.message_type = msgtype
print(f"unimplemented CORE message type: {msg.type_str()}")
return msg
if len(data) > msglen + coreapi.CoreMessage.header_len:
data_size = len(data) - (msglen + coreapi.CoreMessage.header_len)
print(f"received a message of type {msgtype}, dropping {data_size} bytes of extra data")
return msgcls(msgflags, msghdr, msgdata)
def connect_to_session(sock, requested):
"""
Use Session Messages to retrieve the current list of sessions and
connect to the first one.
"""
# request the session list
tlvdata = coreapi.CoreSessionTlv.pack(SessionTlvs.NUMBER.value, "")
flags = MessageFlags.STRING.value
smsg = coreapi.CoreSessionMessage.pack(flags, tlvdata)
sock.sendall(smsg)
print("waiting for session list...")
smsgreply = receive_message(sock)
if smsgreply is None:
print("disconnected")
return False
sessstr = smsgreply.get_tlv(SessionTlvs.NUMBER.value)
if sessstr is None:
print("missing session numbers")
return False
# join the first session (that is not our own connection)
tmp, localport = sock.getsockname()
sessions = sessstr.split("|")
sessions.remove(str(localport))
if len(sessions) == 0:
print("no sessions to join")
return False
if not requested:
session = sessions[0]
elif requested in sessions:
session = requested
else:
print("requested session not found!")
return False
print(f"joining session: {session}")
tlvdata = coreapi.CoreSessionTlv.pack(SessionTlvs.NUMBER.value, session)
flags = MessageFlags.ADD.value
smsg = coreapi.CoreSessionMessage.pack(flags, tlvdata)
sock.sendall(smsg)
return True
def receive_response(sock, opt):
"""
Receive and print a CORE message from the given socket.
"""
print("waiting for response...")
msg = receive_message(sock)
if msg is None:
print(f"disconnected from {opt.address}:{opt.port}")
sys.exit(0)
print(f"received message: {msg}")
def main():
"""
Parse command-line arguments to build and send a CORE message.
"""
types = [message_type.name.lower() for message_type in MessageTypes]
flags = [flag.name.lower() for flag in MessageFlags]
types_usage = " ".join(types)
flags_usage = " ".join(flags)
usagestr = (
"usage: %prog [-h|-H] [options] [message-type] [flags=flags] "
"[message-TLVs]\n\n"
f"Supported message types:\n {types_usage}\n"
f"Supported message flags (flags=f1,f2,...):\n {flags_usage}"
)
parser = optparse.OptionParser(usage=usagestr)
default_address = "localhost"
default_session = None
default_tcp = False
parser.set_defaults(
port=CORE_API_PORT,
address=default_address,
session=default_session,
listen=False,
examples=False,
tlvs=False,
tcp=default_tcp
)
parser.add_option("-H", dest="examples", action="store_true",
help="show example usage help message and exit")
parser.add_option("-p", "--port", dest="port", type=int,
help=f"TCP port to connect to, default: {CORE_API_PORT}")
parser.add_option("-a", "--address", dest="address", type=str,
help=f"Address to connect to, default: {default_address}")
parser.add_option("-s", "--session", dest="session", type=str,
help=f"Session to join, default: {default_session}")
parser.add_option("-l", "--listen", dest="listen", action="store_true",
help="Listen for a response message and print it.")
parser.add_option("-t", "--list-tlvs", dest="tlvs", action="store_true",
help="List TLVs for the specified message type.")
parser.add_option("--tcp", dest="tcp", action="store_true",
help=f"Use TCP instead of UDP and connect to a session default: {default_tcp}")
def usage(msg=None, err=0):
print()
if msg:
print(f"{msg}\n")
parser.print_help()
sys.exit(err)
# parse command line opt
opt, args = parser.parse_args()
if opt.examples:
print_examples(os.path.basename(sys.argv[0]))
sys.exit(0)
if len(args) == 0:
usage("Please specify a message type to send.")
# given a message type t, determine the message and TLV classes
t = args.pop(0)
t = t.lower()
if t not in types:
usage(f"Unknown message type requested: {t}")
message_type = MessageTypes[t.upper()]
msg_cls = coreapi.CLASS_MAP[message_type.value]
tlv_cls = msg_cls.tlv_class
# list TLV types for this message type
if opt.tlvs:
print_available_tlvs(t, tlv_cls)
sys.exit(0)
# build a message consisting of TLVs from "type=value" arguments
flagstr = ""
tlvdata = b""
for a in args:
typevalue = a.split("=")
if len(typevalue) < 2:
usage(f"Use \"type=value\" syntax instead of \"{a}\".")
tlv_typestr = typevalue[0].lower()
tlv_valstr = "=".join(typevalue[1:])
if tlv_typestr == "flags":
flagstr = tlv_valstr
continue
try:
tlv_type = tlv_cls.tlv_type_map[tlv_typestr.upper()]
tlvdata += tlv_cls.pack_string(tlv_type.value, tlv_valstr)
except KeyError:
usage(f"Unknown TLV: \"{tlv_typestr}\"")
flags = 0
for f in flagstr.split(","):
if f == "":
continue
try:
flag_enum = MessageFlags[f.upper()]
n = flag_enum.value
flags |= n
except KeyError:
usage(f"Invalid flag \"{f}\".")
msg = msg_cls.pack(flags, tlvdata)
if opt.tcp:
protocol = socket.SOCK_STREAM
else:
protocol = socket.SOCK_DGRAM
sock = socket.socket(socket.AF_INET, protocol)
sock.setblocking(True)
try:
sock.connect((opt.address, opt.port))
except Exception as e:
print(f"Error connecting to {opt.address}:{opt.port}:\n\t{e}")
sys.exit(1)
if opt.tcp and not connect_to_session(sock, opt.session):
print("warning: continuing without joining a session!")
sock.sendall(msg)
if opt.listen:
receive_response(sock, opt)
if opt.tcp:
sock.shutdown(socket.SHUT_RDWR)
sock.close()
sys.exit(0)
if __name__ == "__main__":
main()

View file

@ -7,11 +7,9 @@ import time
import mock import mock
import pytest import pytest
from mock.mock import MagicMock
from core.api.grpc.client import InterfaceHelper from core.api.grpc.client import InterfaceHelper
from core.api.grpc.server import CoreGrpcServer from core.api.grpc.server import CoreGrpcServer
from core.api.tlv.corehandlers import CoreHandler
from core.emulator.coreemu import CoreEmu from core.emulator.coreemu import CoreEmu
from core.emulator.data import IpPrefixes from core.emulator.data import IpPrefixes
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
@ -61,8 +59,6 @@ def patcher(request):
LinuxNetClient, "get_mac", return_value="00:00:00:00:00:00" LinuxNetClient, "get_mac", return_value="00:00:00:00:00:00"
) )
patch_manager.patch_obj(CoreNode, "create_file") patch_manager.patch_obj(CoreNode, "create_file")
patch_manager.patch_obj(Session, "write_state")
patch_manager.patch_obj(Session, "write_nodes")
yield patch_manager yield patch_manager
patch_manager.shutdown() patch_manager.shutdown()
@ -104,17 +100,6 @@ def module_grpc(global_coreemu):
grpc_server.server.stop(None) grpc_server.server.stop(None)
@pytest.fixture(scope="module")
def module_coretlv(patcher, global_coreemu, global_session):
request_mock = MagicMock()
request_mock.fileno = MagicMock(return_value=1)
server = MockServer(global_coreemu)
request_handler = CoreHandler(request_mock, "", server)
request_handler.session = global_session
request_handler.add_session_handlers()
yield request_handler
@pytest.fixture @pytest.fixture
def grpc_server(module_grpc): def grpc_server(module_grpc):
yield module_grpc yield module_grpc
@ -130,16 +115,6 @@ def session(global_session):
global_session.clear() global_session.clear()
@pytest.fixture
def coretlv(module_coretlv):
session = module_coretlv.session
session.set_state(EventTypes.CONFIGURATION_STATE)
coreemu = module_coretlv.coreemu
coreemu.sessions[session.id] = session
yield module_coretlv
coreemu.shutdown()
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption("--distributed", help="distributed server address") parser.addoption("--distributed", help="distributed server address")
parser.addoption("--mock", action="store_true", help="run without mocking") parser.addoption("--mock", action="store_true", help="run without mocking")

View file

@ -16,10 +16,10 @@ from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
from core.emane.models.rfpipe import EmaneRfPipeModel from core.emane.models.rfpipe import EmaneRfPipeModel
from core.emane.models.tdma import EmaneTdmaModel from core.emane.models.tdma import EmaneTdmaModel
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.data import IpPrefixes
from core.emulator.session import Session from core.emulator.session import Session
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNode from core.nodes.base import CoreNode, Position
_EMANE_MODELS = [ _EMANE_MODELS = [
EmaneIeee80211abgModel, EmaneIeee80211abgModel,
@ -53,19 +53,22 @@ class TestEmane:
""" """
# create emane node for networking the core nodes # create emane node for networking the core nodes
session.set_location(47.57917, -122.13232, 2.00000, 1.0) session.set_location(47.57917, -122.13232, 2.00000, 1.0)
options = NodeOptions() options = EmaneNet.create_options()
options.set_position(80, 50) options.emane_model = EmaneIeee80211abgModel.name
options.emane = EmaneIeee80211abgModel.name position = Position(x=80, y=50)
emane_net1 = session.add_node(EmaneNet, options=options) emane_net1 = session.add_node(EmaneNet, position=position, options=options)
options.emane = EmaneRfPipeModel.name options = EmaneNet.create_options()
emane_net2 = session.add_node(EmaneNet, options=options) options.emane_model = EmaneRfPipeModel.name
position = Position(x=80, y=50)
emane_net2 = session.add_node(EmaneNet, position=position, options=options)
# create nodes # create nodes
options = NodeOptions(model="mdr") options = CoreNode.create_options()
options.set_position(150, 150) options.model = "mdr"
node1 = session.add_node(CoreNode, options=options) position = Position(x=150, y=150)
options.set_position(300, 150) node1 = session.add_node(CoreNode, position=position, options=options)
node2 = session.add_node(CoreNode, options=options) position = Position(x=300, y=150)
node2 = session.add_node(CoreNode, position=position, options=options)
# create interfaces # create interfaces
ip_prefix1 = IpPrefixes("10.0.0.0/24") ip_prefix1 = IpPrefixes("10.0.0.0/24")
@ -100,9 +103,10 @@ class TestEmane:
# create emane node for networking the core nodes # create emane node for networking the core nodes
session.set_location(47.57917, -122.13232, 2.00000, 1.0) session.set_location(47.57917, -122.13232, 2.00000, 1.0)
options = NodeOptions(emane=model.name) options = EmaneNet.create_options()
options.set_position(80, 50) options.emane_model = model.name
emane_network = session.add_node(EmaneNet, options=options) position = Position(x=80, y=50)
emane_network = session.add_node(EmaneNet, position=position, options=options)
# configure tdma # configure tdma
if model == EmaneTdmaModel: if model == EmaneTdmaModel:
@ -111,11 +115,12 @@ class TestEmane:
) )
# create nodes # create nodes
options = NodeOptions(model="mdr") options = CoreNode.create_options()
options.set_position(150, 150) options.model = "mdr"
node1 = session.add_node(CoreNode, options=options) position = Position(x=150, y=150)
options.set_position(300, 150) node1 = session.add_node(CoreNode, position=position, options=options)
node2 = session.add_node(CoreNode, options=options) position = Position(x=300, y=150)
node2 = session.add_node(CoreNode, position=position, options=options)
for i, node in enumerate([node1, node2]): for i, node in enumerate([node1, node2]):
node.setposition(x=150 * (i + 1), y=150) node.setposition(x=150 * (i + 1), y=150)
@ -141,9 +146,10 @@ class TestEmane:
""" """
# create emane node for networking the core nodes # create emane node for networking the core nodes
session.set_location(47.57917, -122.13232, 2.00000, 1.0) session.set_location(47.57917, -122.13232, 2.00000, 1.0)
options = NodeOptions(emane=EmaneIeee80211abgModel.name) options = EmaneNet.create_options()
options.set_position(80, 50) options.emane_model = EmaneIeee80211abgModel.name
emane_network = session.add_node(EmaneNet, options=options) position = Position(x=80, y=50)
emane_network = session.add_node(EmaneNet, position=position, options=options)
config_key = "txpower" config_key = "txpower"
config_value = "10" config_value = "10"
session.emane.set_config( session.emane.set_config(
@ -151,11 +157,12 @@ class TestEmane:
) )
# create nodes # create nodes
options = NodeOptions(model="mdr") options = CoreNode.create_options()
options.set_position(150, 150) options.model = "mdr"
node1 = session.add_node(CoreNode, options=options) position = Position(x=150, y=150)
options.set_position(300, 150) node1 = session.add_node(CoreNode, position=position, options=options)
node2 = session.add_node(CoreNode, options=options) position = Position(x=300, y=150)
node2 = session.add_node(CoreNode, position=position, options=options)
for i, node in enumerate([node1, node2]): for i, node in enumerate([node1, node2]):
node.setposition(x=150 * (i + 1), y=150) node.setposition(x=150 * (i + 1), y=150)
@ -205,14 +212,17 @@ class TestEmane:
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
): ):
# create nodes # create nodes
options = NodeOptions(model="mdr", x=50, y=50) options = CoreNode.create_options()
node1 = session.add_node(CoreNode, options=options) options.model = "mdr"
position = Position(x=50, y=50)
node1 = session.add_node(CoreNode, position=position, options=options)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
node2 = session.add_node(CoreNode, options=options) node2 = session.add_node(CoreNode, position=position, options=options)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
# create emane node # create emane node
options = NodeOptions(model=None, emane=EmaneRfPipeModel.name) options = EmaneNet.create_options()
options.emane_model = EmaneRfPipeModel.name
emane_node = session.add_node(EmaneNet, options=options) emane_node = session.add_node(EmaneNet, options=options)
# create links # create links
@ -255,11 +265,7 @@ class TestEmane:
assert session.get_node(node1.id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2.id, CoreNode) assert session.get_node(node2.id, CoreNode)
assert session.get_node(emane_node.id, EmaneNet) assert session.get_node(emane_node.id, EmaneNet)
links = [] assert len(session.link_manager.links()) == 2
for node_id in session.nodes:
node = session.nodes[node_id]
links += node.links()
assert len(links) == 2
config = session.emane.get_config(node1.id, EmaneRfPipeModel.name) config = session.emane.get_config(node1.id, EmaneRfPipeModel.name)
assert config["datarate"] == datarate assert config["datarate"] == datarate
@ -267,14 +273,17 @@ class TestEmane:
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
): ):
# create nodes # create nodes
options = NodeOptions(model="mdr", x=50, y=50) options = CoreNode.create_options()
node1 = session.add_node(CoreNode, options=options) options.model = "mdr"
position = Position(x=50, y=50)
node1 = session.add_node(CoreNode, position=position, options=options)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
node2 = session.add_node(CoreNode, options=options) node2 = session.add_node(CoreNode, position=position, options=options)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
# create emane node # create emane node
options = NodeOptions(model=None, emane=EmaneRfPipeModel.name) options = EmaneNet.create_options()
options.emane_model = EmaneRfPipeModel.name
emane_node = session.add_node(EmaneNet, options=options) emane_node = session.add_node(EmaneNet, options=options)
# create links # create links
@ -318,10 +327,6 @@ class TestEmane:
assert session.get_node(node1.id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2.id, CoreNode) assert session.get_node(node2.id, CoreNode)
assert session.get_node(emane_node.id, EmaneNet) assert session.get_node(emane_node.id, EmaneNet)
links = [] assert len(session.link_manager.links()) == 2
for node_id in session.nodes:
node = session.nodes[node_id]
links += node.links()
assert len(links) == 2
config = session.emane.get_config(config_id, EmaneRfPipeModel.name) config = session.emane.get_config(config_id, EmaneRfPipeModel.name)
assert config["datarate"] == datarate assert config["datarate"] == datarate

View file

@ -8,8 +8,7 @@ from typing import List, Type
import pytest import pytest
from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.data import IpPrefixes
from core.emulator.enumerations import MessageFlags
from core.emulator.session import Session from core.emulator.session import Session
from core.errors import CoreCommandError from core.errors import CoreCommandError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
@ -63,44 +62,6 @@ class TestCore:
status = ping(node1, node2, ip_prefixes) status = ping(node1, node2, ip_prefixes)
assert not status assert not status
def test_iface(self, session: Session, ip_prefixes: IpPrefixes):
"""
Test interface methods.
:param session: session for test
:param ip_prefixes: generates ip addresses for nodes
"""
# create ptp
ptp_node = session.add_node(PtpNet)
# create nodes
node1 = session.add_node(CoreNode)
node2 = session.add_node(CoreNode)
# link nodes to ptp net
for node in [node1, node2]:
iface = ip_prefixes.create_iface(node)
session.add_link(node.id, ptp_node.id, iface1_data=iface)
# instantiate session
session.instantiate()
# check link data gets generated
assert ptp_node.links(MessageFlags.ADD)
# check common nets exist between linked nodes
assert node1.commonnets(node2)
assert node2.commonnets(node1)
# check we can retrieve interface id
assert 0 in node1.ifaces
assert 0 in node2.ifaces
# delete interface and test that if no longer exists
node1.delete_iface(0)
assert 0 not in node1.ifaces
def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes): def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes):
""" """
Test basic wlan network. Test basic wlan network.
@ -114,8 +75,8 @@ class TestCore:
session.mobility.set_model(wlan_node, BasicRangeModel) session.mobility.set_model(wlan_node, BasicRangeModel)
# create nodes # create nodes
options = NodeOptions(model="mdr") options = CoreNode.create_options()
options.set_position(0, 0) options.model = "mdr"
node1 = session.add_node(CoreNode, options=options) node1 = session.add_node(CoreNode, options=options)
node2 = session.add_node(CoreNode, options=options) node2 = session.add_node(CoreNode, options=options)
@ -144,8 +105,8 @@ class TestCore:
session.mobility.set_model(wlan_node, BasicRangeModel) session.mobility.set_model(wlan_node, BasicRangeModel)
# create nodes # create nodes
options = NodeOptions(model="mdr") options = CoreNode.create_options()
options.set_position(0, 0) options.model = "mdr"
node1 = session.add_node(CoreNode, options=options) node1 = session.add_node(CoreNode, options=options)
node2 = session.add_node(CoreNode, options=options) node2 = session.add_node(CoreNode, options=options)

View file

@ -1,4 +1,3 @@
from core.emulator.data import NodeOptions
from core.emulator.session import Session from core.emulator.session import Session
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
from core.nodes.network import HubNode from core.nodes.network import HubNode
@ -12,8 +11,7 @@ class TestDistributed:
# when # when
session.distributed.add_server(server_name, host) session.distributed.add_server(server_name, host)
options = NodeOptions(server=server_name) node = session.add_node(CoreNode, server=server_name)
node = session.add_node(CoreNode, options=options)
session.instantiate() session.instantiate()
# then # then
@ -29,12 +27,13 @@ class TestDistributed:
# when # when
session.distributed.add_server(server_name, host) session.distributed.add_server(server_name, host)
options = NodeOptions(server=server_name) node1 = session.add_node(HubNode)
node = session.add_node(HubNode, options=options) node2 = session.add_node(HubNode, server=server_name)
session.add_link(node1.id, node2.id)
session.instantiate() session.instantiate()
# then # then
assert node.server is not None assert node2.server is not None
assert node.server.name == server_name assert node2.server.name == server_name
assert node.server.host == host assert node2.server.host == host
assert len(session.distributed.tunnels) > 0 assert len(session.distributed.tunnels) == 1

View file

@ -8,7 +8,7 @@ import grpc
import pytest import pytest
from mock import patch from mock import patch
from core.api.grpc import core_pb2, wrappers from core.api.grpc import wrappers
from core.api.grpc.client import CoreGrpcClient, InterfaceHelper, MoveNodesStreamer from core.api.grpc.client import CoreGrpcClient, InterfaceHelper, MoveNodesStreamer
from core.api.grpc.server import CoreGrpcServer from core.api.grpc.server import CoreGrpcServer
from core.api.grpc.wrappers import ( from core.api.grpc.wrappers import (
@ -22,6 +22,7 @@ from core.api.grpc.wrappers import (
Link, Link,
LinkOptions, LinkOptions,
MobilityAction, MobilityAction,
MoveNodesRequest,
Node, Node,
NodeServiceData, NodeServiceData,
NodeType, NodeType,
@ -31,12 +32,10 @@ from core.api.grpc.wrappers import (
SessionLocation, SessionLocation,
SessionState, SessionState,
) )
from core.api.tlv.dataconversion import ConfigShim
from core.api.tlv.enumerations import ConfigFlags
from core.emane.models.ieee80211abg import EmaneIeee80211abgModel from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import EventData, IpPrefixes, NodeData, NodeOptions from core.emulator.data import EventData, IpPrefixes, NodeData
from core.emulator.enumerations import EventTypes, ExceptionLevels from core.emulator.enumerations import EventTypes, ExceptionLevels, MessageFlags
from core.errors import CoreError from core.errors import CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
@ -163,7 +162,7 @@ class TestGrpc:
real_node1, service_name, service_file real_node1, service_name, service_file
) )
assert service_file.data == service_file_data assert service_file.data == service_file_data
assert option_value == real_session.options.get_config(option_key) assert option_value == real_session.options.get(option_key)
@pytest.mark.parametrize("session_id", [None, 6013]) @pytest.mark.parametrize("session_id", [None, 6013])
def test_create_session( def test_create_session(
@ -351,8 +350,7 @@ class TestGrpc:
client = CoreGrpcClient() client = CoreGrpcClient()
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
session.set_state(EventTypes.CONFIGURATION_STATE) session.set_state(EventTypes.CONFIGURATION_STATE)
options = NodeOptions(model="Host") node = session.add_node(CoreNode)
node = session.add_node(CoreNode, options=options)
session.instantiate() session.instantiate()
expected_output = "hello world" expected_output = "hello world"
expected_status = 0 expected_status = 0
@ -370,8 +368,7 @@ class TestGrpc:
client = CoreGrpcClient() client = CoreGrpcClient()
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
session.set_state(EventTypes.CONFIGURATION_STATE) session.set_state(EventTypes.CONFIGURATION_STATE)
options = NodeOptions(model="Host") node = session.add_node(CoreNode)
node = session.add_node(CoreNode, options=options)
session.instantiate() session.instantiate()
# then # then
@ -415,7 +412,7 @@ class TestGrpc:
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
switch = session.add_node(SwitchNode) switch = session.add_node(SwitchNode)
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
assert len(switch.links()) == 0 assert len(session.link_manager.links()) == 0
iface = InterfaceHelper("10.0.0.0/24").create_iface(node.id, 0) iface = InterfaceHelper("10.0.0.0/24").create_iface(node.id, 0)
link = Link(node.id, switch.id, iface1=iface) link = Link(node.id, switch.id, iface1=iface)
@ -425,7 +422,7 @@ class TestGrpc:
# then # then
assert result is True assert result is True
assert len(switch.links()) == 1 assert len(session.link_manager.links()) == 1
assert iface1.id == iface.id assert iface1.id == iface.id
assert iface1.ip4 == iface.ip4 assert iface1.ip4 == iface.ip4
@ -445,13 +442,14 @@ class TestGrpc:
# given # given
client = CoreGrpcClient() client = CoreGrpcClient()
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
session.set_state(EventTypes.CONFIGURATION_STATE)
switch = session.add_node(SwitchNode) switch = session.add_node(SwitchNode)
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
iface = ip_prefixes.create_iface(node) iface_data = ip_prefixes.create_iface(node)
session.add_link(node.id, switch.id, iface) iface, _ = session.add_link(node.id, switch.id, iface_data)
session.instantiate()
options = LinkOptions(bandwidth=30000) options = LinkOptions(bandwidth=30000)
link = switch.links()[0] assert iface.options.bandwidth != options.bandwidth
assert options.bandwidth != link.options.bandwidth
link = Link(node.id, switch.id, iface1=Interface(id=iface.id), options=options) link = Link(node.id, switch.id, iface1=Interface(id=iface.id), options=options)
# then # then
@ -460,8 +458,7 @@ class TestGrpc:
# then # then
assert result is True assert result is True
link = switch.links()[0] assert options.bandwidth == iface.options.bandwidth
assert options.bandwidth == link.options.bandwidth
def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes):
# given # given
@ -472,13 +469,7 @@ class TestGrpc:
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface2 = ip_prefixes.create_iface(node2) iface2 = ip_prefixes.create_iface(node2)
session.add_link(node1.id, node2.id, iface1, iface2) session.add_link(node1.id, node2.id, iface1, iface2)
link_node = None assert len(session.link_manager.links()) == 1
for node_id in session.nodes:
node = session.nodes[node_id]
if node.id not in {node1.id, node2.id}:
link_node = node
break
assert len(link_node.links()) == 1
link = Link( link = Link(
node1.id, node1.id,
node2.id, node2.id,
@ -492,7 +483,7 @@ class TestGrpc:
# then # then
assert result is True assert result is True
assert len(link_node.links()) == 0 assert len(session.link_manager.links()) == 0
def test_get_wlan_config(self, grpc_server: CoreGrpcServer): def test_get_wlan_config(self, grpc_server: CoreGrpcServer):
# given # given
@ -537,14 +528,15 @@ class TestGrpc:
assert result is True assert result is True
config = session.mobility.get_model_config(wlan.id, BasicRangeModel.name) config = session.mobility.get_model_config(wlan.id, BasicRangeModel.name)
assert config[range_key] == range_value assert config[range_key] == range_value
assert wlan.model.range == int(range_value) assert wlan.wireless_model.range == int(range_value)
def test_set_emane_model_config(self, grpc_server: CoreGrpcServer): def test_set_emane_model_config(self, grpc_server: CoreGrpcServer):
# given # given
client = CoreGrpcClient() client = CoreGrpcClient()
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
session.set_location(47.57917, -122.13232, 2.00000, 1.0) session.set_location(47.57917, -122.13232, 2.00000, 1.0)
options = NodeOptions(emane=EmaneIeee80211abgModel.name) options = EmaneNet.create_options()
options.emane_model = EmaneIeee80211abgModel.name
emane_network = session.add_node(EmaneNet, options=options) emane_network = session.add_node(EmaneNet, options=options)
session.emane.node_models[emane_network.id] = EmaneIeee80211abgModel.name session.emane.node_models[emane_network.id] = EmaneIeee80211abgModel.name
config_key = "bandwidth" config_key = "bandwidth"
@ -574,7 +566,8 @@ class TestGrpc:
client = CoreGrpcClient() client = CoreGrpcClient()
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
session.set_location(47.57917, -122.13232, 2.00000, 1.0) session.set_location(47.57917, -122.13232, 2.00000, 1.0)
options = NodeOptions(emane=EmaneIeee80211abgModel.name) options = EmaneNet.create_options()
options.emane_model = EmaneIeee80211abgModel.name
emane_network = session.add_node(EmaneNet, options=options) emane_network = session.add_node(EmaneNet, options=options)
session.emane.node_models[emane_network.id] = EmaneIeee80211abgModel.name session.emane.node_models[emane_network.id] = EmaneIeee80211abgModel.name
@ -651,16 +644,16 @@ class TestGrpc:
# given # given
client = CoreGrpcClient() client = CoreGrpcClient()
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
node_type = "test" model = "test"
services = ["SSH"] services = ["SSH"]
# then # then
with client.context_connect(): with client.context_connect():
result = client.set_service_defaults(session.id, {node_type: services}) result = client.set_service_defaults(session.id, {model: services})
# then # then
assert result is True assert result is True
assert session.services.default_services[node_type] == services assert session.services.default_services[model] == services
def test_get_node_service(self, grpc_server: CoreGrpcServer): def test_get_node_service(self, grpc_server: CoreGrpcServer):
# given # given
@ -694,7 +687,8 @@ class TestGrpc:
# given # given
client = CoreGrpcClient() client = CoreGrpcClient()
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
options = NodeOptions(legacy=True) options = CoreNode.create_options()
options.legacy = True
node = session.add_node(CoreNode, options=options) node = session.add_node(CoreNode, options=options)
service_name = "DefaultRoute" service_name = "DefaultRoute"
@ -757,9 +751,11 @@ class TestGrpc:
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
wlan = session.add_node(WlanNode) wlan = session.add_node(WlanNode)
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
iface = ip_prefixes.create_iface(node) iface_data = ip_prefixes.create_iface(node)
session.add_link(node.id, wlan.id, iface) session.add_link(node.id, wlan.id, iface_data)
link_data = wlan.links()[0] core_link = list(session.link_manager.links())[0]
link_data = core_link.get_data(MessageFlags.ADD)
queue = Queue() queue = Queue()
def handle_event(event: Event) -> None: def handle_event(event: Event) -> None:
@ -820,30 +816,6 @@ class TestGrpc:
# then # then
queue.get(timeout=5) queue.get(timeout=5)
def test_config_events(self, grpc_server: CoreGrpcServer):
# given
client = CoreGrpcClient()
session = grpc_server.coreemu.create_session()
queue = Queue()
def handle_event(event: Event) -> None:
assert event.session_id == session.id
assert event.config_event is not None
queue.put(event)
# then
with client.context_connect():
client.events(session.id, handle_event)
time.sleep(0.1)
session_config = session.options.get_configs()
config_data = ConfigShim.config_data(
0, None, ConfigFlags.UPDATE.value, session.options, session_config
)
session.broadcast_config(config_data)
# then
queue.get(timeout=5)
def test_exception_events(self, grpc_server: CoreGrpcServer): def test_exception_events(self, grpc_server: CoreGrpcServer):
# given # given
client = CoreGrpcClient() client = CoreGrpcClient()
@ -950,7 +922,7 @@ class TestGrpc:
client = CoreGrpcClient() client = CoreGrpcClient()
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
streamer = MoveNodesStreamer(session.id) streamer = MoveNodesStreamer(session.id)
request = core_pb2.MoveNodesRequest() request = MoveNodesRequest(session.id + 1, 1)
streamer.send(request) streamer.send(request)
streamer.stop() streamer.stop()
@ -958,3 +930,27 @@ class TestGrpc:
with pytest.raises(grpc.RpcError): with pytest.raises(grpc.RpcError):
with client.context_connect(): with client.context_connect():
client.move_nodes(streamer) client.move_nodes(streamer)
def test_wlan_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes):
# given
client = CoreGrpcClient()
session = grpc_server.coreemu.create_session()
session.set_state(EventTypes.CONFIGURATION_STATE)
wlan = session.add_node(WlanNode)
node1 = session.add_node(CoreNode)
node2 = session.add_node(CoreNode)
iface1_data = ip_prefixes.create_iface(node1)
iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node1.id, wlan.id, iface1_data)
session.add_link(node2.id, wlan.id, iface2_data)
session.instantiate()
assert len(session.link_manager.links()) == 2
# when
with client.context_connect():
result1 = client.wlan_link(session.id, wlan.id, node1.id, node2.id, True)
result2 = client.wlan_link(session.id, wlan.id, node1.id, node2.id, False)
# then
assert result1 is True
assert result2 is True

View file

@ -1,941 +0,0 @@
"""
Tests for testing tlv message handling.
"""
import time
from pathlib import Path
from typing import Optional
import mock
import netaddr
import pytest
from mock import MagicMock
from core.api.tlv import coreapi
from core.api.tlv.corehandlers import CoreHandler
from core.api.tlv.enumerations import (
ConfigFlags,
ConfigTlvs,
EventTlvs,
ExecuteTlvs,
FileTlvs,
LinkTlvs,
NodeTlvs,
SessionTlvs,
)
from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
from core.emulator.enumerations import EventTypes, MessageFlags, NodeTypes, RegisterTlvs
from core.errors import CoreError
from core.location.mobility import BasicRangeModel
from core.nodes.base import CoreNode, NodeBase
from core.nodes.network import SwitchNode, WlanNode
def dict_to_str(values) -> str:
return "|".join(f"{x}={values[x]}" for x in values)
class TestGui:
@pytest.mark.parametrize(
"node_type, model",
[
(NodeTypes.DEFAULT, "PC"),
(NodeTypes.EMANE, None),
(NodeTypes.HUB, None),
(NodeTypes.SWITCH, None),
(NodeTypes.WIRELESS_LAN, None),
(NodeTypes.TUNNEL, None),
],
)
def test_node_add(
self, coretlv: CoreHandler, node_type: NodeTypes, model: Optional[str]
):
node_id = 1
name = "node1"
message = coreapi.CoreNodeMessage.create(
MessageFlags.ADD.value,
[
(NodeTlvs.NUMBER, node_id),
(NodeTlvs.TYPE, node_type.value),
(NodeTlvs.NAME, name),
(NodeTlvs.X_POSITION, 0),
(NodeTlvs.Y_POSITION, 0),
(NodeTlvs.MODEL, model),
],
)
coretlv.handle_message(message)
node = coretlv.session.get_node(node_id, NodeBase)
assert node
assert node.name == name
def test_node_update(self, coretlv: CoreHandler):
node_id = 1
coretlv.session.add_node(CoreNode, _id=node_id)
x = 50
y = 100
message = coreapi.CoreNodeMessage.create(
0,
[
(NodeTlvs.NUMBER, node_id),
(NodeTlvs.X_POSITION, x),
(NodeTlvs.Y_POSITION, y),
],
)
coretlv.handle_message(message)
node = coretlv.session.get_node(node_id, NodeBase)
assert node is not None
assert node.position.x == x
assert node.position.y == y
def test_node_delete(self, coretlv: CoreHandler):
node_id = 1
coretlv.session.add_node(CoreNode, _id=node_id)
message = coreapi.CoreNodeMessage.create(
MessageFlags.DELETE.value, [(NodeTlvs.NUMBER, node_id)]
)
coretlv.handle_message(message)
with pytest.raises(CoreError):
coretlv.session.get_node(node_id, NodeBase)
def test_link_add_node_to_net(self, coretlv: CoreHandler):
node1_id = 1
coretlv.session.add_node(CoreNode, _id=node1_id)
switch_id = 2
coretlv.session.add_node(SwitchNode, _id=switch_id)
ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
iface1_ip4 = str(ip_prefix[node1_id])
message = coreapi.CoreLinkMessage.create(
MessageFlags.ADD.value,
[
(LinkTlvs.N1_NUMBER, node1_id),
(LinkTlvs.N2_NUMBER, switch_id),
(LinkTlvs.IFACE1_NUMBER, 0),
(LinkTlvs.IFACE1_IP4, iface1_ip4),
(LinkTlvs.IFACE1_IP4_MASK, 24),
],
)
coretlv.handle_message(message)
switch_node = coretlv.session.get_node(switch_id, SwitchNode)
all_links = switch_node.links()
assert len(all_links) == 1
def test_link_add_net_to_node(self, coretlv: CoreHandler):
node1_id = 1
coretlv.session.add_node(CoreNode, _id=node1_id)
switch_id = 2
coretlv.session.add_node(SwitchNode, _id=switch_id)
ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
iface2_ip4 = str(ip_prefix[node1_id])
message = coreapi.CoreLinkMessage.create(
MessageFlags.ADD.value,
[
(LinkTlvs.N1_NUMBER, switch_id),
(LinkTlvs.N2_NUMBER, node1_id),
(LinkTlvs.IFACE2_NUMBER, 0),
(LinkTlvs.IFACE2_IP4, iface2_ip4),
(LinkTlvs.IFACE2_IP4_MASK, 24),
],
)
coretlv.handle_message(message)
switch_node = coretlv.session.get_node(switch_id, SwitchNode)
all_links = switch_node.links()
assert len(all_links) == 1
def test_link_add_node_to_node(self, coretlv: CoreHandler):
node1_id = 1
coretlv.session.add_node(CoreNode, _id=node1_id)
node2_id = 2
coretlv.session.add_node(CoreNode, _id=node2_id)
ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
iface1_ip4 = str(ip_prefix[node1_id])
iface2_ip4 = str(ip_prefix[node2_id])
message = coreapi.CoreLinkMessage.create(
MessageFlags.ADD.value,
[
(LinkTlvs.N1_NUMBER, node1_id),
(LinkTlvs.N2_NUMBER, node2_id),
(LinkTlvs.IFACE1_NUMBER, 0),
(LinkTlvs.IFACE1_IP4, iface1_ip4),
(LinkTlvs.IFACE1_IP4_MASK, 24),
(LinkTlvs.IFACE2_NUMBER, 0),
(LinkTlvs.IFACE2_IP4, iface2_ip4),
(LinkTlvs.IFACE2_IP4_MASK, 24),
],
)
coretlv.handle_message(message)
all_links = []
for node_id in coretlv.session.nodes:
node = coretlv.session.nodes[node_id]
all_links += node.links()
assert len(all_links) == 1
def test_link_update(self, coretlv: CoreHandler):
node1_id = 1
coretlv.session.add_node(CoreNode, _id=node1_id)
switch_id = 2
coretlv.session.add_node(SwitchNode, _id=switch_id)
ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
iface1_ip4 = str(ip_prefix[node1_id])
message = coreapi.CoreLinkMessage.create(
MessageFlags.ADD.value,
[
(LinkTlvs.N1_NUMBER, node1_id),
(LinkTlvs.N2_NUMBER, switch_id),
(LinkTlvs.IFACE1_NUMBER, 0),
(LinkTlvs.IFACE1_IP4, iface1_ip4),
(LinkTlvs.IFACE1_IP4_MASK, 24),
],
)
coretlv.handle_message(message)
switch_node = coretlv.session.get_node(switch_id, SwitchNode)
all_links = switch_node.links()
assert len(all_links) == 1
link = all_links[0]
assert link.options.bandwidth is None
bandwidth = 50000
message = coreapi.CoreLinkMessage.create(
0,
[
(LinkTlvs.N1_NUMBER, node1_id),
(LinkTlvs.N2_NUMBER, switch_id),
(LinkTlvs.IFACE1_NUMBER, 0),
(LinkTlvs.BANDWIDTH, bandwidth),
],
)
coretlv.handle_message(message)
switch_node = coretlv.session.get_node(switch_id, SwitchNode)
all_links = switch_node.links()
assert len(all_links) == 1
link = all_links[0]
assert link.options.bandwidth == bandwidth
def test_link_delete_node_to_node(self, coretlv: CoreHandler):
node1_id = 1
coretlv.session.add_node(CoreNode, _id=node1_id)
node2_id = 2
coretlv.session.add_node(CoreNode, _id=node2_id)
ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
iface1_ip4 = str(ip_prefix[node1_id])
iface2_ip4 = str(ip_prefix[node2_id])
message = coreapi.CoreLinkMessage.create(
MessageFlags.ADD.value,
[
(LinkTlvs.N1_NUMBER, node1_id),
(LinkTlvs.N2_NUMBER, node2_id),
(LinkTlvs.IFACE1_NUMBER, 0),
(LinkTlvs.IFACE1_IP4, iface1_ip4),
(LinkTlvs.IFACE1_IP4_MASK, 24),
(LinkTlvs.IFACE2_IP4, iface2_ip4),
(LinkTlvs.IFACE2_IP4_MASK, 24),
],
)
coretlv.handle_message(message)
all_links = []
for node_id in coretlv.session.nodes:
node = coretlv.session.nodes[node_id]
all_links += node.links()
assert len(all_links) == 1
message = coreapi.CoreLinkMessage.create(
MessageFlags.DELETE.value,
[
(LinkTlvs.N1_NUMBER, node1_id),
(LinkTlvs.N2_NUMBER, node2_id),
(LinkTlvs.IFACE1_NUMBER, 0),
(LinkTlvs.IFACE2_NUMBER, 0),
],
)
coretlv.handle_message(message)
all_links = []
for node_id in coretlv.session.nodes:
node = coretlv.session.nodes[node_id]
all_links += node.links()
assert len(all_links) == 0
def test_link_delete_node_to_net(self, coretlv: CoreHandler):
node1_id = 1
coretlv.session.add_node(CoreNode, _id=node1_id)
switch_id = 2
coretlv.session.add_node(SwitchNode, _id=switch_id)
ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
iface1_ip4 = str(ip_prefix[node1_id])
message = coreapi.CoreLinkMessage.create(
MessageFlags.ADD.value,
[
(LinkTlvs.N1_NUMBER, node1_id),
(LinkTlvs.N2_NUMBER, switch_id),
(LinkTlvs.IFACE1_NUMBER, 0),
(LinkTlvs.IFACE1_IP4, iface1_ip4),
(LinkTlvs.IFACE1_IP4_MASK, 24),
],
)
coretlv.handle_message(message)
switch_node = coretlv.session.get_node(switch_id, SwitchNode)
all_links = switch_node.links()
assert len(all_links) == 1
message = coreapi.CoreLinkMessage.create(
MessageFlags.DELETE.value,
[
(LinkTlvs.N1_NUMBER, node1_id),
(LinkTlvs.N2_NUMBER, switch_id),
(LinkTlvs.IFACE1_NUMBER, 0),
],
)
coretlv.handle_message(message)
switch_node = coretlv.session.get_node(switch_id, SwitchNode)
all_links = switch_node.links()
assert len(all_links) == 0
def test_link_delete_net_to_node(self, coretlv: CoreHandler):
node1_id = 1
coretlv.session.add_node(CoreNode, _id=node1_id)
switch_id = 2
coretlv.session.add_node(SwitchNode, _id=switch_id)
ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
iface1_ip4 = str(ip_prefix[node1_id])
message = coreapi.CoreLinkMessage.create(
MessageFlags.ADD.value,
[
(LinkTlvs.N1_NUMBER, node1_id),
(LinkTlvs.N2_NUMBER, switch_id),
(LinkTlvs.IFACE1_NUMBER, 0),
(LinkTlvs.IFACE1_IP4, iface1_ip4),
(LinkTlvs.IFACE1_IP4_MASK, 24),
],
)
coretlv.handle_message(message)
switch_node = coretlv.session.get_node(switch_id, SwitchNode)
all_links = switch_node.links()
assert len(all_links) == 1
message = coreapi.CoreLinkMessage.create(
MessageFlags.DELETE.value,
[
(LinkTlvs.N1_NUMBER, switch_id),
(LinkTlvs.N2_NUMBER, node1_id),
(LinkTlvs.IFACE2_NUMBER, 0),
],
)
coretlv.handle_message(message)
switch_node = coretlv.session.get_node(switch_id, SwitchNode)
all_links = switch_node.links()
assert len(all_links) == 0
def test_session_update(self, coretlv: CoreHandler):
session_id = coretlv.session.id
name = "test"
message = coreapi.CoreSessionMessage.create(
0, [(SessionTlvs.NUMBER, str(session_id)), (SessionTlvs.NAME, name)]
)
coretlv.handle_message(message)
assert coretlv.session.name == name
def test_session_query(self, coretlv: CoreHandler):
coretlv.dispatch_replies = mock.MagicMock()
message = coreapi.CoreSessionMessage.create(MessageFlags.STRING.value, [])
coretlv.handle_message(message)
args, _ = coretlv.dispatch_replies.call_args
replies = args[0]
assert len(replies) == 1
def test_session_join(self, coretlv: CoreHandler):
coretlv.dispatch_replies = mock.MagicMock()
session_id = coretlv.session.id
message = coreapi.CoreSessionMessage.create(
MessageFlags.ADD.value, [(SessionTlvs.NUMBER, str(session_id))]
)
coretlv.handle_message(message)
assert coretlv.session.id == session_id
def test_session_delete(self, coretlv: CoreHandler):
assert len(coretlv.coreemu.sessions) == 1
session_id = coretlv.session.id
message = coreapi.CoreSessionMessage.create(
MessageFlags.DELETE.value, [(SessionTlvs.NUMBER, str(session_id))]
)
coretlv.handle_message(message)
assert len(coretlv.coreemu.sessions) == 0
def test_file_hook_add(self, coretlv: CoreHandler):
state = EventTypes.DATACOLLECT_STATE
assert coretlv.session.hooks.get(state) is None
file_name = "test.sh"
file_data = "echo hello"
message = coreapi.CoreFileMessage.create(
MessageFlags.ADD.value,
[
(FileTlvs.TYPE, f"hook:{state.value}"),
(FileTlvs.NAME, file_name),
(FileTlvs.DATA, file_data),
],
)
coretlv.handle_message(message)
hooks = coretlv.session.hooks.get(state)
assert len(hooks) == 1
name, data = hooks[0]
assert file_name == name
assert file_data == data
def test_file_service_file_set(self, coretlv: CoreHandler):
node = coretlv.session.add_node(CoreNode)
service = "DefaultRoute"
file_name = "defaultroute.sh"
file_data = "echo hello"
message = coreapi.CoreFileMessage.create(
MessageFlags.ADD.value,
[
(FileTlvs.NODE, node.id),
(FileTlvs.TYPE, f"service:{service}"),
(FileTlvs.NAME, file_name),
(FileTlvs.DATA, file_data),
],
)
coretlv.handle_message(message)
service_file = coretlv.session.services.get_service_file(
node, service, file_name
)
assert file_data == service_file.data
def test_file_node_file_copy(self, request, coretlv: CoreHandler):
file_path = Path("/var/log/test/node.log")
node = coretlv.session.add_node(CoreNode)
node.makenodedir()
file_data = "echo hello"
message = coreapi.CoreFileMessage.create(
MessageFlags.ADD.value,
[
(FileTlvs.NODE, node.id),
(FileTlvs.NAME, str(file_path)),
(FileTlvs.DATA, file_data),
],
)
coretlv.handle_message(message)
if not request.config.getoption("mock"):
expected_path = node.directory / "var.log/test" / file_path.name
assert expected_path.exists()
def test_exec_node_tty(self, coretlv: CoreHandler):
coretlv.dispatch_replies = mock.MagicMock()
node = coretlv.session.add_node(CoreNode)
message = coreapi.CoreExecMessage.create(
MessageFlags.TTY.value,
[
(ExecuteTlvs.NODE, node.id),
(ExecuteTlvs.NUMBER, 1),
(ExecuteTlvs.COMMAND, "bash"),
],
)
coretlv.handle_message(message)
args, _ = coretlv.dispatch_replies.call_args
replies = args[0]
assert len(replies) == 1
def test_exec_local_command(self, request, coretlv: CoreHandler):
if request.config.getoption("mock"):
pytest.skip("mocking calls")
coretlv.dispatch_replies = mock.MagicMock()
node = coretlv.session.add_node(CoreNode)
cmd = "echo hello"
message = coreapi.CoreExecMessage.create(
MessageFlags.TEXT.value | MessageFlags.LOCAL.value,
[
(ExecuteTlvs.NODE, node.id),
(ExecuteTlvs.NUMBER, 1),
(ExecuteTlvs.COMMAND, cmd),
],
)
coretlv.handle_message(message)
args, _ = coretlv.dispatch_replies.call_args
replies = args[0]
assert len(replies) == 1
def test_exec_node_command(self, coretlv: CoreHandler):
coretlv.dispatch_replies = mock.MagicMock()
node = coretlv.session.add_node(CoreNode)
cmd = "echo hello"
message = coreapi.CoreExecMessage.create(
MessageFlags.TEXT.value,
[
(ExecuteTlvs.NODE, node.id),
(ExecuteTlvs.NUMBER, 1),
(ExecuteTlvs.COMMAND, cmd),
],
)
node.cmd = MagicMock(return_value="hello")
coretlv.handle_message(message)
node.cmd.assert_called_with(cmd)
@pytest.mark.parametrize(
"state",
[
EventTypes.SHUTDOWN_STATE,
EventTypes.RUNTIME_STATE,
EventTypes.DATACOLLECT_STATE,
EventTypes.CONFIGURATION_STATE,
EventTypes.DEFINITION_STATE,
],
)
def test_event_state(self, coretlv: CoreHandler, state: EventTypes):
message = coreapi.CoreEventMessage.create(0, [(EventTlvs.TYPE, state.value)])
coretlv.handle_message(message)
assert coretlv.session.state == state
def test_event_schedule(self, coretlv: CoreHandler):
coretlv.session.add_event = mock.MagicMock()
node = coretlv.session.add_node(CoreNode)
message = coreapi.CoreEventMessage.create(
MessageFlags.ADD.value,
[
(EventTlvs.TYPE, EventTypes.SCHEDULED.value),
(EventTlvs.TIME, str(time.monotonic() + 100)),
(EventTlvs.NODE, node.id),
(EventTlvs.NAME, "event"),
(EventTlvs.DATA, "data"),
],
)
coretlv.handle_message(message)
coretlv.session.add_event.assert_called_once()
def test_event_save_xml(self, coretlv: CoreHandler, tmpdir):
xml_file = tmpdir.join("coretlv.session.xml")
file_path = xml_file.strpath
coretlv.session.add_node(CoreNode)
message = coreapi.CoreEventMessage.create(
0,
[(EventTlvs.TYPE, EventTypes.FILE_SAVE.value), (EventTlvs.NAME, file_path)],
)
coretlv.handle_message(message)
assert Path(file_path).exists()
def test_event_open_xml(self, coretlv: CoreHandler, tmpdir):
xml_file = tmpdir.join("coretlv.session.xml")
file_path = Path(xml_file.strpath)
node = coretlv.session.add_node(CoreNode)
coretlv.session.save_xml(file_path)
coretlv.session.delete_node(node.id)
message = coreapi.CoreEventMessage.create(
0,
[
(EventTlvs.TYPE, EventTypes.FILE_OPEN.value),
(EventTlvs.NAME, str(file_path)),
],
)
coretlv.handle_message(message)
assert coretlv.session.get_node(node.id, NodeBase)
@pytest.mark.parametrize(
"state",
[
EventTypes.START,
EventTypes.STOP,
EventTypes.RESTART,
EventTypes.PAUSE,
EventTypes.RECONFIGURE,
],
)
def test_event_service(self, coretlv: CoreHandler, state: EventTypes):
coretlv.session.broadcast_event = mock.MagicMock()
node = coretlv.session.add_node(CoreNode)
message = coreapi.CoreEventMessage.create(
0,
[
(EventTlvs.TYPE, state.value),
(EventTlvs.NODE, node.id),
(EventTlvs.NAME, "service:DefaultRoute"),
],
)
coretlv.handle_message(message)
coretlv.session.broadcast_event.assert_called_once()
@pytest.mark.parametrize(
"state",
[
EventTypes.START,
EventTypes.STOP,
EventTypes.RESTART,
EventTypes.PAUSE,
EventTypes.RECONFIGURE,
],
)
def test_event_mobility(self, coretlv: CoreHandler, state: EventTypes):
message = coreapi.CoreEventMessage.create(
0, [(EventTlvs.TYPE, state.value), (EventTlvs.NAME, "mobility:ns2script")]
)
coretlv.handle_message(message)
def test_register_gui(self, coretlv: CoreHandler):
message = coreapi.CoreRegMessage.create(0, [(RegisterTlvs.GUI, "gui")])
coretlv.handle_message(message)
def test_register_xml(self, coretlv: CoreHandler, tmpdir):
xml_file = tmpdir.join("coretlv.session.xml")
file_path = xml_file.strpath
node = coretlv.session.add_node(CoreNode)
coretlv.session.save_xml(file_path)
coretlv.session.delete_node(node.id)
message = coreapi.CoreRegMessage.create(
0, [(RegisterTlvs.EXECUTE_SERVER, file_path)]
)
coretlv.session.instantiate()
coretlv.handle_message(message)
assert coretlv.coreemu.sessions[1].get_node(node.id, CoreNode)
def test_register_python(self, coretlv: CoreHandler, tmpdir):
xml_file = tmpdir.join("test.py")
file_path = xml_file.strpath
with open(file_path, "w") as f:
f.write("from core.nodes.base import CoreNode\n")
f.write("coreemu = globals()['coreemu']\n")
f.write(f"session = coreemu.sessions[{coretlv.session.id}]\n")
f.write("session.add_node(CoreNode)\n")
message = coreapi.CoreRegMessage.create(
0, [(RegisterTlvs.EXECUTE_SERVER, file_path)]
)
coretlv.session.instantiate()
coretlv.handle_message(message)
assert len(coretlv.session.nodes) == 1
def test_config_all(self, coretlv: CoreHandler):
message = coreapi.CoreConfMessage.create(
MessageFlags.ADD.value,
[(ConfigTlvs.OBJECT, "all"), (ConfigTlvs.TYPE, ConfigFlags.RESET.value)],
)
coretlv.session.location.refxyz = (10, 10, 10)
coretlv.handle_message(message)
assert coretlv.session.location.refxyz == (0, 0, 0)
def test_config_options_request(self, coretlv: CoreHandler):
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.OBJECT, "session"),
(ConfigTlvs.TYPE, ConfigFlags.REQUEST.value),
],
)
coretlv.handle_broadcast_config = mock.MagicMock()
coretlv.handle_message(message)
coretlv.handle_broadcast_config.assert_called_once()
def test_config_options_update(self, coretlv: CoreHandler):
test_key = "test"
test_value = "test"
values = {test_key: test_value}
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.OBJECT, "session"),
(ConfigTlvs.TYPE, ConfigFlags.UPDATE.value),
(ConfigTlvs.VALUES, dict_to_str(values)),
],
)
coretlv.handle_message(message)
assert coretlv.session.options.get_config(test_key) == test_value
def test_config_location_reset(self, coretlv: CoreHandler):
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.OBJECT, "location"),
(ConfigTlvs.TYPE, ConfigFlags.RESET.value),
],
)
coretlv.session.location.refxyz = (10, 10, 10)
coretlv.handle_message(message)
assert coretlv.session.location.refxyz == (0, 0, 0)
def test_config_location_update(self, coretlv: CoreHandler):
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.OBJECT, "location"),
(ConfigTlvs.TYPE, ConfigFlags.UPDATE.value),
(ConfigTlvs.VALUES, "10|10|70|50|0|0.5"),
],
)
coretlv.handle_message(message)
assert coretlv.session.location.refxyz == (10, 10, 0.0)
assert coretlv.session.location.refgeo == (70, 50, 0)
assert coretlv.session.location.refscale == 0.5
def test_config_metadata_request(self, coretlv: CoreHandler):
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.OBJECT, "metadata"),
(ConfigTlvs.TYPE, ConfigFlags.REQUEST.value),
],
)
coretlv.handle_broadcast_config = mock.MagicMock()
coretlv.handle_message(message)
coretlv.handle_broadcast_config.assert_called_once()
def test_config_metadata_update(self, coretlv: CoreHandler):
test_key = "test"
test_value = "test"
values = {test_key: test_value}
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.OBJECT, "metadata"),
(ConfigTlvs.TYPE, ConfigFlags.UPDATE.value),
(ConfigTlvs.VALUES, dict_to_str(values)),
],
)
coretlv.handle_message(message)
assert coretlv.session.metadata[test_key] == test_value
def test_config_broker_request(self, coretlv: CoreHandler):
server = "test"
host = "10.0.0.1"
port = 50000
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.OBJECT, "broker"),
(ConfigTlvs.TYPE, ConfigFlags.UPDATE.value),
(ConfigTlvs.VALUES, f"{server}:{host}:{port}"),
],
)
coretlv.session.distributed.add_server = mock.MagicMock()
coretlv.handle_message(message)
coretlv.session.distributed.add_server.assert_called_once_with(server, host)
def test_config_services_request_all(self, coretlv: CoreHandler):
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.OBJECT, "services"),
(ConfigTlvs.TYPE, ConfigFlags.REQUEST.value),
],
)
coretlv.handle_broadcast_config = mock.MagicMock()
coretlv.handle_message(message)
coretlv.handle_broadcast_config.assert_called_once()
def test_config_services_request_specific(self, coretlv: CoreHandler):
node = coretlv.session.add_node(CoreNode)
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.NODE, node.id),
(ConfigTlvs.OBJECT, "services"),
(ConfigTlvs.TYPE, ConfigFlags.REQUEST.value),
(ConfigTlvs.OPAQUE, "service:DefaultRoute"),
],
)
coretlv.handle_broadcast_config = mock.MagicMock()
coretlv.handle_message(message)
coretlv.handle_broadcast_config.assert_called_once()
def test_config_services_request_specific_file(self, coretlv: CoreHandler):
node = coretlv.session.add_node(CoreNode)
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.NODE, node.id),
(ConfigTlvs.OBJECT, "services"),
(ConfigTlvs.TYPE, ConfigFlags.REQUEST.value),
(ConfigTlvs.OPAQUE, "service:DefaultRoute:defaultroute.sh"),
],
)
coretlv.session.broadcast_file = mock.MagicMock()
coretlv.handle_message(message)
coretlv.session.broadcast_file.assert_called_once()
def test_config_services_reset(self, coretlv: CoreHandler):
node = coretlv.session.add_node(CoreNode)
service = "DefaultRoute"
coretlv.session.services.set_service(node.id, service)
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.OBJECT, "services"),
(ConfigTlvs.TYPE, ConfigFlags.RESET.value),
],
)
assert coretlv.session.services.get_service(node.id, service) is not None
coretlv.handle_message(message)
assert coretlv.session.services.get_service(node.id, service) is None
def test_config_services_set(self, coretlv: CoreHandler):
node = coretlv.session.add_node(CoreNode)
service = "DefaultRoute"
values = {"meta": "metadata"}
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.NODE, node.id),
(ConfigTlvs.OBJECT, "services"),
(ConfigTlvs.TYPE, ConfigFlags.UPDATE.value),
(ConfigTlvs.OPAQUE, f"service:{service}"),
(ConfigTlvs.VALUES, dict_to_str(values)),
],
)
assert coretlv.session.services.get_service(node.id, service) is None
coretlv.handle_message(message)
assert coretlv.session.services.get_service(node.id, service) is not None
def test_config_mobility_reset(self, coretlv: CoreHandler):
wlan = coretlv.session.add_node(WlanNode)
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.OBJECT, "MobilityManager"),
(ConfigTlvs.TYPE, ConfigFlags.RESET.value),
],
)
coretlv.session.mobility.set_model_config(wlan.id, BasicRangeModel.name, {})
assert len(coretlv.session.mobility.node_configurations) == 1
coretlv.handle_message(message)
assert len(coretlv.session.mobility.node_configurations) == 0
def test_config_mobility_model_request(self, coretlv: CoreHandler):
wlan = coretlv.session.add_node(WlanNode)
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.NODE, wlan.id),
(ConfigTlvs.OBJECT, BasicRangeModel.name),
(ConfigTlvs.TYPE, ConfigFlags.REQUEST.value),
],
)
coretlv.handle_broadcast_config = mock.MagicMock()
coretlv.handle_message(message)
coretlv.handle_broadcast_config.assert_called_once()
def test_config_mobility_model_update(self, coretlv: CoreHandler):
wlan = coretlv.session.add_node(WlanNode)
config_key = "range"
config_value = "1000"
values = {config_key: config_value}
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.NODE, wlan.id),
(ConfigTlvs.OBJECT, BasicRangeModel.name),
(ConfigTlvs.TYPE, ConfigFlags.UPDATE.value),
(ConfigTlvs.VALUES, dict_to_str(values)),
],
)
coretlv.handle_message(message)
config = coretlv.session.mobility.get_model_config(
wlan.id, BasicRangeModel.name
)
assert config[config_key] == config_value
def test_config_emane_model_request(self, coretlv: CoreHandler):
wlan = coretlv.session.add_node(WlanNode)
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.NODE, wlan.id),
(ConfigTlvs.OBJECT, EmaneIeee80211abgModel.name),
(ConfigTlvs.TYPE, ConfigFlags.REQUEST.value),
],
)
coretlv.handle_broadcast_config = mock.MagicMock()
coretlv.handle_message(message)
coretlv.handle_broadcast_config.assert_called_once()
def test_config_emane_model_update(self, coretlv: CoreHandler):
wlan = coretlv.session.add_node(WlanNode)
config_key = "distance"
config_value = "50051"
values = {config_key: config_value}
message = coreapi.CoreConfMessage.create(
0,
[
(ConfigTlvs.NODE, wlan.id),
(ConfigTlvs.OBJECT, EmaneIeee80211abgModel.name),
(ConfigTlvs.TYPE, ConfigFlags.UPDATE.value),
(ConfigTlvs.VALUES, dict_to_str(values)),
],
)
coretlv.handle_message(message)
config = coretlv.session.emane.get_config(wlan.id, EmaneIeee80211abgModel.name)
assert config[config_key] == config_value

View file

@ -46,14 +46,17 @@ class TestLinks:
) )
# then # then
assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1_data.id) assert node1.get_iface(iface1_data.id)
assert node2.get_iface(iface2_data.id) assert node2.get_iface(iface2_data.id)
assert iface1 is not None assert iface1 is not None
assert iface1.options == LINK_OPTIONS
assert iface1.has_netem
assert node1.get_iface(iface1_data.id)
assert iface2 is not None assert iface2 is not None
assert iface1.local_options == LINK_OPTIONS assert iface2.options == LINK_OPTIONS
assert iface1.has_local_netem assert iface2.has_netem
assert iface2.local_options == LINK_OPTIONS assert node1.get_iface(iface1_data.id)
assert iface2.has_local_netem
def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -62,16 +65,20 @@ class TestLinks:
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
# when # when
iface, _ = session.add_link( iface1, iface2 = session.add_link(
node1.id, node2.id, iface1_data=iface1_data, options=LINK_OPTIONS node1.id, node2.id, iface1_data=iface1_data, options=LINK_OPTIONS
) )
# then # then
assert node2.links() assert len(session.link_manager.links()) == 1
assert iface1 is not None
assert iface1.options == LINK_OPTIONS
assert iface1.has_netem
assert node1.get_iface(iface1_data.id) assert node1.get_iface(iface1_data.id)
assert iface is not None assert iface2 is not None
assert iface.local_options == LINK_OPTIONS assert iface2.options == LINK_OPTIONS
assert iface.has_local_netem assert iface2.has_netem
assert node2.get_iface(iface1_data.id)
def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -80,32 +87,37 @@ class TestLinks:
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
# when # when
_, iface = session.add_link( iface1, iface2 = session.add_link(
node1.id, node2.id, iface2_data=iface2_data, options=LINK_OPTIONS node1.id, node2.id, iface2_data=iface2_data, options=LINK_OPTIONS
) )
# then # then
assert node1.links() assert len(session.link_manager.links()) == 1
assert node2.get_iface(iface2_data.id) assert iface1 is not None
assert iface is not None assert iface1.options == LINK_OPTIONS
assert iface.local_options == LINK_OPTIONS assert iface1.has_netem
assert iface.has_local_netem assert node1.get_iface(iface1.id)
assert iface2 is not None
assert iface2.options == LINK_OPTIONS
assert iface2.has_netem
assert node2.get_iface(iface2.id)
def test_add_net_to_net(self, session): def test_add_net_to_net(self, session: Session):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
# when # when
iface, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS) iface1, iface2 = session.add_link(node1.id, node2.id, options=LINK_OPTIONS)
# then # then
assert node1.links() assert len(session.link_manager.links()) == 1
assert iface is not None assert iface1 is not None
assert iface.local_options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface.options == LINK_OPTIONS assert iface1.has_netem
assert iface.has_local_netem assert iface2 is not None
assert iface.has_netem assert iface2.options == LINK_OPTIONS
assert iface2.has_netem
def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes): def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -141,48 +153,52 @@ class TestLinks:
) )
# then # then
assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1_data.id) assert node1.get_iface(iface1_data.id)
assert node2.get_iface(iface2_data.id) assert node2.get_iface(iface2_data.id)
assert iface1 is not None assert iface1 is not None
assert iface1.options == link_options1
assert iface1.has_netem
assert iface2 is not None assert iface2 is not None
assert iface1.local_options == link_options1 assert iface2.options == link_options2
assert iface1.has_local_netem assert iface2.has_netem
assert iface2.local_options == link_options2
assert iface2.has_local_netem
def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(CoreNode) node1 = session.add_node(CoreNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface1, _ = session.add_link(node1.id, node2.id, iface1_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
assert iface1.local_options != LINK_OPTIONS assert len(session.link_manager.links()) == 1
assert iface1.options != LINK_OPTIONS
assert iface2.options != LINK_OPTIONS
# when # when
session.update_link( session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
node1.id, node2.id, iface1_id=iface1_data.id, options=LINK_OPTIONS
)
# then # then
assert iface1.local_options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface1.has_local_netem assert iface1.has_netem
assert iface2.options == LINK_OPTIONS
assert iface2.has_netem
def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
_, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
assert iface2.local_options != LINK_OPTIONS assert iface1.options != LINK_OPTIONS
assert iface2.options != LINK_OPTIONS
# when # when
session.update_link( session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
node1.id, node2.id, iface2_id=iface2_data.id, options=LINK_OPTIONS
)
# then # then
assert iface2.local_options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface2.has_local_netem assert iface1.has_netem
assert iface2.options == LINK_OPTIONS
assert iface2.has_netem
def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -191,55 +207,68 @@ class TestLinks:
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
assert iface1.local_options != LINK_OPTIONS assert iface1.options != LINK_OPTIONS
assert iface2.local_options != LINK_OPTIONS assert iface2.options != LINK_OPTIONS
# when # when
session.update_link( session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
node1.id, node2.id, iface1_data.id, iface2_data.id, LINK_OPTIONS
)
# then # then
assert iface1.local_options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface1.has_local_netem assert iface1.has_netem
assert iface2.local_options == LINK_OPTIONS assert iface2.options == LINK_OPTIONS
assert iface2.has_local_netem assert iface2.has_netem
def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1, _ = session.add_link(node1.id, node2.id) iface1, iface2 = session.add_link(node1.id, node2.id)
assert iface1.local_options != LINK_OPTIONS assert iface1.options != LINK_OPTIONS
assert iface2.options != LINK_OPTIONS
# when # when
session.update_link(node1.id, node2.id, options=LINK_OPTIONS) session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
# then # then
assert iface1.local_options == LINK_OPTIONS
assert iface1.has_local_netem
assert iface1.options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface1.has_netem assert iface1.has_netem
assert iface2.options == LINK_OPTIONS
assert iface2.has_netem
def test_update_error(self, session: Session, ip_prefixes: IpPrefixes):
# given
node1 = session.add_node(CoreNode)
node2 = session.add_node(CoreNode)
iface1_data = ip_prefixes.create_iface(node1)
iface2_data = ip_prefixes.create_iface(node2)
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
assert iface1.options != LINK_OPTIONS
assert iface2.options != LINK_OPTIONS
# when
with pytest.raises(CoreError):
session.delete_link(node1.id, INVALID_ID, iface1.id, iface2.id)
def test_clear_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): def test_clear_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS) iface1, iface2 = session.add_link(node1.id, node2.id, options=LINK_OPTIONS)
assert iface1.local_options == LINK_OPTIONS
assert iface1.has_local_netem
assert iface1.options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface1.has_netem assert iface1.has_netem
assert iface2.options == LINK_OPTIONS
assert iface2.has_netem
# when # when
options = LinkOptions(delay=0, bandwidth=0, loss=0.0, dup=0, jitter=0, buffer=0) options = LinkOptions(delay=0, bandwidth=0, loss=0.0, dup=0, jitter=0, buffer=0)
session.update_link(node1.id, node2.id, options=options) session.update_link(node1.id, node2.id, iface1.id, iface2.id, options)
# then # then
assert iface1.local_options.is_clear()
assert not iface1.has_local_netem
assert iface1.options.is_clear() assert iface1.options.is_clear()
assert not iface1.has_netem assert not iface1.has_netem
assert iface2.options.is_clear()
assert not iface2.has_netem
def test_delete_node_to_node(self, session: Session, ip_prefixes: IpPrefixes): def test_delete_node_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -247,82 +276,100 @@ class TestLinks:
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node1.id, node2.id, iface1_data, iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
assert node1.get_iface(iface1_data.id) assert len(session.link_manager.links()) == 1
assert node2.get_iface(iface2_data.id) assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
session.delete_link(node1.id, node2.id, iface1_data.id, iface2_data.id) session.delete_link(node1.id, node2.id, iface1.id, iface2.id)
# then # then
assert iface1_data.id not in node1.ifaces assert len(session.link_manager.links()) == 0
assert iface2_data.id not in node2.ifaces assert iface1.id not in node1.ifaces
assert iface2.id not in node2.ifaces
def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(CoreNode) node1 = session.add_node(CoreNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
session.add_link(node1.id, node2.id, iface1_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
assert node1.get_iface(iface1_data.id) assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
session.delete_link(node1.id, node2.id, iface1_id=iface1_data.id) session.delete_link(node1.id, node2.id, iface1.id, iface2.id)
# then # then
assert iface1_data.id not in node1.ifaces assert len(session.link_manager.links()) == 0
assert iface1.id not in node1.ifaces
assert iface2.id not in node2.ifaces
def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node1.id, node2.id, iface2_data=iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
assert node2.get_iface(iface2_data.id) assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
session.delete_link(node1.id, node2.id, iface2_id=iface2_data.id) session.delete_link(node1.id, node2.id, iface1.id, iface2.id)
# then # then
assert iface2_data.id not in node2.ifaces assert len(session.link_manager.links()) == 0
assert iface1.id not in node1.ifaces
assert iface2.id not in node2.ifaces
def test_delete_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): def test_delete_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
session.add_link(node1.id, node2.id) iface1, iface2 = session.add_link(node1.id, node2.id)
assert node1.get_linked_iface(node2) assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
session.delete_link(node1.id, node2.id) session.delete_link(node1.id, node2.id, iface1.id, iface2.id)
# then # then
assert not node1.get_linked_iface(node2) assert len(session.link_manager.links()) == 0
assert iface1.id not in node1.ifaces
assert iface2.id not in node2.ifaces
def test_delete_node_error(self, session: Session, ip_prefixes: IpPrefixes): def test_delete_node_error(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
session.add_link(node1.id, node2.id) iface1, iface2 = session.add_link(node1.id, node2.id)
assert node1.get_linked_iface(node2) assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
with pytest.raises(CoreError): with pytest.raises(CoreError):
session.delete_link(node1.id, INVALID_ID) session.delete_link(node1.id, INVALID_ID, iface1.id, iface2.id)
with pytest.raises(CoreError): with pytest.raises(CoreError):
session.delete_link(INVALID_ID, node2.id) session.delete_link(INVALID_ID, node2.id, iface1.id, iface2.id)
def test_delete_net_to_net_error(self, session: Session, ip_prefixes: IpPrefixes): def test_delete_net_to_net_error(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
node3 = session.add_node(SwitchNode) node3 = session.add_node(SwitchNode)
session.add_link(node1.id, node2.id) iface1, iface2 = session.add_link(node1.id, node2.id)
assert node1.get_linked_iface(node2) assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
with pytest.raises(CoreError): with pytest.raises(CoreError):
session.delete_link(node1.id, node3.id) session.delete_link(node1.id, node3.id, iface1.id, iface2.id)
def test_delete_node_to_net_error(self, session: Session, ip_prefixes: IpPrefixes): def test_delete_node_to_net_error(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -330,12 +377,14 @@ class TestLinks:
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
node3 = session.add_node(SwitchNode) node3 = session.add_node(SwitchNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface1, _ = session.add_link(node1.id, node2.id, iface1_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
assert iface1 assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
with pytest.raises(CoreError): with pytest.raises(CoreError):
session.delete_link(node1.id, node3.id) session.delete_link(node1.id, node3.id, iface1.id, iface2.id)
def test_delete_net_to_node_error(self, session: Session, ip_prefixes: IpPrefixes): def test_delete_net_to_node_error(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -343,12 +392,14 @@ class TestLinks:
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
node3 = session.add_node(SwitchNode) node3 = session.add_node(SwitchNode)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
_, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
assert iface2 assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
with pytest.raises(CoreError): with pytest.raises(CoreError):
session.delete_link(node1.id, node3.id) session.delete_link(node1.id, node3.id, iface1.id, iface2.id)
def test_delete_node_to_node_error(self, session: Session, ip_prefixes: IpPrefixes): def test_delete_node_to_node_error(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -358,9 +409,10 @@ class TestLinks:
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
assert iface1 assert len(session.link_manager.links()) == 1
assert iface2 assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
with pytest.raises(CoreError): with pytest.raises(CoreError):
session.delete_link(node1.id, node3.id) session.delete_link(node1.id, node3.id, iface1.id, iface2.id)

View file

@ -1,6 +1,6 @@
import pytest import pytest
from core.emulator.data import InterfaceData, NodeOptions from core.emulator.data import InterfaceData
from core.emulator.session import Session from core.emulator.session import Session
from core.errors import CoreError from core.errors import CoreError
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
@ -14,7 +14,8 @@ class TestNodes:
@pytest.mark.parametrize("model", MODELS) @pytest.mark.parametrize("model", MODELS)
def test_node_add(self, session: Session, model: str): def test_node_add(self, session: Session, model: str):
# given # given
options = NodeOptions(model=model) options = CoreNode.create_options()
options.model = model
# when # when
node = session.add_node(CoreNode, options=options) node = session.add_node(CoreNode, options=options)
@ -60,6 +61,40 @@ class TestNodes:
with pytest.raises(CoreError): with pytest.raises(CoreError):
session.get_node(node.id, CoreNode) session.get_node(node.id, CoreNode)
def test_node_add_iface(self, session: Session):
# given
node = session.add_node(CoreNode)
# when
iface = node.create_iface()
# then
assert iface.id in node.ifaces
def test_node_get_iface(self, session: Session):
# given
node = session.add_node(CoreNode)
iface = node.create_iface()
assert iface.id in node.ifaces
# when
iface2 = node.get_iface(iface.id)
# then
assert iface == iface2
def test_node_delete_iface(self, session: Session):
# given
node = session.add_node(CoreNode)
iface = node.create_iface()
assert iface.id in node.ifaces
# when
node.delete_iface(iface.id)
# then
assert iface.id not in node.ifaces
@pytest.mark.parametrize( @pytest.mark.parametrize(
"mac,expected", "mac,expected",
[ [
@ -70,12 +105,11 @@ class TestNodes:
def test_node_set_mac(self, session: Session, mac: str, expected: str): def test_node_set_mac(self, session: Session, mac: str, expected: str):
# given # given
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
switch = session.add_node(SwitchNode)
iface_data = InterfaceData() iface_data = InterfaceData()
iface = node.new_iface(switch, iface_data) iface = node.create_iface(iface_data)
# when # when
node.set_mac(iface.node_id, mac) iface.set_mac(mac)
# then # then
assert str(iface.mac) == expected assert str(iface.mac) == expected
@ -86,13 +120,12 @@ class TestNodes:
def test_node_set_mac_exception(self, session: Session, mac: str): def test_node_set_mac_exception(self, session: Session, mac: str):
# given # given
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
switch = session.add_node(SwitchNode)
iface_data = InterfaceData() iface_data = InterfaceData()
iface = node.new_iface(switch, iface_data) iface = node.create_iface(iface_data)
# when # when
with pytest.raises(CoreError): with pytest.raises(CoreError):
node.set_mac(iface.node_id, mac) iface.set_mac(mac)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"ip,expected,is_ip6", "ip,expected,is_ip6",
@ -106,12 +139,11 @@ class TestNodes:
def test_node_add_ip(self, session: Session, ip: str, expected: str, is_ip6: bool): def test_node_add_ip(self, session: Session, ip: str, expected: str, is_ip6: bool):
# given # given
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
switch = session.add_node(SwitchNode)
iface_data = InterfaceData() iface_data = InterfaceData()
iface = node.new_iface(switch, iface_data) iface = node.create_iface(iface_data)
# when # when
node.add_ip(iface.node_id, ip) iface.add_ip(ip)
# then # then
if is_ip6: if is_ip6:
@ -122,14 +154,13 @@ class TestNodes:
def test_node_add_ip_exception(self, session): def test_node_add_ip_exception(self, session):
# given # given
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
switch = session.add_node(SwitchNode)
iface_data = InterfaceData() iface_data = InterfaceData()
iface = node.new_iface(switch, iface_data) iface = node.create_iface(iface_data)
ip = "256.168.0.1/24" ip = "256.168.0.1/24"
# when # when
with pytest.raises(CoreError): with pytest.raises(CoreError):
node.add_ip(iface.node_id, ip) iface.add_ip(ip)
@pytest.mark.parametrize("net_type", NET_TYPES) @pytest.mark.parametrize("net_type", NET_TYPES)
def test_net(self, session, net_type): def test_net(self, session, net_type):

View file

@ -53,7 +53,7 @@ class TestServices:
total_service = len(node.services) total_service = len(node.services)
# when # when
session.services.add_services(node, node.type, [SERVICE_ONE, SERVICE_TWO]) session.services.add_services(node, node.model, [SERVICE_ONE, SERVICE_TWO])
# then # then
assert node.services assert node.services

View file

@ -4,13 +4,13 @@ from xml.etree import ElementTree
import pytest import pytest
from core.emulator.data import IpPrefixes, LinkOptions, NodeOptions from core.emulator.data import IpPrefixes, LinkOptions
from core.emulator.enumerations import EventTypes from core.emulator.enumerations import EventTypes
from core.emulator.session import Session from core.emulator.session import Session
from core.errors import CoreError from core.errors import CoreError
from core.location.mobility import BasicRangeModel from core.location.mobility import BasicRangeModel
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
from core.nodes.network import PtpNet, SwitchNode, WlanNode from core.nodes.network import SwitchNode, WlanNode
from core.services.utility import SshService from core.services.utility import SshService
@ -65,25 +65,18 @@ class TestXml:
:param tmpdir: tmpdir to create data in :param tmpdir: tmpdir to create data in
:param ip_prefixes: generates ip addresses for nodes :param ip_prefixes: generates ip addresses for nodes
""" """
# create ptp
ptp_node = session.add_node(PtpNet)
# create nodes # create nodes
node1 = session.add_node(CoreNode) node1 = session.add_node(CoreNode)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
# link nodes to ptp net # link nodes
for node in [node1, node2]: iface1_data = ip_prefixes.create_iface(node1)
iface_data = ip_prefixes.create_iface(node) iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node.id, ptp_node.id, iface1_data=iface_data) session.add_link(node1.id, node2.id, iface1_data, iface2_data)
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = node1.id
node2_id = node2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -98,16 +91,19 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, CoreNode) assert not session.get_node(node2.id, CoreNode)
# verify no links are known
assert len(session.link_manager.links()) == 0
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# verify nodes have been recreated # verify nodes have been recreated
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, CoreNode) assert session.get_node(node2.id, CoreNode)
assert len(session.link_manager.links()) == 1
def test_xml_ptp_services( def test_xml_ptp_services(
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@ -119,18 +115,14 @@ class TestXml:
:param tmpdir: tmpdir to create data in :param tmpdir: tmpdir to create data in
:param ip_prefixes: generates ip addresses for nodes :param ip_prefixes: generates ip addresses for nodes
""" """
# create ptp
ptp_node = session.add_node(PtpNet)
# create nodes # create nodes
options = NodeOptions(model="host") node1 = session.add_node(CoreNode)
node1 = session.add_node(CoreNode, options=options)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
# link nodes to ptp net # link nodes to ptp net
for node in [node1, node2]: iface1_data = ip_prefixes.create_iface(node1)
iface_data = ip_prefixes.create_iface(node) iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node.id, ptp_node.id, iface1_data=iface_data) session.add_link(node1.id, node2.id, iface1_data, iface2_data)
# set custom values for node service # set custom values for node service
session.services.set_service(node1.id, SshService.name) session.services.set_service(node1.id, SshService.name)
@ -143,10 +135,6 @@ class TestXml:
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = node1.id
node2_id = node2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -161,9 +149,9 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, CoreNode) assert not session.get_node(node2.id, CoreNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
@ -172,8 +160,8 @@ class TestXml:
service = session.services.get_service(node1.id, SshService.name) service = session.services.get_service(node1.id, SshService.name)
# verify nodes have been recreated # verify nodes have been recreated
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, CoreNode) assert session.get_node(node2.id, CoreNode)
assert service.config_data.get(service_file) == file_data assert service.config_data.get(service_file) == file_data
def test_xml_mobility( def test_xml_mobility(
@ -187,28 +175,23 @@ class TestXml:
:param ip_prefixes: generates ip addresses for nodes :param ip_prefixes: generates ip addresses for nodes
""" """
# create wlan # create wlan
wlan_node = session.add_node(WlanNode) wlan = session.add_node(WlanNode)
session.mobility.set_model(wlan_node, BasicRangeModel, {"test": "1"}) session.mobility.set_model(wlan, BasicRangeModel, {"test": "1"})
# create nodes # create nodes
options = NodeOptions(model="mdr") options = CoreNode.create_options()
options.set_position(0, 0) options.model = "mdr"
node1 = session.add_node(CoreNode, options=options) node1 = session.add_node(CoreNode, options=options)
node2 = session.add_node(CoreNode, options=options) node2 = session.add_node(CoreNode, options=options)
# link nodes # link nodes
for node in [node1, node2]: for node in [node1, node2]:
iface_data = ip_prefixes.create_iface(node) iface_data = ip_prefixes.create_iface(node)
session.add_link(node.id, wlan_node.id, iface1_data=iface_data) session.add_link(node.id, wlan.id, iface1_data=iface_data)
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
wlan_id = wlan_node.id
node1_id = node1.id
node2_id = node2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -223,20 +206,20 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, CoreNode) assert not session.get_node(node2.id, CoreNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# retrieve configuration we set originally # retrieve configuration we set originally
value = str(session.mobility.get_config("test", wlan_id, BasicRangeModel.name)) value = str(session.mobility.get_config("test", wlan.id, BasicRangeModel.name))
# verify nodes and configuration were restored # verify nodes and configuration were restored
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, CoreNode) assert session.get_node(node2.id, CoreNode)
assert session.get_node(wlan_id, WlanNode) assert session.get_node(wlan.id, WlanNode)
assert value == "1" assert value == "1"
def test_network_to_network(self, session: Session, tmpdir: TemporaryFile): def test_network_to_network(self, session: Session, tmpdir: TemporaryFile):
@ -256,10 +239,6 @@ class TestXml:
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = switch1.id
node2_id = switch2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -274,19 +253,19 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, SwitchNode) assert not session.get_node(switch1.id, SwitchNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, SwitchNode) assert not session.get_node(switch2.id, SwitchNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# verify nodes have been recreated # verify nodes have been recreated
switch1 = session.get_node(node1_id, SwitchNode) switch1 = session.get_node(switch1.id, SwitchNode)
switch2 = session.get_node(node2_id, SwitchNode) switch2 = session.get_node(switch2.id, SwitchNode)
assert switch1 assert switch1
assert switch2 assert switch2
assert len(switch1.links() + switch2.links()) == 1 assert len(session.link_manager.links()) == 1
def test_link_options( def test_link_options(
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@ -316,10 +295,6 @@ class TestXml:
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = node1.id
node2_id = switch.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -334,27 +309,25 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, SwitchNode) assert not session.get_node(switch.id, SwitchNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# verify nodes have been recreated # verify nodes have been recreated
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, SwitchNode) assert session.get_node(switch.id, SwitchNode)
links = [] assert len(session.link_manager.links()) == 1
for node_id in session.nodes: link = list(session.link_manager.links())[0]
node = session.nodes[node_id] link_options = link.options()
links += node.links() assert options.loss == link_options.loss
link = links[0] assert options.bandwidth == link_options.bandwidth
assert options.loss == link.options.loss assert options.jitter == link_options.jitter
assert options.bandwidth == link.options.bandwidth assert options.delay == link_options.delay
assert options.jitter == link.options.jitter assert options.dup == link_options.dup
assert options.delay == link.options.delay assert options.buffer == link_options.buffer
assert options.dup == link.options.dup
assert options.buffer == link.options.buffer
def test_link_options_ptp( def test_link_options_ptp(
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@ -385,10 +358,6 @@ class TestXml:
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = node1.id
node2_id = node2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -403,27 +372,25 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, CoreNode) assert not session.get_node(node2.id, CoreNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# verify nodes have been recreated # verify nodes have been recreated
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, CoreNode) assert session.get_node(node2.id, CoreNode)
links = [] assert len(session.link_manager.links()) == 1
for node_id in session.nodes: link = list(session.link_manager.links())[0]
node = session.nodes[node_id] link_options = link.options()
links += node.links() assert options.loss == link_options.loss
link = links[0] assert options.bandwidth == link_options.bandwidth
assert options.loss == link.options.loss assert options.jitter == link_options.jitter
assert options.bandwidth == link.options.bandwidth assert options.delay == link_options.delay
assert options.jitter == link.options.jitter assert options.dup == link_options.dup
assert options.delay == link.options.delay assert options.buffer == link_options.buffer
assert options.dup == link.options.dup
assert options.buffer == link.options.buffer
def test_link_options_bidirectional( def test_link_options_bidirectional(
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@ -450,7 +417,9 @@ class TestXml:
options1.dup = 5 options1.dup = 5
options1.jitter = 5 options1.jitter = 5
options1.buffer = 50 options1.buffer = 50
session.add_link(node1.id, node2.id, iface1_data, iface2_data, options1) iface1, iface2 = session.add_link(
node1.id, node2.id, iface1_data, iface2_data, options1
)
options2 = LinkOptions() options2 = LinkOptions()
options2.unidirectional = 1 options2.unidirectional = 1
options2.bandwidth = 10000 options2.bandwidth = 10000
@ -459,17 +428,11 @@ class TestXml:
options2.dup = 10 options2.dup = 10
options2.jitter = 10 options2.jitter = 10
options2.buffer = 100 options2.buffer = 100
session.update_link( session.update_link(node2.id, node1.id, iface2.id, iface1.id, options2)
node2.id, node1.id, iface2_data.id, iface1_data.id, options2
)
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = node1.id
node2_id = node2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -484,32 +447,26 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, CoreNode) assert not session.get_node(node2.id, CoreNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# verify nodes have been recreated # verify nodes have been recreated
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, CoreNode) assert session.get_node(node2.id, CoreNode)
links = [] assert len(session.link_manager.links()) == 1
for node_id in session.nodes: assert options1.bandwidth == iface1.options.bandwidth
node = session.nodes[node_id] assert options1.delay == iface1.options.delay
links += node.links() assert options1.loss == iface1.options.loss
assert len(links) == 2 assert options1.dup == iface1.options.dup
link1 = links[0] assert options1.jitter == iface1.options.jitter
link2 = links[1] assert options1.buffer == iface1.options.buffer
assert options1.bandwidth == link1.options.bandwidth assert options2.bandwidth == iface2.options.bandwidth
assert options1.delay == link1.options.delay assert options2.delay == iface2.options.delay
assert options1.loss == link1.options.loss assert options2.loss == iface2.options.loss
assert options1.dup == link1.options.dup assert options2.dup == iface2.options.dup
assert options1.jitter == link1.options.jitter assert options2.jitter == iface2.options.jitter
assert options1.buffer == link1.options.buffer assert options2.buffer == iface2.options.buffer
assert options2.bandwidth == link2.options.bandwidth
assert options2.delay == link2.options.delay
assert options2.loss == link2.options.loss
assert options2.dup == link2.options.dup
assert options2.jitter == link2.options.jitter
assert options2.buffer == link2.options.buffer

View file

@ -0,0 +1,61 @@
# syntax=docker/dockerfile:1
FROM centos:7
LABEL Description="CORE Docker CentOS Image"
# define variables
ARG PREFIX=/usr
ARG BRANCH=master
# define environment
ENV DEBIAN_FRONTEND=noninteractive
ENV LANG en_US.UTF-8
# install basic dependencies
RUN yum -y update && \
yum install -y git sudo wget tzdata unzip
# install python3.9
WORKDIR /opt
RUN wget https://www.python.org/ftp/python/3.9.15/Python-3.9.15.tgz
RUN tar xf Python-3.9.15.tgz
RUN yum install -y make && yum-builddep -y python3
RUN cd Python-3.9.15 && \
./configure --enable-optimizations --with-ensurepip=install && \
make -j$(nproc) altinstall
RUN python3.9 -m pip install --upgrade pip
# install core
WORKDIR /opt
RUN git clone https://github.com/coreemu/core
WORKDIR /opt/core
RUN git checkout ${BRANCH}
RUN PYTHON=/usr/local/bin/python3.9 ./setup.sh
RUN . /root/.bashrc && PYTHON=/usr/local/bin/python3.9 inv install -v -p ${PREFIX} --no-python
ENV PATH "$PATH:/opt/core/venv/bin"
# install emane
RUN yum install -y libpcap-devel libpcre3-devel libxml2-devel protobuf-devel unzip uuid-devel
WORKDIR /opt
RUN wget -q https://adjacentlink.com/downloads/emane/emane-1.3.3-release-1.el7.x86_64.tar.gz && \
tar xf emane-1.3.3-release-1.el7.x86_64.tar.gz && \
cd emane-1.3.3-release-1/rpms/el7/x86_64 && \
yum install -y epel-release && \
yum install -y ./openstatistic*.rpm ./emane*.rpm ./python3-emane_*.rpm && \
cd ../../../.. && \
rm emane-1.3.3-release-1.el7.x86_64.tar.gz && \
rm -rf emane-1.3.3-release-1
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.7.1/protoc-3.7.1-linux-x86_64.zip && \
mkdir protoc && \
unzip protoc-3.7.1-linux-x86_64.zip -d protoc
RUN git clone https://github.com/adjacentlink/emane.git
RUN PATH=/opt/protoc/bin:$PATH && \
cd emane && \
git checkout v1.3.3 && \
./autogen.sh && \
PYTHON=/opt/core/venv/bin/python ./configure --prefix=/usr && \
cd src/python && \
make && \
/opt/core/venv/bin/python -m pip install .
# run daemon
CMD ["core-daemon"]

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