Merge pull request #712 from coreemu/develop
merge develop for 9.0.0 release
This commit is contained in:
commit
5ab71377cc
386 changed files with 6440 additions and 53764 deletions
6
.github/workflows/daemon-checks.yml
vendored
6
.github/workflows/daemon-checks.yml
vendored
|
@ -4,13 +4,13 @@ on: [push]
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.6
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.6
|
||||
python-version: 3.9
|
||||
- name: install poetry
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -14,6 +14,7 @@ config.h.in
|
|||
config.log
|
||||
config.status
|
||||
configure
|
||||
configure~
|
||||
debian
|
||||
stamp-h1
|
||||
|
||||
|
@ -58,3 +59,6 @@ daemon/setup.py
|
|||
|
||||
# python
|
||||
__pycache__
|
||||
|
||||
# ignore core player files
|
||||
*.core
|
||||
|
|
43
CHANGELOG.md
43
CHANGELOG.md
|
@ -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
|
||||
|
||||
* core-gui
|
||||
|
|
100
Dockerfile
100
Dockerfile
|
@ -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"]
|
87
Makefile.am
87
Makefile.am
|
@ -6,10 +6,6 @@ if WANT_DOCS
|
|||
DOCS = docs man
|
||||
endif
|
||||
|
||||
if WANT_GUI
|
||||
GUI = gui
|
||||
endif
|
||||
|
||||
if WANT_DAEMON
|
||||
DAEMON = daemon
|
||||
endif
|
||||
|
@ -19,12 +15,13 @@ if WANT_NETNS
|
|||
endif
|
||||
|
||||
# keep docs last due to dependencies on binaries
|
||||
SUBDIRS = $(GUI) $(DAEMON) $(NETNS) $(DOCS)
|
||||
SUBDIRS = $(DAEMON) $(NETNS) $(DOCS)
|
||||
|
||||
ACLOCAL_AMFLAGS = -I config
|
||||
|
||||
# extra files to include with distribution tarball
|
||||
EXTRA_DIST = bootstrap.sh \
|
||||
package \
|
||||
LICENSE \
|
||||
README.md \
|
||||
ASSIGNMENT_OF_COPYRIGHT.pdf \
|
||||
|
@ -51,7 +48,7 @@ fpm -s dir -t deb -n core-distributed \
|
|||
--description "Common Open Research Emulator Distributed Package" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_distributed_VERSION_ARCH.deb \
|
||||
-p core-distributed_VERSION_ARCH.deb \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
-d "ethtool" \
|
||||
-d "procps" \
|
||||
|
@ -62,7 +59,8 @@ fpm -s dir -t deb -n core-distributed \
|
|||
-d "libev4" \
|
||||
-d "openssh-server" \
|
||||
-d "xterm" \
|
||||
-C $(DESTDIR)
|
||||
netns/vnoded=/usr/bin/ \
|
||||
netns/vcmd=/usr/bin/
|
||||
endef
|
||||
|
||||
define fpm-distributed-rpm =
|
||||
|
@ -72,7 +70,7 @@ fpm -s dir -t rpm -n core-distributed \
|
|||
--description "Common Open Research Emulator Distributed Package" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_distributed_VERSION_ARCH.rpm \
|
||||
-p core-distributed_VERSION_ARCH.rpm \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
-d "ethtool" \
|
||||
-d "procps-ng" \
|
||||
|
@ -83,12 +81,75 @@ fpm -s dir -t rpm -n core-distributed \
|
|||
-d "net-tools" \
|
||||
-d "openssh-server" \
|
||||
-d "xterm" \
|
||||
-C $(DESTDIR)
|
||||
netns/vnoded=/usr/bin/ \
|
||||
netns/vcmd=/usr/bin/
|
||||
endef
|
||||
|
||||
.PHONY: fpm-distributed
|
||||
fpm-distributed: clean-local-fpm
|
||||
$(MAKE) -C netns install DESTDIR=$(DESTDIR)
|
||||
define fpm-rpm =
|
||||
fpm -s dir -t rpm -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.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-rpm)
|
||||
|
||||
|
@ -115,7 +176,6 @@ $(info creating file $1 from $1.in)
|
|||
-e 's,[@]CORE_STATE_DIR[@],$(CORE_STATE_DIR),g' \
|
||||
-e 's,[@]CORE_DATA_DIR[@],$(CORE_DATA_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
|
||||
endef
|
||||
|
||||
|
@ -123,7 +183,6 @@ all: change-files
|
|||
|
||||
.PHONY: change-files
|
||||
change-files:
|
||||
$(call change-files,gui/core-gui-legacy)
|
||||
$(call change-files,daemon/core/constants.py)
|
||||
$(call change-files,netns/setup.py)
|
||||
|
||||
|
|
31
README.md
31
README.md
|
@ -1,5 +1,4 @@
|
|||
# CORE
|
||||
|
||||
CORE: Common Open Research Emulator
|
||||
|
||||
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.
|
||||
|
||||
## About
|
||||
|
||||
The Common Open Research Emulator (CORE) is a tool for emulating
|
||||
networks on one or more machines. You can connect these emulated
|
||||
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.
|
||||
|
||||
## 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+
|
||||
from a clean install, it will prompt you for sudo password. This would
|
||||
### Package Install
|
||||
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
|
||||
[OSPF MDR](https://github.com/USNavalResearchLaboratory/ospf-mdr) from source.
|
||||
For more detailed installation see [here](https://coreemu.github.io/core/install.html).
|
||||
|
||||
```shell
|
||||
git clone https://github.com/coreemu/core.git
|
||||
|
@ -36,7 +56,6 @@ inv install -p /usr
|
|||
```
|
||||
|
||||
## Documentation & Support
|
||||
|
||||
We are leveraging GitHub hosted documentation and Discord for persistent
|
||||
chat rooms. This allows for more dynamic conversations and the
|
||||
capability to respond faster. Feel free to join us at the link below.
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# (c)2010-2012 the Boeing Company
|
||||
#
|
||||
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
|
||||
#
|
||||
# Bootstrap the autoconf system.
|
||||
#
|
||||
|
||||
|
|
68
configure.ac
68
configure.ac
|
@ -2,7 +2,7 @@
|
|||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
# this defines the CORE version number, must be static for AC_INIT
|
||||
AC_INIT(core, 8.2.0)
|
||||
AC_INIT(core, 9.0.0)
|
||||
|
||||
# autoconf and automake initialization
|
||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||
|
@ -30,25 +30,14 @@ AC_SUBST(CORE_CONF_DIR)
|
|||
AC_SUBST(CORE_DATA_DIR)
|
||||
AC_SUBST(CORE_STATE_DIR)
|
||||
|
||||
# CORE GUI configuration files and preferences in CORE_GUI_CONF_DIR
|
||||
# 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)
|
||||
# documentation option
|
||||
AC_ARG_ENABLE([docs],
|
||||
[AS_HELP_STRING([--enable-docs[=ARG]],
|
||||
[build python documentation (default is no)])],
|
||||
[], [enable_docs=no])
|
||||
AC_SUBST(enable_docs)
|
||||
|
||||
# python option
|
||||
AC_ARG_ENABLE([python],
|
||||
[AS_HELP_STRING([--enable-python[=ARG]],
|
||||
[build and install the python bindings (default is yes)])],
|
||||
|
@ -94,28 +83,7 @@ if test "x$enable_daemon" = "xyes"; then
|
|||
want_python=yes
|
||||
want_linux_netns=yes
|
||||
|
||||
# Checks for libraries.
|
||||
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)
|
||||
AM_PATH_PYTHON(3.9)
|
||||
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)
|
||||
|
@ -171,6 +139,25 @@ fi
|
|||
|
||||
if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; then
|
||||
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,
|
||||
AC_MSG_RESULT([found libev using pkgconfig OK])
|
||||
AC_SUBST(libev_CFLAGS)
|
||||
|
@ -209,7 +196,6 @@ if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then
|
|||
fi
|
||||
|
||||
# Variable substitutions
|
||||
AM_CONDITIONAL(WANT_GUI, test x$enable_gui = xyes)
|
||||
AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes)
|
||||
AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes)
|
||||
AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes)
|
||||
|
@ -224,9 +210,6 @@ fi
|
|||
|
||||
# Output files
|
||||
AC_CONFIG_FILES([Makefile
|
||||
gui/version.tcl
|
||||
gui/Makefile
|
||||
gui/icons/Makefile
|
||||
man/Makefile
|
||||
docs/Makefile
|
||||
daemon/Makefile
|
||||
|
@ -248,17 +231,12 @@ Build:
|
|||
Prefix: ${prefix}
|
||||
Exec Prefix: ${exec_prefix}
|
||||
|
||||
GUI:
|
||||
GUI path: ${CORE_LIB_DIR}
|
||||
GUI config: ${CORE_GUI_CONF_DIR}
|
||||
|
||||
Daemon:
|
||||
Daemon path: ${bindir}
|
||||
Daemon config: ${CORE_CONF_DIR}
|
||||
Python: ${PYTHON}
|
||||
|
||||
Features to build:
|
||||
Build GUI: ${enable_gui}
|
||||
Build Daemon: ${enable_daemon}
|
||||
Documentation: ${want_docs}
|
||||
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
# 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.
|
||||
#
|
||||
|
@ -25,10 +21,7 @@ DISTCLEANFILES = Makefile.in
|
|||
|
||||
# files to include with distribution tarball
|
||||
EXTRA_DIST = core \
|
||||
data \
|
||||
doc/conf.py.in \
|
||||
examples \
|
||||
scripts \
|
||||
tests \
|
||||
setup.cfg \
|
||||
poetry.lock \
|
||||
|
|
|
@ -14,9 +14,17 @@ import grpc
|
|||
from core.api.grpc import core_pb2, core_pb2_grpc, emane_pb2, wrappers
|
||||
from core.api.grpc.configservices_pb2 import (
|
||||
GetConfigServiceDefaultsRequest,
|
||||
GetConfigServiceRenderedRequest,
|
||||
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 (
|
||||
EmaneLinkRequest,
|
||||
GetEmaneEventChannelRequest,
|
||||
|
@ -43,17 +51,19 @@ from core.api.grpc.wlan_pb2 import (
|
|||
WlanConfig,
|
||||
WlanLinkRequest,
|
||||
)
|
||||
from core.api.grpc.wrappers import LinkOptions
|
||||
from core.emulator.data import IpPrefixes
|
||||
from core.errors import CoreError
|
||||
from core.utils import SetQueue
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MoveNodesStreamer:
|
||||
def __init__(self, session_id: int = None, source: str = None) -> None:
|
||||
self.session_id = session_id
|
||||
self.source = source
|
||||
self.queue: Queue = Queue()
|
||||
def __init__(self, session_id: int, source: str = None) -> None:
|
||||
self.session_id: int = session_id
|
||||
self.source: Optional[str] = source
|
||||
self.queue: SetQueue = SetQueue()
|
||||
|
||||
def send_position(self, node_id: int, x: float, y: float) -> None:
|
||||
position = wrappers.Position(x=x, y=y)
|
||||
|
@ -563,23 +573,6 @@ class CoreGrpcClient:
|
|||
response = self.stub.GetNodeTerminal(request)
|
||||
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(
|
||||
self, session_id: int, link: wrappers.Link, source: str = None
|
||||
) -> Tuple[bool, wrappers.Interface, wrappers.Interface]:
|
||||
|
@ -741,9 +734,9 @@ class CoreGrpcClient:
|
|||
:raises grpc.RpcError: when session doesn't exist
|
||||
"""
|
||||
defaults = []
|
||||
for node_type in service_defaults:
|
||||
services = service_defaults[node_type]
|
||||
default = ServiceDefaults(node_type=node_type, services=services)
|
||||
for model in service_defaults:
|
||||
services = service_defaults[model]
|
||||
default = ServiceDefaults(model=model, services=services)
|
||||
defaults.append(default)
|
||||
request = SetServiceDefaultsRequest(session_id=session_id, defaults=defaults)
|
||||
response = self.stub.SetServiceDefaults(request)
|
||||
|
@ -987,6 +980,23 @@ class CoreGrpcClient:
|
|||
response = self.stub.GetNodeConfigService(request)
|
||||
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(
|
||||
self, session_id: int, nem_id: int
|
||||
) -> wrappers.EmaneEventChannel:
|
||||
|
@ -1049,6 +1059,81 @@ class CoreGrpcClient:
|
|||
"""
|
||||
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:
|
||||
"""
|
||||
Open connection to server, must be closed manually.
|
||||
|
|
|
@ -3,7 +3,7 @@ from queue import Empty, Queue
|
|||
from typing import Iterable, Optional
|
||||
|
||||
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 (
|
||||
ConfigData,
|
||||
EventData,
|
||||
|
@ -33,7 +33,7 @@ def handle_node_event(node_data: NodeData) -> core_pb2.Event:
|
|||
node_proto = core_pb2.Node(
|
||||
id=node.id,
|
||||
name=node.name,
|
||||
model=node.type,
|
||||
model=node.model,
|
||||
icon=node.icon,
|
||||
position=position,
|
||||
geo=geo,
|
||||
|
@ -51,7 +51,7 @@ def handle_link_event(link_data: LinkData) -> core_pb2.Event:
|
|||
:param link_data: link data
|
||||
: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
|
||||
link_event = core_pb2.LinkEvent(message_type=message_type, link=link)
|
||||
return core_pb2.Event(link_event=link_event, source=link_data.source)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
import time
|
||||
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
|
||||
from grpc import ServicerContext
|
||||
|
@ -17,17 +17,26 @@ from core.api.grpc.services_pb2 import (
|
|||
ServiceDefaults,
|
||||
)
|
||||
from core.config import ConfigurableOptions
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
|
||||
from core.emane.nodes import EmaneNet, EmaneOptions
|
||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
||||
from core.emulator.enumerations import LinkTypes, NodeTypes
|
||||
from core.emulator.links import CoreLink
|
||||
from core.emulator.session import Session
|
||||
from core.errors import CoreError
|
||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
||||
from core.nodes.docker import DockerNode
|
||||
from core.nodes.base import (
|
||||
CoreNode,
|
||||
CoreNodeBase,
|
||||
CoreNodeOptions,
|
||||
NodeBase,
|
||||
NodeOptions,
|
||||
Position,
|
||||
)
|
||||
from core.nodes.docker import DockerNode, DockerOptions
|
||||
from core.nodes.interface import CoreInterface
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -53,34 +62,33 @@ class CpuUsage:
|
|||
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.
|
||||
|
||||
:param _class: node class to create options from
|
||||
:param node_proto: node proto message
|
||||
:return: node type, id, and options
|
||||
"""
|
||||
_id = node_proto.id
|
||||
_type = NodeTypes(node_proto.type)
|
||||
options = NodeOptions(
|
||||
name=node_proto.name,
|
||||
model=node_proto.model,
|
||||
icon=node_proto.icon,
|
||||
image=node_proto.image,
|
||||
services=node_proto.services,
|
||||
config_services=node_proto.config_services,
|
||||
canvas=node_proto.canvas,
|
||||
)
|
||||
if node_proto.emane:
|
||||
options.emane = node_proto.emane
|
||||
if node_proto.server:
|
||||
options.server = node_proto.server
|
||||
position = node_proto.position
|
||||
options.set_position(position.x, position.y)
|
||||
options = _class.create_options()
|
||||
options.icon = node_proto.icon
|
||||
options.canvas = node_proto.canvas
|
||||
if isinstance(options, CoreNodeOptions):
|
||||
options.model = node_proto.model
|
||||
options.services = node_proto.services
|
||||
options.config_services = node_proto.config_services
|
||||
if isinstance(options, EmaneOptions):
|
||||
options.emane_model = node_proto.emane
|
||||
if isinstance(options, DockerOptions):
|
||||
options.image = node_proto.image
|
||||
position = Position()
|
||||
position.set(node_proto.position.x, node_proto.position.y)
|
||||
if node_proto.HasField("geo"):
|
||||
geo = node_proto.geo
|
||||
options.set_location(geo.lat, geo.lon, geo.alt)
|
||||
return _type, _id, options
|
||||
position.set_geo(geo.lon, geo.lat, geo.alt)
|
||||
return position, options
|
||||
|
||||
|
||||
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(
|
||||
link_proto: core_pb2.Link
|
||||
) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]:
|
||||
) -> Tuple[InterfaceData, InterfaceData, LinkOptions]:
|
||||
"""
|
||||
Convert link proto to link interfaces and options data.
|
||||
|
||||
|
@ -119,7 +127,6 @@ def add_link_data(
|
|||
"""
|
||||
iface1_data = link_iface(link_proto.iface1)
|
||||
iface2_data = link_iface(link_proto.iface2)
|
||||
link_type = LinkTypes(link_proto.type)
|
||||
options = LinkOptions()
|
||||
options_proto = link_proto.options
|
||||
if options_proto:
|
||||
|
@ -134,7 +141,7 @@ def add_link_data(
|
|||
options.buffer = options_proto.buffer
|
||||
options.unidirectional = options_proto.unidirectional
|
||||
options.key = options_proto.key
|
||||
return iface1_data, iface2_data, options, link_type
|
||||
return iface1_data, iface2_data, options
|
||||
|
||||
|
||||
def create_nodes(
|
||||
|
@ -149,9 +156,17 @@ def create_nodes(
|
|||
"""
|
||||
funcs = []
|
||||
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)
|
||||
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, {}))
|
||||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
|
@ -174,8 +189,8 @@ def create_links(
|
|||
for link_proto in link_protos:
|
||||
node1_id = link_proto.node1_id
|
||||
node2_id = link_proto.node2_id
|
||||
iface1, iface2, options, link_type = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1, iface2, options, link_type)
|
||||
iface1, iface2, options = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1, iface2, options)
|
||||
funcs.append((session.add_link, args, {}))
|
||||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
|
@ -198,8 +213,8 @@ def edit_links(
|
|||
for link_proto in link_protos:
|
||||
node1_id = link_proto.node1_id
|
||||
node2_id = link_proto.node2_id
|
||||
iface1, iface2, options, link_type = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type)
|
||||
iface1, iface2, options = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1.id, iface2.id, options)
|
||||
funcs.append((session.update_link, args, {}))
|
||||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
|
@ -220,6 +235,22 @@ def convert_value(value: Any) -> str:
|
|||
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(
|
||||
config: Dict[str, str],
|
||||
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
|
||||
)
|
||||
services = [x.name for x in node.services]
|
||||
model = node.type
|
||||
node_dir = None
|
||||
config_services = []
|
||||
if isinstance(node, CoreNodeBase):
|
||||
|
@ -281,7 +311,7 @@ def get_node_proto(
|
|||
channel = str(node.ctrlchnlname)
|
||||
emane_model = None
|
||||
if isinstance(node, EmaneNet):
|
||||
emane_model = node.model.name
|
||||
emane_model = node.wireless_model.name
|
||||
image = None
|
||||
if isinstance(node, (DockerNode, LxcNode)):
|
||||
image = node.image
|
||||
|
@ -291,6 +321,21 @@ def get_node_proto(
|
|||
)
|
||||
if wlan_config:
|
||||
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
|
||||
mobility_config = session.mobility.get_configs(
|
||||
node.id, config_type=Ns2ScriptedMobility.name
|
||||
|
@ -325,7 +370,7 @@ def get_node_proto(
|
|||
id=node.id,
|
||||
name=node.name,
|
||||
emane=emane_model,
|
||||
model=model,
|
||||
model=node.model,
|
||||
type=node_type.value,
|
||||
position=position,
|
||||
geo=geo,
|
||||
|
@ -337,6 +382,7 @@ def get_node_proto(
|
|||
channel=channel,
|
||||
canvas=node.canvas,
|
||||
wlan_config=wlan_config,
|
||||
wireless_config=wireless_config,
|
||||
mobility_config=mobility_config,
|
||||
service_configs=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.
|
||||
|
||||
:param session: session to get links for node
|
||||
:param node: node to get links from
|
||||
: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 = []
|
||||
for link in node.links():
|
||||
link_proto = convert_link(link)
|
||||
links.append(link_proto)
|
||||
node1, iface1 = core_link.node1, core_link.iface1
|
||||
node2, iface2 = core_link.node2, core_link.iface2
|
||||
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
|
||||
|
||||
|
||||
def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface:
|
||||
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:
|
||||
def convert_link_data(link_data: LinkData) -> core_pb2.Link:
|
||||
"""
|
||||
Convert link_data into core protobuf link.
|
||||
|
||||
:param link_data: link to convert
|
||||
:return: core protobuf Link
|
||||
"""
|
||||
iface1 = None
|
||||
if link_data.iface1 is not None:
|
||||
iface1 = convert_iface(link_data.iface1)
|
||||
iface1 = convert_iface_data(link_data.iface1)
|
||||
iface2 = 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)
|
||||
return core_pb2.Link(
|
||||
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]:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
with open("/proc/net/dev", "r") as f:
|
||||
data = f.readlines()[2:]
|
||||
|
||||
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
|
||||
lines = f.readlines()[2:]
|
||||
return parse_proc_net_dev(lines)
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
def iface_to_proto(session: Session, iface: CoreInterface) -> core_pb2.Interface:
|
||||
"""
|
||||
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
|
||||
: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 = str(ip4_net.ip) 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_mask = ip6_net.prefixlen if ip6_net 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(
|
||||
id=_id,
|
||||
net_id=net_id,
|
||||
net2_id=net2_id,
|
||||
node_id=node_id,
|
||||
id=iface.id,
|
||||
name=iface.name,
|
||||
mac=mac,
|
||||
mtu=iface.mtu,
|
||||
|
@ -543,6 +696,8 @@ def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
|
|||
ip4_mask=ip4_mask,
|
||||
ip6=ip6,
|
||||
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]]:
|
||||
"""
|
||||
Get emane model configuration protobuf data.
|
||||
|
||||
:param session: session to get emane model configuration for
|
||||
:return: dict of emane model protobuf configurations
|
||||
"""
|
||||
configs = {}
|
||||
for _id, model_configs in session.emane.node_configs.items():
|
||||
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]:
|
||||
"""
|
||||
Retrieve hook protobuf data for a session.
|
||||
|
||||
:param session: session to get hooks for
|
||||
:return: list of hook protobufs
|
||||
"""
|
||||
hooks = []
|
||||
for state in session.hooks:
|
||||
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]:
|
||||
"""
|
||||
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 = []
|
||||
for name, services in session.services.default_services.items():
|
||||
default_service = ServiceDefaults(node_type=name, services=services)
|
||||
for model, services in session.services.default_services.items():
|
||||
default_service = ServiceDefaults(model=model, services=services)
|
||||
default_services.append(default_service)
|
||||
return default_services
|
||||
|
||||
|
@ -611,6 +784,14 @@ def get_default_services(session: Session) -> List[ServiceDefaults]:
|
|||
def get_mobility_node(
|
||||
session: Session, node_id: int, context: ServicerContext
|
||||
) -> 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:
|
||||
return session.get_node(node_id, WlanNode)
|
||||
except CoreError:
|
||||
|
@ -621,17 +802,26 @@ def get_mobility_node(
|
|||
|
||||
|
||||
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)
|
||||
nodes = []
|
||||
links = []
|
||||
for _id in session.nodes:
|
||||
node = session.nodes[_id]
|
||||
if not isinstance(node, (PtpNet, CtrlNet)):
|
||||
node_emane_configs = emane_configs.get(node.id, [])
|
||||
node_proto = get_node_proto(session, node, node_emane_configs)
|
||||
nodes.append(node_proto)
|
||||
node_links = get_links(node)
|
||||
links.extend(node_links)
|
||||
if isinstance(node, (WlanNode, EmaneNet)):
|
||||
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)
|
||||
x, y, z = session.location.refxyz
|
||||
lat, lon, alt = session.location.refgeo
|
||||
|
@ -640,7 +830,7 @@ def convert_session(session: Session) -> wrappers.Session:
|
|||
)
|
||||
hooks = get_hooks(session)
|
||||
session_file = str(session.file_path) if session.file_path else None
|
||||
options = get_config_options(session.options.get_configs(), session.options)
|
||||
options = convert_session_options(session)
|
||||
servers = [
|
||||
core_pb2.Server(name=x.name, host=x.host)
|
||||
for x in session.distributed.servers.values()
|
||||
|
@ -665,6 +855,15 @@ def convert_session(session: Session) -> wrappers.Session:
|
|||
def configure_node(
|
||||
session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext
|
||||
) -> 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:
|
||||
_id = utils.iface_config_id(node.id, emane_config.iface_id)
|
||||
config = {k: v.value for k, v in emane_config.config.items()}
|
||||
|
@ -675,6 +874,9 @@ def configure_node(
|
|||
if node.mobility_config:
|
||||
config = {k: v.value for k, v in node.mobility_config.items()}
|
||||
session.mobility.set_model_config(node.id, Ns2ScriptedMobility.name, config)
|
||||
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():
|
||||
data = service_config.data
|
||||
config = ServiceConfig(
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from concurrent import futures
|
||||
|
@ -23,10 +24,22 @@ from core.api.grpc.configservices_pb2 import (
|
|||
ConfigService,
|
||||
GetConfigServiceDefaultsRequest,
|
||||
GetConfigServiceDefaultsResponse,
|
||||
GetConfigServiceRenderedRequest,
|
||||
GetConfigServiceRenderedResponse,
|
||||
GetNodeConfigServiceRequest,
|
||||
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 (
|
||||
EmaneLinkRequest,
|
||||
EmaneLinkResponse,
|
||||
|
@ -79,19 +92,20 @@ from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
|||
from core.emulator.enumerations import (
|
||||
EventTypes,
|
||||
ExceptionLevels,
|
||||
LinkTypes,
|
||||
MessageFlags,
|
||||
NodeTypes,
|
||||
)
|
||||
from core.emulator.session import NT, Session
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_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
|
||||
|
||||
|
||||
|
@ -107,11 +121,20 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
self.coreemu: CoreEmu = coreemu
|
||||
self.running: bool = True
|
||||
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:
|
||||
logger.debug("catching exit, stop running")
|
||||
def _signal_handler(self, signal_number: int, _) -> None:
|
||||
logger.info("caught signal: %s", signal_number)
|
||||
self.coreemu.shutdown()
|
||||
self.running = False
|
||||
if self.server:
|
||||
self.server.stop(None)
|
||||
sys.exit(signal_number)
|
||||
|
||||
def _is_running(self, context) -> bool:
|
||||
return self.running and context.is_active()
|
||||
|
@ -248,18 +271,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
|
||||
# clear previous state and setup for creation
|
||||
session.clear()
|
||||
session.directory.mkdir(exist_ok=True)
|
||||
if request.definition:
|
||||
state = EventTypes.DEFINITION_STATE
|
||||
else:
|
||||
state = EventTypes.CONFIGURATION_STATE
|
||||
session.directory.mkdir(exist_ok=True)
|
||||
session.set_state(state)
|
||||
session.user = request.session.user
|
||||
if request.session.user:
|
||||
session.set_user(request.session.user)
|
||||
|
||||
# session options
|
||||
session.options.config_reset()
|
||||
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)
|
||||
|
||||
# add servers
|
||||
|
@ -378,11 +401,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
self, request: core_pb2.GetSessionsRequest, context: ServicerContext
|
||||
) -> 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
|
||||
:return: a delete-session response
|
||||
:return: a get sessions response
|
||||
"""
|
||||
logger.debug("get sessions: %s", request)
|
||||
sessions = []
|
||||
|
@ -469,7 +492,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
while self._is_running(context):
|
||||
now = time.monotonic()
|
||||
stats = get_net_stats()
|
||||
|
||||
# calculate average
|
||||
if last_check is not None:
|
||||
interval = now - last_check
|
||||
|
@ -486,7 +508,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
(current_rxtx["tx"] - previous_rxtx["tx"]) * 8.0 / interval
|
||||
)
|
||||
throughput = rx_kbps + tx_kbps
|
||||
if key.startswith("veth"):
|
||||
if key.startswith("beth"):
|
||||
key = key.split(".")
|
||||
node_id = _INTERFACE_REGEX.search(key[0]).group("node")
|
||||
node_id = int(node_id, base=16)
|
||||
|
@ -512,7 +534,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
bridge_throughput.throughput = throughput
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
yield throughputs_event
|
||||
|
||||
last_check = now
|
||||
|
@ -540,9 +561,17 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logger.debug("add node: %s", request)
|
||||
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)
|
||||
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)
|
||||
source = request.source if request.source else None
|
||||
session.broadcast_node(node, MessageFlags.ADD, source)
|
||||
|
@ -564,12 +593,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
ifaces = []
|
||||
for iface_id in node.ifaces:
|
||||
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)
|
||||
emane_configs = grpcutils.get_emane_model_configs_dict(session)
|
||||
node_emane_configs = emane_configs.get(node.id, [])
|
||||
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)
|
||||
|
||||
def MoveNode(
|
||||
|
@ -705,18 +734,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
node2_id = request.link.node2_id
|
||||
self.get_node(session, node1_id, context, NodeBase)
|
||||
self.get_node(session, node2_id, context, NodeBase)
|
||||
iface1_data, iface2_data, options, link_type = grpcutils.add_link_data(
|
||||
request.link
|
||||
)
|
||||
iface1_data, iface2_data, options = grpcutils.add_link_data(request.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
|
||||
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
|
||||
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
|
||||
link_data = LinkData(
|
||||
message_type=MessageFlags.ADD,
|
||||
|
@ -731,9 +764,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
iface1_proto = None
|
||||
iface2_proto = None
|
||||
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:
|
||||
iface2_proto = grpcutils.iface_to_proto(node2_id, node2_iface)
|
||||
iface2_proto = grpcutils.iface_to_proto(session, node2_iface)
|
||||
return core_pb2.AddLinkResponse(
|
||||
result=True, iface1=iface1_proto, iface2=iface2_proto
|
||||
)
|
||||
|
@ -912,7 +945,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
session.services.default_services.clear()
|
||||
for service_defaults in request.defaults:
|
||||
session.services.default_services[
|
||||
service_defaults.node_type
|
||||
service_defaults.model
|
||||
] = service_defaults.services
|
||||
return SetServiceDefaultsResponse(result=True)
|
||||
|
||||
|
@ -1163,7 +1196,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
self, request: core_pb2.GetInterfacesRequest, context: ServicerContext
|
||||
) -> 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 context: context object
|
||||
|
@ -1188,32 +1222,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logger.debug("emane link: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
nem1 = request.nem1
|
||||
iface1 = session.emane.get_iface(nem1)
|
||||
if not iface1:
|
||||
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,
|
||||
)
|
||||
flag = MessageFlags.ADD if request.linked else MessageFlags.DELETE
|
||||
link = session.emane.get_nem_link(request.nem1, request.nem2, flag)
|
||||
if link:
|
||||
session.broadcast_link(link)
|
||||
return EmaneLinkResponse(result=True)
|
||||
else:
|
||||
|
@ -1240,6 +1251,27 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
config = {x.id: x.default for x in service.default_configs}
|
||||
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(
|
||||
self, request: GetConfigServiceDefaultsRequest, context: ServicerContext
|
||||
) -> GetConfigServiceDefaultsResponse:
|
||||
|
@ -1299,18 +1331,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
) -> WlanLinkResponse:
|
||||
session = self.get_session(request.session_id, context)
|
||||
wlan = self.get_node(session, request.wlan, context, WlanNode)
|
||||
if not isinstance(wlan.model, BasicRangeModel):
|
||||
if not isinstance(wlan.wireless_model, BasicRangeModel):
|
||||
context.abort(
|
||||
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)
|
||||
node2 = self.get_node(session, request.node2_id, context, CoreNode)
|
||||
node1_iface, node2_iface = None, None
|
||||
for net, iface1, iface2 in node1.commonnets(node2):
|
||||
if net == wlan:
|
||||
node1_iface = iface1
|
||||
node2_iface = iface2
|
||||
for iface in node1.get_ifaces(control=False):
|
||||
if iface.net == wlan:
|
||||
node1_iface = iface
|
||||
break
|
||||
for iface in node2.get_ifaces(control=False):
|
||||
if iface.net == wlan:
|
||||
node2_iface = iface
|
||||
break
|
||||
result = False
|
||||
if node1_iface and node2_iface:
|
||||
|
@ -1318,7 +1353,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
wlan.link(node1_iface, node2_iface)
|
||||
else:
|
||||
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
|
||||
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)
|
||||
session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
|
||||
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)
|
||||
|
|
|
@ -67,6 +67,7 @@ class NodeType(Enum):
|
|||
CONTROL_NET = 13
|
||||
DOCKER = 15
|
||||
LXC = 16
|
||||
WIRELESS = 17
|
||||
|
||||
|
||||
class LinkType(Enum):
|
||||
|
@ -209,12 +210,12 @@ class Service:
|
|||
|
||||
@dataclass
|
||||
class ServiceDefault:
|
||||
node_type: str
|
||||
model: str
|
||||
services: List[str]
|
||||
|
||||
@classmethod
|
||||
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
|
||||
|
@ -480,6 +481,8 @@ class Interface:
|
|||
mtu: int = None
|
||||
node_id: int = None
|
||||
net2_id: int = None
|
||||
nem_id: int = None
|
||||
nem_port: int = None
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: core_pb2.Interface) -> "Interface":
|
||||
|
@ -496,6 +499,8 @@ class Interface:
|
|||
mtu=proto.mtu,
|
||||
node_id=proto.node_id,
|
||||
net2_id=proto.net2_id,
|
||||
nem_id=proto.nem_id,
|
||||
nem_port=proto.nem_port,
|
||||
)
|
||||
|
||||
def to_proto(self) -> core_pb2.Interface:
|
||||
|
@ -736,6 +741,7 @@ class Node:
|
|||
Tuple[str, Optional[int]], 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)
|
||||
service_configs: Dict[str, NodeServiceData] = field(
|
||||
default_factory=dict, repr=False
|
||||
|
@ -770,7 +776,7 @@ class Node:
|
|||
id=proto.id,
|
||||
name=proto.name,
|
||||
type=NodeType(proto.type),
|
||||
model=proto.model,
|
||||
model=proto.model or None,
|
||||
position=Position.from_proto(proto.position),
|
||||
services=set(proto.services),
|
||||
config_services=set(proto.config_services),
|
||||
|
@ -788,6 +794,7 @@ class Node:
|
|||
service_file_configs=service_file_configs,
|
||||
config_service_configs=config_service_configs,
|
||||
emane_model_configs=emane_configs,
|
||||
wireless_config=ConfigOption.from_dict(proto.wireless_config),
|
||||
)
|
||||
|
||||
def to_proto(self) -> core_pb2.Node:
|
||||
|
@ -839,6 +846,7 @@ class Node:
|
|||
service_configs=service_configs,
|
||||
config_service_configs=config_service_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:
|
||||
|
@ -883,9 +891,7 @@ class 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}
|
||||
links = [Link.from_proto(x) for x in proto.links]
|
||||
default_services = {
|
||||
x.node_type: set(x.services) for x in proto.default_services
|
||||
}
|
||||
default_services = {x.model: set(x.services) for x in proto.default_services}
|
||||
hooks = {x.file: Hook.from_proto(x) for x in proto.hooks}
|
||||
file_path = Path(proto.file) if proto.file else None
|
||||
options = ConfigOption.from_dict(proto.options)
|
||||
|
@ -913,9 +919,9 @@ class Session:
|
|||
options = {k: v.to_proto() for k, v in self.options.items()}
|
||||
servers = [x.to_proto() for x in self.servers]
|
||||
default_services = []
|
||||
for node_type, services in self.default_services.items():
|
||||
for model, services in self.default_services.items():
|
||||
default_service = services_pb2.ServiceDefaults(
|
||||
node_type=node_type, services=services
|
||||
model=model, services=services
|
||||
)
|
||||
default_services.append(default_service)
|
||||
file = str(self.file) if self.file else None
|
||||
|
@ -1102,7 +1108,6 @@ class ConfigEvent:
|
|||
data_types=list(proto.data_types),
|
||||
data_values=proto.data_values,
|
||||
captions=proto.captions,
|
||||
bitmap=proto.bitmap,
|
||||
possible_values=proto.possible_values,
|
||||
groups=proto.groups,
|
||||
iface_id=proto.iface_id,
|
||||
|
@ -1194,13 +1199,13 @@ class EmanePathlossesRequest:
|
|||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class MoveNodesRequest:
|
||||
session_id: int
|
||||
node_id: int
|
||||
source: str = None
|
||||
position: Position = None
|
||||
geo: Geo = None
|
||||
source: str = field(compare=False, default=None)
|
||||
position: Position = field(compare=False, default=None)
|
||||
geo: Geo = field(compare=False, default=None)
|
||||
|
||||
def to_proto(self) -> core_pb2.MoveNodesRequest:
|
||||
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
|
@ -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()
|
|
@ -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,
|
||||
)
|
|
@ -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
|
|
@ -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
|
|
@ -44,6 +44,7 @@ class Configuration:
|
|||
label: str = None
|
||||
default: str = ""
|
||||
options: List[str] = field(default_factory=list)
|
||||
group: str = "Configuration"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.label = self.label if self.label else self.id
|
||||
|
@ -78,6 +79,7 @@ class ConfigBool(Configuration):
|
|||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.BOOL
|
||||
value: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -87,6 +89,7 @@ class ConfigFloat(Configuration):
|
|||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.FLOAT
|
||||
value: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -96,6 +99,7 @@ class ConfigInt(Configuration):
|
|||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.INT32
|
||||
value: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -105,6 +109,7 @@ class ConfigString(Configuration):
|
|||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.STRING
|
||||
value: str = ""
|
||||
|
||||
|
||||
class ConfigurableOptions:
|
||||
|
@ -113,7 +118,6 @@ class ConfigurableOptions:
|
|||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
bitmap: Optional[str] = None
|
||||
options: List[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -331,17 +331,15 @@ class ConfigService(abc.ABC):
|
|||
templates[file] = template
|
||||
return templates
|
||||
|
||||
def create_files(self) -> None:
|
||||
"""
|
||||
Creates service files inside associated node.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
def get_rendered_templates(self) -> Dict[str, str]:
|
||||
templates = {}
|
||||
data = self.data()
|
||||
for file in sorted(self.files):
|
||||
logger.debug(
|
||||
"node(%s) service(%s) template(%s)", self.node.name, self.name, file
|
||||
)
|
||||
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:
|
||||
|
@ -358,6 +356,21 @@ class ConfigService(abc.ABC):
|
|||
f"failure getting template: {e}"
|
||||
)
|
||||
rendered = self.render_text(text, data)
|
||||
return rendered
|
||||
|
||||
def create_files(self) -> None:
|
||||
"""
|
||||
Creates service files inside associated node.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
data = self.data()
|
||||
for file in sorted(self.files):
|
||||
logger.debug(
|
||||
"node(%s) service(%s) template(%s)", self.node.name, self.name, file
|
||||
)
|
||||
rendered = self._get_rendered_template(file, data)
|
||||
file_path = Path(file)
|
||||
self.node.create_file(file_path, rendered)
|
||||
|
||||
def run_startup(self, wait: bool) -> None:
|
||||
|
@ -459,7 +472,7 @@ class ConfigService(abc.ABC):
|
|||
except Exception:
|
||||
raise CoreError(
|
||||
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:
|
||||
|
|
|
@ -4,14 +4,26 @@ from typing import Any, Dict, List
|
|||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
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.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"
|
||||
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:
|
||||
"""
|
||||
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"
|
||||
|
||||
|
||||
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):
|
||||
name: str = "FRRzebra"
|
||||
group: str = GROUP
|
||||
|
@ -74,10 +100,10 @@ class FRRZebra(ConfigService):
|
|||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
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"
|
||||
).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"
|
||||
).strip('"')
|
||||
|
||||
|
@ -158,7 +184,7 @@ class FRROspfv2(FrrService, ConfigService):
|
|||
addresses = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
addresses.append(str(ip4.ip))
|
||||
addresses.append(str(ip4))
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
|
@ -166,15 +192,31 @@ class FRROspfv2(FrrService, ConfigService):
|
|||
% for addr in addresses:
|
||||
network ${addr} area 0
|
||||
% endfor
|
||||
ospf opaque-lsa
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
if has_mtu_mismatch(iface):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
has_mtu = has_mtu_mismatch(iface)
|
||||
has_rj45 = rj45_check(iface)
|
||||
is_ptp = isinstance(iface.net, PtpNet)
|
||||
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):
|
||||
|
@ -324,7 +366,7 @@ class FRRBabel(FrrService, ConfigService):
|
|||
return self.render_text(text, data)
|
||||
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
if is_wireless(iface.net):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
|
|
|
@ -48,6 +48,10 @@ bootdaemon()
|
|||
flags="$flags -6"
|
||||
fi
|
||||
|
||||
if [ "$1" = "ospfd" ]; then
|
||||
flags="$flags --apiserver"
|
||||
fi
|
||||
|
||||
#force FRR to use CORE generated conf file
|
||||
flags="$flags -d -f $FRR_CONF"
|
||||
$FRR_SBIN_DIR/$1 $flags
|
||||
|
|
|
@ -66,7 +66,6 @@ class NrlSmf(ConfigService):
|
|||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_arouted = "arouted" in self.node.config_services
|
||||
has_nhdp = "NHDP" in self.node.config_services
|
||||
has_olsr = "OLSR" in self.node.config_services
|
||||
ifnames = []
|
||||
|
@ -78,11 +77,7 @@ class NrlSmf(ConfigService):
|
|||
ip4_prefix = f"{ip4.ip}/{24}"
|
||||
break
|
||||
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
|
||||
default_configs: List[Configuration] = []
|
||||
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)
|
||||
|
|
|
@ -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 &
|
|
@ -1,8 +1,5 @@
|
|||
<%
|
||||
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:
|
||||
flood = "ecds"
|
||||
elif has_olsr:
|
||||
|
@ -12,4 +9,4 @@
|
|||
%>
|
||||
#!/bin/sh
|
||||
# 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 &
|
||||
|
|
|
@ -5,16 +5,27 @@ from typing import Any, Dict, List
|
|||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
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.network import PtpNet, WlanNode
|
||||
from core.nodes.physical import Rj45Node
|
||||
from core.nodes.wireless import WirelessNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
GROUP: str = "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:
|
||||
"""
|
||||
Helper to detect MTU mismatch and add the appropriate OSPF
|
||||
|
@ -89,10 +100,10 @@ class Zebra(ConfigService):
|
|||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
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"
|
||||
).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"
|
||||
).strip('"')
|
||||
quagga_state_dir = QUAGGA_STATE_DIR
|
||||
|
@ -265,7 +276,7 @@ class Ospfv3mdr(Ospfv3):
|
|||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
config = super().quagga_iface_config(iface)
|
||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
if is_wireless(iface.net):
|
||||
config = self.clean_text(
|
||||
f"""
|
||||
{config}
|
||||
|
@ -295,9 +306,6 @@ class Bgp(QuaggaService, ConfigService):
|
|||
ipv6_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
return ""
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
text = f"""
|
||||
! BGP configuration
|
||||
|
@ -311,6 +319,9 @@ class Bgp(QuaggaService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class Rip(QuaggaService, ConfigService):
|
||||
"""
|
||||
|
@ -390,7 +401,7 @@ class Babel(QuaggaService, ConfigService):
|
|||
return self.render_text(text, data)
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
if is_wireless(iface.net):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
|
|
|
@ -12,12 +12,12 @@ from core import utils
|
|||
from core.emane.emanemodel import EmaneModel
|
||||
from core.emane.linkmonitor import EmaneLinkMonitor
|
||||
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.enumerations import LinkTypes, MessageFlags, RegisterTlvs
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode, NodeBase
|
||||
from core.nodes.interface import CoreInterface, TunTap
|
||||
from core.nodes.base import CoreNode, NodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.xml import emanexml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -45,8 +45,6 @@ except ImportError:
|
|||
EventServiceException = None
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
DEFAULT_EMANE_PREFIX = "/usr"
|
||||
DEFAULT_DEV = "ctrl0"
|
||||
DEFAULT_LOG_LEVEL: int = 3
|
||||
|
||||
|
||||
|
@ -133,10 +131,10 @@ class EmaneManager:
|
|||
self._emane_nets: Dict[int, EmaneNet] = {}
|
||||
self._emane_node_lock: threading.Lock = threading.Lock()
|
||||
# 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
|
||||
)
|
||||
self.transformport: int = self.session.options.get_config_int(
|
||||
self.transformport: int = self.session.options.get_int(
|
||||
"emane_transform_port", 8200
|
||||
)
|
||||
self.doeventloop: bool = False
|
||||
|
@ -153,7 +151,7 @@ class EmaneManager:
|
|||
self.nem_service: Dict[int, EmaneEventService] = {}
|
||||
|
||||
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:
|
||||
nem_id += 1
|
||||
self.nems_to_ifaces[nem_id] = iface
|
||||
|
@ -223,11 +221,9 @@ class EmaneManager:
|
|||
:param iface: interface running emane
|
||||
:return: net, node, or interface model configuration
|
||||
"""
|
||||
model_name = emane_net.model.name
|
||||
config = None
|
||||
model_name = emane_net.wireless_model.name
|
||||
# try to retrieve interface specific configuration
|
||||
if iface.node_id is not None:
|
||||
key = utils.iface_config_id(iface.node.id, iface.node_id)
|
||||
key = utils.iface_config_id(iface.node.id, iface.id)
|
||||
config = self.get_config(key, model_name, default=False)
|
||||
# attempt to retrieve node specific config, when iface config is not present
|
||||
if not config:
|
||||
|
@ -239,7 +235,7 @@ class EmaneManager:
|
|||
config = self.get_config(emane_net.id, model_name, default=False)
|
||||
# return default config values, when a config is not present
|
||||
if not config:
|
||||
config = emane_net.model.default_values()
|
||||
config = emane_net.wireless_model.default_values()
|
||||
return config
|
||||
|
||||
def config_reset(self, node_id: int = None) -> None:
|
||||
|
@ -272,6 +268,7 @@ class EmaneManager:
|
|||
nodes = set()
|
||||
for emane_net in self._emane_nets.values():
|
||||
for iface in emane_net.get_ifaces():
|
||||
if isinstance(iface.node, CoreNode):
|
||||
nodes.add(iface.node)
|
||||
return nodes
|
||||
|
||||
|
@ -323,7 +320,7 @@ class EmaneManager:
|
|||
for emane_net, iface in self.get_ifaces():
|
||||
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_port = self.get_nem_port(iface)
|
||||
logger.info(
|
||||
|
@ -338,10 +335,10 @@ class EmaneManager:
|
|||
self.start_daemon(iface)
|
||||
self.install_iface(iface, config)
|
||||
|
||||
def get_ifaces(self) -> List[Tuple[EmaneNet, CoreInterface]]:
|
||||
def get_ifaces(self) -> List[Tuple[EmaneNet, TunTap]]:
|
||||
ifaces = []
|
||||
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)
|
||||
continue
|
||||
for iface in emane_net.get_ifaces():
|
||||
|
@ -352,8 +349,9 @@ class EmaneManager:
|
|||
iface.name,
|
||||
)
|
||||
continue
|
||||
if isinstance(iface, TunTap):
|
||||
ifaces.append((emane_net, iface))
|
||||
return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].node_id))
|
||||
return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].id))
|
||||
|
||||
def setup_control_channels(
|
||||
self, nem_id: int, iface: CoreInterface, config: Dict[str, str]
|
||||
|
@ -384,6 +382,8 @@ class EmaneManager:
|
|||
service = EmaneEventService(
|
||||
self, event_net.brname, eventgroup, int(eventport)
|
||||
)
|
||||
if self.doeventmonitor():
|
||||
service.start()
|
||||
self.services[event_net.brname] = service
|
||||
self.nem_service[nem_id] = service
|
||||
except EventServiceException:
|
||||
|
@ -484,7 +484,7 @@ class EmaneManager:
|
|||
logger.exception("error writing to emane nem file")
|
||||
|
||||
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:
|
||||
"""
|
||||
|
@ -498,7 +498,7 @@ class EmaneManager:
|
|||
"post startup for emane node: %s - %s", emane_net.id, emane_net.name
|
||||
)
|
||||
for iface in emane_net.get_ifaces():
|
||||
emane_net.model.post_startup(iface)
|
||||
emane_net.wireless_model.post_startup(iface)
|
||||
if events_enabled:
|
||||
iface.setposition()
|
||||
|
||||
|
@ -550,9 +550,11 @@ class EmaneManager:
|
|||
emane_net = self._emane_nets[node_id]
|
||||
logger.debug("checking emane model for node: %s", node_id)
|
||||
# skip nodes that already have a model set
|
||||
if emane_net.model:
|
||||
if emane_net.wireless_model:
|
||||
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
|
||||
# set model configured for node, due to legacy messaging configuration
|
||||
|
@ -602,8 +604,8 @@ class EmaneManager:
|
|||
"""
|
||||
node = iface.node
|
||||
loglevel = str(DEFAULT_LOG_LEVEL)
|
||||
cfgloglevel = self.session.options.get_config_int("emane_log_level")
|
||||
realtime = self.session.options.get_config_bool("emane_realtime", default=True)
|
||||
cfgloglevel = self.session.options.get_int("emane_log_level", 2)
|
||||
realtime = self.session.options.get_bool("emane_realtime", True)
|
||||
if cfgloglevel:
|
||||
logger.info("setting user-defined emane log level: %d", cfgloglevel)
|
||||
loglevel = str(cfgloglevel)
|
||||
|
@ -622,9 +624,9 @@ class EmaneManager:
|
|||
args = f"{emanecmd} -f {log_file} {platform_xml}"
|
||||
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")
|
||||
if isinstance(iface, TunTap) and external == "0":
|
||||
if external == "0":
|
||||
iface.set_ips()
|
||||
# at this point we register location handlers for generating
|
||||
# EMANE location events
|
||||
|
@ -636,20 +638,13 @@ class EmaneManager:
|
|||
"""
|
||||
Returns boolean whether or not EMANE events will be monitored.
|
||||
"""
|
||||
# this support must be explicitly turned on; by default, CORE will
|
||||
# generate the EMANE events when nodes are moved
|
||||
return self.session.options.get_config_bool("emane_event_monitor")
|
||||
return self.session.options.get_bool("emane_event_monitor", False)
|
||||
|
||||
def genlocationevents(self) -> bool:
|
||||
"""
|
||||
Returns boolean whether or not EMANE events will be generated.
|
||||
"""
|
||||
# By default, CORE generates EMANE location events when nodes
|
||||
# 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
|
||||
return self.session.options.get_bool("emane_event_generate", True)
|
||||
|
||||
def handlelocationevent(self, rxnemid: int, eid: int, data: str) -> None:
|
||||
"""
|
||||
|
@ -732,9 +727,6 @@ class EmaneManager:
|
|||
self.session.broadcast_node(node)
|
||||
return True
|
||||
|
||||
def is_emane_net(self, net: Optional[CoreNetworkBase]) -> bool:
|
||||
return isinstance(net, EmaneNet)
|
||||
|
||||
def emanerunning(self, node: CoreNode) -> bool:
|
||||
"""
|
||||
Return True if an EMANE process associated with the given node is running,
|
||||
|
|
|
@ -190,9 +190,9 @@ class EmaneLinkMonitor:
|
|||
|
||||
def start(self) -> None:
|
||||
options = self.emane_manager.session.options
|
||||
self.loss_threshold = options.get_config_int("loss_threshold")
|
||||
self.link_interval = options.get_config_int("link_interval")
|
||||
self.link_timeout = options.get_config_int("link_timeout")
|
||||
self.loss_threshold = options.get_int("loss_threshold")
|
||||
self.link_interval = options.get_int("link_interval")
|
||||
self.link_timeout = options.get_int("link_timeout")
|
||||
self.initialize()
|
||||
if not self.clients:
|
||||
logger.info("no valid emane models to monitor links")
|
||||
|
|
|
@ -4,19 +4,15 @@ share the same MAC+PHY model.
|
|||
"""
|
||||
|
||||
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.distributed import DistributedServer
|
||||
from core.emulator.enumerations import (
|
||||
EventTypes,
|
||||
LinkTypes,
|
||||
MessageFlags,
|
||||
NodeTypes,
|
||||
RegisterTlvs,
|
||||
)
|
||||
from core.errors import CoreError
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||
from core.emulator.enumerations import EventTypes, MessageFlags, RegisterTlvs
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode, NodeOptions
|
||||
from core.nodes.interface import CoreInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -24,10 +20,7 @@ logger = logging.getLogger(__name__)
|
|||
if TYPE_CHECKING:
|
||||
from core.emane.emanemodel import EmaneModel
|
||||
from core.emulator.session import Session
|
||||
from core.location.mobility import WirelessModel, WayPointMobility
|
||||
|
||||
OptionalEmaneModel = Optional[EmaneModel]
|
||||
WirelessModelType = Type[WirelessModel]
|
||||
from core.location.mobility import WayPointMobility
|
||||
|
||||
try:
|
||||
from emane.events import LocationEvent
|
||||
|
@ -39,6 +32,120 @@ except ImportError:
|
|||
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):
|
||||
"""
|
||||
EMANE node contains NEM configuration and causes connected nodes
|
||||
|
@ -46,22 +153,26 @@ class EmaneNet(CoreNetworkBase):
|
|||
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__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
server: DistributedServer = None,
|
||||
options: EmaneOptions = None,
|
||||
) -> None:
|
||||
super().__init__(session, _id, name, server)
|
||||
options = options or EmaneOptions()
|
||||
super().__init__(session, _id, name, server, options)
|
||||
self.conf: str = ""
|
||||
self.model: "OptionalEmaneModel" = 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(
|
||||
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||
|
@ -69,18 +180,15 @@ class EmaneNet(CoreNetworkBase):
|
|||
"""
|
||||
The CommEffect model supports link configuration.
|
||||
"""
|
||||
if not self.model:
|
||||
if not self.wireless_model:
|
||||
return
|
||||
self.model.linkconfig(iface, options, iface2)
|
||||
|
||||
def config(self, conf: str) -> None:
|
||||
self.conf = conf
|
||||
self.wireless_model.linkconfig(iface, options, iface2)
|
||||
|
||||
def startup(self) -> None:
|
||||
pass
|
||||
self.up = True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
pass
|
||||
self.up = False
|
||||
|
||||
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||
pass
|
||||
|
@ -88,30 +196,37 @@ class EmaneNet(CoreNetworkBase):
|
|||
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||
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:
|
||||
if not self.model:
|
||||
raise CoreError(f"no model set to update for node({self.name})")
|
||||
logger.info("node(%s) updating model(%s): %s", self.id, self.model.name, config)
|
||||
self.model.update_config(config)
|
||||
"""
|
||||
Update configuration for the current model.
|
||||
|
||||
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
|
||||
"""
|
||||
if model.config_type == RegisterTlvs.WIRELESS:
|
||||
# EmaneModel really uses values from ConfigurableManager
|
||||
# when buildnemxml() is called, not during init()
|
||||
self.model = model(session=self.session, _id=self.id)
|
||||
self.model.update_config(config)
|
||||
self.wireless_model = model(session=self.session, _id=self.id)
|
||||
self.wireless_model.update_config(config)
|
||||
elif model.config_type == RegisterTlvs.MOBILITY:
|
||||
self.mobility = model(session=self.session, _id=self.id)
|
||||
self.mobility.update_config(config)
|
||||
|
||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
||||
links = super().links(flags)
|
||||
links = []
|
||||
emane_manager = self.session.emane
|
||||
# gather current emane links
|
||||
nem_ids = set()
|
||||
|
@ -132,22 +247,44 @@ class EmaneNet(CoreNetworkBase):
|
|||
# ignore incomplete links
|
||||
if (nem2, nem1) not in emane_links:
|
||||
continue
|
||||
link = emane_manager.get_nem_link(nem1, nem2)
|
||||
link = emane_manager.get_nem_link(nem1, nem2, flags)
|
||||
if link:
|
||||
links.append(link)
|
||||
return links
|
||||
|
||||
def custom_iface(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
|
||||
# namespace after it has been bound removes addressing;
|
||||
# save addresses with the interface now
|
||||
iface_id = node.newtuntap(iface_data.id, iface_data.name)
|
||||
node.attachnet(iface_id, self)
|
||||
iface = node.get_iface(iface_id)
|
||||
def create_tuntap(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
||||
"""
|
||||
Create a tuntap interface for the provided node.
|
||||
|
||||
:param node: node to create tuntap interface for
|
||||
:param iface_data: interface data to create interface with
|
||||
:return: created tuntap interface
|
||||
"""
|
||||
with node.lock:
|
||||
if iface_data.id is not None and iface_data.id in node.ifaces:
|
||||
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:
|
||||
self.session.emane.start_iface(self, iface)
|
||||
return iface
|
||||
|
||||
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||
raise CoreError(
|
||||
f"emane network({self.name}) do not support adopting interfaces"
|
||||
)
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Type
|
||||
|
||||
|
@ -18,25 +15,6 @@ logger = logging.getLogger(__name__)
|
|||
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:
|
||||
"""
|
||||
Provides logic for creating and configuring CORE sessions and the nodes within them.
|
||||
|
@ -70,9 +48,6 @@ class CoreEmu:
|
|||
# check executables exist on path
|
||||
self._validate_env()
|
||||
|
||||
# catch exit event
|
||||
atexit.register(self.shutdown)
|
||||
|
||||
def _validate_env(self) -> None:
|
||||
"""
|
||||
Validates executables CORE depends on exist on path.
|
||||
|
@ -140,10 +115,8 @@ class CoreEmu:
|
|||
:return: nothing
|
||||
"""
|
||||
logger.info("shutting down all sessions")
|
||||
sessions = self.sessions.copy()
|
||||
self.sessions.clear()
|
||||
for _id in sessions:
|
||||
session = sessions[_id]
|
||||
while self.sessions:
|
||||
_, session = self.sessions.popitem()
|
||||
session.shutdown()
|
||||
|
||||
def create_session(self, _id: int = None, _cls: Type[Session] = Session) -> Session:
|
||||
|
|
|
@ -92,6 +92,10 @@ class NodeOptions:
|
|||
image: str = None
|
||||
emane: str = None
|
||||
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:
|
||||
"""
|
||||
|
|
|
@ -15,6 +15,7 @@ from fabric import Connection
|
|||
from invoke import UnexpectedExit
|
||||
|
||||
from core import utils
|
||||
from core.emulator.links import CoreLink
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.executables import get_requirements
|
||||
from core.nodes.interface import GreTap
|
||||
|
@ -124,9 +125,7 @@ class DistributedController:
|
|||
self.session: "Session" = session
|
||||
self.servers: Dict[str, DistributedServer] = OrderedDict()
|
||||
self.tunnels: Dict[int, Tuple[GreTap, GreTap]] = {}
|
||||
self.address: str = self.session.options.get_config(
|
||||
"distributed_address", default=None
|
||||
)
|
||||
self.address: str = self.session.options.get("distributed_address")
|
||||
|
||||
def add_server(self, name: str, host: str) -> None:
|
||||
"""
|
||||
|
@ -183,21 +182,36 @@ class DistributedController:
|
|||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
Start distributed network tunnels.
|
||||
Start distributed network tunnels for control networks.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
mtu = self.session.options.get_config_int("mtu")
|
||||
mtu = self.session.options.get_int("mtu")
|
||||
for node_id in self.session.nodes:
|
||||
node = self.session.nodes[node_id]
|
||||
if not isinstance(node, CoreNetwork):
|
||||
continue
|
||||
if isinstance(node, CtrlNet) and node.serverintf is not None:
|
||||
if not isinstance(node, CtrlNet) or node.serverintf is not None:
|
||||
continue
|
||||
for name in self.servers:
|
||||
server = self.servers[name]
|
||||
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(
|
||||
self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool
|
||||
) -> Tuple[GreTap, GreTap]:
|
||||
|
|
|
@ -20,6 +20,17 @@ class MessageFlags(Enum):
|
|||
TTY = 0x40
|
||||
|
||||
|
||||
class ConfigFlags(Enum):
|
||||
"""
|
||||
Configuration flags.
|
||||
"""
|
||||
|
||||
NONE = 0x00
|
||||
REQUEST = 0x01
|
||||
UPDATE = 0x02
|
||||
RESET = 0x03
|
||||
|
||||
|
||||
class NodeTypes(Enum):
|
||||
"""
|
||||
Node types.
|
||||
|
@ -38,6 +49,7 @@ class NodeTypes(Enum):
|
|||
CONTROL_NET = 13
|
||||
DOCKER = 15
|
||||
LXC = 16
|
||||
WIRELESS = 17
|
||||
|
||||
|
||||
class LinkTypes(Enum):
|
||||
|
|
256
daemon/core/emulator/links.py
Normal file
256
daemon/core/emulator/links.py
Normal 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
|
@ -1,23 +1,15 @@
|
|||
from typing import Any, List
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from core.config import (
|
||||
ConfigBool,
|
||||
ConfigInt,
|
||||
ConfigString,
|
||||
ConfigurableManager,
|
||||
ConfigurableOptions,
|
||||
Configuration,
|
||||
)
|
||||
from core.emulator.enumerations import RegisterTlvs
|
||||
from core.config import ConfigBool, ConfigInt, ConfigString, Configuration
|
||||
from core.errors import CoreError
|
||||
from core.plugins.sdt import Sdt
|
||||
|
||||
|
||||
class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
||||
class SessionConfig:
|
||||
"""
|
||||
Provides session configuration.
|
||||
"""
|
||||
|
||||
name: str = "session"
|
||||
options: List[Configuration] = [
|
||||
ConfigString(id="controlnet", label="Control Network"),
|
||||
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="mtu", default="0", label="MTU for All Devices"),
|
||||
]
|
||||
config_type: RegisterTlvs = RegisterTlvs.UTILITY
|
||||
|
||||
def __init__(self) -> 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:
|
||||
def __init__(self, config: Dict[str, str] = None) -> None:
|
||||
"""
|
||||
Retrieves a specific configuration for a node and configuration type.
|
||||
Create a SessionConfig instance.
|
||||
|
||||
:param _id: specific configuration to retrieve
|
||||
: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
|
||||
:param config: configuration to initialize with
|
||||
"""
|
||||
value = super().get_config(_id, node_id, config_type, default)
|
||||
if value == "":
|
||||
value = default
|
||||
return value
|
||||
self._config: Dict[str, str] = {x.id: x.default for x in self.options}
|
||||
self._config.update(config or {})
|
||||
|
||||
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.
|
||||
|
||||
|
@ -77,12 +89,15 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
|||
:param default: default value if not found
|
||||
: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:
|
||||
return default
|
||||
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.
|
||||
|
||||
|
@ -90,17 +105,10 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
|||
:param default: default value if not found
|
||||
:return: int for configuration value
|
||||
"""
|
||||
value = self.get_config(name, default=default)
|
||||
if value is not None:
|
||||
value = int(value)
|
||||
return value
|
||||
|
||||
def config_reset(self, node_id: int = None) -> None:
|
||||
"""
|
||||
Clear prior configuration files and reset to default values.
|
||||
|
||||
:param node_id: node id to store configuration for
|
||||
:return: nothing
|
||||
"""
|
||||
super().config_reset(node_id)
|
||||
self.set_configs(self.default_values())
|
||||
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:
|
||||
return default
|
||||
else:
|
||||
return int(value)
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
from typing import List
|
||||
|
||||
BASH: str = "bash"
|
||||
VNODED: str = "vnoded"
|
||||
VCMD: str = "vcmd"
|
||||
SYSCTL: str = "sysctl"
|
||||
IP: str = "ip"
|
||||
ETHTOOL: str = "ethtool"
|
||||
TC: str = "tc"
|
||||
IP: str = "ip"
|
||||
MOUNT: str = "mount"
|
||||
UMOUNT: str = "umount"
|
||||
OVS_VSCTL: str = "ovs-vsctl"
|
||||
TEST: str = "test"
|
||||
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] = [
|
||||
BASH,
|
||||
NFTABLES,
|
||||
ETHTOOL,
|
||||
IP,
|
||||
MOUNT,
|
||||
NFTABLES,
|
||||
SYSCTL,
|
||||
TC,
|
||||
UMOUNT,
|
||||
TEST,
|
||||
UMOUNT,
|
||||
VCMD,
|
||||
VNODED,
|
||||
]
|
||||
VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD]
|
||||
OVS_REQUIREMENTS: List[str] = [OVS_VSCTL]
|
||||
|
||||
|
||||
|
@ -38,6 +39,4 @@ def get_requirements(use_ovs: bool) -> List[str]:
|
|||
requirements = COMMON_REQUIREMENTS
|
||||
if use_ovs:
|
||||
requirements += OVS_REQUIREMENTS
|
||||
else:
|
||||
requirements += VCMD_REQUIREMENTS
|
||||
return requirements
|
||||
|
|
|
@ -70,6 +70,9 @@ class CoreClient:
|
|||
self.session: Optional[Session] = None
|
||||
self.user = getpass.getuser()
|
||||
|
||||
# menu options
|
||||
self.show_throughputs: tk.BooleanVar = tk.BooleanVar(value=False)
|
||||
|
||||
# global service settings
|
||||
self.services: Dict[str, Set[str]] = {}
|
||||
self.config_services_groups: Dict[str, Set[str]] = {}
|
||||
|
@ -242,6 +245,7 @@ class CoreClient:
|
|||
logger.warning("unknown node event: %s", event)
|
||||
|
||||
def enable_throughputs(self) -> None:
|
||||
if not self.handling_throughputs:
|
||||
self.handling_throughputs = self.client.throughputs(
|
||||
self.session.id, self.handle_throughputs
|
||||
)
|
||||
|
@ -404,9 +408,11 @@ class CoreClient:
|
|||
for edge in self.links.values():
|
||||
link = edge.link
|
||||
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()
|
||||
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()
|
||||
links.append(link)
|
||||
if edge.asymmetric_link:
|
||||
|
@ -429,13 +435,15 @@ class CoreClient:
|
|||
definition,
|
||||
result,
|
||||
)
|
||||
if self.show_throughputs.get():
|
||||
self.enable_throughputs()
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Start Session Error", e)
|
||||
return result, exceptions
|
||||
|
||||
def stop_session(self, session_id: int = None) -> bool:
|
||||
if not session_id:
|
||||
session_id = self.session.id
|
||||
session_id = session_id or self.session.id
|
||||
self.cancel_throughputs()
|
||||
result = False
|
||||
try:
|
||||
result = self.client.stop_session(session_id)
|
||||
|
@ -665,10 +673,10 @@ class CoreClient:
|
|||
self.links[edge.token] = edge
|
||||
src_node = edge.src.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
|
||||
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
|
||||
self.iface_to_edge[(dst_node.id, dst_iface_id)] = edge
|
||||
|
||||
|
@ -741,6 +749,9 @@ class CoreClient:
|
|||
configs.append(config)
|
||||
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(
|
||||
self
|
||||
) -> List[configservices_pb2.ConfigServiceConfig]:
|
||||
|
@ -774,6 +785,9 @@ class CoreClient:
|
|||
)
|
||||
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]:
|
||||
config = self.client.get_mobility_config(self.session.id, node_id)
|
||||
logger.debug(
|
||||
|
|
BIN
daemon/core/gui/data/icons/wireless.png
Normal file
BIN
daemon/core/gui/data/icons/wireless.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
|
@ -34,10 +34,10 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.core: "CoreClient" = app.core
|
||||
self.node: Node = node
|
||||
self.service_name: str = service_name
|
||||
self.radiovar: tk.IntVar = tk.IntVar()
|
||||
self.radiovar.set(2)
|
||||
self.radiovar: tk.IntVar = tk.IntVar(value=2)
|
||||
self.directories: List[str] = []
|
||||
self.templates: List[str] = []
|
||||
self.rendered: Dict[str, str] = {}
|
||||
self.dependencies: List[str] = []
|
||||
self.executables: List[str] = []
|
||||
self.startup_commands: List[str] = []
|
||||
|
@ -48,10 +48,9 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.default_shutdown: List[str] = []
|
||||
self.validation_mode: Optional[ServiceValidationMode] = 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.mode_configs: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
self.notebook: Optional[ttk.Notebook] = None
|
||||
self.templates_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_mode_entry: Optional[ttk.Entry] = None
|
||||
self.template_text: Optional[CodeText] = None
|
||||
self.rendered_text: Optional[CodeText] = None
|
||||
self.validation_period_entry: Optional[ttk.Entry] = None
|
||||
self.original_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_time = service.validation_timer
|
||||
self.validation_period.set(service.validation_period)
|
||||
|
||||
defaults = self.core.client.get_config_service_defaults(self.service_name)
|
||||
self.original_service_files = defaults.templates
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
|
@ -95,6 +94,9 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.mode_configs = defaults.modes
|
||||
self.config = ConfigOption.from_dict(defaults.config)
|
||||
self.default_config = {x.name: x.value for x in self.config.values()}
|
||||
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)
|
||||
if service_config:
|
||||
for key, value in service_config.config.items():
|
||||
|
@ -110,7 +112,6 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
# draw notebook
|
||||
self.notebook = ttk.Notebook(self.top)
|
||||
self.notebook.grid(sticky=tk.NSEW, pady=PADY)
|
||||
|
@ -125,6 +126,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
tab.rowconfigure(2, weight=1)
|
||||
self.notebook.add(tab, text="Directories/Files")
|
||||
|
||||
label = ttk.Label(
|
||||
|
@ -137,33 +139,54 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Directories")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
directories_combobox = ttk.Combobox(
|
||||
frame, values=self.directories, state="readonly"
|
||||
)
|
||||
state = "readonly" if self.directories else tk.DISABLED
|
||||
directories_combobox = ttk.Combobox(frame, values=self.directories, state=state)
|
||||
directories_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
if self.directories:
|
||||
directories_combobox.current(0)
|
||||
|
||||
label = ttk.Label(frame, text="Templates")
|
||||
label = ttk.Label(frame, text="Files")
|
||||
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
|
||||
state = "readonly" if self.templates else tk.DISABLED
|
||||
self.templates_combobox = ttk.Combobox(
|
||||
frame, values=self.templates, state="readonly"
|
||||
frame, values=self.templates, state=state
|
||||
)
|
||||
self.templates_combobox.bind(
|
||||
"<<ComboboxSelected>>", self.handle_template_changed
|
||||
)
|
||||
self.templates_combobox.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
self.template_text = CodeText(tab)
|
||||
# draw file template 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)
|
||||
tab.rowconfigure(self.template_text.grid_info()["row"], weight=1)
|
||||
self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||
if self.templates:
|
||||
self.templates_combobox.current(0)
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert(
|
||||
"end", self.temp_service_files[self.templates[0]]
|
||||
)
|
||||
self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||
template_name = self.templates[0]
|
||||
temp_data = self.temp_service_files[template_name]
|
||||
self.template_text.set_text(temp_data)
|
||||
rendered_data = self.rendered[template_name]
|
||||
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:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
|
@ -243,7 +266,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
label = ttk.Label(frame, text="Validation Time")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
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.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
|
@ -323,9 +346,11 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.destroy()
|
||||
|
||||
def handle_template_changed(self, event: tk.Event) -> None:
|
||||
template = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[template])
|
||||
template_name = self.templates_combobox.get()
|
||||
temp_data = self.temp_service_files[template_name]
|
||||
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:
|
||||
mode = self.modes_combobox.get()
|
||||
|
@ -333,10 +358,13 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
logger.info("mode config: %s", config)
|
||||
self.config_frame.set_values(config)
|
||||
|
||||
def update_template_file_data(self, event: tk.Event) -> None:
|
||||
scrolledtext = event.widget
|
||||
def update_template_file_data(self, _event: tk.Event) -> None:
|
||||
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]:
|
||||
self.modified_files.add(template)
|
||||
else:
|
||||
|
@ -351,14 +379,24 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
return has_custom_templates or has_custom_config
|
||||
|
||||
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.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(
|
||||
"cleared config service config: %s", self.node.config_service_configs
|
||||
)
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
filename = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[filename])
|
||||
# reset current selected file data and config data, if present
|
||||
template_name = self.templates_combobox.get()
|
||||
temp_data = self.temp_service_files[template_name]
|
||||
self.template_text.set_text(temp_data)
|
||||
rendered_data = self.rendered[template_name]
|
||||
self.rendered_text.set_text(rendered_data)
|
||||
if self.config_frame:
|
||||
logger.info("resetting defaults: %s", self.default_config)
|
||||
self.config_frame.set_values(self.default_config)
|
||||
|
|
|
@ -23,7 +23,7 @@ class ServicesSelectDialog(Dialog):
|
|||
def __init__(
|
||||
self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
|
||||
) -> None:
|
||||
super().__init__(app, "Node Services", master=master)
|
||||
super().__init__(app, "Node Config Services", master=master)
|
||||
self.groups: Optional[ListboxScroll] = None
|
||||
self.services: Optional[CheckboxList] = None
|
||||
self.current: Optional[ListboxScroll] = None
|
||||
|
@ -45,7 +45,7 @@ class ServicesSelectDialog(Dialog):
|
|||
label_frame.columnconfigure(0, weight=1)
|
||||
self.groups = ListboxScroll(label_frame)
|
||||
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.bind("<<ListboxSelect>>", self.handle_group_change)
|
||||
self.groups.listbox.selection_set(0)
|
||||
|
@ -86,7 +86,7 @@ class ServicesSelectDialog(Dialog):
|
|||
index = selection[0]
|
||||
group = self.groups.listbox.get(index)
|
||||
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
|
||||
self.services.add(name, checked)
|
||||
|
||||
|
@ -147,7 +147,7 @@ class CustomNodesDialog(Dialog):
|
|||
frame, text="Icon", compound=tk.LEFT, command=self.click_icon
|
||||
)
|
||||
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)
|
||||
|
||||
def draw_node_buttons(self) -> None:
|
||||
|
|
|
@ -230,13 +230,8 @@ class NodeConfigDialog(Dialog):
|
|||
if nutils.is_model(self.node):
|
||||
label = ttk.Label(frame, text="Type")
|
||||
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
|
||||
combobox = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.type,
|
||||
values=list(nutils.NODE_MODELS),
|
||||
state=combo_state,
|
||||
)
|
||||
combobox.grid(row=row, column=1, sticky=tk.EW)
|
||||
entry = ttk.Entry(frame, textvariable=self.type, state=tk.DISABLED)
|
||||
entry.grid(row=row, column=1, sticky=tk.EW)
|
||||
row += 1
|
||||
|
||||
# container image field
|
||||
|
@ -275,7 +270,7 @@ class NodeConfigDialog(Dialog):
|
|||
ifaces_scroll.listbox.bind("<<ListboxSelect>>", self.iface_select)
|
||||
|
||||
# interfaces
|
||||
if self.canvas_node.ifaces:
|
||||
if nutils.is_container(self.node):
|
||||
self.draw_ifaces()
|
||||
|
||||
self.draw_spacer()
|
||||
|
|
55
daemon/core/gui/dialogs/wirelessconfig.py
Normal file
55
daemon/core/gui/dialogs/wirelessconfig.py
Normal 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()
|
|
@ -416,6 +416,8 @@ class Edge:
|
|||
self.src_label2 = None
|
||||
self.dst_label = None
|
||||
self.dst_label2 = None
|
||||
if self.dst:
|
||||
self.arc_common_edges()
|
||||
|
||||
def hide(self) -> None:
|
||||
self.hidden = True
|
||||
|
@ -507,6 +509,7 @@ class CanvasWirelessEdge(Edge):
|
|||
if self.src.hidden or self.dst.hidden:
|
||||
self.hide()
|
||||
self.set_binding()
|
||||
self.arc_common_edges()
|
||||
|
||||
def set_binding(self) -> None:
|
||||
self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
|
||||
|
@ -758,6 +761,4 @@ class CanvasEdge(Edge):
|
|||
self.src.delete_antenna()
|
||||
self.app.core.deleted_canvas_edges([self])
|
||||
super().delete()
|
||||
if self.dst:
|
||||
self.arc_common_edges()
|
||||
self.manager.edges.pop(self.token, None)
|
||||
|
|
|
@ -16,6 +16,7 @@ from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
|
|||
from core.gui.dialogs.nodeconfig import NodeConfigDialog
|
||||
from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog
|
||||
from core.gui.dialogs.nodeservice import NodeServiceDialog
|
||||
from core.gui.dialogs.wirelessconfig import WirelessConfigDialog
|
||||
from core.gui.dialogs.wlanconfig import WlanConfigDialog
|
||||
from core.gui.frames.node import NodeInfoFrame
|
||||
from core.gui.graph import tags
|
||||
|
@ -219,6 +220,7 @@ class CanvasNode:
|
|||
# clear existing menu
|
||||
self.context.delete(0, tk.END)
|
||||
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_mobility = is_wlan or is_emane
|
||||
if self.app.core.is_runtime():
|
||||
|
@ -231,6 +233,10 @@ class CanvasNode:
|
|||
self.context.add_command(
|
||||
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:
|
||||
self.context.add_command(
|
||||
label="Mobility Player", command=self.show_mobility_player
|
||||
|
@ -268,6 +274,10 @@ class CanvasNode:
|
|||
self.context.add_command(
|
||||
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:
|
||||
self.context.add_command(
|
||||
label="Mobility Config", command=self.show_mobility_config
|
||||
|
@ -298,7 +308,10 @@ class CanvasNode:
|
|||
other_iface = edge.other_iface(self)
|
||||
label = other_node.core_node.name
|
||||
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)
|
||||
unlink_menu.add_command(label=label, command=func_unlink)
|
||||
themes.style_menu(unlink_menu)
|
||||
|
@ -343,6 +356,10 @@ class CanvasNode:
|
|||
dialog = NodeConfigDialog(self.app, self)
|
||||
dialog.show()
|
||||
|
||||
def show_wireless_config(self) -> None:
|
||||
dialog = WirelessConfigDialog(self.app, self)
|
||||
dialog.show()
|
||||
|
||||
def show_wlan_config(self) -> None:
|
||||
dialog = WlanConfigDialog(self.app, self)
|
||||
if not dialog.has_error:
|
||||
|
|
|
@ -53,6 +53,7 @@ class ImageEnum(Enum):
|
|||
LINK = "link"
|
||||
HUB = "hub"
|
||||
WLAN = "wlan"
|
||||
WIRELESS = "wireless"
|
||||
EMANE = "emane"
|
||||
RJ45 = "rj45"
|
||||
TUNNEL = "tunnel"
|
||||
|
@ -92,14 +93,15 @@ TYPE_MAP: Dict[Tuple[NodeType, str], ImageEnum] = {
|
|||
(NodeType.DEFAULT, "host"): ImageEnum.HOST,
|
||||
(NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
|
||||
(NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
|
||||
(NodeType.HUB, ""): ImageEnum.HUB,
|
||||
(NodeType.SWITCH, ""): ImageEnum.SWITCH,
|
||||
(NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
|
||||
(NodeType.EMANE, ""): ImageEnum.EMANE,
|
||||
(NodeType.RJ45, ""): ImageEnum.RJ45,
|
||||
(NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
|
||||
(NodeType.DOCKER, ""): ImageEnum.DOCKER,
|
||||
(NodeType.LXC, ""): ImageEnum.LXC,
|
||||
(NodeType.HUB, None): ImageEnum.HUB,
|
||||
(NodeType.SWITCH, None): ImageEnum.SWITCH,
|
||||
(NodeType.WIRELESS_LAN, None): ImageEnum.WLAN,
|
||||
(NodeType.WIRELESS, None): ImageEnum.WIRELESS,
|
||||
(NodeType.EMANE, None): ImageEnum.EMANE,
|
||||
(NodeType.RJ45, None): ImageEnum.RJ45,
|
||||
(NodeType.TUNNEL, None): ImageEnum.TUNNEL,
|
||||
(NodeType.DOCKER, None): ImageEnum.DOCKER,
|
||||
(NodeType.LXC, None): ImageEnum.LXC,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -241,10 +241,10 @@ class InterfaceManager:
|
|||
dst_node = edge.dst.core_node
|
||||
self.determine_subnets(edge.src, edge.dst)
|
||||
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)
|
||||
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)
|
||||
link = Link(
|
||||
type=LinkType.WIRED,
|
||||
|
@ -258,6 +258,10 @@ class InterfaceManager:
|
|||
|
||||
def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface:
|
||||
node = canvas_node.core_node
|
||||
if nutils.is_bridge(node):
|
||||
iface_id = canvas_node.next_iface_id()
|
||||
iface = Interface(id=iface_id)
|
||||
else:
|
||||
ip4, ip6 = self.get_ips(node)
|
||||
if wireless_link:
|
||||
ip4_mask = WIRELESS_IP4_MASK
|
||||
|
|
|
@ -235,7 +235,11 @@ class Menubar(tk.Menu):
|
|||
menu.add_command(
|
||||
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)
|
||||
|
||||
def draw_widgets_menu(self) -> None:
|
||||
|
@ -393,7 +397,7 @@ class Menubar(tk.Menu):
|
|||
dialog.show()
|
||||
|
||||
def click_throughput(self) -> None:
|
||||
if not self.core.handling_throughputs:
|
||||
if self.core.show_throughputs.get():
|
||||
self.core.enable_throughputs()
|
||||
else:
|
||||
self.core.cancel_throughputs()
|
||||
|
|
|
@ -18,12 +18,16 @@ NETWORK_NODES: List["NodeDraw"] = []
|
|||
NODE_ICONS = {}
|
||||
CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, 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}
|
||||
BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH}
|
||||
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET}
|
||||
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"}
|
||||
ANTENNA_ICON: Optional[PhotoImage] = None
|
||||
|
||||
|
@ -46,6 +50,7 @@ def setup() -> None:
|
|||
(ImageEnum.HUB, NodeType.HUB, "Hub"),
|
||||
(ImageEnum.SWITCH, NodeType.SWITCH, "Switch"),
|
||||
(ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"),
|
||||
(ImageEnum.WIRELESS, NodeType.WIRELESS, "Wireless"),
|
||||
(ImageEnum.EMANE, NodeType.EMANE, "EMANE"),
|
||||
(ImageEnum.RJ45, NodeType.RJ45, "RJ45"),
|
||||
(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
|
||||
|
||||
|
||||
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]:
|
||||
for custom_node in gui_config.nodes:
|
||||
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:
|
||||
scale = app.app_scale
|
||||
image = None
|
||||
# node icon was overriden with a specific value
|
||||
# node icon was overridden with a specific value
|
||||
if node.icon:
|
||||
try:
|
||||
image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale)
|
||||
|
|
|
@ -257,6 +257,13 @@ class CodeText(ttk.Frame):
|
|||
yscrollbar.grid(row=0, column=1, sticky=tk.NS)
|
||||
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):
|
||||
def __init__(self, master: tk.BaseWidget = None, **kwargs: Any) -> None:
|
||||
|
|
|
@ -225,7 +225,6 @@ class WirelessModel(ConfigurableOptions):
|
|||
"""
|
||||
|
||||
config_type: RegisterTlvs = RegisterTlvs.WIRELESS
|
||||
bitmap: str = None
|
||||
position_callback: Callable[[CoreInterface], None] = None
|
||||
|
||||
def __init__(self, session: "Session", _id: int) -> None:
|
||||
|
@ -321,7 +320,8 @@ class BasicRangeModel(WirelessModel):
|
|||
loss=self.loss,
|
||||
jitter=self.jitter,
|
||||
)
|
||||
iface.config(options)
|
||||
iface.options.update(options)
|
||||
iface.set_config()
|
||||
|
||||
def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]:
|
||||
"""
|
||||
|
@ -627,7 +627,7 @@ class WayPointMobility(WirelessModel):
|
|||
moved_ifaces.append(iface)
|
||||
|
||||
# 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
|
||||
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
|
||||
self.setnodeposition(node, x, y, z)
|
||||
moved_ifaces.append(iface)
|
||||
self.net.model.update(moved_ifaces)
|
||||
self.net.wireless_model.update(moved_ifaces)
|
||||
|
||||
def addwaypoint(
|
||||
self,
|
||||
|
|
|
@ -3,8 +3,10 @@ Defines the base logic for nodes used within core.
|
|||
"""
|
||||
import abc
|
||||
import logging
|
||||
import shlex
|
||||
import shutil
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from threading import RLock
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union
|
||||
|
@ -13,11 +15,10 @@ import netaddr
|
|||
|
||||
from core import utils
|
||||
from core.configservice.dependencies import ConfigServiceDependencies
|
||||
from core.emulator.data import InterfaceData, LinkData
|
||||
from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes
|
||||
from core.emulator.data import InterfaceData, LinkOptions
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -34,19 +35,106 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
"""
|
||||
Base class for CORE nodes (nodes and networks)
|
||||
"""
|
||||
|
||||
apitype: Optional[NodeTypes] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
server: "DistributedServer" = None,
|
||||
options: NodeOptions = None,
|
||||
) -> None:
|
||||
"""
|
||||
Creates a NodeBase instance.
|
||||
|
@ -56,27 +144,29 @@ class NodeBase(abc.ABC):
|
|||
:param name: object name
|
||||
:param server: remote server node
|
||||
will run on, default is None for localhost
|
||||
:param options: options to create node with
|
||||
"""
|
||||
|
||||
self.session: "Session" = session
|
||||
if _id is None:
|
||||
_id = session.next_node_id()
|
||||
self.id: int = _id
|
||||
if name is None:
|
||||
name = f"o{self.id}"
|
||||
self.name: str = name
|
||||
self.id: int = _id if _id is not None else self.session.next_node_id()
|
||||
self.name: str = name or f"{self.__class__.__name__}{self.id}"
|
||||
self.server: "DistributedServer" = server
|
||||
self.type: Optional[str] = None
|
||||
self.model: Optional[str] = None
|
||||
self.services: CoreServices = []
|
||||
self.ifaces: Dict[int, CoreInterface] = {}
|
||||
self.iface_id: int = 0
|
||||
self.canvas: Optional[int] = None
|
||||
self.icon: Optional[str] = None
|
||||
self.position: Position = Position()
|
||||
self.up: bool = False
|
||||
self.lock: RLock = RLock()
|
||||
self.net_client: LinuxNetClient = get_net_client(
|
||||
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
|
||||
def startup(self) -> None:
|
||||
|
@ -96,6 +186,18 @@ class NodeBase(abc.ABC):
|
|||
"""
|
||||
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(
|
||||
self,
|
||||
args: str,
|
||||
|
@ -120,6 +222,19 @@ class NodeBase(abc.ABC):
|
|||
else:
|
||||
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:
|
||||
"""
|
||||
Set the (x,y,z) position of the object.
|
||||
|
@ -139,6 +254,71 @@ class NodeBase(abc.ABC):
|
|||
"""
|
||||
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:
|
||||
"""
|
||||
Retrieve interface based on id.
|
||||
|
@ -191,15 +371,6 @@ class NodeBase(abc.ABC):
|
|||
self.iface_id += 1
|
||||
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):
|
||||
"""
|
||||
|
@ -212,6 +383,7 @@ class CoreNodeBase(NodeBase):
|
|||
_id: int = None,
|
||||
name: str = None,
|
||||
server: "DistributedServer" = None,
|
||||
options: NodeOptions = None,
|
||||
) -> None:
|
||||
"""
|
||||
Create a CoreNodeBase instance.
|
||||
|
@ -222,19 +394,11 @@ class CoreNodeBase(NodeBase):
|
|||
:param server: remote server node
|
||||
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.directory: Optional[Path] = None
|
||||
self.tmpnodedir: bool = False
|
||||
|
||||
@abc.abstractmethod
|
||||
def startup(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def shutdown(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_dir(self, dir_path: Path) -> None:
|
||||
"""
|
||||
|
@ -270,19 +434,6 @@ class CoreNodeBase(NodeBase):
|
|||
"""
|
||||
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
|
||||
def termcmdstring(self, sh: str) -> str:
|
||||
"""
|
||||
|
@ -293,19 +444,6 @@ class CoreNodeBase(NodeBase):
|
|||
"""
|
||||
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
|
||||
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:
|
||||
"""
|
||||
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 is_dir: True if path is a directory path, False otherwise
|
||||
|
@ -387,60 +525,12 @@ class CoreNodeBase(NodeBase):
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
preserve = self.session.options.get_config("preservedir") == "1"
|
||||
preserve = self.session.options.get_int("preservedir") == 1
|
||||
if preserve:
|
||||
return
|
||||
if self.tmpnodedir:
|
||||
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:
|
||||
"""
|
||||
Set position.
|
||||
|
@ -455,40 +545,19 @@ class CoreNodeBase(NodeBase):
|
|||
for iface in self.get_ifaces():
|
||||
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):
|
||||
"""
|
||||
Provides standard core node logic.
|
||||
"""
|
||||
|
||||
apitype: NodeTypes = NodeTypes.DEFAULT
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
directory: Path = None,
|
||||
server: "DistributedServer" = None,
|
||||
options: CoreNodeOptions = None,
|
||||
) -> None:
|
||||
"""
|
||||
Create a CoreNode instance.
|
||||
|
@ -496,19 +565,37 @@ class CoreNode(CoreNodeBase):
|
|||
:param session: core session instance
|
||||
:param _id: object id
|
||||
:param name: object name
|
||||
:param directory: node directory
|
||||
:param server: remote server node
|
||||
will run on, default is None for localhost
|
||||
:param options: options to create node with
|
||||
"""
|
||||
super().__init__(session, _id, name, server)
|
||||
self.directory: Optional[Path] = directory
|
||||
options = options or CoreNodeOptions()
|
||||
super().__init__(session, _id, name, server, options)
|
||||
self.directory: Optional[Path] = options.directory
|
||||
self.ctrlchnlname: Path = self.session.directory / self.name
|
||||
self.pid: Optional[int] = None
|
||||
self.lock: RLock = RLock()
|
||||
self._mounts: List[Tuple[Path, Path]] = []
|
||||
self.node_net_client: LinuxNetClient = self.create_node_net_client(
|
||||
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:
|
||||
"""
|
||||
|
@ -585,6 +672,10 @@ class CoreNode(CoreNodeBase):
|
|||
self._mounts = []
|
||||
# shutdown all interfaces
|
||||
for iface in self.get_ifaces():
|
||||
try:
|
||||
self.node_net_client.device_flush(iface.name)
|
||||
except CoreCommandError:
|
||||
pass
|
||||
iface.shutdown()
|
||||
# kill node process if present
|
||||
try:
|
||||
|
@ -604,7 +695,7 @@ class CoreNode(CoreNodeBase):
|
|||
finally:
|
||||
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.
|
||||
|
||||
|
@ -613,7 +704,7 @@ class CoreNode(CoreNodeBase):
|
|||
:return: node command
|
||||
"""
|
||||
if shell:
|
||||
args = f'{BASH} -c "{args}"'
|
||||
args = f"{BASH} -c {shlex.quote(args)}"
|
||||
return f"{VCMD} -c {self.ctrlchnlname} -- {args}"
|
||||
|
||||
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
|
||||
|
@ -627,7 +718,7 @@ class CoreNode(CoreNodeBase):
|
|||
:return: combined stdout and stderr
|
||||
: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:
|
||||
return utils.cmd(args, wait=wait, shell=shell)
|
||||
else:
|
||||
|
@ -653,7 +744,7 @@ class CoreNode(CoreNodeBase):
|
|||
:param sh: shell to execute command in
|
||||
:return: str
|
||||
"""
|
||||
terminal = self._create_cmd(sh)
|
||||
terminal = self.create_cmd(sh)
|
||||
if self.server is None:
|
||||
return terminal
|
||||
else:
|
||||
|
@ -691,150 +782,6 @@ class CoreNode(CoreNodeBase):
|
|||
self.cmd(f"{MOUNT} -n --bind {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]:
|
||||
"""
|
||||
Check if there is a mounted parent directory created for this node.
|
||||
|
@ -910,21 +857,62 @@ class CoreNode(CoreNodeBase):
|
|||
if mode is not None:
|
||||
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):
|
||||
"""
|
||||
Base class for networks
|
||||
"""
|
||||
|
||||
linktype: LinkTypes = LinkTypes.WIRED
|
||||
has_custom_iface: bool = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int,
|
||||
name: str,
|
||||
server: "DistributedServer" = None,
|
||||
options: NodeOptions = None,
|
||||
) -> None:
|
||||
"""
|
||||
Create a CoreNetworkBase instance.
|
||||
|
@ -934,64 +922,15 @@ class CoreNetworkBase(NodeBase):
|
|||
:param name: object name
|
||||
:param server: remote server node
|
||||
will run on, default is None for localhost
|
||||
:param options: options to create node with
|
||||
"""
|
||||
super().__init__(session, _id, name, server)
|
||||
self.mtu: int = DEFAULT_MTU
|
||||
super().__init__(session, _id, name, server, options)
|
||||
mtu = self.session.options.get_int("mtu")
|
||||
self.mtu: int = mtu if mtu > 0 else DEFAULT_MTU
|
||||
self.brname: Optional[str] = None
|
||||
self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {}
|
||||
self.linked_lock: threading.Lock = threading.Lock()
|
||||
|
||||
@abc.abstractmethod
|
||||
def startup(self) -> None:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Attach network interface.
|
||||
|
@ -999,9 +938,10 @@ class CoreNetworkBase(NodeBase):
|
|||
:param iface: network interface to attach
|
||||
:return: nothing
|
||||
"""
|
||||
i = self.next_iface_id()
|
||||
self.ifaces[i] = iface
|
||||
iface.net_id = i
|
||||
iface_id = self.next_iface_id()
|
||||
self.ifaces[iface_id] = iface
|
||||
iface.net = self
|
||||
iface.net_id = iface_id
|
||||
with self.linked_lock:
|
||||
self.linked[iface] = {}
|
||||
|
||||
|
@ -1013,118 +953,7 @@ class CoreNetworkBase(NodeBase):
|
|||
:return: nothing
|
||||
"""
|
||||
del self.ifaces[iface.net_id]
|
||||
iface.net = None
|
||||
iface.net_id = None
|
||||
with self.linked_lock:
|
||||
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
|
||||
|
|
|
@ -1,112 +1,114 @@
|
|||
import json
|
||||
import logging
|
||||
import shlex
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
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.enumerations import NodeTypes
|
||||
from core.errors import CoreCommandError
|
||||
from core.nodes.base import CoreNode
|
||||
from core.nodes.netclient import LinuxNetClient, get_net_client
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.executables import BASH
|
||||
from core.nodes.base import CoreNode, CoreNodeOptions
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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:
|
||||
self.run(
|
||||
f"docker run -td --init --net=none --hostname {self.name} "
|
||||
f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 "
|
||||
f"--privileged {self.image} /bin/bash"
|
||||
)
|
||||
self.pid = self.get_pid()
|
||||
return self.pid
|
||||
@dataclass
|
||||
class DockerOptions(CoreNodeOptions):
|
||||
image: str = "ubuntu"
|
||||
"""image used when creating container"""
|
||||
binds: List[Tuple[str, str]] = field(default_factory=list)
|
||||
"""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 get_info(self) -> Dict:
|
||||
args = f"docker inspect {self.name}"
|
||||
output = self.run(args)
|
||||
data = json.loads(output)
|
||||
if not data:
|
||||
raise CoreCommandError(1, args, f"docker({self.name}) not present")
|
||||
return data[0]
|
||||
unique is True for node unique volume naming
|
||||
delete is True for deleting volume mount during shutdown
|
||||
"""
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
try:
|
||||
data = self.get_info()
|
||||
return data["State"]["Running"]
|
||||
except CoreCommandError:
|
||||
return False
|
||||
|
||||
def stop_container(self) -> None:
|
||||
self.run(f"docker rm -f {self.name}")
|
||||
|
||||
def check_cmd(self, cmd: str, wait: bool = True, shell: bool = False) -> str:
|
||||
logger.info("docker cmd output: %s", cmd)
|
||||
return utils.cmd(f"docker exec {self.name} {cmd}", wait=wait, shell=shell)
|
||||
|
||||
def create_ns_cmd(self, cmd: str) -> str:
|
||||
return f"nsenter -t {self.pid} -a {cmd}"
|
||||
|
||||
def get_pid(self) -> str:
|
||||
args = f"docker inspect -f '{{{{.State.Pid}}}}' {self.name}"
|
||||
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)
|
||||
@dataclass
|
||||
class DockerVolume:
|
||||
src: str
|
||||
"""volume mount name"""
|
||||
dst: str
|
||||
"""volume mount destination directory"""
|
||||
unique: bool = True
|
||||
"""True to create a node unique prefixed name for this volume"""
|
||||
delete: bool = True
|
||||
"""True to delete the volume during shutdown"""
|
||||
path: str = None
|
||||
"""path to the volume on the host"""
|
||||
|
||||
|
||||
class DockerNode(CoreNode):
|
||||
apitype = NodeTypes.DOCKER
|
||||
"""
|
||||
Provides logic for creating a Docker based node.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
directory: str = None,
|
||||
server: DistributedServer = None,
|
||||
image: str = None,
|
||||
options: DockerOptions = None,
|
||||
) -> None:
|
||||
"""
|
||||
Create a DockerNode instance.
|
||||
|
||||
:param session: core session instance
|
||||
:param _id: object id
|
||||
:param name: object name
|
||||
:param directory: node directory
|
||||
:param _id: node id
|
||||
:param name: node name
|
||||
:param server: remote server node
|
||||
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:
|
||||
image = "ubuntu"
|
||||
self.image: str = image
|
||||
super().__init__(session, _id, name, directory, server)
|
||||
options = options or DockerOptions()
|
||||
super().__init__(session, _id, name, server, options)
|
||||
self.image: str = options.image
|
||||
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
|
||||
container.
|
||||
Return default creation options, which can be used during node creation.
|
||||
|
||||
:param use_ovs: True for OVS bridges, False for Linux bridges
|
||||
:return:node network client
|
||||
:return: docker options
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
|
@ -114,22 +116,52 @@ class DockerNode(CoreNode):
|
|||
|
||||
: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:
|
||||
"""
|
||||
Start a new namespace node by invoking the vnoded process that
|
||||
allocates a new namespace. Bring up the loopback device and set
|
||||
the hostname.
|
||||
Create a docker container instance for the specified image.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
with self.lock:
|
||||
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.client = DockerClient(self.name, self.image, self.host_cmd)
|
||||
self.pid = self.client.create_container()
|
||||
binds = ""
|
||||
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
|
||||
|
||||
def shutdown(self) -> None:
|
||||
|
@ -141,20 +173,14 @@ class DockerNode(CoreNode):
|
|||
# nothing to do if node is not up
|
||||
if not self.up:
|
||||
return
|
||||
|
||||
with self.lock:
|
||||
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
|
||||
|
||||
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:
|
||||
"""
|
||||
Create a terminal command string.
|
||||
|
@ -162,7 +188,11 @@ class DockerNode(CoreNode):
|
|||
:param sh: shell to execute command in
|
||||
: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:
|
||||
"""
|
||||
|
@ -172,8 +202,7 @@ class DockerNode(CoreNode):
|
|||
:return: nothing
|
||||
"""
|
||||
logger.debug("creating node dir: %s", dir_path)
|
||||
args = f"mkdir -p {dir_path}"
|
||||
self.cmd(args)
|
||||
self.cmd(f"mkdir -p {dir_path}")
|
||||
|
||||
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}")
|
||||
if self.server is not None:
|
||||
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}")
|
||||
if self.server is not None:
|
||||
self.host_cmd(f"rm -f {temp_path}")
|
||||
|
@ -231,6 +260,6 @@ class DockerNode(CoreNode):
|
|||
temp_path = Path(temp.name)
|
||||
src_path = temp_path
|
||||
self.server.remote_put(src_path, temp_path)
|
||||
self.client.copy_file(src_path, dst_path)
|
||||
self.host_cmd(f"{DOCKER} cp {src_path} {self.name}:{dst_path}")
|
||||
if mode is not None:
|
||||
self.cmd(f"chmod {mode:o} {dst_path}")
|
||||
|
|
|
@ -4,7 +4,6 @@ virtual ethernet classes that implement the interfaces available under Linux.
|
|||
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from pathlib import Path
|
||||
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__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.distributed import DistributedServer
|
||||
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
|
||||
IFACE_NAME_LENGTH: int = 15
|
||||
|
||||
|
||||
def tc_clear_cmd(name: str) -> str:
|
||||
|
@ -78,35 +78,42 @@ class CoreInterface:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int,
|
||||
name: str,
|
||||
localname: str,
|
||||
use_ovs: bool,
|
||||
mtu: int = DEFAULT_MTU,
|
||||
node: "NodeBase" = None,
|
||||
server: "DistributedServer" = None,
|
||||
node: "CoreNode" = None,
|
||||
) -> None:
|
||||
"""
|
||||
Creates a CoreInterface instance.
|
||||
|
||||
:param session: core session instance
|
||||
:param _id: interface id for associated node
|
||||
:param name: interface name
|
||||
:param localname: interface local name
|
||||
:param use_ovs: True to use ovs, False otherwise
|
||||
: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 node: node for interface
|
||||
"""
|
||||
if len(name) >= 16:
|
||||
raise CoreError(f"interface name ({name}) too long, max 16")
|
||||
if len(localname) >= 16:
|
||||
raise CoreError(f"interface local name ({localname}) too long, max 16")
|
||||
self.session: "Session" = session
|
||||
self.node: Optional["CoreNode"] = node
|
||||
if len(name) >= IFACE_NAME_LENGTH:
|
||||
raise CoreError(
|
||||
f"interface name ({name}) too long, max {IFACE_NAME_LENGTH}"
|
||||
)
|
||||
if len(localname) >= IFACE_NAME_LENGTH:
|
||||
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.localname: str = localname
|
||||
self.up: bool = False
|
||||
self.mtu: int = mtu
|
||||
self.net: Optional[CoreNetworkBase] = None
|
||||
self.othernet: Optional[CoreNetworkBase] = None
|
||||
self.ip4s: List[netaddr.IPNetwork] = []
|
||||
self.ip6s: List[netaddr.IPNetwork] = []
|
||||
self.mac: Optional[netaddr.EUI] = None
|
||||
|
@ -114,20 +121,12 @@ class CoreInterface:
|
|||
self.poshook: Callable[[CoreInterface], None] = lambda x: None
|
||||
# used with EMANE
|
||||
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
|
||||
self.flow_id: Optional[int] = None
|
||||
self.server: Optional["DistributedServer"] = server
|
||||
self.net_client: LinuxNetClient = get_net_client(
|
||||
self.session.use_ovs(), self.host_cmd
|
||||
)
|
||||
self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd)
|
||||
self.control: bool = False
|
||||
# configuration data
|
||||
self.has_local_netem: bool = False
|
||||
self.local_options: LinkOptions = LinkOptions()
|
||||
self.has_netem: bool = False
|
||||
self.options: LinkOptions = LinkOptions()
|
||||
|
||||
|
@ -161,7 +160,13 @@ class CoreInterface:
|
|||
|
||||
: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:
|
||||
"""
|
||||
|
@ -169,29 +174,14 @@ class CoreInterface:
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
if not self.up:
|
||||
return
|
||||
if self.localname:
|
||||
try:
|
||||
self.net_client.delete_device(self.localname)
|
||||
except CoreCommandError:
|
||||
pass
|
||||
|
||||
def attachnet(self, net: "CoreNetworkBase") -> None:
|
||||
"""
|
||||
Attach network.
|
||||
|
||||
:param net: network to attach
|
||||
:return: nothing
|
||||
"""
|
||||
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)
|
||||
self.up = False
|
||||
|
||||
def add_ip(self, ip: str) -> None:
|
||||
"""
|
||||
|
@ -303,40 +293,23 @@ class CoreInterface:
|
|||
"""
|
||||
return self.transport_type == TransportType.VIRTUAL
|
||||
|
||||
def config(self, options: LinkOptions, use_local: bool = True) -> 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
|
||||
def set_config(self) -> None:
|
||||
# clear current settings
|
||||
if current_options.is_clear():
|
||||
clear_local_netem = use_local and self.has_local_netem
|
||||
clear_netem = not use_local and self.has_netem
|
||||
if clear_local_netem or clear_netem:
|
||||
cmd = tc_clear_cmd(name)
|
||||
self.host_cmd(cmd)
|
||||
if use_local:
|
||||
self.has_local_netem = False
|
||||
if self.options.is_clear():
|
||||
if self.has_netem:
|
||||
cmd = tc_clear_cmd(self.name)
|
||||
if self.node:
|
||||
self.node.cmd(cmd)
|
||||
else:
|
||||
self.host_cmd(cmd)
|
||||
self.has_netem = False
|
||||
# set updated settings
|
||||
else:
|
||||
cmd = tc_cmd(name, current_options, self.mtu)
|
||||
self.host_cmd(cmd)
|
||||
if use_local:
|
||||
self.has_local_netem = True
|
||||
cmd = tc_cmd(self.name, self.options, self.mtu)
|
||||
if self.node:
|
||||
self.node.cmd(cmd)
|
||||
else:
|
||||
self.host_cmd(cmd)
|
||||
self.has_netem = True
|
||||
|
||||
def get_data(self) -> InterfaceData:
|
||||
|
@ -345,231 +318,22 @@ class CoreInterface:
|
|||
|
||||
: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()
|
||||
if ip4:
|
||||
data.ip4 = str(ip4.ip)
|
||||
data.ip4_mask = ip4.prefixlen
|
||||
ip4_addr = str(ip4.ip) if ip4 else None
|
||||
ip4_mask = ip4.prefixlen if ip4 else None
|
||||
ip6 = self.get_ip6()
|
||||
if ip6:
|
||||
data.ip6 = str(ip6.ip)
|
||||
data.ip6_mask = ip6.prefixlen
|
||||
return data
|
||||
|
||||
|
||||
class Veth(CoreInterface):
|
||||
"""
|
||||
Provides virtual ethernet functionality for core nodes.
|
||||
"""
|
||||
|
||||
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))
|
||||
ip6_addr = str(ip6.ip) if ip6 else None
|
||||
ip6_mask = ip6.prefixlen if ip6 else None
|
||||
mac = str(self.mac) if self.mac else None
|
||||
return InterfaceData(
|
||||
id=self.id,
|
||||
name=self.name,
|
||||
mac=mac,
|
||||
ip4=ip4_addr,
|
||||
ip4_mask=ip4_mask,
|
||||
ip6=ip6_addr,
|
||||
ip6_mask=ip6_mask,
|
||||
)
|
||||
|
||||
|
||||
class GreTap(CoreInterface):
|
||||
|
@ -594,7 +358,7 @@ class GreTap(CoreInterface):
|
|||
"""
|
||||
Creates a GreTap instance.
|
||||
|
||||
:param session: core session instance
|
||||
:param session: session for this gre tap
|
||||
:param remoteip: remote address
|
||||
:param key: gre tap key
|
||||
:param node: related core node
|
||||
|
@ -612,7 +376,7 @@ class GreTap(CoreInterface):
|
|||
sessionid = session.short_session_id()
|
||||
localname = f"gt.{self.id}.{sessionid}"
|
||||
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.remote_ip: str = remoteip
|
||||
self.ttl: int = ttl
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import json
|
||||
import logging
|
||||
import shlex
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
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.enumerations import NodeTypes
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -18,65 +20,29 @@ if TYPE_CHECKING:
|
|||
from core.emulator.session import Session
|
||||
|
||||
|
||||
class LxdClient:
|
||||
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[int] = None
|
||||
@dataclass
|
||||
class LxcOptions(CoreNodeOptions):
|
||||
image: str = "ubuntu"
|
||||
"""image used when creating container"""
|
||||
binds: List[Tuple[str, str]] = field(default_factory=list)
|
||||
"""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:
|
||||
self.run(f"lxc launch {self.image} {self.name}")
|
||||
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)
|
||||
unique is True for node unique volume naming
|
||||
delete is True for deleting volume mount during shutdown
|
||||
"""
|
||||
|
||||
|
||||
class LxcNode(CoreNode):
|
||||
apitype = NodeTypes.LXC
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
directory: str = None,
|
||||
server: DistributedServer = None,
|
||||
image: str = None,
|
||||
options: LxcOptions = None,
|
||||
) -> None:
|
||||
"""
|
||||
Create a LxcNode instance.
|
||||
|
@ -84,15 +50,37 @@ class LxcNode(CoreNode):
|
|||
:param session: core session instance
|
||||
:param _id: object id
|
||||
:param name: object name
|
||||
:param directory: node directory
|
||||
:param server: remote server node
|
||||
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:
|
||||
image = "ubuntu"
|
||||
self.image: str = image
|
||||
super().__init__(session, _id, name, directory, server)
|
||||
options = options or LxcOptions()
|
||||
super().__init__(session, _id, name, server, options)
|
||||
self.image: str = options.image
|
||||
|
||||
@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:
|
||||
"""
|
||||
|
@ -100,7 +88,11 @@ class LxcNode(CoreNode):
|
|||
|
||||
: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:
|
||||
"""
|
||||
|
@ -112,8 +104,9 @@ class LxcNode(CoreNode):
|
|||
if self.up:
|
||||
raise ValueError("starting a node that is already up")
|
||||
self.makenodedir()
|
||||
self.client = LxdClient(self.name, self.image, self.host_cmd)
|
||||
self.pid = self.client.create_container()
|
||||
self.host_cmd(f"lxc launch {self.image} {self.name}")
|
||||
data = self._get_info()
|
||||
self.pid = data["state"]["pid"]
|
||||
self.up = True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
|
@ -125,10 +118,9 @@ class LxcNode(CoreNode):
|
|||
# nothing to do if node is not up
|
||||
if not self.up:
|
||||
return
|
||||
|
||||
with self.lock:
|
||||
self.ifaces.clear()
|
||||
self.client.stop_container()
|
||||
self.host_cmd(f"lxc delete --force {self.name}")
|
||||
self.up = False
|
||||
|
||||
def termcmdstring(self, sh: str = "/bin/sh") -> str:
|
||||
|
@ -138,7 +130,11 @@ class LxcNode(CoreNode):
|
|||
:param sh: shell to execute command in
|
||||
: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:
|
||||
"""
|
||||
|
@ -182,7 +178,9 @@ class LxcNode(CoreNode):
|
|||
self.cmd(f"mkdir -m {0o755:o} -p {directory}")
|
||||
if self.server is not None:
|
||||
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}")
|
||||
if self.server is not None:
|
||||
self.host_cmd(f"rm -f {temp_path}")
|
||||
|
@ -208,11 +206,16 @@ class LxcNode(CoreNode):
|
|||
temp_path = Path(temp.name)
|
||||
src_path = temp_path
|
||||
self.server.remote_put(src_path, temp_path)
|
||||
self.client.copy_file(src_path, dst_path)
|
||||
if 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:
|
||||
self.cmd(f"chmod {mode:o} {dst_path}")
|
||||
|
||||
def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
|
||||
super().add_iface(iface, iface_id)
|
||||
def create_iface(
|
||||
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
|
||||
time.sleep(0.5)
|
||||
return iface
|
||||
|
|
|
@ -28,6 +28,7 @@ class LinuxNetClient:
|
|||
:param name: name for hostname
|
||||
:return: nothing
|
||||
"""
|
||||
name = name.replace("_", "-")
|
||||
self.run(f"hostname {name}")
|
||||
|
||||
def create_route(self, route: str, device: str) -> None:
|
||||
|
|
|
@ -4,26 +4,19 @@ Defines network nodes used within core.
|
|||
|
||||
import logging
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Type
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.emulator.data import InterfaceData, LinkData
|
||||
from core.emulator.enumerations import (
|
||||
LinkTypes,
|
||||
MessageFlags,
|
||||
NetworkPolicy,
|
||||
NodeTypes,
|
||||
RegisterTlvs,
|
||||
)
|
||||
from core.emulator.enumerations import MessageFlags, NetworkPolicy, RegisterTlvs
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.executables import NFTABLES
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||
from core.nodes.interface import CoreInterface, GreTap, Veth
|
||||
from core.nodes.base import CoreNetworkBase, NodeOptions
|
||||
from core.nodes.interface import CoreInterface, GreTap
|
||||
from core.nodes.netclient import get_net_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -33,27 +26,9 @@ if TYPE_CHECKING:
|
|||
from core.emulator.session import Session
|
||||
from core.location.mobility import WirelessModel, WayPointMobility
|
||||
|
||||
WirelessModelType = Type[WirelessModel]
|
||||
|
||||
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:
|
||||
"""
|
||||
Helper class for queuing up nftables commands into rate-limited
|
||||
|
@ -78,7 +53,7 @@ class NftablesQueue:
|
|||
# list of pending nftables commands
|
||||
self.cmds: List[str] = []
|
||||
# list of WLANs requiring update
|
||||
self.updates: SetQueue = SetQueue()
|
||||
self.updates: utils.SetQueue = utils.SetQueue()
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
|
@ -206,6 +181,12 @@ class 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):
|
||||
"""
|
||||
Provides linux bridge network functionality for core nodes.
|
||||
|
@ -219,28 +200,29 @@ class CoreNetwork(CoreNetworkBase):
|
|||
_id: int = None,
|
||||
name: str = None,
|
||||
server: "DistributedServer" = None,
|
||||
policy: NetworkPolicy = None,
|
||||
options: NetworkOptions = None,
|
||||
) -> None:
|
||||
"""
|
||||
Creates a LxBrNet instance.
|
||||
Creates a CoreNetwork instance.
|
||||
|
||||
:param session: core session instance
|
||||
:param _id: object id
|
||||
:param name: object name
|
||||
:param server: remote server node
|
||||
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)
|
||||
if name is None:
|
||||
name = str(self.id)
|
||||
if policy is not None:
|
||||
self.policy: NetworkPolicy = policy
|
||||
self.name: Optional[str] = name
|
||||
options = options or NetworkOptions()
|
||||
super().__init__(session, _id, name, server, options)
|
||||
self.policy: NetworkPolicy = options.policy if options.policy else self.policy
|
||||
sessionid = self.session.short_session_id()
|
||||
self.brname: str = f"b.{self.id}.{sessionid}"
|
||||
self.has_nftables_chain: bool = False
|
||||
|
||||
@classmethod
|
||||
def create_options(cls) -> NetworkOptions:
|
||||
return NetworkOptions()
|
||||
|
||||
def host_cmd(
|
||||
self,
|
||||
args: str,
|
||||
|
@ -280,6 +262,17 @@ class CoreNetwork(CoreNetworkBase):
|
|||
self.up = True
|
||||
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:
|
||||
"""
|
||||
Linux bridge shutdown logic.
|
||||
|
@ -309,9 +302,9 @@ class CoreNetwork(CoreNetworkBase):
|
|||
:param iface: network interface to attach
|
||||
:return: nothing
|
||||
"""
|
||||
super().attach(iface)
|
||||
if self.up:
|
||||
iface.net_client.set_iface_master(self.brname, iface.localname)
|
||||
super().attach(iface)
|
||||
|
||||
def detach(self, iface: CoreInterface) -> None:
|
||||
"""
|
||||
|
@ -320,9 +313,9 @@ class CoreNetwork(CoreNetworkBase):
|
|||
:param iface: network interface to detach
|
||||
:return: nothing
|
||||
"""
|
||||
super().detach(iface)
|
||||
if self.up:
|
||||
iface.net_client.delete_iface(self.brname, iface.localname)
|
||||
super().detach(iface)
|
||||
|
||||
def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool:
|
||||
"""
|
||||
|
@ -378,67 +371,6 @@ class CoreNetwork(CoreNetworkBase):
|
|||
self.linked[iface1][iface2] = True
|
||||
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):
|
||||
"""
|
||||
|
@ -558,6 +490,20 @@ class GreTapBridge(CoreNetwork):
|
|||
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):
|
||||
"""
|
||||
Control network functionality.
|
||||
|
@ -576,36 +522,32 @@ class CtrlNet(CoreNetwork):
|
|||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
prefix: str,
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
hostid: int = None,
|
||||
server: "DistributedServer" = None,
|
||||
assign_address: bool = True,
|
||||
updown_script: str = None,
|
||||
serverintf: str = None,
|
||||
options: CtrlNetOptions = None,
|
||||
) -> None:
|
||||
"""
|
||||
Creates a CtrlNet instance.
|
||||
|
||||
:param session: core session instance
|
||||
:param _id: node id
|
||||
:param name: node namee
|
||||
:param prefix: control network ipv4 prefix
|
||||
:param hostid: host id
|
||||
:param name: node name
|
||||
:param server: remote server node
|
||||
will run on, default is None for localhost
|
||||
:param assign_address: assigned address
|
||||
:param updown_script: updown script
|
||||
:param serverintf: server interface
|
||||
:return:
|
||||
:param options: node options for creation
|
||||
"""
|
||||
self.prefix: netaddr.IPNetwork = netaddr.IPNetwork(prefix).cidr
|
||||
self.hostid: Optional[int] = hostid
|
||||
self.assign_address: bool = assign_address
|
||||
self.updown_script: Optional[str] = updown_script
|
||||
self.serverintf: Optional[str] = serverintf
|
||||
super().__init__(session, _id, name, server)
|
||||
options = options or CtrlNetOptions()
|
||||
super().__init__(session, _id, name, server, options)
|
||||
self.prefix: netaddr.IPNetwork = netaddr.IPNetwork(options.prefix).cidr
|
||||
self.hostid: Optional[int] = options.hostid
|
||||
self.assign_address: bool = options.assign_address
|
||||
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:
|
||||
"""
|
||||
|
@ -686,15 +628,6 @@ class CtrlNet(CoreNetwork):
|
|||
|
||||
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):
|
||||
"""
|
||||
|
@ -714,59 +647,13 @@ class PtpNet(CoreNetwork):
|
|||
raise CoreError("ptp links support at most 2 network interfaces")
|
||||
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):
|
||||
"""
|
||||
Provides switch functionality within a core node.
|
||||
"""
|
||||
|
||||
apitype: NodeTypes = NodeTypes.SWITCH
|
||||
policy: NetworkPolicy = NetworkPolicy.ACCEPT
|
||||
type: str = "lanswitch"
|
||||
|
||||
|
||||
class HubNode(CoreNetwork):
|
||||
|
@ -775,9 +662,7 @@ class HubNode(CoreNetwork):
|
|||
ports by turning off MAC address learning.
|
||||
"""
|
||||
|
||||
apitype: NodeTypes = NodeTypes.HUB
|
||||
policy: NetworkPolicy = NetworkPolicy.ACCEPT
|
||||
type: str = "hub"
|
||||
|
||||
def startup(self) -> None:
|
||||
"""
|
||||
|
@ -794,10 +679,7 @@ class WlanNode(CoreNetwork):
|
|||
Provides wireless lan functionality within a core node.
|
||||
"""
|
||||
|
||||
apitype: NodeTypes = NodeTypes.WIRELESS_LAN
|
||||
linktype: LinkTypes = LinkTypes.WIRED
|
||||
policy: NetworkPolicy = NetworkPolicy.DROP
|
||||
type: str = "wlan"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -805,7 +687,7 @@ class WlanNode(CoreNetwork):
|
|||
_id: int = None,
|
||||
name: str = None,
|
||||
server: "DistributedServer" = None,
|
||||
policy: NetworkPolicy = None,
|
||||
options: NetworkOptions = None,
|
||||
) -> None:
|
||||
"""
|
||||
Create a WlanNode instance.
|
||||
|
@ -815,11 +697,11 @@ class WlanNode(CoreNetwork):
|
|||
:param name: node name
|
||||
:param server: remote server node
|
||||
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)
|
||||
self.model: Optional[WirelessModel] = None
|
||||
self.wireless_model: Optional[WirelessModel] = None
|
||||
self.mobility: Optional[WayPointMobility] = None
|
||||
|
||||
def startup(self) -> None:
|
||||
|
@ -839,27 +721,27 @@ class WlanNode(CoreNetwork):
|
|||
:return: nothing
|
||||
"""
|
||||
super().attach(iface)
|
||||
if self.model:
|
||||
iface.poshook = self.model.position_callback
|
||||
if self.wireless_model:
|
||||
iface.poshook = self.wireless_model.position_callback
|
||||
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.
|
||||
|
||||
:param model: wireless model to set to
|
||||
:param wireless_model: wireless model to set to
|
||||
:param config: configuration for model being set
|
||||
:return: nothing
|
||||
"""
|
||||
logger.debug("node(%s) setting model: %s", self.name, model.name)
|
||||
if model.config_type == RegisterTlvs.WIRELESS:
|
||||
self.model = model(session=self.session, _id=self.id)
|
||||
logger.debug("node(%s) setting model: %s", self.name, wireless_model.name)
|
||||
if wireless_model.config_type == RegisterTlvs.WIRELESS:
|
||||
self.wireless_model = wireless_model(session=self.session, _id=self.id)
|
||||
for iface in self.get_ifaces():
|
||||
iface.poshook = self.model.position_callback
|
||||
iface.poshook = self.wireless_model.position_callback
|
||||
iface.setposition()
|
||||
self.updatemodel(config)
|
||||
elif model.config_type == RegisterTlvs.MOBILITY:
|
||||
self.mobility = model(session=self.session, _id=self.id)
|
||||
elif wireless_model.config_type == RegisterTlvs.MOBILITY:
|
||||
self.mobility = wireless_model(session=self.session, _id=self.id)
|
||||
self.mobility.update_config(config)
|
||||
|
||||
def update_mobility(self, config: Dict[str, str]) -> None:
|
||||
|
@ -868,12 +750,12 @@ class WlanNode(CoreNetwork):
|
|||
self.mobility.update_config(config)
|
||||
|
||||
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})")
|
||||
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():
|
||||
iface.setposition()
|
||||
|
||||
|
@ -884,10 +766,10 @@ class WlanNode(CoreNetwork):
|
|||
:param flags: message flags
|
||||
:return: list of link data
|
||||
"""
|
||||
links = super().links(flags)
|
||||
if self.model:
|
||||
links.extend(self.model.links(flags))
|
||||
return links
|
||||
if self.wireless_model:
|
||||
return self.wireless_model.links(flags)
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class TunnelNode(GreTapBridge):
|
||||
|
@ -895,6 +777,4 @@ class TunnelNode(GreTapBridge):
|
|||
Provides tunnel functionality in a core node.
|
||||
"""
|
||||
|
||||
apitype: NodeTypes = NodeTypes.TUNNEL
|
||||
policy: NetworkPolicy = NetworkPolicy.ACCEPT
|
||||
type: str = "tunnel"
|
||||
|
|
|
@ -3,17 +3,18 @@ PhysicalNode class for including real systems in the emulated network.
|
|||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
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.enumerations import NodeTypes, TransportType
|
||||
from core.emulator.enumerations import TransportType
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.executables import MOUNT, TEST, UMOUNT
|
||||
from core.nodes.base import CoreNetworkBase, CoreNodeBase
|
||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.executables import BASH, TEST, UMOUNT
|
||||
from core.nodes.base import CoreNode, CoreNodeBase, CoreNodeOptions, NodeOptions
|
||||
from core.nodes.interface import CoreInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -21,201 +22,19 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
"""
|
||||
RJ45Node is a physical interface on the host linked to the emulated
|
||||
network.
|
||||
"""
|
||||
|
||||
apitype: NodeTypes = NodeTypes.RJ45
|
||||
type: str = "rj45"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
mtu: int = DEFAULT_MTU,
|
||||
server: DistributedServer = None,
|
||||
options: NodeOptions = None,
|
||||
) -> None:
|
||||
"""
|
||||
Create an RJ45Node instance.
|
||||
|
@ -223,17 +42,15 @@ class Rj45Node(CoreNodeBase):
|
|||
:param session: core session instance
|
||||
:param _id: node id
|
||||
:param name: node name
|
||||
:param mtu: rj45 mtu
|
||||
:param server: remote server node
|
||||
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(
|
||||
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.lock: threading.RLock = threading.RLock()
|
||||
self.iface_id: Optional[int] = None
|
||||
self.old_up: bool = False
|
||||
self.old_addrs: List[Tuple[str, Optional[str]]] = []
|
||||
|
||||
|
@ -245,7 +62,7 @@ class Rj45Node(CoreNodeBase):
|
|||
:raises CoreCommandError: when there is a command exception
|
||||
"""
|
||||
# interface will also be marked up during net.attach()
|
||||
self.savestate()
|
||||
self.save_state()
|
||||
self.net_client.device_up(self.iface.localname)
|
||||
self.up = True
|
||||
|
||||
|
@ -266,7 +83,7 @@ class Rj45Node(CoreNodeBase):
|
|||
except CoreCommandError:
|
||||
pass
|
||||
self.up = False
|
||||
self.restorestate()
|
||||
self.restore_state()
|
||||
|
||||
def path_exists(self, path: str) -> bool:
|
||||
"""
|
||||
|
@ -281,34 +98,29 @@ class Rj45Node(CoreNodeBase):
|
|||
except CoreCommandError:
|
||||
return False
|
||||
|
||||
def new_iface(
|
||||
self, net: CoreNetworkBase, iface_data: InterfaceData
|
||||
def create_iface(
|
||||
self, iface_data: InterfaceData = None, options: LinkOptions = None
|
||||
) -> 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:
|
||||
iface_id = iface_data.id
|
||||
if iface_id is None:
|
||||
iface_id = 0
|
||||
if self.iface.net is not None:
|
||||
if self.iface.id in self.ifaces:
|
||||
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
|
||||
self.iface_id = iface_id
|
||||
self.iface.attachnet(net)
|
||||
if iface_data and iface_data.mtu is not None:
|
||||
self.iface.mtu = iface_data.mtu
|
||||
self.iface.ip4s.clear()
|
||||
self.iface.ip6s.clear()
|
||||
for ip in iface_data.get_ips():
|
||||
self.add_ip(ip)
|
||||
self.iface.add_ip(ip)
|
||||
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:
|
||||
"""
|
||||
Delete a network interface.
|
||||
|
@ -318,16 +130,10 @@ class Rj45Node(CoreNodeBase):
|
|||
"""
|
||||
self.get_iface(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()
|
||||
|
||||
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")
|
||||
return self.iface
|
||||
|
||||
|
@ -341,42 +147,17 @@ class Rj45Node(CoreNodeBase):
|
|||
"""
|
||||
if iface is not self.iface:
|
||||
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:
|
||||
"""
|
||||
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:
|
||||
def save_state(self) -> None:
|
||||
"""
|
||||
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
|
||||
:raises CoreCommandError: when there is a command exception
|
||||
"""
|
||||
# TODO: save/restore the PROMISC flag
|
||||
self.old_up = False
|
||||
self.old_addrs: List[Tuple[str, Optional[str]]] = []
|
||||
localname = self.iface.localname
|
||||
|
@ -397,7 +178,7 @@ class Rj45Node(CoreNodeBase):
|
|||
self.old_addrs.append((items[1], None))
|
||||
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.
|
||||
|
||||
|
@ -437,3 +218,69 @@ class Rj45Node(CoreNodeBase):
|
|||
|
||||
def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
|
||||
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)
|
||||
|
|
345
daemon/core/nodes/wireless.py
Normal file
345
daemon/core/nodes/wireless.py
Normal 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
450
daemon/core/player.py
Normal 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
|
||||
)
|
|
@ -4,22 +4,46 @@ sdt.py: Scripted Display Tool (SDT3D) helper
|
|||
|
||||
import logging
|
||||
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 core.constants import CORE_CONF_DIR, CORE_DATA_DIR
|
||||
from core.constants import CORE_CONF_DIR
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import LinkData, NodeData
|
||||
from core.emulator.enumerations import EventTypes, MessageFlags
|
||||
from core.errors import CoreError
|
||||
from core.nodes.base import CoreNetworkBase, NodeBase
|
||||
from core.nodes.network import WlanNode
|
||||
from core.nodes.base import CoreNode, NodeBase
|
||||
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__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
Helper class for exporting session objects to NRL"s SDT3D.
|
||||
|
@ -48,16 +64,18 @@ class Sdt:
|
|||
DEFAULT_ALT: int = 2500
|
||||
# TODO: read in user"s nodes.conf here; below are default node types from the GUI
|
||||
DEFAULT_SPRITES: Dict[str, str] = [
|
||||
("router", "router.gif"),
|
||||
("host", "host.gif"),
|
||||
("PC", "pc.gif"),
|
||||
("mdr", "mdr.gif"),
|
||||
("prouter", "router_green.gif"),
|
||||
("hub", "hub.gif"),
|
||||
("lanswitch", "lanswitch.gif"),
|
||||
("wlan", "wlan.gif"),
|
||||
("rj45", "rj45.gif"),
|
||||
("tunnel", "tunnel.gif"),
|
||||
("router", "router.png"),
|
||||
("host", "host.png"),
|
||||
("PC", "pc.png"),
|
||||
("mdr", "mdr.png"),
|
||||
("prouter", "prouter.png"),
|
||||
("hub", "hub.png"),
|
||||
("lanswitch", "lanswitch.png"),
|
||||
("wlan", "wlan.png"),
|
||||
("emane", "emane.png"),
|
||||
("wireless", "wireless.png"),
|
||||
("rj45", "rj45.png"),
|
||||
("tunnel", "tunnel.png"),
|
||||
]
|
||||
|
||||
def __init__(self, session: "Session") -> None:
|
||||
|
@ -67,7 +85,7 @@ class Sdt:
|
|||
:param session: session this manager is tied to
|
||||
"""
|
||||
self.session: "Session" = session
|
||||
self.sock: Optional[IO] = None
|
||||
self.sock: Optional[socket.socket] = None
|
||||
self.connected: bool = False
|
||||
self.url: str = self.DEFAULT_SDT_URL
|
||||
self.address: Optional[Tuple[Optional[str], Optional[int]]] = None
|
||||
|
@ -83,7 +101,7 @@ class Sdt:
|
|||
|
||||
: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:
|
||||
"""
|
||||
|
@ -92,7 +110,7 @@ class Sdt:
|
|||
|
||||
: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.address = (self.url.hostname, self.url.port)
|
||||
self.protocol = self.url.scheme
|
||||
|
@ -140,7 +158,7 @@ class Sdt:
|
|||
|
||||
: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
|
||||
# send node type to icon mappings
|
||||
for node_type, icon in self.DEFAULT_SPRITES:
|
||||
|
@ -162,7 +180,6 @@ class Sdt:
|
|||
logger.error("error closing socket")
|
||||
finally:
|
||||
self.sock = None
|
||||
|
||||
self.connected = False
|
||||
|
||||
def shutdown(self) -> None:
|
||||
|
@ -190,7 +207,6 @@ class Sdt:
|
|||
"""
|
||||
if self.sock is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
cmd = f"{cmdstr}\n".encode()
|
||||
logger.debug("sdt cmd: %s", cmd)
|
||||
|
@ -210,26 +226,23 @@ class Sdt:
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
nets = []
|
||||
# create layers
|
||||
for layer in CORE_LAYERS:
|
||||
self.cmd(f"layer {layer}")
|
||||
|
||||
with self.session.nodes_lock:
|
||||
for node_id in self.session.nodes:
|
||||
node = self.session.nodes[node_id]
|
||||
if isinstance(node, CoreNetworkBase):
|
||||
nets = []
|
||||
for node in self.session.nodes.values():
|
||||
if isinstance(node, (EmaneNet, WlanNode)):
|
||||
nets.append(node)
|
||||
if not isinstance(node, NodeBase):
|
||||
continue
|
||||
self.add_node(node)
|
||||
|
||||
for net in nets:
|
||||
all_links = net.links(flags=MessageFlags.ADD)
|
||||
for link_data in all_links:
|
||||
is_wireless = isinstance(net, (WlanNode, EmaneNet))
|
||||
if is_wireless and link_data.node1_id == net.id:
|
||||
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 link_data in net.links(MessageFlags.ADD):
|
||||
self.handle_link_update(link_data)
|
||||
|
||||
def get_node_position(self, node: NodeBase) -> Optional[str]:
|
||||
|
@ -258,13 +271,14 @@ class Sdt:
|
|||
pos = self.get_node_position(node)
|
||||
if not pos:
|
||||
return
|
||||
node_type = node.type
|
||||
if node_type is None:
|
||||
node_type = type(node).type
|
||||
if isinstance(node, CoreNode):
|
||||
node_type = node.model
|
||||
else:
|
||||
node_type = NODE_TYPES.get(type(node), "PC")
|
||||
icon = node.icon
|
||||
if icon:
|
||||
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))
|
||||
self.cmd(f"sprite {node_type} image {icon}")
|
||||
self.cmd(
|
||||
|
@ -341,7 +355,7 @@ class Sdt:
|
|||
result = False
|
||||
try:
|
||||
node = self.session.get_node(node_id, NodeBase)
|
||||
result = isinstance(node, (WlanNode, EmaneNet))
|
||||
result = isinstance(node, (WlanNode, EmaneNet, WirelessNode))
|
||||
except CoreError:
|
||||
pass
|
||||
return result
|
||||
|
|
104
daemon/core/scripts/cleanup.py
Executable file
104
daemon/core/scripts/cleanup.py
Executable 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()
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
from argparse import (
|
||||
|
@ -28,11 +27,13 @@ from core.api.grpc.wrappers import (
|
|||
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]:
|
||||
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:
|
||||
|
@ -122,18 +123,15 @@ def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int:
|
|||
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_mask = ip4_net.prefixlen if ip4_net else None
|
||||
ip6 = str(ip6_net.ip) if ip6_net else None
|
||||
ip6_mask = ip6_net.prefixlen if ip6_net else None
|
||||
return Interface(
|
||||
id=iface_id,
|
||||
mac=mac,
|
||||
ip4=ip4,
|
||||
ip4_mask=ip4_mask,
|
||||
ip6=ip6,
|
||||
ip6_mask=ip6_mask,
|
||||
id=iface_id, 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():
|
||||
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}"
|
||||
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")
|
||||
for link in session.links:
|
||||
n1 = session.nodes[link.node1_id].name
|
||||
n2 = session.nodes[link.node2_id].name
|
||||
print(f"Node | ", end="")
|
||||
print("Node | ", end="")
|
||||
print_iface_header()
|
||||
print(f"{n1:<6} | ", end="")
|
||||
if link.iface1:
|
||||
|
@ -248,7 +248,9 @@ def query_node(core: CoreGrpcClient, args: Namespace) -> None:
|
|||
print("ID | Name | Type | XY | Geo")
|
||||
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}"
|
||||
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:
|
||||
print("Interfaces")
|
||||
print("Connected To | ", end="")
|
||||
|
@ -348,10 +350,14 @@ def add_link(core: CoreGrpcClient, args: Namespace) -> None:
|
|||
session_id = get_current_session(core, args.session)
|
||||
iface1 = 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
|
||||
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(
|
||||
bandwidth=args.bandwidth,
|
||||
loss=args.loss,
|
||||
|
@ -432,13 +438,17 @@ def setup_node_parser(parent) -> None:
|
|||
add_parser.add_argument(
|
||||
"-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_argument("-p", "--pos", type=position_type, help="x,y 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("-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)
|
||||
|
||||
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.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.add_argument("-p", "--pos", type=position_type, help="x,y 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("-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-m", "--iface1-mac", type=mac_type, help="node1 interface mac")
|
||||
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(
|
||||
"-i1-m", "--iface1-mac", type=mac_type, help="node1 interface mac"
|
||||
)
|
||||
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-m", "--iface2-mac", type=mac_type, help="node2 interface mac")
|
||||
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(
|
||||
"-i2-m", "--iface2-mac", type=mac_type, help="node2 interface mac"
|
||||
)
|
||||
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("-l", "--loss", type=float, help="loss (%%)")
|
||||
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("-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)
|
||||
|
||||
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.formatter_class = ArgumentDefaultsHelpFormatter
|
||||
delete_parser.add_argument("-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(
|
||||
"-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("-i2", "--iface2", type=int, help="node2 interface id")
|
||||
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.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)
|
||||
|
||||
node_parser = subparsers.add_parser("node", help="query node")
|
||||
node_parser.formatter_class = ArgumentDefaultsHelpFormatter
|
||||
node_parser.add_argument("-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.add_argument(
|
||||
"-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)
|
||||
|
||||
|
||||
def setup_xml_parser(parent) -> None:
|
||||
parser = parent.add_parser("xml", help="open session xml")
|
||||
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.set_defaults(func=open_xml)
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
core-daemon: the CORE daemon is a server process that receives CORE API
|
||||
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 logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
|
||||
from core import constants
|
||||
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.utils import close_onexec, load_logging_config
|
||||
from core.emulator.coreemu import CoreEmu
|
||||
from core.utils import load_logging_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -34,20 +29,6 @@ def banner():
|
|||
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):
|
||||
"""
|
||||
Start the CoreServer object and enter the server loop.
|
||||
|
@ -55,34 +36,13 @@ def cored(cfg):
|
|||
:param dict cfg: core configuration
|
||||
: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
|
||||
grpc_server = CoreGrpcServer(server.coreemu)
|
||||
coreemu = CoreEmu(cfg)
|
||||
grpc_server = CoreGrpcServer(coreemu)
|
||||
address_config = cfg["grpcaddress"]
|
||||
port_config = cfg["grpcport"]
|
||||
grpc_address = f"{address_config}:{port_config}"
|
||||
grpc_thread = threading.Thread(target=grpc_server.listen, args=(grpc_address,), daemon=True)
|
||||
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()
|
||||
grpc_server.listen(grpc_address)
|
||||
|
||||
|
||||
def get_merged_config(filename):
|
||||
|
@ -98,49 +58,55 @@ def get_merged_config(filename):
|
|||
default_grpc_port = "50051"
|
||||
default_address = "localhost"
|
||||
defaults = {
|
||||
"port": str(CORE_API_PORT),
|
||||
"listenaddr": default_address,
|
||||
"grpcport": default_grpc_port,
|
||||
"grpcaddress": default_address,
|
||||
"logfile": default_log
|
||||
"logfile": default_log,
|
||||
}
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
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("-p", "--port", dest="port", type=int,
|
||||
help=f"port number to listen on; default = {CORE_API_PORT}")
|
||||
parser.add_argument("--ovs", 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}")
|
||||
|
||||
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(
|
||||
"--ovs",
|
||||
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
|
||||
args = parser.parse_args()
|
||||
|
||||
# convert ovs to internal format
|
||||
args.ovs = "1" if args.ovs else "0"
|
||||
|
||||
# read the config file
|
||||
if args.configfile is not None:
|
||||
filename = args.configfile
|
||||
del args.configfile
|
||||
cfg = ConfigParser(defaults)
|
||||
cfg.read(filename)
|
||||
|
||||
section = "core-daemon"
|
||||
if not cfg.has_section(section):
|
||||
cfg.add_section(section)
|
||||
|
||||
# merge argparse with configparser
|
||||
for opt in vars(args):
|
||||
val = getattr(args, opt)
|
||||
if val is not None:
|
||||
cfg.set(section, opt, str(val))
|
||||
|
||||
return dict(cfg.items(section))
|
||||
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
@ -9,12 +8,19 @@ from core.gui.app import Application
|
|||
|
||||
def main() -> None:
|
||||
# parse flags
|
||||
parser = argparse.ArgumentParser(description=f"CORE Python GUI")
|
||||
parser.add_argument("-l", "--level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], default="INFO",
|
||||
help="logging level")
|
||||
parser = argparse.ArgumentParser(description="CORE Python GUI")
|
||||
parser.add_argument(
|
||||
"-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("-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()
|
||||
|
||||
# check home directory exists and create if necessary
|
||||
|
@ -25,9 +31,13 @@ def main() -> None:
|
|||
# setup logging
|
||||
log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s"
|
||||
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)
|
||||
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)
|
||||
|
||||
# start app
|
51
daemon/core/scripts/player.py
Executable file
51
daemon/core/scripts/player.py
Executable 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()
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import enum
|
||||
import select
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import re
|
||||
from io import TextIOWrapper
|
||||
|
@ -6,9 +5,15 @@ from io import TextIOWrapper
|
|||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=f"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")
|
||||
description="Helps transition older CORE services to work with newer versions"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--file",
|
||||
dest="file",
|
||||
type=argparse.FileType("r"),
|
||||
help="service file to update",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
|
@ -20,17 +25,32 @@ def update_service(service_file: TextIOWrapper) -> None:
|
|||
# rename dirs to directories
|
||||
line = re.sub(r"^(\s+)dirs", r"\1directories", line)
|
||||
# fix import states for service
|
||||
line = re.sub(r"^.+import.+CoreService.+$",
|
||||
r"from core.services.coreservices import CoreService", line)
|
||||
line = re.sub(
|
||||
r"^.+import.+CoreService.+$",
|
||||
r"from core.services.coreservices import CoreService",
|
||||
line,
|
||||
)
|
||||
# fix method signatures
|
||||
line = re.sub(r"def generateconfig\(cls, node, filename, services\)",
|
||||
r"def generate_config(cls, node, filename)", line)
|
||||
line = re.sub(r"def getvalidate\(cls, node, services\)",
|
||||
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)
|
||||
line = re.sub(
|
||||
r"def generateconfig\(cls, node, filename, services\)",
|
||||
r"def generate_config(cls, node, filename)",
|
||||
line,
|
||||
)
|
||||
line = re.sub(
|
||||
r"def getvalidate\(cls, node, services\)",
|
||||
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
|
||||
if re.search(r"addservice\(", line):
|
||||
continue
|
|
@ -109,114 +109,6 @@ class ServiceDependencies:
|
|||
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:
|
||||
"""
|
||||
Manages services available for CORE nodes to use.
|
||||
|
@ -342,26 +234,6 @@ class CoreServices:
|
|||
"""
|
||||
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(
|
||||
self, node_id: int, service_name: str, default_service: bool = False
|
||||
) -> "CoreService":
|
||||
|
@ -401,21 +273,21 @@ class CoreServices:
|
|||
node_services[service.name] = service
|
||||
|
||||
def add_services(
|
||||
self, node: CoreNode, node_type: str, services: List[str] = None
|
||||
self, node: CoreNode, model: str, services: List[str] = None
|
||||
) -> None:
|
||||
"""
|
||||
Add services to a node.
|
||||
|
||||
: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
|
||||
:return: nothing
|
||||
"""
|
||||
if not services:
|
||||
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)
|
||||
for service_name in services:
|
||||
service = self.get_service(node.id, service_name, default_service=True)
|
||||
|
|
|
@ -7,15 +7,26 @@ from typing import Optional, Tuple
|
|||
import netaddr
|
||||
|
||||
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.network import PtpNet, WlanNode
|
||||
from core.nodes.physical import Rj45Node
|
||||
from core.nodes.wireless import WirelessNode
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
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):
|
||||
name: str = "FRRzebra"
|
||||
group: str = "FRR"
|
||||
|
@ -127,11 +138,11 @@ class FRRZebra(CoreService):
|
|||
"""
|
||||
Generate a shell script used to boot the FRR daemons.
|
||||
"""
|
||||
frr_bin_search = node.session.options.get_config(
|
||||
"frr_bin_search", default='"/usr/local/bin /usr/bin /usr/lib/frr"'
|
||||
frr_bin_search = node.session.options.get(
|
||||
"frr_bin_search", '"/usr/local/bin /usr/bin /usr/lib/frr"'
|
||||
)
|
||||
frr_sbin_search = node.session.options.get_config(
|
||||
"frr_sbin_search", default='"/usr/local/sbin /usr/sbin /usr/lib/frr"'
|
||||
frr_sbin_search = node.session.options.get(
|
||||
"frr_sbin_search", '"/usr/local/sbin /usr/sbin /usr/lib/frr"'
|
||||
)
|
||||
cfg = """\
|
||||
#!/bin/sh
|
||||
|
@ -184,6 +195,10 @@ bootdaemon()
|
|||
flags="$flags -6"
|
||||
fi
|
||||
|
||||
if [ "$1" = "ospfd" ]; then
|
||||
flags="$flags --apiserver"
|
||||
fi
|
||||
|
||||
#force FRR to use CORE generated conf file
|
||||
flags="$flags -d -f $FRR_CONF"
|
||||
$FRR_SBIN_DIR/$1 $flags
|
||||
|
@ -414,12 +429,25 @@ class FRROspfv2(FrrService):
|
|||
for iface in node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
cfg += f" network {ip4} area 0\n"
|
||||
cfg += " ospf opaque-lsa\n"
|
||||
cfg += "!\n"
|
||||
return cfg
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
|
@ -485,18 +513,6 @@ class FRROspfv3(FrrService):
|
|||
@classmethod
|
||||
def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
|
||||
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):
|
||||
|
@ -593,7 +609,7 @@ class FRRBabel(FrrService):
|
|||
|
||||
@classmethod
|
||||
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"
|
||||
else:
|
||||
return " babel wired\n babel split-horizon\n"
|
||||
|
|
|
@ -118,12 +118,6 @@ class NrlSmf(NrlService):
|
|||
ifaces = node.get_ifaces(control=False)
|
||||
if len(ifaces) == 0:
|
||||
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 "NHDP" in servicenames:
|
||||
comments += "# NHDP service is enabled\n"
|
||||
|
@ -586,46 +580,3 @@ class MgenActor(NrlService):
|
|||
return ""
|
||||
cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n"
|
||||
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
|
||||
|
|
|
@ -6,16 +6,26 @@ from typing import Optional, Tuple
|
|||
import netaddr
|
||||
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.enumerations import LinkTypes
|
||||
from core.nodes.base import CoreNode
|
||||
from core.nodes.base import CoreNode, NodeBase
|
||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.nodes.network import PtpNet, WlanNode
|
||||
from core.nodes.physical import Rj45Node
|
||||
from core.nodes.wireless import WirelessNode
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
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):
|
||||
name: str = "zebra"
|
||||
group: str = "Quagga"
|
||||
|
@ -124,11 +134,11 @@ class Zebra(CoreService):
|
|||
"""
|
||||
Generate a shell script used to boot the Quagga daemons.
|
||||
"""
|
||||
quagga_bin_search = node.session.options.get_config(
|
||||
"quagga_bin_search", default='"/usr/local/bin /usr/bin /usr/lib/quagga"'
|
||||
quagga_bin_search = node.session.options.get(
|
||||
"quagga_bin_search", '"/usr/local/bin /usr/bin /usr/lib/quagga"'
|
||||
)
|
||||
quagga_sbin_search = node.session.options.get_config(
|
||||
"quagga_sbin_search", default='"/usr/local/sbin /usr/sbin /usr/lib/quagga"'
|
||||
quagga_sbin_search = node.session.options.get(
|
||||
"quagga_sbin_search", '"/usr/local/sbin /usr/sbin /usr/lib/quagga"'
|
||||
)
|
||||
return """\
|
||||
#!/bin/sh
|
||||
|
@ -431,7 +441,7 @@ class Ospfv3mdr(Ospfv3):
|
|||
@classmethod
|
||||
def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
|
||||
cfg = cls.mtu_check(iface)
|
||||
if iface.net is not None and isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
if is_wireless(iface.net):
|
||||
return (
|
||||
cfg
|
||||
+ """\
|
||||
|
@ -542,7 +552,7 @@ class Babel(QuaggaService):
|
|||
|
||||
@classmethod
|
||||
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"
|
||||
else:
|
||||
return " babel wired\n babel split-horizon\n"
|
||||
|
|
|
@ -44,9 +44,7 @@ class Ucarp(CoreService):
|
|||
"""
|
||||
Returns configuration file text.
|
||||
"""
|
||||
ucarp_bin = node.session.options.get_config(
|
||||
"ucarp_bin", default="/usr/sbin/ucarp"
|
||||
)
|
||||
ucarp_bin = node.session.options.get("ucarp_bin", "/usr/sbin/ucarp")
|
||||
return """\
|
||||
#!/bin/sh
|
||||
# Location of UCARP executable
|
||||
|
|
|
@ -16,7 +16,9 @@ import shlex
|
|||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from subprocess import PIPE, STDOUT, Popen
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
|
@ -214,8 +216,7 @@ def cmd(
|
|||
shell: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Execute a command on the host and return a tuple containing the exit status and
|
||||
result string. stderr output is folded into the stdout result string.
|
||||
Execute a command on the host and returns the combined stderr stdout output.
|
||||
|
||||
:param args: command arguments
|
||||
:param env: environment to run command with
|
||||
|
@ -248,6 +249,25 @@ def cmd(
|
|||
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:
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
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(
|
||||
funcs: List[Tuple[Callable, Iterable[Any], Dict[Any, Any]]], workers: int = 10
|
||||
) -> 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
|
||||
node_id = config_id // IFACE_CONFIG_FACTOR
|
||||
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
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Type, TypeVar
|
||||
from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar
|
||||
|
||||
from lxml import etree
|
||||
|
||||
import core.nodes.base
|
||||
import core.nodes.physical
|
||||
from core import utils
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
|
||||
from core.config import Configuration
|
||||
from core.emane.nodes import EmaneNet, EmaneOptions
|
||||
from core.emulator.data import InterfaceData, LinkOptions
|
||||
from core.emulator.enumerations import EventTypes, NodeTypes
|
||||
from core.errors import CoreXmlError
|
||||
from core.nodes.base import CoreNodeBase, NodeBase
|
||||
from core.nodes.docker import DockerNode
|
||||
from core.nodes.base import CoreNodeBase, CoreNodeOptions, NodeBase, Position
|
||||
from core.nodes.docker import DockerNode, DockerOptions
|
||||
from core.nodes.interface import CoreInterface
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -209,7 +212,7 @@ class ServiceElement:
|
|||
class DeviceElement(NodeElement):
|
||||
def __init__(self, session: "Session", node: NodeBase) -> None:
|
||||
super().__init__(session, node, "device")
|
||||
add_attribute(self.element, "type", node.type)
|
||||
add_attribute(self.element, "type", node.model)
|
||||
self.add_class()
|
||||
self.add_services()
|
||||
|
||||
|
@ -242,21 +245,31 @@ class DeviceElement(NodeElement):
|
|||
class NetworkElement(NodeElement):
|
||||
def __init__(self, session: "Session", node: NodeBase) -> None:
|
||||
super().__init__(session, node, "network")
|
||||
if isinstance(self.node, (WlanNode, EmaneNet)):
|
||||
if self.node.model:
|
||||
add_attribute(self.element, "model", self.node.model.name)
|
||||
if isinstance(self.node, WlanNode):
|
||||
if self.node.wireless_model:
|
||||
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:
|
||||
add_attribute(self.element, "mobility", self.node.mobility.name)
|
||||
if isinstance(self.node, GreTapBridge):
|
||||
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()
|
||||
|
||||
def add_type(self) -> None:
|
||||
if self.node.apitype:
|
||||
node_type = self.node.apitype.name
|
||||
else:
|
||||
node_type = self.node.__class__.__name__
|
||||
add_attribute(self.element, "type", node_type)
|
||||
node_type = self.session.get_node_type(type(self.node))
|
||||
add_attribute(self.element, "type", node_type.name)
|
||||
|
||||
def add_wireless_config(self, config: Dict[str, Configuration]) -> None:
|
||||
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:
|
||||
|
@ -269,8 +282,8 @@ class CoreXmlWriter:
|
|||
|
||||
def write_session(self) -> None:
|
||||
# generate xml content
|
||||
links = self.write_nodes()
|
||||
self.write_links(links)
|
||||
self.write_nodes()
|
||||
self.write_links()
|
||||
self.write_mobility_configs()
|
||||
self.write_emane_configs()
|
||||
self.write_service_configs()
|
||||
|
@ -334,16 +347,9 @@ class CoreXmlWriter:
|
|||
|
||||
def write_session_options(self) -> None:
|
||||
option_elements = etree.Element("session_options")
|
||||
options_config = self.session.options.get_configs()
|
||||
if not options_config:
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
for option in self.session.options.options:
|
||||
value = self.session.options.get(option.id)
|
||||
add_configuration(option_elements, option.id, value)
|
||||
if option_elements.getchildren():
|
||||
self.scenario.append(option_elements)
|
||||
|
||||
|
@ -439,51 +445,47 @@ class CoreXmlWriter:
|
|||
self.scenario.append(service_configurations)
|
||||
|
||||
def write_default_services(self) -> None:
|
||||
node_types = etree.Element("default_services")
|
||||
for node_type in self.session.services.default_services:
|
||||
services = self.session.services.default_services[node_type]
|
||||
node_type = etree.SubElement(node_types, "node", type=node_type)
|
||||
models = etree.Element("default_services")
|
||||
for model in self.session.services.default_services:
|
||||
services = self.session.services.default_services[model]
|
||||
model = etree.SubElement(models, "node", type=model)
|
||||
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():
|
||||
self.scenario.append(node_types)
|
||||
|
||||
def write_nodes(self) -> List[LinkData]:
|
||||
links = []
|
||||
for node_id in self.session.nodes:
|
||||
node = self.session.nodes[node_id]
|
||||
def write_nodes(self) -> None:
|
||||
for node in self.session.nodes.values():
|
||||
# network node
|
||||
is_network_or_rj45 = isinstance(
|
||||
node, (core.nodes.base.CoreNetworkBase, core.nodes.physical.Rj45Node)
|
||||
)
|
||||
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)
|
||||
# device node
|
||||
elif isinstance(node, core.nodes.base.CoreNodeBase):
|
||||
self.write_device(node)
|
||||
|
||||
# add known links
|
||||
links.extend(node.links())
|
||||
return links
|
||||
|
||||
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)
|
||||
self.networks.append(network.element)
|
||||
|
||||
def write_links(self, links: List[LinkData]) -> None:
|
||||
def write_links(self) -> None:
|
||||
link_elements = etree.Element("links")
|
||||
# add link data
|
||||
for link_data in links:
|
||||
# skip basic range links
|
||||
if link_data.iface1 is None and link_data.iface2 is None:
|
||||
continue
|
||||
link_element = self.create_link_element(link_data)
|
||||
for core_link in self.session.link_manager.links():
|
||||
node1, iface1 = core_link.node1, core_link.iface1
|
||||
node2, iface2 = core_link.node2, core_link.iface2
|
||||
unidirectional = core_link.is_unidirectional()
|
||||
link_element = self.create_link_element(
|
||||
node1, iface1, node2, iface2, core_link.options(), unidirectional
|
||||
)
|
||||
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():
|
||||
self.scenario.append(link_elements)
|
||||
|
@ -493,67 +495,71 @@ class CoreXmlWriter:
|
|||
self.devices.append(device.element)
|
||||
|
||||
def create_iface_element(
|
||||
self, element_name: str, node_id: int, iface_data: InterfaceData
|
||||
self, element_name: str, iface: CoreInterface
|
||||
) -> etree.Element:
|
||||
iface_element = etree.Element(element_name)
|
||||
node = self.session.get_node(node_id, NodeBase)
|
||||
if isinstance(node, CoreNodeBase):
|
||||
iface = node.get_iface(iface_data.id)
|
||||
# check if emane interface
|
||||
if isinstance(iface.net, EmaneNet):
|
||||
# check if interface if connected to emane
|
||||
if isinstance(iface.node, CoreNodeBase) and isinstance(iface.net, EmaneNet):
|
||||
nem_id = self.session.emane.get_nem_id(iface)
|
||||
add_attribute(iface_element, "nem", nem_id)
|
||||
add_attribute(iface_element, "id", iface_data.id)
|
||||
add_attribute(iface_element, "name", iface_data.name)
|
||||
add_attribute(iface_element, "mac", iface_data.mac)
|
||||
add_attribute(iface_element, "ip4", iface_data.ip4)
|
||||
add_attribute(iface_element, "ip4_mask", iface_data.ip4_mask)
|
||||
add_attribute(iface_element, "ip6", iface_data.ip6)
|
||||
add_attribute(iface_element, "ip6_mask", iface_data.ip6_mask)
|
||||
ip4 = iface.get_ip4()
|
||||
ip4_mask = None
|
||||
if ip4:
|
||||
ip4_mask = ip4.prefixlen
|
||||
ip4 = str(ip4.ip)
|
||||
ip6 = iface.get_ip6()
|
||||
ip6_mask = None
|
||||
if ip6:
|
||||
ip6_mask = ip6.prefixlen
|
||||
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
|
||||
|
||||
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")
|
||||
add_attribute(link_element, "node1", link_data.node1_id)
|
||||
add_attribute(link_element, "node2", link_data.node2_id)
|
||||
|
||||
add_attribute(link_element, "node1", node1.id)
|
||||
add_attribute(link_element, "node2", node2.id)
|
||||
# check for interface one
|
||||
if link_data.iface1 is not None:
|
||||
iface1 = self.create_iface_element(
|
||||
"iface1", link_data.node1_id, link_data.iface1
|
||||
)
|
||||
if iface1 is not None:
|
||||
iface1 = self.create_iface_element("iface1", iface1)
|
||||
link_element.append(iface1)
|
||||
|
||||
# check for interface two
|
||||
if link_data.iface2 is not None:
|
||||
iface2 = self.create_iface_element(
|
||||
"iface2", link_data.node2_id, link_data.iface2
|
||||
)
|
||||
if iface2 is not None:
|
||||
iface2 = self.create_iface_element("iface2", iface2)
|
||||
link_element.append(iface2)
|
||||
|
||||
# check for options, don't write for emane/wlan links
|
||||
node1 = self.session.get_node(link_data.node1_id, NodeBase)
|
||||
node2 = self.session.get_node(link_data.node2_id, NodeBase)
|
||||
is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet))
|
||||
is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet))
|
||||
if not any([is_node1_wireless, is_node2_wireless]):
|
||||
options_data = link_data.options
|
||||
options = etree.Element("options")
|
||||
add_attribute(options, "delay", options_data.delay)
|
||||
add_attribute(options, "bandwidth", options_data.bandwidth)
|
||||
add_attribute(options, "loss", options_data.loss)
|
||||
add_attribute(options, "dup", options_data.dup)
|
||||
add_attribute(options, "jitter", options_data.jitter)
|
||||
add_attribute(options, "mer", options_data.mer)
|
||||
add_attribute(options, "burst", options_data.burst)
|
||||
add_attribute(options, "mburst", options_data.mburst)
|
||||
add_attribute(options, "unidirectional", options_data.unidirectional)
|
||||
add_attribute(options, "network_id", link_data.network_id)
|
||||
add_attribute(options, "key", options_data.key)
|
||||
add_attribute(options, "buffer", options_data.buffer)
|
||||
if options.items():
|
||||
link_element.append(options)
|
||||
|
||||
is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet, WirelessNode))
|
||||
is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet, WirelessNode))
|
||||
if not (is_node1_wireless or is_node2_wireless):
|
||||
unidirectional = 1 if unidirectional else 0
|
||||
options_element = etree.Element("options")
|
||||
add_attribute(options_element, "delay", options.delay)
|
||||
add_attribute(options_element, "bandwidth", options.bandwidth)
|
||||
add_attribute(options_element, "loss", options.loss)
|
||||
add_attribute(options_element, "dup", options.dup)
|
||||
add_attribute(options_element, "jitter", options.jitter)
|
||||
add_attribute(options_element, "mer", options.mer)
|
||||
add_attribute(options_element, "burst", options.burst)
|
||||
add_attribute(options_element, "mburst", options.mburst)
|
||||
add_attribute(options_element, "unidirectional", unidirectional)
|
||||
add_attribute(options_element, "key", options.key)
|
||||
add_attribute(options_element, "buffer", options.buffer)
|
||||
if options_element.items():
|
||||
link_element.append(options_element)
|
||||
return link_element
|
||||
|
||||
|
||||
|
@ -586,14 +592,12 @@ class CoreXmlReader:
|
|||
return
|
||||
|
||||
for node in default_services.iterchildren():
|
||||
node_type = node.get("type")
|
||||
model = node.get("type")
|
||||
services = []
|
||||
for service in node.iterchildren():
|
||||
services.append(service.get("name"))
|
||||
logger.info(
|
||||
"reading default services for nodes(%s): %s", node_type, services
|
||||
)
|
||||
self.session.services.default_services[node_type] = services
|
||||
logger.info("reading default services for nodes(%s): %s", model, services)
|
||||
self.session.services.default_services[model] = services
|
||||
|
||||
def read_session_metadata(self) -> None:
|
||||
session_metadata = self.scenario.find("session_metadata")
|
||||
|
@ -618,8 +622,7 @@ class CoreXmlReader:
|
|||
value = configuration.get("value")
|
||||
xml_config[name] = value
|
||||
logger.info("reading session options: %s", xml_config)
|
||||
config = self.session.options.get_configs()
|
||||
config.update(xml_config)
|
||||
self.session.options.update(xml_config)
|
||||
|
||||
def read_session_hooks(self) -> None:
|
||||
session_hooks = self.scenario.find("session_hooks")
|
||||
|
@ -799,71 +802,85 @@ class CoreXmlReader:
|
|||
clazz = device_element.get("class")
|
||||
image = device_element.get("image")
|
||||
server = device_element.get("server")
|
||||
options = NodeOptions(
|
||||
name=name, model=model, image=image, icon=icon, server=server
|
||||
)
|
||||
canvas = get_int(device_element, "canvas")
|
||||
node_type = NodeTypes.DEFAULT
|
||||
if clazz == "docker":
|
||||
node_type = NodeTypes.DOCKER
|
||||
elif clazz == "lxc":
|
||||
node_type = NodeTypes.LXC
|
||||
_class = self.session.get_node_class(node_type)
|
||||
|
||||
options = _class.create_options()
|
||||
options.icon = icon
|
||||
options.canvas = canvas
|
||||
# check for special options
|
||||
if isinstance(options, CoreNodeOptions):
|
||||
options.model = model
|
||||
service_elements = device_element.find("services")
|
||||
if service_elements is not None:
|
||||
options.services = [x.get("name") for x in 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 = [
|
||||
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 = None
|
||||
if position_element is not None:
|
||||
position = Position()
|
||||
x = get_float(position_element, "x")
|
||||
y = get_float(position_element, "y")
|
||||
if all([x, y]):
|
||||
options.set_position(x, y)
|
||||
|
||||
position.set(x, y)
|
||||
lat = get_float(position_element, "lat")
|
||||
lon = get_float(position_element, "lon")
|
||||
alt = get_float(position_element, "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)
|
||||
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:
|
||||
node_id = get_int(network_element, "id")
|
||||
name = network_element.get("name")
|
||||
server = network_element.get("server")
|
||||
node_type = NodeTypes[network_element.get("type")]
|
||||
_class = self.session.get_node_class(node_type)
|
||||
icon = network_element.get("icon")
|
||||
server = network_element.get("server")
|
||||
options = NodeOptions(name=name, icon=icon, server=server)
|
||||
if node_type == NodeTypes.EMANE:
|
||||
model = network_element.get("model")
|
||||
options.emane = model
|
||||
|
||||
options = _class.create_options()
|
||||
options.canvas = get_int(network_element, "canvas")
|
||||
options.icon = network_element.get("icon")
|
||||
if isinstance(options, EmaneOptions):
|
||||
options.emane_model = network_element.get("model")
|
||||
position_element = network_element.find("position")
|
||||
position = None
|
||||
if position_element is not None:
|
||||
position = Position()
|
||||
x = get_float(position_element, "x")
|
||||
y = get_float(position_element, "y")
|
||||
if all([x, y]):
|
||||
options.set_position(x, y)
|
||||
|
||||
position.set(x, y)
|
||||
lat = get_float(position_element, "lat")
|
||||
lon = get_float(position_element, "lon")
|
||||
alt = get_float(position_element, "alt")
|
||||
if all([lat, lon, alt]):
|
||||
options.set_location(lat, lon, alt)
|
||||
|
||||
position.set_geo(lon, lat, alt)
|
||||
logger.info(
|
||||
"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:
|
||||
configservice_configs = self.scenario.find("configservice_configurations")
|
||||
|
|
|
@ -162,12 +162,14 @@ def build_platform_xml(
|
|||
"""
|
||||
# create top level platform element
|
||||
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
|
||||
value = config[configuration.id]
|
||||
add_param(platform_element, name, value)
|
||||
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
|
||||
|
@ -177,7 +179,7 @@ def build_platform_xml(
|
|||
)
|
||||
|
||||
# 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
|
||||
if is_external(config):
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
# 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.
|
||||
#
|
||||
|
|
1066
daemon/poetry.lock
generated
1066
daemon/poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -58,3 +58,13 @@ message GetNodeConfigServiceRequest {
|
|||
message GetNodeConfigServiceResponse {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -61,6 +61,8 @@ service CoreApi {
|
|||
}
|
||||
rpc DeleteLink (DeleteLinkRequest) returns (DeleteLinkResponse) {
|
||||
}
|
||||
rpc Linked (LinkedRequest) returns (LinkedResponse) {
|
||||
}
|
||||
|
||||
// mobility rpc
|
||||
rpc GetMobilityConfig (mobility.GetMobilityConfigRequest) returns (mobility.GetMobilityConfigResponse) {
|
||||
|
@ -89,6 +91,8 @@ service CoreApi {
|
|||
}
|
||||
rpc ConfigServiceAction (services.ServiceActionRequest) returns (services.ServiceActionResponse) {
|
||||
}
|
||||
rpc GetConfigServiceRendered (configservices.GetConfigServiceRenderedRequest) returns (configservices.GetConfigServiceRenderedResponse) {
|
||||
}
|
||||
|
||||
// wlan rpc
|
||||
rpc GetWlanConfig (wlan.GetWlanConfigRequest) returns (wlan.GetWlanConfigResponse) {
|
||||
|
@ -98,6 +102,14 @@ service CoreApi {
|
|||
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
|
||||
rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) {
|
||||
}
|
||||
|
@ -280,12 +292,11 @@ message ConfigEvent {
|
|||
repeated int32 data_types = 5;
|
||||
string data_values = 6;
|
||||
string captions = 7;
|
||||
string bitmap = 8;
|
||||
string possible_values = 9;
|
||||
string groups = 10;
|
||||
int32 iface_id = 11;
|
||||
int32 network_id = 12;
|
||||
string opaque = 13;
|
||||
string possible_values = 8;
|
||||
string groups = 9;
|
||||
int32 iface_id = 10;
|
||||
int32 network_id = 11;
|
||||
string opaque = 12;
|
||||
}
|
||||
|
||||
message ExceptionEvent {
|
||||
|
@ -615,6 +626,7 @@ message Node {
|
|||
map<string, services.NodeServiceConfig> service_configs = 18;
|
||||
map<string, configservices.ConfigServiceConfig> config_service_configs= 19;
|
||||
repeated emane.NodeEmaneConfig emane_configs = 20;
|
||||
map<string, common.ConfigOption> wireless_config = 21;
|
||||
}
|
||||
|
||||
message Link {
|
||||
|
@ -656,6 +668,8 @@ message Interface {
|
|||
int32 mtu = 10;
|
||||
int32 node_id = 11;
|
||||
int32 net2_id = 12;
|
||||
int32 nem_id = 13;
|
||||
int32 nem_port = 14;
|
||||
}
|
||||
|
||||
message SessionLocation {
|
||||
|
@ -684,3 +698,47 @@ message Server {
|
|||
string name = 1;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ message ServiceAction {
|
|||
}
|
||||
|
||||
message ServiceDefaults {
|
||||
string node_type = 1;
|
||||
string model = 1;
|
||||
repeated string services = 2;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "core"
|
||||
version = "8.2.0"
|
||||
version = "9.0.0"
|
||||
description = "CORE Common Open Research Emulator"
|
||||
authors = ["Boeing Research and Technology"]
|
||||
license = "BSD-2-Clause"
|
||||
|
@ -14,29 +14,38 @@ include = [
|
|||
]
|
||||
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]
|
||||
python = "^3.6"
|
||||
dataclasses = { version = "*", python = "~3.6" }
|
||||
fabric = "2.5.0"
|
||||
grpcio = "1.27.2"
|
||||
python = "^3.9"
|
||||
fabric = "2.7.1"
|
||||
grpcio = "1.49.1"
|
||||
invoke = "1.4.1"
|
||||
lxml = "4.6.5"
|
||||
mako = "1.1.3"
|
||||
lxml = "4.9.1"
|
||||
netaddr = "0.7.19"
|
||||
pillow = "8.3.2"
|
||||
protobuf = "3.19.4"
|
||||
pyproj = "2.6.1.post1"
|
||||
protobuf = "3.19.5"
|
||||
pyproj = "3.3.1"
|
||||
pyyaml = "5.4"
|
||||
Pillow = "9.2.0"
|
||||
Mako = "1.2.3"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "==19.3b0"
|
||||
flake8 = "3.8.2"
|
||||
grpcio-tools = "1.27.2"
|
||||
grpcio-tools = "1.43.0"
|
||||
isort = "4.3.21"
|
||||
mock = "4.0.2"
|
||||
pre-commit = "2.1.1"
|
||||
pytest = "5.4.3"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "6.2.5"
|
||||
|
||||
[tool.isort]
|
||||
skip_glob = "*_pb2*.py,doc,build"
|
||||
|
|
|
@ -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*
|
|
@ -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)
|
|
@ -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()
|
|
@ -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()
|
|
@ -7,11 +7,9 @@ import time
|
|||
|
||||
import mock
|
||||
import pytest
|
||||
from mock.mock import MagicMock
|
||||
|
||||
from core.api.grpc.client import InterfaceHelper
|
||||
from core.api.grpc.server import CoreGrpcServer
|
||||
from core.api.tlv.corehandlers import CoreHandler
|
||||
from core.emulator.coreemu import CoreEmu
|
||||
from core.emulator.data import IpPrefixes
|
||||
from core.emulator.distributed import DistributedServer
|
||||
|
@ -61,8 +59,6 @@ def patcher(request):
|
|||
LinuxNetClient, "get_mac", return_value="00:00:00:00:00:00"
|
||||
)
|
||||
patch_manager.patch_obj(CoreNode, "create_file")
|
||||
patch_manager.patch_obj(Session, "write_state")
|
||||
patch_manager.patch_obj(Session, "write_nodes")
|
||||
yield patch_manager
|
||||
patch_manager.shutdown()
|
||||
|
||||
|
@ -104,17 +100,6 @@ def module_grpc(global_coreemu):
|
|||
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
|
||||
def grpc_server(module_grpc):
|
||||
yield module_grpc
|
||||
|
@ -130,16 +115,6 @@ def session(global_session):
|
|||
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):
|
||||
parser.addoption("--distributed", help="distributed server address")
|
||||
parser.addoption("--mock", action="store_true", help="run without mocking")
|
||||
|
|
|
@ -16,10 +16,10 @@ from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
|
|||
from core.emane.models.rfpipe import EmaneRfPipeModel
|
||||
from core.emane.models.tdma import EmaneTdmaModel
|
||||
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.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
from core.nodes.base import CoreNode, Position
|
||||
|
||||
_EMANE_MODELS = [
|
||||
EmaneIeee80211abgModel,
|
||||
|
@ -53,19 +53,22 @@ class TestEmane:
|
|||
"""
|
||||
# create emane node for networking the core nodes
|
||||
session.set_location(47.57917, -122.13232, 2.00000, 1.0)
|
||||
options = NodeOptions()
|
||||
options.set_position(80, 50)
|
||||
options.emane = EmaneIeee80211abgModel.name
|
||||
emane_net1 = session.add_node(EmaneNet, options=options)
|
||||
options.emane = EmaneRfPipeModel.name
|
||||
emane_net2 = session.add_node(EmaneNet, options=options)
|
||||
options = EmaneNet.create_options()
|
||||
options.emane_model = EmaneIeee80211abgModel.name
|
||||
position = Position(x=80, y=50)
|
||||
emane_net1 = session.add_node(EmaneNet, position=position, options=options)
|
||||
options = EmaneNet.create_options()
|
||||
options.emane_model = EmaneRfPipeModel.name
|
||||
position = Position(x=80, y=50)
|
||||
emane_net2 = session.add_node(EmaneNet, position=position, options=options)
|
||||
|
||||
# create nodes
|
||||
options = NodeOptions(model="mdr")
|
||||
options.set_position(150, 150)
|
||||
node1 = session.add_node(CoreNode, options=options)
|
||||
options.set_position(300, 150)
|
||||
node2 = session.add_node(CoreNode, options=options)
|
||||
options = CoreNode.create_options()
|
||||
options.model = "mdr"
|
||||
position = Position(x=150, y=150)
|
||||
node1 = session.add_node(CoreNode, position=position, options=options)
|
||||
position = Position(x=300, y=150)
|
||||
node2 = session.add_node(CoreNode, position=position, options=options)
|
||||
|
||||
# create interfaces
|
||||
ip_prefix1 = IpPrefixes("10.0.0.0/24")
|
||||
|
@ -100,9 +103,10 @@ class TestEmane:
|
|||
|
||||
# create emane node for networking the core nodes
|
||||
session.set_location(47.57917, -122.13232, 2.00000, 1.0)
|
||||
options = NodeOptions(emane=model.name)
|
||||
options.set_position(80, 50)
|
||||
emane_network = session.add_node(EmaneNet, options=options)
|
||||
options = EmaneNet.create_options()
|
||||
options.emane_model = model.name
|
||||
position = Position(x=80, y=50)
|
||||
emane_network = session.add_node(EmaneNet, position=position, options=options)
|
||||
|
||||
# configure tdma
|
||||
if model == EmaneTdmaModel:
|
||||
|
@ -111,11 +115,12 @@ class TestEmane:
|
|||
)
|
||||
|
||||
# create nodes
|
||||
options = NodeOptions(model="mdr")
|
||||
options.set_position(150, 150)
|
||||
node1 = session.add_node(CoreNode, options=options)
|
||||
options.set_position(300, 150)
|
||||
node2 = session.add_node(CoreNode, options=options)
|
||||
options = CoreNode.create_options()
|
||||
options.model = "mdr"
|
||||
position = Position(x=150, y=150)
|
||||
node1 = session.add_node(CoreNode, position=position, options=options)
|
||||
position = Position(x=300, y=150)
|
||||
node2 = session.add_node(CoreNode, position=position, options=options)
|
||||
|
||||
for i, node in enumerate([node1, node2]):
|
||||
node.setposition(x=150 * (i + 1), y=150)
|
||||
|
@ -141,9 +146,10 @@ class TestEmane:
|
|||
"""
|
||||
# create emane node for networking the core nodes
|
||||
session.set_location(47.57917, -122.13232, 2.00000, 1.0)
|
||||
options = NodeOptions(emane=EmaneIeee80211abgModel.name)
|
||||
options.set_position(80, 50)
|
||||
emane_network = session.add_node(EmaneNet, options=options)
|
||||
options = EmaneNet.create_options()
|
||||
options.emane_model = EmaneIeee80211abgModel.name
|
||||
position = Position(x=80, y=50)
|
||||
emane_network = session.add_node(EmaneNet, position=position, options=options)
|
||||
config_key = "txpower"
|
||||
config_value = "10"
|
||||
session.emane.set_config(
|
||||
|
@ -151,11 +157,12 @@ class TestEmane:
|
|||
)
|
||||
|
||||
# create nodes
|
||||
options = NodeOptions(model="mdr")
|
||||
options.set_position(150, 150)
|
||||
node1 = session.add_node(CoreNode, options=options)
|
||||
options.set_position(300, 150)
|
||||
node2 = session.add_node(CoreNode, options=options)
|
||||
options = CoreNode.create_options()
|
||||
options.model = "mdr"
|
||||
position = Position(x=150, y=150)
|
||||
node1 = session.add_node(CoreNode, position=position, options=options)
|
||||
position = Position(x=300, y=150)
|
||||
node2 = session.add_node(CoreNode, position=position, options=options)
|
||||
|
||||
for i, node in enumerate([node1, node2]):
|
||||
node.setposition(x=150 * (i + 1), y=150)
|
||||
|
@ -205,14 +212,17 @@ class TestEmane:
|
|||
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
||||
):
|
||||
# create nodes
|
||||
options = NodeOptions(model="mdr", x=50, y=50)
|
||||
node1 = session.add_node(CoreNode, options=options)
|
||||
options = CoreNode.create_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)
|
||||
node2 = session.add_node(CoreNode, options=options)
|
||||
node2 = session.add_node(CoreNode, position=position, options=options)
|
||||
iface2_data = ip_prefixes.create_iface(node2)
|
||||
|
||||
# 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)
|
||||
|
||||
# create links
|
||||
|
@ -255,11 +265,7 @@ class TestEmane:
|
|||
assert session.get_node(node1.id, CoreNode)
|
||||
assert session.get_node(node2.id, CoreNode)
|
||||
assert session.get_node(emane_node.id, EmaneNet)
|
||||
links = []
|
||||
for node_id in session.nodes:
|
||||
node = session.nodes[node_id]
|
||||
links += node.links()
|
||||
assert len(links) == 2
|
||||
assert len(session.link_manager.links()) == 2
|
||||
config = session.emane.get_config(node1.id, EmaneRfPipeModel.name)
|
||||
assert config["datarate"] == datarate
|
||||
|
||||
|
@ -267,14 +273,17 @@ class TestEmane:
|
|||
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
||||
):
|
||||
# create nodes
|
||||
options = NodeOptions(model="mdr", x=50, y=50)
|
||||
node1 = session.add_node(CoreNode, options=options)
|
||||
options = CoreNode.create_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)
|
||||
node2 = session.add_node(CoreNode, options=options)
|
||||
node2 = session.add_node(CoreNode, position=position, options=options)
|
||||
iface2_data = ip_prefixes.create_iface(node2)
|
||||
|
||||
# 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)
|
||||
|
||||
# create links
|
||||
|
@ -318,10 +327,6 @@ class TestEmane:
|
|||
assert session.get_node(node1.id, CoreNode)
|
||||
assert session.get_node(node2.id, CoreNode)
|
||||
assert session.get_node(emane_node.id, EmaneNet)
|
||||
links = []
|
||||
for node_id in session.nodes:
|
||||
node = session.nodes[node_id]
|
||||
links += node.links()
|
||||
assert len(links) == 2
|
||||
assert len(session.link_manager.links()) == 2
|
||||
config = session.emane.get_config(config_id, EmaneRfPipeModel.name)
|
||||
assert config["datarate"] == datarate
|
||||
|
|
|
@ -8,8 +8,7 @@ from typing import List, Type
|
|||
|
||||
import pytest
|
||||
|
||||
from core.emulator.data import IpPrefixes, NodeOptions
|
||||
from core.emulator.enumerations import MessageFlags
|
||||
from core.emulator.data import IpPrefixes
|
||||
from core.emulator.session import Session
|
||||
from core.errors import CoreCommandError
|
||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||
|
@ -63,44 +62,6 @@ class TestCore:
|
|||
status = ping(node1, node2, ip_prefixes)
|
||||
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):
|
||||
"""
|
||||
Test basic wlan network.
|
||||
|
@ -114,8 +75,8 @@ class TestCore:
|
|||
session.mobility.set_model(wlan_node, BasicRangeModel)
|
||||
|
||||
# create nodes
|
||||
options = NodeOptions(model="mdr")
|
||||
options.set_position(0, 0)
|
||||
options = CoreNode.create_options()
|
||||
options.model = "mdr"
|
||||
node1 = 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)
|
||||
|
||||
# create nodes
|
||||
options = NodeOptions(model="mdr")
|
||||
options.set_position(0, 0)
|
||||
options = CoreNode.create_options()
|
||||
options.model = "mdr"
|
||||
node1 = session.add_node(CoreNode, options=options)
|
||||
node2 = session.add_node(CoreNode, options=options)
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from core.emulator.data import NodeOptions
|
||||
from core.emulator.session import Session
|
||||
from core.nodes.base import CoreNode
|
||||
from core.nodes.network import HubNode
|
||||
|
@ -12,8 +11,7 @@ class TestDistributed:
|
|||
|
||||
# when
|
||||
session.distributed.add_server(server_name, host)
|
||||
options = NodeOptions(server=server_name)
|
||||
node = session.add_node(CoreNode, options=options)
|
||||
node = session.add_node(CoreNode, server=server_name)
|
||||
session.instantiate()
|
||||
|
||||
# then
|
||||
|
@ -29,12 +27,13 @@ class TestDistributed:
|
|||
|
||||
# when
|
||||
session.distributed.add_server(server_name, host)
|
||||
options = NodeOptions(server=server_name)
|
||||
node = session.add_node(HubNode, options=options)
|
||||
node1 = session.add_node(HubNode)
|
||||
node2 = session.add_node(HubNode, server=server_name)
|
||||
session.add_link(node1.id, node2.id)
|
||||
session.instantiate()
|
||||
|
||||
# then
|
||||
assert node.server is not None
|
||||
assert node.server.name == server_name
|
||||
assert node.server.host == host
|
||||
assert len(session.distributed.tunnels) > 0
|
||||
assert node2.server is not None
|
||||
assert node2.server.name == server_name
|
||||
assert node2.server.host == host
|
||||
assert len(session.distributed.tunnels) == 1
|
||||
|
|
|
@ -8,7 +8,7 @@ import grpc
|
|||
import pytest
|
||||
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.server import CoreGrpcServer
|
||||
from core.api.grpc.wrappers import (
|
||||
|
@ -22,6 +22,7 @@ from core.api.grpc.wrappers import (
|
|||
Link,
|
||||
LinkOptions,
|
||||
MobilityAction,
|
||||
MoveNodesRequest,
|
||||
Node,
|
||||
NodeServiceData,
|
||||
NodeType,
|
||||
|
@ -31,12 +32,10 @@ from core.api.grpc.wrappers import (
|
|||
SessionLocation,
|
||||
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.nodes import EmaneNet
|
||||
from core.emulator.data import EventData, IpPrefixes, NodeData, NodeOptions
|
||||
from core.emulator.enumerations import EventTypes, ExceptionLevels
|
||||
from core.emulator.data import EventData, IpPrefixes, NodeData
|
||||
from core.emulator.enumerations import EventTypes, ExceptionLevels, MessageFlags
|
||||
from core.errors import CoreError
|
||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||
from core.nodes.base import CoreNode
|
||||
|
@ -163,7 +162,7 @@ class TestGrpc:
|
|||
real_node1, service_name, service_file
|
||||
)
|
||||
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])
|
||||
def test_create_session(
|
||||
|
@ -351,8 +350,7 @@ class TestGrpc:
|
|||
client = CoreGrpcClient()
|
||||
session = grpc_server.coreemu.create_session()
|
||||
session.set_state(EventTypes.CONFIGURATION_STATE)
|
||||
options = NodeOptions(model="Host")
|
||||
node = session.add_node(CoreNode, options=options)
|
||||
node = session.add_node(CoreNode)
|
||||
session.instantiate()
|
||||
expected_output = "hello world"
|
||||
expected_status = 0
|
||||
|
@ -370,8 +368,7 @@ class TestGrpc:
|
|||
client = CoreGrpcClient()
|
||||
session = grpc_server.coreemu.create_session()
|
||||
session.set_state(EventTypes.CONFIGURATION_STATE)
|
||||
options = NodeOptions(model="Host")
|
||||
node = session.add_node(CoreNode, options=options)
|
||||
node = session.add_node(CoreNode)
|
||||
session.instantiate()
|
||||
|
||||
# then
|
||||
|
@ -415,7 +412,7 @@ class TestGrpc:
|
|||
session = grpc_server.coreemu.create_session()
|
||||
switch = session.add_node(SwitchNode)
|
||||
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)
|
||||
link = Link(node.id, switch.id, iface1=iface)
|
||||
|
||||
|
@ -425,7 +422,7 @@ class TestGrpc:
|
|||
|
||||
# then
|
||||
assert result is True
|
||||
assert len(switch.links()) == 1
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert iface1.id == iface.id
|
||||
assert iface1.ip4 == iface.ip4
|
||||
|
||||
|
@ -445,13 +442,14 @@ class TestGrpc:
|
|||
# given
|
||||
client = CoreGrpcClient()
|
||||
session = grpc_server.coreemu.create_session()
|
||||
session.set_state(EventTypes.CONFIGURATION_STATE)
|
||||
switch = session.add_node(SwitchNode)
|
||||
node = session.add_node(CoreNode)
|
||||
iface = ip_prefixes.create_iface(node)
|
||||
session.add_link(node.id, switch.id, iface)
|
||||
iface_data = ip_prefixes.create_iface(node)
|
||||
iface, _ = session.add_link(node.id, switch.id, iface_data)
|
||||
session.instantiate()
|
||||
options = LinkOptions(bandwidth=30000)
|
||||
link = switch.links()[0]
|
||||
assert options.bandwidth != link.options.bandwidth
|
||||
assert iface.options.bandwidth != options.bandwidth
|
||||
link = Link(node.id, switch.id, iface1=Interface(id=iface.id), options=options)
|
||||
|
||||
# then
|
||||
|
@ -460,8 +458,7 @@ class TestGrpc:
|
|||
|
||||
# then
|
||||
assert result is True
|
||||
link = switch.links()[0]
|
||||
assert options.bandwidth == link.options.bandwidth
|
||||
assert options.bandwidth == iface.options.bandwidth
|
||||
|
||||
def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes):
|
||||
# given
|
||||
|
@ -472,13 +469,7 @@ class TestGrpc:
|
|||
node2 = session.add_node(CoreNode)
|
||||
iface2 = ip_prefixes.create_iface(node2)
|
||||
session.add_link(node1.id, node2.id, iface1, iface2)
|
||||
link_node = None
|
||||
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
|
||||
assert len(session.link_manager.links()) == 1
|
||||
link = Link(
|
||||
node1.id,
|
||||
node2.id,
|
||||
|
@ -492,7 +483,7 @@ class TestGrpc:
|
|||
|
||||
# then
|
||||
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):
|
||||
# given
|
||||
|
@ -537,14 +528,15 @@ class TestGrpc:
|
|||
assert result is True
|
||||
config = session.mobility.get_model_config(wlan.id, BasicRangeModel.name)
|
||||
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):
|
||||
# given
|
||||
client = CoreGrpcClient()
|
||||
session = grpc_server.coreemu.create_session()
|
||||
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)
|
||||
session.emane.node_models[emane_network.id] = EmaneIeee80211abgModel.name
|
||||
config_key = "bandwidth"
|
||||
|
@ -574,7 +566,8 @@ class TestGrpc:
|
|||
client = CoreGrpcClient()
|
||||
session = grpc_server.coreemu.create_session()
|
||||
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)
|
||||
session.emane.node_models[emane_network.id] = EmaneIeee80211abgModel.name
|
||||
|
||||
|
@ -651,16 +644,16 @@ class TestGrpc:
|
|||
# given
|
||||
client = CoreGrpcClient()
|
||||
session = grpc_server.coreemu.create_session()
|
||||
node_type = "test"
|
||||
model = "test"
|
||||
services = ["SSH"]
|
||||
|
||||
# then
|
||||
with client.context_connect():
|
||||
result = client.set_service_defaults(session.id, {node_type: services})
|
||||
result = client.set_service_defaults(session.id, {model: services})
|
||||
|
||||
# then
|
||||
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):
|
||||
# given
|
||||
|
@ -694,7 +687,8 @@ class TestGrpc:
|
|||
# given
|
||||
client = CoreGrpcClient()
|
||||
session = grpc_server.coreemu.create_session()
|
||||
options = NodeOptions(legacy=True)
|
||||
options = CoreNode.create_options()
|
||||
options.legacy = True
|
||||
node = session.add_node(CoreNode, options=options)
|
||||
service_name = "DefaultRoute"
|
||||
|
||||
|
@ -757,9 +751,11 @@ class TestGrpc:
|
|||
session = grpc_server.coreemu.create_session()
|
||||
wlan = session.add_node(WlanNode)
|
||||
node = session.add_node(CoreNode)
|
||||
iface = ip_prefixes.create_iface(node)
|
||||
session.add_link(node.id, wlan.id, iface)
|
||||
link_data = wlan.links()[0]
|
||||
iface_data = ip_prefixes.create_iface(node)
|
||||
session.add_link(node.id, wlan.id, iface_data)
|
||||
core_link = list(session.link_manager.links())[0]
|
||||
link_data = core_link.get_data(MessageFlags.ADD)
|
||||
|
||||
queue = Queue()
|
||||
|
||||
def handle_event(event: Event) -> None:
|
||||
|
@ -820,30 +816,6 @@ class TestGrpc:
|
|||
# then
|
||||
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):
|
||||
# given
|
||||
client = CoreGrpcClient()
|
||||
|
@ -950,7 +922,7 @@ class TestGrpc:
|
|||
client = CoreGrpcClient()
|
||||
session = grpc_server.coreemu.create_session()
|
||||
streamer = MoveNodesStreamer(session.id)
|
||||
request = core_pb2.MoveNodesRequest()
|
||||
request = MoveNodesRequest(session.id + 1, 1)
|
||||
streamer.send(request)
|
||||
streamer.stop()
|
||||
|
||||
|
@ -958,3 +930,27 @@ class TestGrpc:
|
|||
with pytest.raises(grpc.RpcError):
|
||||
with client.context_connect():
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -46,14 +46,17 @@ class TestLinks:
|
|||
)
|
||||
|
||||
# then
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1_data.id)
|
||||
assert node2.get_iface(iface2_data.id)
|
||||
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 iface1.local_options == LINK_OPTIONS
|
||||
assert iface1.has_local_netem
|
||||
assert iface2.local_options == LINK_OPTIONS
|
||||
assert iface2.has_local_netem
|
||||
assert iface2.options == LINK_OPTIONS
|
||||
assert iface2.has_netem
|
||||
assert node1.get_iface(iface1_data.id)
|
||||
|
||||
def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
||||
# given
|
||||
|
@ -62,16 +65,20 @@ class TestLinks:
|
|||
iface1_data = ip_prefixes.create_iface(node1)
|
||||
|
||||
# when
|
||||
iface, _ = session.add_link(
|
||||
iface1, iface2 = session.add_link(
|
||||
node1.id, node2.id, iface1_data=iface1_data, options=LINK_OPTIONS
|
||||
)
|
||||
|
||||
# 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 iface is not None
|
||||
assert iface.local_options == LINK_OPTIONS
|
||||
assert iface.has_local_netem
|
||||
assert iface2 is not None
|
||||
assert iface2.options == LINK_OPTIONS
|
||||
assert iface2.has_netem
|
||||
assert node2.get_iface(iface1_data.id)
|
||||
|
||||
def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
|
||||
# given
|
||||
|
@ -80,32 +87,37 @@ class TestLinks:
|
|||
iface2_data = ip_prefixes.create_iface(node2)
|
||||
|
||||
# when
|
||||
_, iface = session.add_link(
|
||||
iface1, iface2 = session.add_link(
|
||||
node1.id, node2.id, iface2_data=iface2_data, options=LINK_OPTIONS
|
||||
)
|
||||
|
||||
# then
|
||||
assert node1.links()
|
||||
assert node2.get_iface(iface2_data.id)
|
||||
assert iface is not None
|
||||
assert iface.local_options == LINK_OPTIONS
|
||||
assert iface.has_local_netem
|
||||
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.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
|
||||
node1 = session.add_node(SwitchNode)
|
||||
node2 = session.add_node(SwitchNode)
|
||||
|
||||
# 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
|
||||
assert node1.links()
|
||||
assert iface is not None
|
||||
assert iface.local_options == LINK_OPTIONS
|
||||
assert iface.options == LINK_OPTIONS
|
||||
assert iface.has_local_netem
|
||||
assert iface.has_netem
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert iface1 is not None
|
||||
assert iface1.options == LINK_OPTIONS
|
||||
assert iface1.has_netem
|
||||
assert iface2 is not None
|
||||
assert iface2.options == LINK_OPTIONS
|
||||
assert iface2.has_netem
|
||||
|
||||
def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes):
|
||||
# given
|
||||
|
@ -141,48 +153,52 @@ class TestLinks:
|
|||
)
|
||||
|
||||
# then
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1_data.id)
|
||||
assert node2.get_iface(iface2_data.id)
|
||||
assert iface1 is not None
|
||||
assert iface1.options == link_options1
|
||||
assert iface1.has_netem
|
||||
assert iface2 is not None
|
||||
assert iface1.local_options == link_options1
|
||||
assert iface1.has_local_netem
|
||||
assert iface2.local_options == link_options2
|
||||
assert iface2.has_local_netem
|
||||
assert iface2.options == link_options2
|
||||
assert iface2.has_netem
|
||||
|
||||
def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
||||
# given
|
||||
node1 = session.add_node(CoreNode)
|
||||
node2 = session.add_node(SwitchNode)
|
||||
iface1_data = ip_prefixes.create_iface(node1)
|
||||
iface1, _ = session.add_link(node1.id, node2.id, iface1_data)
|
||||
assert iface1.local_options != LINK_OPTIONS
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert iface1.options != LINK_OPTIONS
|
||||
assert iface2.options != LINK_OPTIONS
|
||||
|
||||
# when
|
||||
session.update_link(
|
||||
node1.id, node2.id, iface1_id=iface1_data.id, options=LINK_OPTIONS
|
||||
)
|
||||
session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
|
||||
|
||||
# then
|
||||
assert iface1.local_options == LINK_OPTIONS
|
||||
assert iface1.has_local_netem
|
||||
assert iface1.options == LINK_OPTIONS
|
||||
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):
|
||||
# given
|
||||
node1 = session.add_node(SwitchNode)
|
||||
node2 = session.add_node(CoreNode)
|
||||
iface2_data = ip_prefixes.create_iface(node2)
|
||||
_, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
||||
assert iface2.local_options != LINK_OPTIONS
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
||||
assert iface1.options != LINK_OPTIONS
|
||||
assert iface2.options != LINK_OPTIONS
|
||||
|
||||
# when
|
||||
session.update_link(
|
||||
node1.id, node2.id, iface2_id=iface2_data.id, options=LINK_OPTIONS
|
||||
)
|
||||
session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
|
||||
|
||||
# then
|
||||
assert iface2.local_options == LINK_OPTIONS
|
||||
assert iface2.has_local_netem
|
||||
assert iface1.options == LINK_OPTIONS
|
||||
assert iface1.has_netem
|
||||
assert iface2.options == LINK_OPTIONS
|
||||
assert iface2.has_netem
|
||||
|
||||
def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes):
|
||||
# given
|
||||
|
@ -191,55 +207,68 @@ class TestLinks:
|
|||
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.local_options != LINK_OPTIONS
|
||||
assert iface2.local_options != LINK_OPTIONS
|
||||
assert iface1.options != LINK_OPTIONS
|
||||
assert iface2.options != LINK_OPTIONS
|
||||
|
||||
# when
|
||||
session.update_link(
|
||||
node1.id, node2.id, iface1_data.id, iface2_data.id, LINK_OPTIONS
|
||||
)
|
||||
session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
|
||||
|
||||
# then
|
||||
assert iface1.local_options == LINK_OPTIONS
|
||||
assert iface1.has_local_netem
|
||||
assert iface2.local_options == LINK_OPTIONS
|
||||
assert iface2.has_local_netem
|
||||
assert iface1.options == LINK_OPTIONS
|
||||
assert iface1.has_netem
|
||||
assert iface2.options == LINK_OPTIONS
|
||||
assert iface2.has_netem
|
||||
|
||||
def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
||||
# given
|
||||
node1 = session.add_node(SwitchNode)
|
||||
node2 = session.add_node(SwitchNode)
|
||||
iface1, _ = session.add_link(node1.id, node2.id)
|
||||
assert iface1.local_options != LINK_OPTIONS
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id)
|
||||
assert iface1.options != LINK_OPTIONS
|
||||
assert iface2.options != LINK_OPTIONS
|
||||
|
||||
# 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
|
||||
assert iface1.local_options == LINK_OPTIONS
|
||||
assert iface1.has_local_netem
|
||||
assert iface1.options == LINK_OPTIONS
|
||||
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):
|
||||
# given
|
||||
node1 = session.add_node(SwitchNode)
|
||||
node2 = session.add_node(SwitchNode)
|
||||
iface1, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS)
|
||||
assert iface1.local_options == LINK_OPTIONS
|
||||
assert iface1.has_local_netem
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id, options=LINK_OPTIONS)
|
||||
assert iface1.options == LINK_OPTIONS
|
||||
assert iface1.has_netem
|
||||
assert iface2.options == LINK_OPTIONS
|
||||
assert iface2.has_netem
|
||||
|
||||
# when
|
||||
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
|
||||
assert iface1.local_options.is_clear()
|
||||
assert not iface1.has_local_netem
|
||||
assert iface1.options.is_clear()
|
||||
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):
|
||||
# given
|
||||
|
@ -247,82 +276,100 @@ class TestLinks:
|
|||
node2 = session.add_node(CoreNode)
|
||||
iface1_data = ip_prefixes.create_iface(node1)
|
||||
iface2_data = ip_prefixes.create_iface(node2)
|
||||
session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
||||
assert node1.get_iface(iface1_data.id)
|
||||
assert node2.get_iface(iface2_data.id)
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1.id)
|
||||
assert node2.get_iface(iface2.id)
|
||||
|
||||
# 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
|
||||
assert iface1_data.id not in node1.ifaces
|
||||
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_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
||||
# given
|
||||
node1 = session.add_node(CoreNode)
|
||||
node2 = session.add_node(SwitchNode)
|
||||
iface1_data = ip_prefixes.create_iface(node1)
|
||||
session.add_link(node1.id, node2.id, iface1_data)
|
||||
assert node1.get_iface(iface1_data.id)
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1.id)
|
||||
assert node2.get_iface(iface2.id)
|
||||
|
||||
# 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
|
||||
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):
|
||||
# given
|
||||
node1 = session.add_node(SwitchNode)
|
||||
node2 = session.add_node(CoreNode)
|
||||
iface2_data = ip_prefixes.create_iface(node2)
|
||||
session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
||||
assert node2.get_iface(iface2_data.id)
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1.id)
|
||||
assert node2.get_iface(iface2.id)
|
||||
|
||||
# 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
|
||||
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):
|
||||
# given
|
||||
node1 = session.add_node(SwitchNode)
|
||||
node2 = session.add_node(SwitchNode)
|
||||
session.add_link(node1.id, node2.id)
|
||||
assert node1.get_linked_iface(node2)
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1.id)
|
||||
assert node2.get_iface(iface2.id)
|
||||
|
||||
# when
|
||||
session.delete_link(node1.id, node2.id)
|
||||
session.delete_link(node1.id, node2.id, iface1.id, iface2.id)
|
||||
|
||||
# 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):
|
||||
# given
|
||||
node1 = session.add_node(SwitchNode)
|
||||
node2 = session.add_node(SwitchNode)
|
||||
session.add_link(node1.id, node2.id)
|
||||
assert node1.get_linked_iface(node2)
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1.id)
|
||||
assert node2.get_iface(iface2.id)
|
||||
|
||||
# when
|
||||
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):
|
||||
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):
|
||||
# given
|
||||
node1 = session.add_node(SwitchNode)
|
||||
node2 = session.add_node(SwitchNode)
|
||||
node3 = session.add_node(SwitchNode)
|
||||
session.add_link(node1.id, node2.id)
|
||||
assert node1.get_linked_iface(node2)
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1.id)
|
||||
assert node2.get_iface(iface2.id)
|
||||
|
||||
# when
|
||||
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):
|
||||
# given
|
||||
|
@ -330,12 +377,14 @@ class TestLinks:
|
|||
node2 = session.add_node(SwitchNode)
|
||||
node3 = session.add_node(SwitchNode)
|
||||
iface1_data = ip_prefixes.create_iface(node1)
|
||||
iface1, _ = session.add_link(node1.id, node2.id, iface1_data)
|
||||
assert iface1
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1.id)
|
||||
assert node2.get_iface(iface2.id)
|
||||
|
||||
# when
|
||||
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):
|
||||
# given
|
||||
|
@ -343,12 +392,14 @@ class TestLinks:
|
|||
node2 = session.add_node(CoreNode)
|
||||
node3 = session.add_node(SwitchNode)
|
||||
iface2_data = ip_prefixes.create_iface(node2)
|
||||
_, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
||||
assert iface2
|
||||
iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1.id)
|
||||
assert node2.get_iface(iface2.id)
|
||||
|
||||
# when
|
||||
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):
|
||||
# given
|
||||
|
@ -358,9 +409,10 @@ class TestLinks:
|
|||
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
|
||||
assert iface2
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert node1.get_iface(iface1.id)
|
||||
assert node2.get_iface(iface2.id)
|
||||
|
||||
# when
|
||||
with pytest.raises(CoreError):
|
||||
session.delete_link(node1.id, node3.id)
|
||||
session.delete_link(node1.id, node3.id, iface1.id, iface2.id)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import pytest
|
||||
|
||||
from core.emulator.data import InterfaceData, NodeOptions
|
||||
from core.emulator.data import InterfaceData
|
||||
from core.emulator.session import Session
|
||||
from core.errors import CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
|
@ -14,7 +14,8 @@ class TestNodes:
|
|||
@pytest.mark.parametrize("model", MODELS)
|
||||
def test_node_add(self, session: Session, model: str):
|
||||
# given
|
||||
options = NodeOptions(model=model)
|
||||
options = CoreNode.create_options()
|
||||
options.model = model
|
||||
|
||||
# when
|
||||
node = session.add_node(CoreNode, options=options)
|
||||
|
@ -60,6 +61,40 @@ class TestNodes:
|
|||
with pytest.raises(CoreError):
|
||||
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(
|
||||
"mac,expected",
|
||||
[
|
||||
|
@ -70,12 +105,11 @@ class TestNodes:
|
|||
def test_node_set_mac(self, session: Session, mac: str, expected: str):
|
||||
# given
|
||||
node = session.add_node(CoreNode)
|
||||
switch = session.add_node(SwitchNode)
|
||||
iface_data = InterfaceData()
|
||||
iface = node.new_iface(switch, iface_data)
|
||||
iface = node.create_iface(iface_data)
|
||||
|
||||
# when
|
||||
node.set_mac(iface.node_id, mac)
|
||||
iface.set_mac(mac)
|
||||
|
||||
# then
|
||||
assert str(iface.mac) == expected
|
||||
|
@ -86,13 +120,12 @@ class TestNodes:
|
|||
def test_node_set_mac_exception(self, session: Session, mac: str):
|
||||
# given
|
||||
node = session.add_node(CoreNode)
|
||||
switch = session.add_node(SwitchNode)
|
||||
iface_data = InterfaceData()
|
||||
iface = node.new_iface(switch, iface_data)
|
||||
iface = node.create_iface(iface_data)
|
||||
|
||||
# when
|
||||
with pytest.raises(CoreError):
|
||||
node.set_mac(iface.node_id, mac)
|
||||
iface.set_mac(mac)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"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):
|
||||
# given
|
||||
node = session.add_node(CoreNode)
|
||||
switch = session.add_node(SwitchNode)
|
||||
iface_data = InterfaceData()
|
||||
iface = node.new_iface(switch, iface_data)
|
||||
iface = node.create_iface(iface_data)
|
||||
|
||||
# when
|
||||
node.add_ip(iface.node_id, ip)
|
||||
iface.add_ip(ip)
|
||||
|
||||
# then
|
||||
if is_ip6:
|
||||
|
@ -122,14 +154,13 @@ class TestNodes:
|
|||
def test_node_add_ip_exception(self, session):
|
||||
# given
|
||||
node = session.add_node(CoreNode)
|
||||
switch = session.add_node(SwitchNode)
|
||||
iface_data = InterfaceData()
|
||||
iface = node.new_iface(switch, iface_data)
|
||||
iface = node.create_iface(iface_data)
|
||||
ip = "256.168.0.1/24"
|
||||
|
||||
# when
|
||||
with pytest.raises(CoreError):
|
||||
node.add_ip(iface.node_id, ip)
|
||||
iface.add_ip(ip)
|
||||
|
||||
@pytest.mark.parametrize("net_type", NET_TYPES)
|
||||
def test_net(self, session, net_type):
|
||||
|
|
|
@ -53,7 +53,7 @@ class TestServices:
|
|||
total_service = len(node.services)
|
||||
|
||||
# when
|
||||
session.services.add_services(node, node.type, [SERVICE_ONE, SERVICE_TWO])
|
||||
session.services.add_services(node, node.model, [SERVICE_ONE, SERVICE_TWO])
|
||||
|
||||
# then
|
||||
assert node.services
|
||||
|
|
|
@ -4,13 +4,13 @@ from xml.etree import ElementTree
|
|||
|
||||
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.session import Session
|
||||
from core.errors import CoreError
|
||||
from core.location.mobility import BasicRangeModel
|
||||
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
|
||||
|
||||
|
||||
|
@ -65,25 +65,18 @@ class TestXml:
|
|||
:param tmpdir: tmpdir to create data in
|
||||
: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_data = ip_prefixes.create_iface(node)
|
||||
session.add_link(node.id, ptp_node.id, iface1_data=iface_data)
|
||||
# link nodes
|
||||
iface1_data = ip_prefixes.create_iface(node1)
|
||||
iface2_data = ip_prefixes.create_iface(node2)
|
||||
session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
||||
|
||||
# instantiate session
|
||||
session.instantiate()
|
||||
|
||||
# get ids for nodes
|
||||
node1_id = node1.id
|
||||
node2_id = node2.id
|
||||
|
||||
# save xml
|
||||
xml_file = tmpdir.join("session.xml")
|
||||
file_path = Path(xml_file.strpath)
|
||||
|
@ -98,16 +91,19 @@ class TestXml:
|
|||
|
||||
# verify nodes have been removed from session
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node1_id, CoreNode)
|
||||
assert not session.get_node(node1.id, CoreNode)
|
||||
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
|
||||
session.open_xml(file_path, start=True)
|
||||
|
||||
# verify nodes have been recreated
|
||||
assert session.get_node(node1_id, CoreNode)
|
||||
assert session.get_node(node2_id, CoreNode)
|
||||
assert session.get_node(node1.id, CoreNode)
|
||||
assert session.get_node(node2.id, CoreNode)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
|
||||
def test_xml_ptp_services(
|
||||
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
||||
|
@ -119,18 +115,14 @@ class TestXml:
|
|||
:param tmpdir: tmpdir to create data in
|
||||
:param ip_prefixes: generates ip addresses for nodes
|
||||
"""
|
||||
# create ptp
|
||||
ptp_node = session.add_node(PtpNet)
|
||||
|
||||
# create nodes
|
||||
options = NodeOptions(model="host")
|
||||
node1 = session.add_node(CoreNode, options=options)
|
||||
node1 = session.add_node(CoreNode)
|
||||
node2 = session.add_node(CoreNode)
|
||||
|
||||
# link nodes to ptp net
|
||||
for node in [node1, node2]:
|
||||
iface_data = ip_prefixes.create_iface(node)
|
||||
session.add_link(node.id, ptp_node.id, iface1_data=iface_data)
|
||||
iface1_data = ip_prefixes.create_iface(node1)
|
||||
iface2_data = ip_prefixes.create_iface(node2)
|
||||
session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
||||
|
||||
# set custom values for node service
|
||||
session.services.set_service(node1.id, SshService.name)
|
||||
|
@ -143,10 +135,6 @@ class TestXml:
|
|||
# instantiate session
|
||||
session.instantiate()
|
||||
|
||||
# get ids for nodes
|
||||
node1_id = node1.id
|
||||
node2_id = node2.id
|
||||
|
||||
# save xml
|
||||
xml_file = tmpdir.join("session.xml")
|
||||
file_path = Path(xml_file.strpath)
|
||||
|
@ -161,9 +149,9 @@ class TestXml:
|
|||
|
||||
# verify nodes have been removed from session
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node1_id, CoreNode)
|
||||
assert not session.get_node(node1.id, CoreNode)
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node2_id, CoreNode)
|
||||
assert not session.get_node(node2.id, CoreNode)
|
||||
|
||||
# load saved xml
|
||||
session.open_xml(file_path, start=True)
|
||||
|
@ -172,8 +160,8 @@ class TestXml:
|
|||
service = session.services.get_service(node1.id, SshService.name)
|
||||
|
||||
# verify nodes have been recreated
|
||||
assert session.get_node(node1_id, CoreNode)
|
||||
assert session.get_node(node2_id, CoreNode)
|
||||
assert session.get_node(node1.id, CoreNode)
|
||||
assert session.get_node(node2.id, CoreNode)
|
||||
assert service.config_data.get(service_file) == file_data
|
||||
|
||||
def test_xml_mobility(
|
||||
|
@ -187,28 +175,23 @@ class TestXml:
|
|||
:param ip_prefixes: generates ip addresses for nodes
|
||||
"""
|
||||
# create wlan
|
||||
wlan_node = session.add_node(WlanNode)
|
||||
session.mobility.set_model(wlan_node, BasicRangeModel, {"test": "1"})
|
||||
wlan = session.add_node(WlanNode)
|
||||
session.mobility.set_model(wlan, BasicRangeModel, {"test": "1"})
|
||||
|
||||
# create nodes
|
||||
options = NodeOptions(model="mdr")
|
||||
options.set_position(0, 0)
|
||||
options = CoreNode.create_options()
|
||||
options.model = "mdr"
|
||||
node1 = session.add_node(CoreNode, options=options)
|
||||
node2 = session.add_node(CoreNode, options=options)
|
||||
|
||||
# link nodes
|
||||
for node in [node1, node2]:
|
||||
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
|
||||
session.instantiate()
|
||||
|
||||
# get ids for nodes
|
||||
wlan_id = wlan_node.id
|
||||
node1_id = node1.id
|
||||
node2_id = node2.id
|
||||
|
||||
# save xml
|
||||
xml_file = tmpdir.join("session.xml")
|
||||
file_path = Path(xml_file.strpath)
|
||||
|
@ -223,20 +206,20 @@ class TestXml:
|
|||
|
||||
# verify nodes have been removed from session
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node1_id, CoreNode)
|
||||
assert not session.get_node(node1.id, CoreNode)
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node2_id, CoreNode)
|
||||
assert not session.get_node(node2.id, CoreNode)
|
||||
|
||||
# load saved xml
|
||||
session.open_xml(file_path, start=True)
|
||||
|
||||
# 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
|
||||
assert session.get_node(node1_id, CoreNode)
|
||||
assert session.get_node(node2_id, CoreNode)
|
||||
assert session.get_node(wlan_id, WlanNode)
|
||||
assert session.get_node(node1.id, CoreNode)
|
||||
assert session.get_node(node2.id, CoreNode)
|
||||
assert session.get_node(wlan.id, WlanNode)
|
||||
assert value == "1"
|
||||
|
||||
def test_network_to_network(self, session: Session, tmpdir: TemporaryFile):
|
||||
|
@ -256,10 +239,6 @@ class TestXml:
|
|||
# instantiate session
|
||||
session.instantiate()
|
||||
|
||||
# get ids for nodes
|
||||
node1_id = switch1.id
|
||||
node2_id = switch2.id
|
||||
|
||||
# save xml
|
||||
xml_file = tmpdir.join("session.xml")
|
||||
file_path = Path(xml_file.strpath)
|
||||
|
@ -274,19 +253,19 @@ class TestXml:
|
|||
|
||||
# verify nodes have been removed from session
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node1_id, SwitchNode)
|
||||
assert not session.get_node(switch1.id, SwitchNode)
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node2_id, SwitchNode)
|
||||
assert not session.get_node(switch2.id, SwitchNode)
|
||||
|
||||
# load saved xml
|
||||
session.open_xml(file_path, start=True)
|
||||
|
||||
# verify nodes have been recreated
|
||||
switch1 = session.get_node(node1_id, SwitchNode)
|
||||
switch2 = session.get_node(node2_id, SwitchNode)
|
||||
switch1 = session.get_node(switch1.id, SwitchNode)
|
||||
switch2 = session.get_node(switch2.id, SwitchNode)
|
||||
assert switch1
|
||||
assert switch2
|
||||
assert len(switch1.links() + switch2.links()) == 1
|
||||
assert len(session.link_manager.links()) == 1
|
||||
|
||||
def test_link_options(
|
||||
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
||||
|
@ -316,10 +295,6 @@ class TestXml:
|
|||
# instantiate session
|
||||
session.instantiate()
|
||||
|
||||
# get ids for nodes
|
||||
node1_id = node1.id
|
||||
node2_id = switch.id
|
||||
|
||||
# save xml
|
||||
xml_file = tmpdir.join("session.xml")
|
||||
file_path = Path(xml_file.strpath)
|
||||
|
@ -334,27 +309,25 @@ class TestXml:
|
|||
|
||||
# verify nodes have been removed from session
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node1_id, CoreNode)
|
||||
assert not session.get_node(node1.id, CoreNode)
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node2_id, SwitchNode)
|
||||
assert not session.get_node(switch.id, SwitchNode)
|
||||
|
||||
# load saved xml
|
||||
session.open_xml(file_path, start=True)
|
||||
|
||||
# verify nodes have been recreated
|
||||
assert session.get_node(node1_id, CoreNode)
|
||||
assert session.get_node(node2_id, SwitchNode)
|
||||
links = []
|
||||
for node_id in session.nodes:
|
||||
node = session.nodes[node_id]
|
||||
links += node.links()
|
||||
link = links[0]
|
||||
assert options.loss == link.options.loss
|
||||
assert options.bandwidth == link.options.bandwidth
|
||||
assert options.jitter == link.options.jitter
|
||||
assert options.delay == link.options.delay
|
||||
assert options.dup == link.options.dup
|
||||
assert options.buffer == link.options.buffer
|
||||
assert session.get_node(node1.id, CoreNode)
|
||||
assert session.get_node(switch.id, SwitchNode)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
link = list(session.link_manager.links())[0]
|
||||
link_options = link.options()
|
||||
assert options.loss == link_options.loss
|
||||
assert options.bandwidth == link_options.bandwidth
|
||||
assert options.jitter == link_options.jitter
|
||||
assert options.delay == link_options.delay
|
||||
assert options.dup == link_options.dup
|
||||
assert options.buffer == link_options.buffer
|
||||
|
||||
def test_link_options_ptp(
|
||||
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
||||
|
@ -385,10 +358,6 @@ class TestXml:
|
|||
# instantiate session
|
||||
session.instantiate()
|
||||
|
||||
# get ids for nodes
|
||||
node1_id = node1.id
|
||||
node2_id = node2.id
|
||||
|
||||
# save xml
|
||||
xml_file = tmpdir.join("session.xml")
|
||||
file_path = Path(xml_file.strpath)
|
||||
|
@ -403,27 +372,25 @@ class TestXml:
|
|||
|
||||
# verify nodes have been removed from session
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node1_id, CoreNode)
|
||||
assert not session.get_node(node1.id, CoreNode)
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node2_id, CoreNode)
|
||||
assert not session.get_node(node2.id, CoreNode)
|
||||
|
||||
# load saved xml
|
||||
session.open_xml(file_path, start=True)
|
||||
|
||||
# verify nodes have been recreated
|
||||
assert session.get_node(node1_id, CoreNode)
|
||||
assert session.get_node(node2_id, CoreNode)
|
||||
links = []
|
||||
for node_id in session.nodes:
|
||||
node = session.nodes[node_id]
|
||||
links += node.links()
|
||||
link = links[0]
|
||||
assert options.loss == link.options.loss
|
||||
assert options.bandwidth == link.options.bandwidth
|
||||
assert options.jitter == link.options.jitter
|
||||
assert options.delay == link.options.delay
|
||||
assert options.dup == link.options.dup
|
||||
assert options.buffer == link.options.buffer
|
||||
assert session.get_node(node1.id, CoreNode)
|
||||
assert session.get_node(node2.id, CoreNode)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
link = list(session.link_manager.links())[0]
|
||||
link_options = link.options()
|
||||
assert options.loss == link_options.loss
|
||||
assert options.bandwidth == link_options.bandwidth
|
||||
assert options.jitter == link_options.jitter
|
||||
assert options.delay == link_options.delay
|
||||
assert options.dup == link_options.dup
|
||||
assert options.buffer == link_options.buffer
|
||||
|
||||
def test_link_options_bidirectional(
|
||||
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
||||
|
@ -450,7 +417,9 @@ class TestXml:
|
|||
options1.dup = 5
|
||||
options1.jitter = 5
|
||||
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.unidirectional = 1
|
||||
options2.bandwidth = 10000
|
||||
|
@ -459,17 +428,11 @@ class TestXml:
|
|||
options2.dup = 10
|
||||
options2.jitter = 10
|
||||
options2.buffer = 100
|
||||
session.update_link(
|
||||
node2.id, node1.id, iface2_data.id, iface1_data.id, options2
|
||||
)
|
||||
session.update_link(node2.id, node1.id, iface2.id, iface1.id, options2)
|
||||
|
||||
# instantiate session
|
||||
session.instantiate()
|
||||
|
||||
# get ids for nodes
|
||||
node1_id = node1.id
|
||||
node2_id = node2.id
|
||||
|
||||
# save xml
|
||||
xml_file = tmpdir.join("session.xml")
|
||||
file_path = Path(xml_file.strpath)
|
||||
|
@ -484,32 +447,26 @@ class TestXml:
|
|||
|
||||
# verify nodes have been removed from session
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node1_id, CoreNode)
|
||||
assert not session.get_node(node1.id, CoreNode)
|
||||
with pytest.raises(CoreError):
|
||||
assert not session.get_node(node2_id, CoreNode)
|
||||
assert not session.get_node(node2.id, CoreNode)
|
||||
|
||||
# load saved xml
|
||||
session.open_xml(file_path, start=True)
|
||||
|
||||
# verify nodes have been recreated
|
||||
assert session.get_node(node1_id, CoreNode)
|
||||
assert session.get_node(node2_id, CoreNode)
|
||||
links = []
|
||||
for node_id in session.nodes:
|
||||
node = session.nodes[node_id]
|
||||
links += node.links()
|
||||
assert len(links) == 2
|
||||
link1 = links[0]
|
||||
link2 = links[1]
|
||||
assert options1.bandwidth == link1.options.bandwidth
|
||||
assert options1.delay == link1.options.delay
|
||||
assert options1.loss == link1.options.loss
|
||||
assert options1.dup == link1.options.dup
|
||||
assert options1.jitter == link1.options.jitter
|
||||
assert options1.buffer == link1.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
|
||||
assert session.get_node(node1.id, CoreNode)
|
||||
assert session.get_node(node2.id, CoreNode)
|
||||
assert len(session.link_manager.links()) == 1
|
||||
assert options1.bandwidth == iface1.options.bandwidth
|
||||
assert options1.delay == iface1.options.delay
|
||||
assert options1.loss == iface1.options.loss
|
||||
assert options1.dup == iface1.options.dup
|
||||
assert options1.jitter == iface1.options.jitter
|
||||
assert options1.buffer == iface1.options.buffer
|
||||
assert options2.bandwidth == iface2.options.bandwidth
|
||||
assert options2.delay == iface2.options.delay
|
||||
assert options2.loss == iface2.options.loss
|
||||
assert options2.dup == iface2.options.dup
|
||||
assert options2.jitter == iface2.options.jitter
|
||||
assert options2.buffer == iface2.options.buffer
|
||||
|
|
61
dockerfiles/Dockerfile.centos
Normal file
61
dockerfiles/Dockerfile.centos
Normal 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
Loading…
Reference in a new issue