Merge branch 'develop' into enhancement/core-player
14
CHANGELOG.md
|
@ -1,3 +1,17 @@
|
||||||
|
## 2022-03-21 CORE 8.2.0
|
||||||
|
|
||||||
|
* core-gui
|
||||||
|
* improved failed starts to trigger runtime to allow node investigation
|
||||||
|
* core-daemon
|
||||||
|
* improved default service loading to use a full import path
|
||||||
|
* updated session instantiation to always set to a runtime state
|
||||||
|
* core-cli
|
||||||
|
* \#672 - fixed xml loading
|
||||||
|
* \#578 - restored json flag and added geo output to session overview
|
||||||
|
* Documentation
|
||||||
|
* updated emane example and documentation
|
||||||
|
* improved table markdown
|
||||||
|
|
||||||
## 2022-02-18 CORE 8.1.0
|
## 2022-02-18 CORE 8.1.0
|
||||||
|
|
||||||
* Installation
|
* Installation
|
||||||
|
|
|
@ -6,10 +6,6 @@ if WANT_DOCS
|
||||||
DOCS = docs man
|
DOCS = docs man
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if WANT_GUI
|
|
||||||
GUI = gui
|
|
||||||
endif
|
|
||||||
|
|
||||||
if WANT_DAEMON
|
if WANT_DAEMON
|
||||||
DAEMON = daemon
|
DAEMON = daemon
|
||||||
endif
|
endif
|
||||||
|
@ -19,7 +15,7 @@ if WANT_NETNS
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# keep docs last due to dependencies on binaries
|
# keep docs last due to dependencies on binaries
|
||||||
SUBDIRS = $(GUI) $(DAEMON) $(NETNS) $(DOCS)
|
SUBDIRS = $(DAEMON) $(NETNS) $(DOCS)
|
||||||
|
|
||||||
ACLOCAL_AMFLAGS = -I config
|
ACLOCAL_AMFLAGS = -I config
|
||||||
|
|
||||||
|
@ -115,7 +111,6 @@ $(info creating file $1 from $1.in)
|
||||||
-e 's,[@]CORE_STATE_DIR[@],$(CORE_STATE_DIR),g' \
|
-e 's,[@]CORE_STATE_DIR[@],$(CORE_STATE_DIR),g' \
|
||||||
-e 's,[@]CORE_DATA_DIR[@],$(CORE_DATA_DIR),g' \
|
-e 's,[@]CORE_DATA_DIR[@],$(CORE_DATA_DIR),g' \
|
||||||
-e 's,[@]CORE_CONF_DIR[@],$(CORE_CONF_DIR),g' \
|
-e 's,[@]CORE_CONF_DIR[@],$(CORE_CONF_DIR),g' \
|
||||||
-e 's,[@]CORE_GUI_CONF_DIR[@],$(CORE_GUI_CONF_DIR),g' \
|
|
||||||
< $1.in > $1
|
< $1.in > $1
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
@ -123,7 +118,6 @@ all: change-files
|
||||||
|
|
||||||
.PHONY: change-files
|
.PHONY: change-files
|
||||||
change-files:
|
change-files:
|
||||||
$(call change-files,gui/core-gui-legacy)
|
|
||||||
$(call change-files,daemon/core/constants.py)
|
$(call change-files,daemon/core/constants.py)
|
||||||
$(call change-files,netns/setup.py)
|
$(call change-files,netns/setup.py)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
#
|
#
|
||||||
# (c)2010-2012 the Boeing Company
|
|
||||||
#
|
|
||||||
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
|
|
||||||
#
|
|
||||||
# Bootstrap the autoconf system.
|
# Bootstrap the autoconf system.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
66
configure.ac
|
@ -2,7 +2,7 @@
|
||||||
# Process this file with autoconf to produce a configure script.
|
# Process this file with autoconf to produce a configure script.
|
||||||
|
|
||||||
# this defines the CORE version number, must be static for AC_INIT
|
# this defines the CORE version number, must be static for AC_INIT
|
||||||
AC_INIT(core, 8.1.0)
|
AC_INIT(core, 8.2.0)
|
||||||
|
|
||||||
# autoconf and automake initialization
|
# autoconf and automake initialization
|
||||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||||
|
@ -30,25 +30,14 @@ AC_SUBST(CORE_CONF_DIR)
|
||||||
AC_SUBST(CORE_DATA_DIR)
|
AC_SUBST(CORE_DATA_DIR)
|
||||||
AC_SUBST(CORE_STATE_DIR)
|
AC_SUBST(CORE_STATE_DIR)
|
||||||
|
|
||||||
# CORE GUI configuration files and preferences in CORE_GUI_CONF_DIR
|
# documentation option
|
||||||
# scenario files in ~/.core/configs/
|
|
||||||
AC_ARG_WITH([guiconfdir],
|
|
||||||
[AS_HELP_STRING([--with-guiconfdir=dir],
|
|
||||||
[specify GUI configuration directory])],
|
|
||||||
[CORE_GUI_CONF_DIR="$with_guiconfdir"],
|
|
||||||
[CORE_GUI_CONF_DIR="\$\${HOME}/.core"])
|
|
||||||
AC_SUBST(CORE_GUI_CONF_DIR)
|
|
||||||
AC_ARG_ENABLE([gui],
|
|
||||||
[AS_HELP_STRING([--enable-gui[=ARG]],
|
|
||||||
[build and install the GUI (default is yes)])],
|
|
||||||
[], [enable_gui=yes])
|
|
||||||
AC_SUBST(enable_gui)
|
|
||||||
AC_ARG_ENABLE([docs],
|
AC_ARG_ENABLE([docs],
|
||||||
[AS_HELP_STRING([--enable-docs[=ARG]],
|
[AS_HELP_STRING([--enable-docs[=ARG]],
|
||||||
[build python documentation (default is no)])],
|
[build python documentation (default is no)])],
|
||||||
[], [enable_docs=no])
|
[], [enable_docs=no])
|
||||||
AC_SUBST(enable_docs)
|
AC_SUBST(enable_docs)
|
||||||
|
|
||||||
|
# python option
|
||||||
AC_ARG_ENABLE([python],
|
AC_ARG_ENABLE([python],
|
||||||
[AS_HELP_STRING([--enable-python[=ARG]],
|
[AS_HELP_STRING([--enable-python[=ARG]],
|
||||||
[build and install the python bindings (default is yes)])],
|
[build and install the python bindings (default is yes)])],
|
||||||
|
@ -94,27 +83,6 @@ if test "x$enable_daemon" = "xyes"; then
|
||||||
want_python=yes
|
want_python=yes
|
||||||
want_linux_netns=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.6)
|
||||||
AS_IF([$PYTHON -m grpc_tools.protoc -h &> /dev/null], [], [AC_MSG_ERROR([please install python grpcio-tools])])
|
AS_IF([$PYTHON -m grpc_tools.protoc -h &> /dev/null], [], [AC_MSG_ERROR([please install python grpcio-tools])])
|
||||||
|
|
||||||
|
@ -171,6 +139,25 @@ fi
|
||||||
|
|
||||||
if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; then
|
if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; then
|
||||||
want_linux_netns=yes
|
want_linux_netns=yes
|
||||||
|
|
||||||
|
# Checks for header files.
|
||||||
|
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h stdint.h stdlib.h string.h sys/ioctl.h sys/mount.h sys/socket.h sys/time.h termios.h unistd.h])
|
||||||
|
|
||||||
|
# Checks for typedefs, structures, and compiler characteristics.
|
||||||
|
AC_C_INLINE
|
||||||
|
AC_TYPE_INT32_T
|
||||||
|
AC_TYPE_PID_T
|
||||||
|
AC_TYPE_SIZE_T
|
||||||
|
AC_TYPE_SSIZE_T
|
||||||
|
AC_TYPE_UINT32_T
|
||||||
|
AC_TYPE_UINT8_T
|
||||||
|
|
||||||
|
# Checks for library functions.
|
||||||
|
AC_FUNC_FORK
|
||||||
|
AC_FUNC_MALLOC
|
||||||
|
AC_FUNC_REALLOC
|
||||||
|
AC_CHECK_FUNCS([atexit dup2 gettimeofday memset socket strerror uname])
|
||||||
|
|
||||||
PKG_CHECK_MODULES(libev, libev,
|
PKG_CHECK_MODULES(libev, libev,
|
||||||
AC_MSG_RESULT([found libev using pkgconfig OK])
|
AC_MSG_RESULT([found libev using pkgconfig OK])
|
||||||
AC_SUBST(libev_CFLAGS)
|
AC_SUBST(libev_CFLAGS)
|
||||||
|
@ -209,7 +196,6 @@ if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Variable substitutions
|
# Variable substitutions
|
||||||
AM_CONDITIONAL(WANT_GUI, test x$enable_gui = xyes)
|
|
||||||
AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes)
|
AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes)
|
||||||
AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes)
|
AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes)
|
||||||
AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes)
|
AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes)
|
||||||
|
@ -224,9 +210,6 @@ fi
|
||||||
|
|
||||||
# Output files
|
# Output files
|
||||||
AC_CONFIG_FILES([Makefile
|
AC_CONFIG_FILES([Makefile
|
||||||
gui/version.tcl
|
|
||||||
gui/Makefile
|
|
||||||
gui/icons/Makefile
|
|
||||||
man/Makefile
|
man/Makefile
|
||||||
docs/Makefile
|
docs/Makefile
|
||||||
daemon/Makefile
|
daemon/Makefile
|
||||||
|
@ -248,17 +231,12 @@ Build:
|
||||||
Prefix: ${prefix}
|
Prefix: ${prefix}
|
||||||
Exec Prefix: ${exec_prefix}
|
Exec Prefix: ${exec_prefix}
|
||||||
|
|
||||||
GUI:
|
|
||||||
GUI path: ${CORE_LIB_DIR}
|
|
||||||
GUI config: ${CORE_GUI_CONF_DIR}
|
|
||||||
|
|
||||||
Daemon:
|
Daemon:
|
||||||
Daemon path: ${bindir}
|
Daemon path: ${bindir}
|
||||||
Daemon config: ${CORE_CONF_DIR}
|
Daemon config: ${CORE_CONF_DIR}
|
||||||
Python: ${PYTHON}
|
Python: ${PYTHON}
|
||||||
|
|
||||||
Features to build:
|
Features to build:
|
||||||
Build GUI: ${enable_gui}
|
|
||||||
Build Daemon: ${enable_daemon}
|
Build Daemon: ${enable_daemon}
|
||||||
Documentation: ${want_docs}
|
Documentation: ${want_docs}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
# CORE
|
# CORE
|
||||||
# (c)2010-2012 the Boeing Company.
|
|
||||||
# See the LICENSE file included in this distribution.
|
|
||||||
#
|
|
||||||
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
|
|
||||||
#
|
#
|
||||||
# Makefile for building netns components.
|
# Makefile for building netns components.
|
||||||
#
|
#
|
||||||
|
|
|
@ -16,7 +16,13 @@ from core.api.grpc.configservices_pb2 import (
|
||||||
GetConfigServiceDefaultsRequest,
|
GetConfigServiceDefaultsRequest,
|
||||||
GetNodeConfigServiceRequest,
|
GetNodeConfigServiceRequest,
|
||||||
)
|
)
|
||||||
from core.api.grpc.core_pb2 import ExecuteScriptRequest, GetConfigRequest
|
from core.api.grpc.core_pb2 import (
|
||||||
|
ExecuteScriptRequest,
|
||||||
|
GetConfigRequest,
|
||||||
|
LinkedRequest,
|
||||||
|
WirelessConfigRequest,
|
||||||
|
WirelessLinkedRequest,
|
||||||
|
)
|
||||||
from core.api.grpc.emane_pb2 import (
|
from core.api.grpc.emane_pb2 import (
|
||||||
EmaneLinkRequest,
|
EmaneLinkRequest,
|
||||||
GetEmaneEventChannelRequest,
|
GetEmaneEventChannelRequest,
|
||||||
|
@ -43,6 +49,7 @@ from core.api.grpc.wlan_pb2 import (
|
||||||
WlanConfig,
|
WlanConfig,
|
||||||
WlanLinkRequest,
|
WlanLinkRequest,
|
||||||
)
|
)
|
||||||
|
from core.api.grpc.wrappers import LinkOptions
|
||||||
from core.emulator.data import IpPrefixes
|
from core.emulator.data import IpPrefixes
|
||||||
from core.errors import CoreError
|
from core.errors import CoreError
|
||||||
|
|
||||||
|
@ -1049,6 +1056,72 @@ class CoreGrpcClient:
|
||||||
"""
|
"""
|
||||||
self.stub.EmanePathlosses(streamer.iter())
|
self.stub.EmanePathlosses(streamer.iter())
|
||||||
|
|
||||||
|
def linked(
|
||||||
|
self,
|
||||||
|
session_id: int,
|
||||||
|
node1_id: int,
|
||||||
|
node2_id: int,
|
||||||
|
iface1_id: int,
|
||||||
|
iface2_id: int,
|
||||||
|
linked: bool,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Link or unlink an existing core wired link.
|
||||||
|
|
||||||
|
:param session_id: session containing the link
|
||||||
|
:param node1_id: first node in link
|
||||||
|
:param node2_id: second node in link
|
||||||
|
:param iface1_id: node1 interface
|
||||||
|
:param iface2_id: node2 interface
|
||||||
|
:param linked: True to connect link, False to disconnect
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
request = LinkedRequest(
|
||||||
|
session_id=session_id,
|
||||||
|
node1_id=node1_id,
|
||||||
|
node2_id=node2_id,
|
||||||
|
iface1_id=iface1_id,
|
||||||
|
iface2_id=iface2_id,
|
||||||
|
linked=linked,
|
||||||
|
)
|
||||||
|
self.stub.Linked(request)
|
||||||
|
|
||||||
|
def wireless_linked(
|
||||||
|
self,
|
||||||
|
session_id: int,
|
||||||
|
wireless_id: int,
|
||||||
|
node1_id: int,
|
||||||
|
node2_id: int,
|
||||||
|
linked: bool,
|
||||||
|
) -> None:
|
||||||
|
request = WirelessLinkedRequest(
|
||||||
|
session_id=session_id,
|
||||||
|
wireless_id=wireless_id,
|
||||||
|
node1_id=node1_id,
|
||||||
|
node2_id=node2_id,
|
||||||
|
linked=linked,
|
||||||
|
)
|
||||||
|
self.stub.WirelessLinked(request)
|
||||||
|
|
||||||
|
def wireless_config(
|
||||||
|
self,
|
||||||
|
session_id: int,
|
||||||
|
wireless_id: int,
|
||||||
|
node1_id: int,
|
||||||
|
node2_id: int,
|
||||||
|
options1: LinkOptions,
|
||||||
|
options2: LinkOptions = None,
|
||||||
|
) -> None:
|
||||||
|
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 connect(self) -> None:
|
def connect(self) -> None:
|
||||||
"""
|
"""
|
||||||
Open connection to server, must be closed manually.
|
Open connection to server, must be closed manually.
|
||||||
|
|
|
@ -3,7 +3,7 @@ from queue import Empty, Queue
|
||||||
from typing import Iterable, Optional
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
from core.api.grpc.grpcutils import convert_link
|
from core.api.grpc.grpcutils import convert_link_data
|
||||||
from core.emulator.data import (
|
from core.emulator.data import (
|
||||||
ConfigData,
|
ConfigData,
|
||||||
EventData,
|
EventData,
|
||||||
|
@ -51,7 +51,7 @@ def handle_link_event(link_data: LinkData) -> core_pb2.Event:
|
||||||
:param link_data: link data
|
:param link_data: link data
|
||||||
:return: link event that has message type and link information
|
:return: link event that has message type and link information
|
||||||
"""
|
"""
|
||||||
link = convert_link(link_data)
|
link = convert_link_data(link_data)
|
||||||
message_type = link_data.message_type.value
|
message_type = link_data.message_type.value
|
||||||
link_event = core_pb2.LinkEvent(message_type=message_type, link=link)
|
link_event = core_pb2.LinkEvent(message_type=message_type, link=link)
|
||||||
return core_pb2.Event(link_event=link_event, source=link_data.source)
|
return core_pb2.Event(link_event=link_event, source=link_data.source)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Tuple, Type, Union
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
from grpc import ServicerContext
|
from grpc import ServicerContext
|
||||||
|
@ -20,6 +20,7 @@ from core.config import ConfigurableOptions
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
|
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
|
||||||
from core.emulator.enumerations import LinkTypes, NodeTypes
|
from core.emulator.enumerations import LinkTypes, NodeTypes
|
||||||
|
from core.emulator.links import CoreLink
|
||||||
from core.emulator.session import Session
|
from core.emulator.session import Session
|
||||||
from core.errors import CoreError
|
from core.errors import CoreError
|
||||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||||
|
@ -27,7 +28,7 @@ from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
||||||
from core.nodes.docker import DockerNode
|
from core.nodes.docker import DockerNode
|
||||||
from core.nodes.interface import CoreInterface
|
from core.nodes.interface import CoreInterface
|
||||||
from core.nodes.lxd import LxcNode
|
from core.nodes.lxd import LxcNode
|
||||||
from core.nodes.network import CtrlNet, PtpNet, WlanNode
|
from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode
|
||||||
from core.services.coreservices import CoreService
|
from core.services.coreservices import CoreService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -110,7 +111,7 @@ def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
|
||||||
|
|
||||||
def add_link_data(
|
def add_link_data(
|
||||||
link_proto: core_pb2.Link
|
link_proto: core_pb2.Link
|
||||||
) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]:
|
) -> Tuple[InterfaceData, InterfaceData, LinkOptions]:
|
||||||
"""
|
"""
|
||||||
Convert link proto to link interfaces and options data.
|
Convert link proto to link interfaces and options data.
|
||||||
|
|
||||||
|
@ -119,7 +120,6 @@ def add_link_data(
|
||||||
"""
|
"""
|
||||||
iface1_data = link_iface(link_proto.iface1)
|
iface1_data = link_iface(link_proto.iface1)
|
||||||
iface2_data = link_iface(link_proto.iface2)
|
iface2_data = link_iface(link_proto.iface2)
|
||||||
link_type = LinkTypes(link_proto.type)
|
|
||||||
options = LinkOptions()
|
options = LinkOptions()
|
||||||
options_proto = link_proto.options
|
options_proto = link_proto.options
|
||||||
if options_proto:
|
if options_proto:
|
||||||
|
@ -134,7 +134,7 @@ def add_link_data(
|
||||||
options.buffer = options_proto.buffer
|
options.buffer = options_proto.buffer
|
||||||
options.unidirectional = options_proto.unidirectional
|
options.unidirectional = options_proto.unidirectional
|
||||||
options.key = options_proto.key
|
options.key = options_proto.key
|
||||||
return iface1_data, iface2_data, options, link_type
|
return iface1_data, iface2_data, options
|
||||||
|
|
||||||
|
|
||||||
def create_nodes(
|
def create_nodes(
|
||||||
|
@ -174,8 +174,8 @@ def create_links(
|
||||||
for link_proto in link_protos:
|
for link_proto in link_protos:
|
||||||
node1_id = link_proto.node1_id
|
node1_id = link_proto.node1_id
|
||||||
node2_id = link_proto.node2_id
|
node2_id = link_proto.node2_id
|
||||||
iface1, iface2, options, link_type = add_link_data(link_proto)
|
iface1, iface2, options = add_link_data(link_proto)
|
||||||
args = (node1_id, node2_id, iface1, iface2, options, link_type)
|
args = (node1_id, node2_id, iface1, iface2, options)
|
||||||
funcs.append((session.add_link, args, {}))
|
funcs.append((session.add_link, args, {}))
|
||||||
start = time.monotonic()
|
start = time.monotonic()
|
||||||
results, exceptions = utils.threadpool(funcs)
|
results, exceptions = utils.threadpool(funcs)
|
||||||
|
@ -198,8 +198,8 @@ def edit_links(
|
||||||
for link_proto in link_protos:
|
for link_proto in link_protos:
|
||||||
node1_id = link_proto.node1_id
|
node1_id = link_proto.node1_id
|
||||||
node2_id = link_proto.node2_id
|
node2_id = link_proto.node2_id
|
||||||
iface1, iface2, options, link_type = add_link_data(link_proto)
|
iface1, iface2, options = add_link_data(link_proto)
|
||||||
args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type)
|
args = (node1_id, node2_id, iface1.id, iface2.id, options)
|
||||||
funcs.append((session.update_link, args, {}))
|
funcs.append((session.update_link, args, {}))
|
||||||
start = time.monotonic()
|
start = time.monotonic()
|
||||||
results, exceptions = utils.threadpool(funcs)
|
results, exceptions = utils.threadpool(funcs)
|
||||||
|
@ -344,61 +344,84 @@ def get_node_proto(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_links(node: NodeBase):
|
def get_links(session: Session, node: NodeBase) -> List[core_pb2.Link]:
|
||||||
"""
|
"""
|
||||||
Retrieve a list of links for grpc to use.
|
Retrieve a list of links for grpc to use.
|
||||||
|
|
||||||
|
:param session: session to get links for node
|
||||||
:param node: node to get links from
|
:param node: node to get links from
|
||||||
:return: protobuf links
|
:return: protobuf links
|
||||||
"""
|
"""
|
||||||
|
link_protos = []
|
||||||
|
for core_link in session.link_manager.node_links(node):
|
||||||
|
link_protos.extend(convert_core_link(core_link))
|
||||||
|
if isinstance(node, (WlanNode, EmaneNet)):
|
||||||
|
for link_data in node.links():
|
||||||
|
link_protos.append(convert_link_data(link_data))
|
||||||
|
return link_protos
|
||||||
|
|
||||||
|
|
||||||
|
def convert_iface(iface: CoreInterface) -> core_pb2.Interface:
|
||||||
|
"""
|
||||||
|
Convert interface to protobuf.
|
||||||
|
|
||||||
|
:param iface: interface to convert
|
||||||
|
:return: protobuf interface
|
||||||
|
"""
|
||||||
|
if isinstance(iface.node, CoreNetwork):
|
||||||
|
return core_pb2.Interface(id=iface.id)
|
||||||
|
else:
|
||||||
|
ip4 = iface.get_ip4()
|
||||||
|
ip4_mask = ip4.prefixlen if ip4 else None
|
||||||
|
ip4 = str(ip4.ip) if ip4 else None
|
||||||
|
ip6 = iface.get_ip6()
|
||||||
|
ip6_mask = ip6.prefixlen if ip6 else None
|
||||||
|
ip6 = str(ip6.ip) if ip6 else None
|
||||||
|
mac = str(iface.mac) if iface.mac else None
|
||||||
|
return core_pb2.Interface(
|
||||||
|
id=iface.id,
|
||||||
|
name=iface.name,
|
||||||
|
mac=mac,
|
||||||
|
ip4=ip4,
|
||||||
|
ip4_mask=ip4_mask,
|
||||||
|
ip6=ip6,
|
||||||
|
ip6_mask=ip6_mask,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_core_link(core_link: CoreLink) -> List[core_pb2.Link]:
|
||||||
|
"""
|
||||||
|
Convert core link to protobuf data.
|
||||||
|
|
||||||
|
:param core_link: core link to convert
|
||||||
|
:return: protobuf link data
|
||||||
|
"""
|
||||||
links = []
|
links = []
|
||||||
for link in node.links():
|
node1, iface1 = core_link.node1, core_link.iface1
|
||||||
link_proto = convert_link(link)
|
node2, iface2 = core_link.node2, core_link.iface2
|
||||||
links.append(link_proto)
|
unidirectional = core_link.is_unidirectional()
|
||||||
|
link = convert_link(node1, iface1, node2, iface2, iface1.options, unidirectional)
|
||||||
|
links.append(link)
|
||||||
|
if unidirectional:
|
||||||
|
link = convert_link(
|
||||||
|
node2, iface2, node1, iface1, iface2.options, unidirectional
|
||||||
|
)
|
||||||
|
links.append(link)
|
||||||
return links
|
return links
|
||||||
|
|
||||||
|
|
||||||
def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface:
|
def convert_link_data(link_data: LinkData) -> core_pb2.Link:
|
||||||
return core_pb2.Interface(
|
|
||||||
id=iface_data.id,
|
|
||||||
name=iface_data.name,
|
|
||||||
mac=iface_data.mac,
|
|
||||||
ip4=iface_data.ip4,
|
|
||||||
ip4_mask=iface_data.ip4_mask,
|
|
||||||
ip6=iface_data.ip6,
|
|
||||||
ip6_mask=iface_data.ip6_mask,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions:
|
|
||||||
return core_pb2.LinkOptions(
|
|
||||||
jitter=options_data.jitter,
|
|
||||||
key=options_data.key,
|
|
||||||
mburst=options_data.mburst,
|
|
||||||
mer=options_data.mer,
|
|
||||||
loss=options_data.loss,
|
|
||||||
bandwidth=options_data.bandwidth,
|
|
||||||
burst=options_data.burst,
|
|
||||||
delay=options_data.delay,
|
|
||||||
dup=options_data.dup,
|
|
||||||
buffer=options_data.buffer,
|
|
||||||
unidirectional=options_data.unidirectional,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_link(link_data: LinkData) -> core_pb2.Link:
|
|
||||||
"""
|
"""
|
||||||
Convert link_data into core protobuf link.
|
Convert link_data into core protobuf link.
|
||||||
|
|
||||||
:param link_data: link to convert
|
:param link_data: link to convert
|
||||||
:return: core protobuf Link
|
:return: core protobuf Link
|
||||||
"""
|
"""
|
||||||
iface1 = None
|
iface1 = None
|
||||||
if link_data.iface1 is not None:
|
if link_data.iface1 is not None:
|
||||||
iface1 = convert_iface(link_data.iface1)
|
iface1 = convert_iface_data(link_data.iface1)
|
||||||
iface2 = None
|
iface2 = None
|
||||||
if link_data.iface2 is not None:
|
if link_data.iface2 is not None:
|
||||||
iface2 = convert_iface(link_data.iface2)
|
iface2 = convert_iface_data(link_data.iface2)
|
||||||
options = convert_link_options(link_data.options)
|
options = convert_link_options(link_data.options)
|
||||||
return core_pb2.Link(
|
return core_pb2.Link(
|
||||||
type=link_data.type.value,
|
type=link_data.type.value,
|
||||||
|
@ -413,6 +436,89 @@ 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_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 get_net_stats() -> Dict[str, Dict]:
|
def get_net_stats() -> Dict[str, Dict]:
|
||||||
"""
|
"""
|
||||||
Retrieve status about the current interfaces in the system
|
Retrieve status about the current interfaces in the system
|
||||||
|
@ -490,39 +596,13 @@ def get_service_configuration(service: CoreService) -> NodeServiceData:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def iface_to_data(iface: CoreInterface) -> InterfaceData:
|
def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface:
|
||||||
ip4 = iface.get_ip4()
|
|
||||||
ip4_addr = str(ip4.ip) if ip4 else None
|
|
||||||
ip4_mask = ip4.prefixlen if ip4 else None
|
|
||||||
ip6 = iface.get_ip6()
|
|
||||||
ip6_addr = str(ip6.ip) if ip6 else None
|
|
||||||
ip6_mask = ip6.prefixlen if ip6 else None
|
|
||||||
return InterfaceData(
|
|
||||||
id=iface.node_id,
|
|
||||||
name=iface.name,
|
|
||||||
mac=str(iface.mac),
|
|
||||||
ip4=ip4_addr,
|
|
||||||
ip4_mask=ip4_mask,
|
|
||||||
ip6=ip6_addr,
|
|
||||||
ip6_mask=ip6_mask,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
|
|
||||||
"""
|
"""
|
||||||
Convenience for converting a core interface to the protobuf representation.
|
Convenience for converting a core interface to the protobuf representation.
|
||||||
|
|
||||||
:param node_id: id of node to convert interface for
|
|
||||||
:param iface: interface to convert
|
:param iface: interface to convert
|
||||||
:return: interface proto
|
:return: interface proto
|
||||||
"""
|
"""
|
||||||
if iface.node and iface.node.id == node_id:
|
|
||||||
_id = iface.node_id
|
|
||||||
else:
|
|
||||||
_id = iface.net_id
|
|
||||||
net_id = iface.net.id if iface.net else None
|
|
||||||
node_id = iface.node.id if iface.node else None
|
|
||||||
net2_id = iface.othernet.id if iface.othernet else None
|
|
||||||
ip4_net = iface.get_ip4()
|
ip4_net = iface.get_ip4()
|
||||||
ip4 = str(ip4_net.ip) if ip4_net else None
|
ip4 = str(ip4_net.ip) if ip4_net else None
|
||||||
ip4_mask = ip4_net.prefixlen if ip4_net else None
|
ip4_mask = ip4_net.prefixlen if ip4_net else None
|
||||||
|
@ -531,10 +611,7 @@ def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
|
||||||
ip6_mask = ip6_net.prefixlen if ip6_net else None
|
ip6_mask = ip6_net.prefixlen if ip6_net else None
|
||||||
mac = str(iface.mac) if iface.mac else None
|
mac = str(iface.mac) if iface.mac else None
|
||||||
return core_pb2.Interface(
|
return core_pb2.Interface(
|
||||||
id=_id,
|
id=iface.id,
|
||||||
net_id=net_id,
|
|
||||||
net2_id=net2_id,
|
|
||||||
node_id=node_id,
|
|
||||||
name=iface.name,
|
name=iface.name,
|
||||||
mac=mac,
|
mac=mac,
|
||||||
mtu=iface.mtu,
|
mtu=iface.mtu,
|
||||||
|
@ -574,6 +651,12 @@ def get_nem_id(
|
||||||
|
|
||||||
|
|
||||||
def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]:
|
def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]:
|
||||||
|
"""
|
||||||
|
Get emane model configuration protobuf data.
|
||||||
|
|
||||||
|
:param session: session to get emane model configuration for
|
||||||
|
:return: dict of emane model protobuf configurations
|
||||||
|
"""
|
||||||
configs = {}
|
configs = {}
|
||||||
for _id, model_configs in session.emane.node_configs.items():
|
for _id, model_configs in session.emane.node_configs.items():
|
||||||
for model_name in model_configs:
|
for model_name in model_configs:
|
||||||
|
@ -591,6 +674,12 @@ def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneCo
|
||||||
|
|
||||||
|
|
||||||
def get_hooks(session: Session) -> List[core_pb2.Hook]:
|
def get_hooks(session: Session) -> List[core_pb2.Hook]:
|
||||||
|
"""
|
||||||
|
Retrieve hook protobuf data for a session.
|
||||||
|
|
||||||
|
:param session: session to get hooks for
|
||||||
|
:return: list of hook protobufs
|
||||||
|
"""
|
||||||
hooks = []
|
hooks = []
|
||||||
for state in session.hooks:
|
for state in session.hooks:
|
||||||
state_hooks = session.hooks[state]
|
state_hooks = session.hooks[state]
|
||||||
|
@ -601,6 +690,12 @@ def get_hooks(session: Session) -> List[core_pb2.Hook]:
|
||||||
|
|
||||||
|
|
||||||
def get_default_services(session: Session) -> List[ServiceDefaults]:
|
def get_default_services(session: Session) -> List[ServiceDefaults]:
|
||||||
|
"""
|
||||||
|
Retrieve the default service sets for a given session.
|
||||||
|
|
||||||
|
:param session: session to get default service sets for
|
||||||
|
:return: list of default service sets
|
||||||
|
"""
|
||||||
default_services = []
|
default_services = []
|
||||||
for name, services in session.services.default_services.items():
|
for name, services in session.services.default_services.items():
|
||||||
default_service = ServiceDefaults(node_type=name, services=services)
|
default_service = ServiceDefaults(node_type=name, services=services)
|
||||||
|
@ -611,6 +706,14 @@ def get_default_services(session: Session) -> List[ServiceDefaults]:
|
||||||
def get_mobility_node(
|
def get_mobility_node(
|
||||||
session: Session, node_id: int, context: ServicerContext
|
session: Session, node_id: int, context: ServicerContext
|
||||||
) -> Union[WlanNode, EmaneNet]:
|
) -> Union[WlanNode, EmaneNet]:
|
||||||
|
"""
|
||||||
|
Get mobility node.
|
||||||
|
|
||||||
|
:param session: session to get node from
|
||||||
|
:param node_id: id of node to get
|
||||||
|
:param context: grpc context
|
||||||
|
:return: wlan or emane node
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return session.get_node(node_id, WlanNode)
|
return session.get_node(node_id, WlanNode)
|
||||||
except CoreError:
|
except CoreError:
|
||||||
|
@ -621,17 +724,26 @@ def get_mobility_node(
|
||||||
|
|
||||||
|
|
||||||
def convert_session(session: Session) -> wrappers.Session:
|
def convert_session(session: Session) -> wrappers.Session:
|
||||||
links = []
|
"""
|
||||||
nodes = []
|
Convert session to its wrapped version.
|
||||||
|
|
||||||
|
:param session: session to convert
|
||||||
|
:return: wrapped session data
|
||||||
|
"""
|
||||||
emane_configs = get_emane_model_configs_dict(session)
|
emane_configs = get_emane_model_configs_dict(session)
|
||||||
|
nodes = []
|
||||||
|
links = []
|
||||||
for _id in session.nodes:
|
for _id in session.nodes:
|
||||||
node = session.nodes[_id]
|
node = session.nodes[_id]
|
||||||
if not isinstance(node, (PtpNet, CtrlNet)):
|
if not isinstance(node, (PtpNet, CtrlNet)):
|
||||||
node_emane_configs = emane_configs.get(node.id, [])
|
node_emane_configs = emane_configs.get(node.id, [])
|
||||||
node_proto = get_node_proto(session, node, node_emane_configs)
|
node_proto = get_node_proto(session, node, node_emane_configs)
|
||||||
nodes.append(node_proto)
|
nodes.append(node_proto)
|
||||||
node_links = get_links(node)
|
if isinstance(node, (WlanNode, EmaneNet)):
|
||||||
links.extend(node_links)
|
for link_data in node.links():
|
||||||
|
links.append(convert_link_data(link_data))
|
||||||
|
for core_link in session.link_manager.links():
|
||||||
|
links.extend(convert_core_link(core_link))
|
||||||
default_services = get_default_services(session)
|
default_services = get_default_services(session)
|
||||||
x, y, z = session.location.refxyz
|
x, y, z = session.location.refxyz
|
||||||
lat, lon, alt = session.location.refgeo
|
lat, lon, alt = session.location.refgeo
|
||||||
|
@ -665,6 +777,15 @@ def convert_session(session: Session) -> wrappers.Session:
|
||||||
def configure_node(
|
def configure_node(
|
||||||
session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext
|
session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Configure a node using all provided protobuf data.
|
||||||
|
|
||||||
|
:param session: session for node
|
||||||
|
:param node: node protobuf data
|
||||||
|
:param core_node: session node
|
||||||
|
:param context: grpc context
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
for emane_config in node.emane_configs:
|
for emane_config in node.emane_configs:
|
||||||
_id = utils.iface_config_id(node.id, emane_config.iface_id)
|
_id = utils.iface_config_id(node.id, emane_config.iface_id)
|
||||||
config = {k: v.value for k, v in emane_config.config.items()}
|
config = {k: v.value for k, v in emane_config.config.items()}
|
||||||
|
|
|
@ -26,7 +26,15 @@ from core.api.grpc.configservices_pb2 import (
|
||||||
GetNodeConfigServiceRequest,
|
GetNodeConfigServiceRequest,
|
||||||
GetNodeConfigServiceResponse,
|
GetNodeConfigServiceResponse,
|
||||||
)
|
)
|
||||||
from core.api.grpc.core_pb2 import ExecuteScriptResponse
|
from core.api.grpc.core_pb2 import (
|
||||||
|
ExecuteScriptResponse,
|
||||||
|
LinkedRequest,
|
||||||
|
LinkedResponse,
|
||||||
|
WirelessConfigRequest,
|
||||||
|
WirelessConfigResponse,
|
||||||
|
WirelessLinkedRequest,
|
||||||
|
WirelessLinkedResponse,
|
||||||
|
)
|
||||||
from core.api.grpc.emane_pb2 import (
|
from core.api.grpc.emane_pb2 import (
|
||||||
EmaneLinkRequest,
|
EmaneLinkRequest,
|
||||||
EmaneLinkResponse,
|
EmaneLinkResponse,
|
||||||
|
@ -76,17 +84,13 @@ from core.configservice.base import ConfigServiceBootError
|
||||||
from core.emane.modelmanager import EmaneModelManager
|
from core.emane.modelmanager import EmaneModelManager
|
||||||
from core.emulator.coreemu import CoreEmu
|
from core.emulator.coreemu import CoreEmu
|
||||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
||||||
from core.emulator.enumerations import (
|
from core.emulator.enumerations import EventTypes, ExceptionLevels, MessageFlags
|
||||||
EventTypes,
|
|
||||||
ExceptionLevels,
|
|
||||||
LinkTypes,
|
|
||||||
MessageFlags,
|
|
||||||
)
|
|
||||||
from core.emulator.session import NT, Session
|
from core.emulator.session import NT, Session
|
||||||
from core.errors import CoreCommandError, CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||||
from core.nodes.base import CoreNode, NodeBase
|
from core.nodes.base import CoreNode, NodeBase
|
||||||
from core.nodes.network import WlanNode
|
from core.nodes.network import CoreNetwork, WlanNode
|
||||||
|
from core.nodes.wireless import WirelessNode
|
||||||
from core.services.coreservices import ServiceManager
|
from core.services.coreservices import ServiceManager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -248,13 +252,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
|
|
||||||
# clear previous state and setup for creation
|
# clear previous state and setup for creation
|
||||||
session.clear()
|
session.clear()
|
||||||
|
session.directory.mkdir(exist_ok=True)
|
||||||
if request.definition:
|
if request.definition:
|
||||||
state = EventTypes.DEFINITION_STATE
|
state = EventTypes.DEFINITION_STATE
|
||||||
else:
|
else:
|
||||||
state = EventTypes.CONFIGURATION_STATE
|
state = EventTypes.CONFIGURATION_STATE
|
||||||
session.directory.mkdir(exist_ok=True)
|
|
||||||
session.set_state(state)
|
session.set_state(state)
|
||||||
session.user = request.session.user
|
if request.session.user:
|
||||||
|
session.set_user(request.session.user)
|
||||||
|
|
||||||
# session options
|
# session options
|
||||||
session.options.config_reset()
|
session.options.config_reset()
|
||||||
|
@ -564,12 +569,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
ifaces = []
|
ifaces = []
|
||||||
for iface_id in node.ifaces:
|
for iface_id in node.ifaces:
|
||||||
iface = node.ifaces[iface_id]
|
iface = node.ifaces[iface_id]
|
||||||
iface_proto = grpcutils.iface_to_proto(request.node_id, iface)
|
iface_proto = grpcutils.iface_to_proto(iface)
|
||||||
ifaces.append(iface_proto)
|
ifaces.append(iface_proto)
|
||||||
emane_configs = grpcutils.get_emane_model_configs_dict(session)
|
emane_configs = grpcutils.get_emane_model_configs_dict(session)
|
||||||
node_emane_configs = emane_configs.get(node.id, [])
|
node_emane_configs = emane_configs.get(node.id, [])
|
||||||
node_proto = grpcutils.get_node_proto(session, node, node_emane_configs)
|
node_proto = grpcutils.get_node_proto(session, node, node_emane_configs)
|
||||||
links = get_links(node)
|
links = get_links(session, node)
|
||||||
return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces, links=links)
|
return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces, links=links)
|
||||||
|
|
||||||
def MoveNode(
|
def MoveNode(
|
||||||
|
@ -705,18 +710,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
node2_id = request.link.node2_id
|
node2_id = request.link.node2_id
|
||||||
self.get_node(session, node1_id, context, NodeBase)
|
self.get_node(session, node1_id, context, NodeBase)
|
||||||
self.get_node(session, node2_id, context, NodeBase)
|
self.get_node(session, node2_id, context, NodeBase)
|
||||||
iface1_data, iface2_data, options, link_type = grpcutils.add_link_data(
|
iface1_data, iface2_data, options = grpcutils.add_link_data(request.link)
|
||||||
request.link
|
|
||||||
)
|
|
||||||
node1_iface, node2_iface = session.add_link(
|
node1_iface, node2_iface = session.add_link(
|
||||||
node1_id, node2_id, iface1_data, iface2_data, options, link_type
|
node1_id, node2_id, iface1_data, iface2_data, options
|
||||||
)
|
)
|
||||||
iface1_data = None
|
iface1_data = None
|
||||||
if node1_iface:
|
if node1_iface:
|
||||||
iface1_data = grpcutils.iface_to_data(node1_iface)
|
if isinstance(node1_iface.node, CoreNetwork):
|
||||||
|
iface1_data = InterfaceData(id=node1_iface.id)
|
||||||
|
else:
|
||||||
|
iface1_data = node1_iface.get_data()
|
||||||
iface2_data = None
|
iface2_data = None
|
||||||
if node2_iface:
|
if node2_iface:
|
||||||
iface2_data = grpcutils.iface_to_data(node2_iface)
|
if isinstance(node2_iface.node, CoreNetwork):
|
||||||
|
iface2_data = InterfaceData(id=node2_iface.id)
|
||||||
|
else:
|
||||||
|
iface2_data = node2_iface.get_data()
|
||||||
source = request.source if request.source else None
|
source = request.source if request.source else None
|
||||||
link_data = LinkData(
|
link_data = LinkData(
|
||||||
message_type=MessageFlags.ADD,
|
message_type=MessageFlags.ADD,
|
||||||
|
@ -731,9 +740,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
iface1_proto = None
|
iface1_proto = None
|
||||||
iface2_proto = None
|
iface2_proto = None
|
||||||
if node1_iface:
|
if node1_iface:
|
||||||
iface1_proto = grpcutils.iface_to_proto(node1_id, node1_iface)
|
iface1_proto = grpcutils.iface_to_proto(node1_iface)
|
||||||
if node2_iface:
|
if node2_iface:
|
||||||
iface2_proto = grpcutils.iface_to_proto(node2_id, node2_iface)
|
iface2_proto = grpcutils.iface_to_proto(node2_iface)
|
||||||
return core_pb2.AddLinkResponse(
|
return core_pb2.AddLinkResponse(
|
||||||
result=True, iface1=iface1_proto, iface2=iface2_proto
|
result=True, iface1=iface1_proto, iface2=iface2_proto
|
||||||
)
|
)
|
||||||
|
@ -1163,7 +1172,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
self, request: core_pb2.GetInterfacesRequest, context: ServicerContext
|
self, request: core_pb2.GetInterfacesRequest, context: ServicerContext
|
||||||
) -> core_pb2.GetInterfacesResponse:
|
) -> core_pb2.GetInterfacesResponse:
|
||||||
"""
|
"""
|
||||||
Retrieve all the interfaces of the system including bridges, virtual ethernet, and loopback
|
Retrieve all the interfaces of the system including bridges, virtual ethernet,
|
||||||
|
and loopback.
|
||||||
|
|
||||||
:param request: get-interfaces request
|
:param request: get-interfaces request
|
||||||
:param context: context object
|
:param context: context object
|
||||||
|
@ -1188,32 +1198,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
"""
|
"""
|
||||||
logger.debug("emane link: %s", request)
|
logger.debug("emane link: %s", request)
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
nem1 = request.nem1
|
flag = MessageFlags.ADD if request.linked else MessageFlags.DELETE
|
||||||
iface1 = session.emane.get_iface(nem1)
|
link = session.emane.get_nem_link(request.nem1, request.nem2, flag)
|
||||||
if not iface1:
|
if link:
|
||||||
context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found")
|
|
||||||
node1 = iface1.node
|
|
||||||
|
|
||||||
nem2 = request.nem2
|
|
||||||
iface2 = session.emane.get_iface(nem2)
|
|
||||||
if not iface2:
|
|
||||||
context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found")
|
|
||||||
node2 = iface2.node
|
|
||||||
|
|
||||||
if iface1.net == iface2.net:
|
|
||||||
if request.linked:
|
|
||||||
flag = MessageFlags.ADD
|
|
||||||
else:
|
|
||||||
flag = MessageFlags.DELETE
|
|
||||||
color = session.get_link_color(iface1.net.id)
|
|
||||||
link = LinkData(
|
|
||||||
message_type=flag,
|
|
||||||
type=LinkTypes.WIRELESS,
|
|
||||||
node1_id=node1.id,
|
|
||||||
node2_id=node2.id,
|
|
||||||
network_id=iface1.net.id,
|
|
||||||
color=color,
|
|
||||||
)
|
|
||||||
session.broadcast_link(link)
|
session.broadcast_link(link)
|
||||||
return EmaneLinkResponse(result=True)
|
return EmaneLinkResponse(result=True)
|
||||||
else:
|
else:
|
||||||
|
@ -1302,15 +1289,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
if not isinstance(wlan.model, BasicRangeModel):
|
if not isinstance(wlan.model, BasicRangeModel):
|
||||||
context.abort(
|
context.abort(
|
||||||
grpc.StatusCode.NOT_FOUND,
|
grpc.StatusCode.NOT_FOUND,
|
||||||
f"wlan node {request.wlan} does not using BasicRangeModel",
|
f"wlan node {request.wlan} is not using BasicRangeModel",
|
||||||
)
|
)
|
||||||
node1 = self.get_node(session, request.node1_id, context, CoreNode)
|
node1 = self.get_node(session, request.node1_id, context, CoreNode)
|
||||||
node2 = self.get_node(session, request.node2_id, context, CoreNode)
|
node2 = self.get_node(session, request.node2_id, context, CoreNode)
|
||||||
node1_iface, node2_iface = None, None
|
node1_iface, node2_iface = None, None
|
||||||
for net, iface1, iface2 in node1.commonnets(node2):
|
for iface in node1.get_ifaces(control=False):
|
||||||
if net == wlan:
|
if iface.net == wlan:
|
||||||
node1_iface = iface1
|
node1_iface = iface
|
||||||
node2_iface = iface2
|
break
|
||||||
|
for iface in node2.get_ifaces(control=False):
|
||||||
|
if iface.net == wlan:
|
||||||
|
node2_iface = iface
|
||||||
break
|
break
|
||||||
result = False
|
result = False
|
||||||
if node1_iface and node2_iface:
|
if node1_iface and node2_iface:
|
||||||
|
@ -1335,3 +1325,36 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context)
|
nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context)
|
||||||
session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
|
session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
|
||||||
return EmanePathlossesResponse()
|
return EmanePathlossesResponse()
|
||||||
|
|
||||||
|
def Linked(
|
||||||
|
self, request: LinkedRequest, context: ServicerContext
|
||||||
|
) -> LinkedResponse:
|
||||||
|
session = self.get_session(request.session_id, context)
|
||||||
|
session.linked(
|
||||||
|
request.node1_id,
|
||||||
|
request.node2_id,
|
||||||
|
request.iface1_id,
|
||||||
|
request.iface2_id,
|
||||||
|
request.linked,
|
||||||
|
)
|
||||||
|
return LinkedResponse()
|
||||||
|
|
||||||
|
def WirelessLinked(
|
||||||
|
self, request: WirelessLinkedRequest, context: ServicerContext
|
||||||
|
) -> WirelessLinkedResponse:
|
||||||
|
session = self.get_session(request.session_id, context)
|
||||||
|
wireless = self.get_node(session, request.wireless_id, context, WirelessNode)
|
||||||
|
wireless.link_control(request.node1_id, request.node2_id, request.linked)
|
||||||
|
return WirelessLinkedResponse()
|
||||||
|
|
||||||
|
def WirelessConfig(
|
||||||
|
self, request: WirelessConfigRequest, context: ServicerContext
|
||||||
|
) -> WirelessConfigResponse:
|
||||||
|
session = self.get_session(request.session_id, context)
|
||||||
|
wireless = self.get_node(session, request.wireless_id, context, WirelessNode)
|
||||||
|
options1 = request.options1
|
||||||
|
options2 = options1
|
||||||
|
if request.HasField("options2"):
|
||||||
|
options2 = request.options2
|
||||||
|
wireless.link_config(request.node1_id, request.node2_id, options1, options2)
|
||||||
|
return WirelessConfigResponse()
|
||||||
|
|
|
@ -67,6 +67,7 @@ class NodeType(Enum):
|
||||||
CONTROL_NET = 13
|
CONTROL_NET = 13
|
||||||
DOCKER = 15
|
DOCKER = 15
|
||||||
LXC = 16
|
LXC = 16
|
||||||
|
WIRELESS = 17
|
||||||
|
|
||||||
|
|
||||||
class LinkType(Enum):
|
class LinkType(Enum):
|
||||||
|
@ -637,6 +638,15 @@ class SessionSummary:
|
||||||
dir=proto.dir,
|
dir=proto.dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_proto(self) -> core_pb2.SessionSummary:
|
||||||
|
return core_pb2.SessionSummary(
|
||||||
|
id=self.id,
|
||||||
|
state=self.state.value,
|
||||||
|
nodes=self.nodes,
|
||||||
|
file=self.file,
|
||||||
|
dir=self.dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Hook:
|
class Hook:
|
||||||
|
@ -1093,7 +1103,6 @@ class ConfigEvent:
|
||||||
data_types=list(proto.data_types),
|
data_types=list(proto.data_types),
|
||||||
data_values=proto.data_values,
|
data_values=proto.data_values,
|
||||||
captions=proto.captions,
|
captions=proto.captions,
|
||||||
bitmap=proto.bitmap,
|
|
||||||
possible_values=proto.possible_values,
|
possible_values=proto.possible_values,
|
||||||
groups=proto.groups,
|
groups=proto.groups,
|
||||||
iface_id=proto.iface_id,
|
iface_id=proto.iface_id,
|
||||||
|
|
|
@ -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
|
|
|
@ -113,7 +113,6 @@ class ConfigurableOptions:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
bitmap: Optional[str] = None
|
|
||||||
options: List[Configuration] = []
|
options: List[Configuration] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -4,14 +4,25 @@ from typing import Any, Dict, List
|
||||||
from core.config import Configuration
|
from core.config import Configuration
|
||||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.nodes.base import CoreNodeBase
|
from core.nodes.base import CoreNodeBase, NodeBase
|
||||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||||
from core.nodes.network import WlanNode
|
from core.nodes.network import WlanNode
|
||||||
|
from core.nodes.wireless import WirelessNode
|
||||||
|
|
||||||
GROUP: str = "FRR"
|
GROUP: str = "FRR"
|
||||||
FRR_STATE_DIR: str = "/var/run/frr"
|
FRR_STATE_DIR: str = "/var/run/frr"
|
||||||
|
|
||||||
|
|
||||||
|
def is_wireless(node: NodeBase) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the node is a wireless type node.
|
||||||
|
|
||||||
|
:param node: node to check type for
|
||||||
|
:return: True if wireless type, False otherwise
|
||||||
|
"""
|
||||||
|
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
|
||||||
|
|
||||||
|
|
||||||
def has_mtu_mismatch(iface: CoreInterface) -> bool:
|
def has_mtu_mismatch(iface: CoreInterface) -> bool:
|
||||||
"""
|
"""
|
||||||
Helper to detect MTU mismatch and add the appropriate FRR
|
Helper to detect MTU mismatch and add the appropriate FRR
|
||||||
|
@ -324,7 +335,7 @@ class FRRBabel(FrrService, ConfigService):
|
||||||
return self.render_text(text, data)
|
return self.render_text(text, data)
|
||||||
|
|
||||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
if is_wireless(iface.net):
|
||||||
text = """
|
text = """
|
||||||
babel wireless
|
babel wireless
|
||||||
no babel split-horizon
|
no babel split-horizon
|
||||||
|
|
|
@ -5,16 +5,27 @@ from typing import Any, Dict, List
|
||||||
from core.config import Configuration
|
from core.config import Configuration
|
||||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.nodes.base import CoreNodeBase
|
from core.nodes.base import CoreNodeBase, NodeBase
|
||||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||||
from core.nodes.network import PtpNet, WlanNode
|
from core.nodes.network import PtpNet, WlanNode
|
||||||
from core.nodes.physical import Rj45Node
|
from core.nodes.physical import Rj45Node
|
||||||
|
from core.nodes.wireless import WirelessNode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
GROUP: str = "Quagga"
|
GROUP: str = "Quagga"
|
||||||
QUAGGA_STATE_DIR: str = "/var/run/quagga"
|
QUAGGA_STATE_DIR: str = "/var/run/quagga"
|
||||||
|
|
||||||
|
|
||||||
|
def is_wireless(node: NodeBase) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the node is a wireless type node.
|
||||||
|
|
||||||
|
:param node: node to check type for
|
||||||
|
:return: True if wireless type, False otherwise
|
||||||
|
"""
|
||||||
|
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
|
||||||
|
|
||||||
|
|
||||||
def has_mtu_mismatch(iface: CoreInterface) -> bool:
|
def has_mtu_mismatch(iface: CoreInterface) -> bool:
|
||||||
"""
|
"""
|
||||||
Helper to detect MTU mismatch and add the appropriate OSPF
|
Helper to detect MTU mismatch and add the appropriate OSPF
|
||||||
|
@ -265,7 +276,7 @@ class Ospfv3mdr(Ospfv3):
|
||||||
|
|
||||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
config = super().quagga_iface_config(iface)
|
config = super().quagga_iface_config(iface)
|
||||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
if is_wireless(iface.net):
|
||||||
config = self.clean_text(
|
config = self.clean_text(
|
||||||
f"""
|
f"""
|
||||||
{config}
|
{config}
|
||||||
|
@ -390,7 +401,7 @@ class Babel(QuaggaService, ConfigService):
|
||||||
return self.render_text(text, data)
|
return self.render_text(text, data)
|
||||||
|
|
||||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
if is_wireless(iface.net):
|
||||||
text = """
|
text = """
|
||||||
babel wireless
|
babel wireless
|
||||||
no babel split-horizon
|
no babel split-horizon
|
||||||
|
|
|
@ -12,12 +12,12 @@ from core import utils
|
||||||
from core.emane.emanemodel import EmaneModel
|
from core.emane.emanemodel import EmaneModel
|
||||||
from core.emane.linkmonitor import EmaneLinkMonitor
|
from core.emane.linkmonitor import EmaneLinkMonitor
|
||||||
from core.emane.modelmanager import EmaneModelManager
|
from core.emane.modelmanager import EmaneModelManager
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet, TunTap
|
||||||
from core.emulator.data import LinkData
|
from core.emulator.data import LinkData
|
||||||
from core.emulator.enumerations import LinkTypes, MessageFlags, RegisterTlvs
|
from core.emulator.enumerations import LinkTypes, MessageFlags, RegisterTlvs
|
||||||
from core.errors import CoreCommandError, CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.nodes.base import CoreNetworkBase, CoreNode, NodeBase
|
from core.nodes.base import CoreNode, NodeBase
|
||||||
from core.nodes.interface import CoreInterface, TunTap
|
from core.nodes.interface import CoreInterface
|
||||||
from core.xml import emanexml
|
from core.xml import emanexml
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -224,11 +224,9 @@ class EmaneManager:
|
||||||
:return: net, node, or interface model configuration
|
:return: net, node, or interface model configuration
|
||||||
"""
|
"""
|
||||||
model_name = emane_net.model.name
|
model_name = emane_net.model.name
|
||||||
config = None
|
|
||||||
# try to retrieve interface specific configuration
|
# try to retrieve interface specific configuration
|
||||||
if iface.node_id is not None:
|
key = utils.iface_config_id(iface.node.id, iface.id)
|
||||||
key = utils.iface_config_id(iface.node.id, iface.node_id)
|
config = self.get_config(key, model_name, default=False)
|
||||||
config = self.get_config(key, model_name, default=False)
|
|
||||||
# attempt to retrieve node specific config, when iface config is not present
|
# attempt to retrieve node specific config, when iface config is not present
|
||||||
if not config:
|
if not config:
|
||||||
config = self.get_config(iface.node.id, model_name, default=False)
|
config = self.get_config(iface.node.id, model_name, default=False)
|
||||||
|
@ -272,7 +270,8 @@ class EmaneManager:
|
||||||
nodes = set()
|
nodes = set()
|
||||||
for emane_net in self._emane_nets.values():
|
for emane_net in self._emane_nets.values():
|
||||||
for iface in emane_net.get_ifaces():
|
for iface in emane_net.get_ifaces():
|
||||||
nodes.add(iface.node)
|
if isinstance(iface.node, CoreNode):
|
||||||
|
nodes.add(iface.node)
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
def setup(self) -> EmaneState:
|
def setup(self) -> EmaneState:
|
||||||
|
@ -323,7 +322,7 @@ class EmaneManager:
|
||||||
for emane_net, iface in self.get_ifaces():
|
for emane_net, iface in self.get_ifaces():
|
||||||
self.start_iface(emane_net, iface)
|
self.start_iface(emane_net, iface)
|
||||||
|
|
||||||
def start_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
|
def start_iface(self, emane_net: EmaneNet, iface: TunTap) -> None:
|
||||||
nem_id = self.next_nem_id(iface)
|
nem_id = self.next_nem_id(iface)
|
||||||
nem_port = self.get_nem_port(iface)
|
nem_port = self.get_nem_port(iface)
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -338,7 +337,7 @@ class EmaneManager:
|
||||||
self.start_daemon(iface)
|
self.start_daemon(iface)
|
||||||
self.install_iface(iface, config)
|
self.install_iface(iface, config)
|
||||||
|
|
||||||
def get_ifaces(self) -> List[Tuple[EmaneNet, CoreInterface]]:
|
def get_ifaces(self) -> List[Tuple[EmaneNet, TunTap]]:
|
||||||
ifaces = []
|
ifaces = []
|
||||||
for emane_net in self._emane_nets.values():
|
for emane_net in self._emane_nets.values():
|
||||||
if not emane_net.model:
|
if not emane_net.model:
|
||||||
|
@ -352,8 +351,9 @@ class EmaneManager:
|
||||||
iface.name,
|
iface.name,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
ifaces.append((emane_net, iface))
|
if isinstance(iface, TunTap):
|
||||||
return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].node_id))
|
ifaces.append((emane_net, iface))
|
||||||
|
return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].id))
|
||||||
|
|
||||||
def setup_control_channels(
|
def setup_control_channels(
|
||||||
self, nem_id: int, iface: CoreInterface, config: Dict[str, str]
|
self, nem_id: int, iface: CoreInterface, config: Dict[str, str]
|
||||||
|
@ -622,9 +622,9 @@ class EmaneManager:
|
||||||
args = f"{emanecmd} -f {log_file} {platform_xml}"
|
args = f"{emanecmd} -f {log_file} {platform_xml}"
|
||||||
node.host_cmd(args, cwd=self.session.directory)
|
node.host_cmd(args, cwd=self.session.directory)
|
||||||
|
|
||||||
def install_iface(self, iface: CoreInterface, config: Dict[str, str]) -> None:
|
def install_iface(self, iface: TunTap, config: Dict[str, str]) -> None:
|
||||||
external = config.get("external", "0")
|
external = config.get("external", "0")
|
||||||
if isinstance(iface, TunTap) and external == "0":
|
if external == "0":
|
||||||
iface.set_ips()
|
iface.set_ips()
|
||||||
# at this point we register location handlers for generating
|
# at this point we register location handlers for generating
|
||||||
# EMANE location events
|
# EMANE location events
|
||||||
|
@ -732,9 +732,6 @@ class EmaneManager:
|
||||||
self.session.broadcast_node(node)
|
self.session.broadcast_node(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_emane_net(self, net: Optional[CoreNetworkBase]) -> bool:
|
|
||||||
return isinstance(net, EmaneNet)
|
|
||||||
|
|
||||||
def emanerunning(self, node: CoreNode) -> bool:
|
def emanerunning(self, node: CoreNode) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if an EMANE process associated with the given node is running,
|
Return True if an EMANE process associated with the given node is running,
|
||||||
|
|
|
@ -4,7 +4,8 @@ share the same MAC+PHY model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Type
|
import time
|
||||||
|
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type
|
||||||
|
|
||||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
||||||
from core.emulator.distributed import DistributedServer
|
from core.emulator.distributed import DistributedServer
|
||||||
|
@ -15,7 +16,7 @@ from core.emulator.enumerations import (
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
RegisterTlvs,
|
RegisterTlvs,
|
||||||
)
|
)
|
||||||
from core.errors import CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||||
from core.nodes.interface import CoreInterface
|
from core.nodes.interface import CoreInterface
|
||||||
|
|
||||||
|
@ -39,6 +40,114 @@ except ImportError:
|
||||||
logger.debug("compatible emane python bindings not installed")
|
logger.debug("compatible emane python bindings not installed")
|
||||||
|
|
||||||
|
|
||||||
|
class TunTap(CoreInterface):
|
||||||
|
"""
|
||||||
|
TUN/TAP virtual device in TAP mode
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
_id: int,
|
||||||
|
name: str,
|
||||||
|
localname: str,
|
||||||
|
use_ovs: bool,
|
||||||
|
node: CoreNode = None,
|
||||||
|
server: "DistributedServer" = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(_id, name, localname, use_ovs, node=node, server=server)
|
||||||
|
self.node: CoreNode = node
|
||||||
|
|
||||||
|
def startup(self) -> None:
|
||||||
|
"""
|
||||||
|
Startup logic for a tunnel tap.
|
||||||
|
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
self.up = True
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
"""
|
||||||
|
Shutdown functionality for a tunnel tap.
|
||||||
|
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
if not self.up:
|
||||||
|
return
|
||||||
|
self.up = False
|
||||||
|
|
||||||
|
def waitfor(
|
||||||
|
self, func: Callable[[], int], attempts: int = 10, maxretrydelay: float = 0.25
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Wait for func() to return zero with exponential backoff.
|
||||||
|
|
||||||
|
:param func: function to wait for a result of zero
|
||||||
|
:param attempts: number of attempts to wait for a zero result
|
||||||
|
:param maxretrydelay: maximum retry delay
|
||||||
|
:return: True if wait succeeded, False otherwise
|
||||||
|
"""
|
||||||
|
delay = 0.01
|
||||||
|
result = False
|
||||||
|
for i in range(1, attempts + 1):
|
||||||
|
r = func()
|
||||||
|
if r == 0:
|
||||||
|
result = True
|
||||||
|
break
|
||||||
|
msg = f"attempt {i} failed with nonzero exit status {r}"
|
||||||
|
if i < attempts + 1:
|
||||||
|
msg += ", retrying..."
|
||||||
|
logger.info(msg)
|
||||||
|
time.sleep(delay)
|
||||||
|
delay += delay
|
||||||
|
if delay > maxretrydelay:
|
||||||
|
delay = maxretrydelay
|
||||||
|
else:
|
||||||
|
msg += ", giving up"
|
||||||
|
logger.info(msg)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def nodedevexists(self) -> int:
|
||||||
|
"""
|
||||||
|
Checks if device exists.
|
||||||
|
|
||||||
|
:return: 0 if device exists, 1 otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.node.node_net_client.device_show(self.name)
|
||||||
|
return 0
|
||||||
|
except CoreCommandError:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def waitfordevicenode(self) -> None:
|
||||||
|
"""
|
||||||
|
Check for presence of a node device - tap device may not appear right away waits.
|
||||||
|
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
logger.debug("waiting for device node: %s", self.name)
|
||||||
|
count = 0
|
||||||
|
while True:
|
||||||
|
result = self.waitfor(self.nodedevexists)
|
||||||
|
if result:
|
||||||
|
break
|
||||||
|
should_retry = count < 5
|
||||||
|
is_emane_running = self.node.session.emane.emanerunning(self.node)
|
||||||
|
if all([should_retry, is_emane_running]):
|
||||||
|
count += 1
|
||||||
|
else:
|
||||||
|
raise RuntimeError("node device failed to exist")
|
||||||
|
|
||||||
|
def set_ips(self) -> None:
|
||||||
|
"""
|
||||||
|
Set interface ip addresses.
|
||||||
|
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
self.waitfordevicenode()
|
||||||
|
for ip in self.ips():
|
||||||
|
self.node.node_net_client.create_address(self.name, str(ip))
|
||||||
|
|
||||||
|
|
||||||
class EmaneNet(CoreNetworkBase):
|
class EmaneNet(CoreNetworkBase):
|
||||||
"""
|
"""
|
||||||
EMANE node contains NEM configuration and causes connected nodes
|
EMANE node contains NEM configuration and causes connected nodes
|
||||||
|
@ -49,7 +158,6 @@ class EmaneNet(CoreNetworkBase):
|
||||||
apitype: NodeTypes = NodeTypes.EMANE
|
apitype: NodeTypes = NodeTypes.EMANE
|
||||||
linktype: LinkTypes = LinkTypes.WIRED
|
linktype: LinkTypes = LinkTypes.WIRED
|
||||||
type: str = "wlan"
|
type: str = "wlan"
|
||||||
has_custom_iface: bool = True
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -73,14 +181,11 @@ class EmaneNet(CoreNetworkBase):
|
||||||
return
|
return
|
||||||
self.model.linkconfig(iface, options, iface2)
|
self.model.linkconfig(iface, options, iface2)
|
||||||
|
|
||||||
def config(self, conf: str) -> None:
|
|
||||||
self.conf = conf
|
|
||||||
|
|
||||||
def startup(self) -> None:
|
def startup(self) -> None:
|
||||||
pass
|
self.up = True
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
pass
|
self.up = False
|
||||||
|
|
||||||
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||||
pass
|
pass
|
||||||
|
@ -88,10 +193,13 @@ class EmaneNet(CoreNetworkBase):
|
||||||
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
|
|
||||||
raise CoreError("emane networks cannot be linked to other networks")
|
|
||||||
|
|
||||||
def updatemodel(self, config: Dict[str, str]) -> None:
|
def updatemodel(self, config: Dict[str, str]) -> None:
|
||||||
|
"""
|
||||||
|
Update configuration for the current model.
|
||||||
|
|
||||||
|
:param config: configuration to update model with
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
if not self.model:
|
if not self.model:
|
||||||
raise CoreError(f"no model set to update for node({self.name})")
|
raise CoreError(f"no model set to update for node({self.name})")
|
||||||
logger.info("node(%s) updating model(%s): %s", self.id, self.model.name, config)
|
logger.info("node(%s) updating model(%s): %s", self.id, self.model.name, config)
|
||||||
|
@ -111,7 +219,7 @@ class EmaneNet(CoreNetworkBase):
|
||||||
self.mobility.update_config(config)
|
self.mobility.update_config(config)
|
||||||
|
|
||||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
||||||
links = super().links(flags)
|
links = []
|
||||||
emane_manager = self.session.emane
|
emane_manager = self.session.emane
|
||||||
# gather current emane links
|
# gather current emane links
|
||||||
nem_ids = set()
|
nem_ids = set()
|
||||||
|
@ -132,22 +240,44 @@ class EmaneNet(CoreNetworkBase):
|
||||||
# ignore incomplete links
|
# ignore incomplete links
|
||||||
if (nem2, nem1) not in emane_links:
|
if (nem2, nem1) not in emane_links:
|
||||||
continue
|
continue
|
||||||
link = emane_manager.get_nem_link(nem1, nem2)
|
link = emane_manager.get_nem_link(nem1, nem2, flags)
|
||||||
if link:
|
if link:
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return links
|
return links
|
||||||
|
|
||||||
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
def create_tuntap(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
||||||
# TUN/TAP is not ready for addressing yet; the device may
|
"""
|
||||||
# take some time to appear, and installing it into a
|
Create a tuntap interface for the provided node.
|
||||||
# namespace after it has been bound removes addressing;
|
|
||||||
# save addresses with the interface now
|
:param node: node to create tuntap interface for
|
||||||
iface_id = node.newtuntap(iface_data.id, iface_data.name)
|
:param iface_data: interface data to create interface with
|
||||||
node.attachnet(iface_id, self)
|
:return: created tuntap interface
|
||||||
iface = node.get_iface(iface_id)
|
"""
|
||||||
iface.set_mac(iface_data.mac)
|
with node.lock:
|
||||||
for ip in iface_data.get_ips():
|
if iface_data.id is not None and iface_data.id in node.ifaces:
|
||||||
iface.add_ip(ip)
|
raise CoreError(
|
||||||
|
f"node({self.id}) interface({iface_data.id}) already exists"
|
||||||
|
)
|
||||||
|
iface_id = (
|
||||||
|
iface_data.id if iface_data.id is not None else node.next_iface_id()
|
||||||
|
)
|
||||||
|
name = iface_data.name if iface_data.name is not None else f"eth{iface_id}"
|
||||||
|
session_id = self.session.short_session_id()
|
||||||
|
localname = f"tap{node.id}.{iface_id}.{session_id}"
|
||||||
|
iface = TunTap(iface_id, name, localname, self.session.use_ovs(), node=node)
|
||||||
|
if iface_data.mac:
|
||||||
|
iface.set_mac(iface_data.mac)
|
||||||
|
for ip in iface_data.get_ips():
|
||||||
|
iface.add_ip(ip)
|
||||||
|
node.ifaces[iface_id] = iface
|
||||||
|
self.attach(iface)
|
||||||
|
if self.up:
|
||||||
|
iface.startup()
|
||||||
if self.session.state == EventTypes.RUNTIME_STATE:
|
if self.session.state == EventTypes.RUNTIME_STATE:
|
||||||
self.session.emane.start_iface(self, iface)
|
self.session.emane.start_iface(self, iface)
|
||||||
return iface
|
return iface
|
||||||
|
|
||||||
|
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||||
|
raise CoreError(
|
||||||
|
f"emane network({self.name}) do not support adopting interfaces"
|
||||||
|
)
|
||||||
|
|
|
@ -15,6 +15,7 @@ from fabric import Connection
|
||||||
from invoke import UnexpectedExit
|
from invoke import UnexpectedExit
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
from core.emulator.links import CoreLink
|
||||||
from core.errors import CoreCommandError, CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.executables import get_requirements
|
from core.executables import get_requirements
|
||||||
from core.nodes.interface import GreTap
|
from core.nodes.interface import GreTap
|
||||||
|
@ -183,21 +184,36 @@ class DistributedController:
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""
|
"""
|
||||||
Start distributed network tunnels.
|
Start distributed network tunnels for control networks.
|
||||||
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
mtu = self.session.options.get_config_int("mtu")
|
mtu = self.session.options.get_config_int("mtu")
|
||||||
for node_id in self.session.nodes:
|
for node_id in self.session.nodes:
|
||||||
node = self.session.nodes[node_id]
|
node = self.session.nodes[node_id]
|
||||||
if not isinstance(node, CoreNetwork):
|
if not isinstance(node, CtrlNet) or node.serverintf is not None:
|
||||||
continue
|
|
||||||
if isinstance(node, CtrlNet) and node.serverintf is not None:
|
|
||||||
continue
|
continue
|
||||||
for name in self.servers:
|
for name in self.servers:
|
||||||
server = self.servers[name]
|
server = self.servers[name]
|
||||||
self.create_gre_tunnel(node, server, mtu, True)
|
self.create_gre_tunnel(node, server, mtu, True)
|
||||||
|
|
||||||
|
def create_gre_tunnels(self, core_link: CoreLink) -> None:
|
||||||
|
"""
|
||||||
|
Creates gre tunnels for a core link with a ptp network connection.
|
||||||
|
|
||||||
|
:param core_link: core link to create gre tunnel for
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
if not self.servers:
|
||||||
|
return
|
||||||
|
if not core_link.ptp:
|
||||||
|
raise CoreError(
|
||||||
|
"attempted to create gre tunnel for core link without a ptp network"
|
||||||
|
)
|
||||||
|
mtu = self.session.options.get_config_int("mtu")
|
||||||
|
for server in self.servers.values():
|
||||||
|
self.create_gre_tunnel(core_link.ptp, server, mtu, True)
|
||||||
|
|
||||||
def create_gre_tunnel(
|
def create_gre_tunnel(
|
||||||
self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool
|
self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool
|
||||||
) -> Tuple[GreTap, GreTap]:
|
) -> Tuple[GreTap, GreTap]:
|
||||||
|
|
|
@ -20,6 +20,17 @@ class MessageFlags(Enum):
|
||||||
TTY = 0x40
|
TTY = 0x40
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlags(Enum):
|
||||||
|
"""
|
||||||
|
Configuration flags.
|
||||||
|
"""
|
||||||
|
|
||||||
|
NONE = 0x00
|
||||||
|
REQUEST = 0x01
|
||||||
|
UPDATE = 0x02
|
||||||
|
RESET = 0x03
|
||||||
|
|
||||||
|
|
||||||
class NodeTypes(Enum):
|
class NodeTypes(Enum):
|
||||||
"""
|
"""
|
||||||
Node types.
|
Node types.
|
||||||
|
@ -38,6 +49,7 @@ class NodeTypes(Enum):
|
||||||
CONTROL_NET = 13
|
CONTROL_NET = 13
|
||||||
DOCKER = 15
|
DOCKER = 15
|
||||||
LXC = 16
|
LXC = 16
|
||||||
|
WIRELESS = 17
|
||||||
|
|
||||||
|
|
||||||
class LinkTypes(Enum):
|
class LinkTypes(Enum):
|
||||||
|
|
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()
|
|
@ -35,10 +35,10 @@ from core.emulator.distributed import DistributedController
|
||||||
from core.emulator.enumerations import (
|
from core.emulator.enumerations import (
|
||||||
EventTypes,
|
EventTypes,
|
||||||
ExceptionLevels,
|
ExceptionLevels,
|
||||||
LinkTypes,
|
|
||||||
MessageFlags,
|
MessageFlags,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
)
|
)
|
||||||
|
from core.emulator.links import CoreLink, LinkManager
|
||||||
from core.emulator.sessionconfig import SessionConfig
|
from core.emulator.sessionconfig import SessionConfig
|
||||||
from core.errors import CoreError
|
from core.errors import CoreError
|
||||||
from core.location.event import EventLoop
|
from core.location.event import EventLoop
|
||||||
|
@ -58,6 +58,7 @@ from core.nodes.network import (
|
||||||
WlanNode,
|
WlanNode,
|
||||||
)
|
)
|
||||||
from core.nodes.physical import PhysicalNode, Rj45Node
|
from core.nodes.physical import PhysicalNode, Rj45Node
|
||||||
|
from core.nodes.wireless import WirelessNode
|
||||||
from core.plugins.sdt import Sdt
|
from core.plugins.sdt import Sdt
|
||||||
from core.services.coreservices import CoreServices
|
from core.services.coreservices import CoreServices
|
||||||
from core.xml import corexml, corexmldeployment
|
from core.xml import corexml, corexmldeployment
|
||||||
|
@ -80,12 +81,18 @@ NODES: Dict[NodeTypes, Type[NodeBase]] = {
|
||||||
NodeTypes.CONTROL_NET: CtrlNet,
|
NodeTypes.CONTROL_NET: CtrlNet,
|
||||||
NodeTypes.DOCKER: DockerNode,
|
NodeTypes.DOCKER: DockerNode,
|
||||||
NodeTypes.LXC: LxcNode,
|
NodeTypes.LXC: LxcNode,
|
||||||
|
NodeTypes.WIRELESS: WirelessNode,
|
||||||
}
|
}
|
||||||
NODES_TYPE: Dict[Type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES}
|
NODES_TYPE: Dict[Type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES}
|
||||||
CONTAINER_NODES: Set[Type[NodeBase]] = {DockerNode, LxcNode}
|
CONTAINER_NODES: Set[Type[NodeBase]] = {DockerNode, LxcNode}
|
||||||
CTRL_NET_ID: int = 9001
|
CTRL_NET_ID: int = 9001
|
||||||
LINK_COLORS: List[str] = ["green", "blue", "orange", "purple", "turquoise"]
|
LINK_COLORS: List[str] = ["green", "blue", "orange", "purple", "turquoise"]
|
||||||
NT: TypeVar = TypeVar("NT", bound=NodeBase)
|
NT: TypeVar = TypeVar("NT", bound=NodeBase)
|
||||||
|
WIRELESS_TYPE: Tuple[Type[WlanNode], Type[EmaneNet], Type[WirelessNode]] = (
|
||||||
|
WlanNode,
|
||||||
|
EmaneNet,
|
||||||
|
WirelessNode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Session:
|
class Session:
|
||||||
|
@ -119,7 +126,8 @@ class Session:
|
||||||
|
|
||||||
# dict of nodes: all nodes and nets
|
# dict of nodes: all nodes and nets
|
||||||
self.nodes: Dict[int, NodeBase] = {}
|
self.nodes: Dict[int, NodeBase] = {}
|
||||||
self.nodes_lock = threading.Lock()
|
self.nodes_lock: threading.Lock = threading.Lock()
|
||||||
|
self.link_manager: LinkManager = LinkManager()
|
||||||
|
|
||||||
# states and hooks handlers
|
# states and hooks handlers
|
||||||
self.state: EventTypes = EventTypes.DEFINITION_STATE
|
self.state: EventTypes = EventTypes.DEFINITION_STATE
|
||||||
|
@ -187,43 +195,48 @@ class Session:
|
||||||
raise CoreError(f"invalid node class: {_class}")
|
raise CoreError(f"invalid node class: {_class}")
|
||||||
return node_type
|
return node_type
|
||||||
|
|
||||||
def _link_wireless(
|
|
||||||
self, node1: CoreNodeBase, node2: CoreNodeBase, connect: bool
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Objects to deal with when connecting/disconnecting wireless links.
|
|
||||||
|
|
||||||
:param node1: node one for wireless link
|
|
||||||
:param node2: node two for wireless link
|
|
||||||
:param connect: link interfaces if True, unlink otherwise
|
|
||||||
:return: nothing
|
|
||||||
:raises core.CoreError: when objects to link is less than 2, or no common
|
|
||||||
networks are found
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
"handling wireless linking node1(%s) node2(%s): %s",
|
|
||||||
node1.name,
|
|
||||||
node2.name,
|
|
||||||
connect,
|
|
||||||
)
|
|
||||||
common_networks = node1.commonnets(node1)
|
|
||||||
if not common_networks:
|
|
||||||
raise CoreError("no common network found for wireless link/unlink")
|
|
||||||
for common_network, iface1, iface2 in common_networks:
|
|
||||||
if not isinstance(common_network, (WlanNode, EmaneNet)):
|
|
||||||
logger.info(
|
|
||||||
"skipping common network that is not wireless/emane: %s",
|
|
||||||
common_network,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
if connect:
|
|
||||||
common_network.link(iface1, iface2)
|
|
||||||
else:
|
|
||||||
common_network.unlink(iface1, iface2)
|
|
||||||
|
|
||||||
def use_ovs(self) -> bool:
|
def use_ovs(self) -> bool:
|
||||||
return self.options.get_config("ovs") == "1"
|
return self.options.get_config("ovs") == "1"
|
||||||
|
|
||||||
|
def linked(
|
||||||
|
self, node1_id: int, node2_id: int, iface1_id: int, iface2_id: int, linked: bool
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Links or unlinks wired core link interfaces from being connected to the same
|
||||||
|
bridge.
|
||||||
|
|
||||||
|
: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 if interfaces should be connected, False for disconnected
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
node1 = self.get_node(node1_id, NodeBase)
|
||||||
|
node2 = self.get_node(node2_id, NodeBase)
|
||||||
|
logger.info(
|
||||||
|
"link node(%s):interface(%s) node(%s):interface(%s) linked(%s)",
|
||||||
|
node1.name,
|
||||||
|
iface1_id,
|
||||||
|
node2.name,
|
||||||
|
iface2_id,
|
||||||
|
linked,
|
||||||
|
)
|
||||||
|
iface1 = node1.get_iface(iface1_id)
|
||||||
|
iface2 = node2.get_iface(iface2_id)
|
||||||
|
core_link = self.link_manager.get_link(node1, iface1, node2, iface2)
|
||||||
|
if not core_link:
|
||||||
|
raise CoreError(
|
||||||
|
f"there is no link for node({node1.name}):interface({iface1_id}) "
|
||||||
|
f"node({node2.name}):interface({iface2_id})"
|
||||||
|
)
|
||||||
|
if linked:
|
||||||
|
core_link.ptp.attach(iface1)
|
||||||
|
core_link.ptp.attach(iface2)
|
||||||
|
else:
|
||||||
|
core_link.ptp.detach(iface1)
|
||||||
|
core_link.ptp.detach(iface2)
|
||||||
|
|
||||||
def add_link(
|
def add_link(
|
||||||
self,
|
self,
|
||||||
node1_id: int,
|
node1_id: int,
|
||||||
|
@ -231,8 +244,7 @@ class Session:
|
||||||
iface1_data: InterfaceData = None,
|
iface1_data: InterfaceData = None,
|
||||||
iface2_data: InterfaceData = None,
|
iface2_data: InterfaceData = None,
|
||||||
options: LinkOptions = None,
|
options: LinkOptions = None,
|
||||||
link_type: LinkTypes = LinkTypes.WIRED,
|
) -> Tuple[Optional[CoreInterface], Optional[CoreInterface]]:
|
||||||
) -> Tuple[CoreInterface, CoreInterface]:
|
|
||||||
"""
|
"""
|
||||||
Add a link between nodes.
|
Add a link between nodes.
|
||||||
|
|
||||||
|
@ -244,89 +256,129 @@ class Session:
|
||||||
data, defaults to none
|
data, defaults to none
|
||||||
:param options: data for creating link,
|
:param options: data for creating link,
|
||||||
defaults to no options
|
defaults to no options
|
||||||
:param link_type: type of link to add
|
|
||||||
:return: tuple of created core interfaces, depending on link
|
:return: tuple of created core interfaces, depending on link
|
||||||
"""
|
"""
|
||||||
if not options:
|
options = options if options else LinkOptions()
|
||||||
options = LinkOptions()
|
|
||||||
node1 = self.get_node(node1_id, NodeBase)
|
|
||||||
node2 = self.get_node(node2_id, NodeBase)
|
|
||||||
iface1 = None
|
|
||||||
iface2 = None
|
|
||||||
# set mtu
|
# set mtu
|
||||||
mtu = self.options.get_config_int("mtu") or DEFAULT_MTU
|
mtu = self.options.get_config_int("mtu") or DEFAULT_MTU
|
||||||
if iface1_data:
|
if iface1_data:
|
||||||
iface1_data.mtu = mtu
|
iface1_data.mtu = mtu
|
||||||
if iface2_data:
|
if iface2_data:
|
||||||
iface2_data.mtu = mtu
|
iface2_data.mtu = mtu
|
||||||
# wireless link
|
node1 = self.get_node(node1_id, NodeBase)
|
||||||
if link_type == LinkTypes.WIRELESS:
|
node2 = self.get_node(node2_id, NodeBase)
|
||||||
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
|
# check for invalid linking
|
||||||
self._link_wireless(node1, node2, connect=True)
|
if (
|
||||||
else:
|
isinstance(node1, WIRELESS_TYPE)
|
||||||
raise CoreError(
|
and isinstance(node2, WIRELESS_TYPE)
|
||||||
f"cannot wireless link node1({type(node1)}) node2({type(node2)})"
|
or isinstance(node1, WIRELESS_TYPE)
|
||||||
)
|
and not isinstance(node2, CoreNodeBase)
|
||||||
# wired link
|
or not isinstance(node1, CoreNodeBase)
|
||||||
|
and isinstance(node2, WIRELESS_TYPE)
|
||||||
|
):
|
||||||
|
raise CoreError(f"cannot link node({type(node1)}) node({type(node2)})")
|
||||||
|
# custom links
|
||||||
|
iface1 = None
|
||||||
|
iface2 = None
|
||||||
|
if isinstance(node1, (WlanNode, WirelessNode)):
|
||||||
|
iface2 = self._add_wlan_link(node2, iface2_data, node1)
|
||||||
|
elif isinstance(node2, (WlanNode, WirelessNode)):
|
||||||
|
iface1 = self._add_wlan_link(node1, iface1_data, node2)
|
||||||
|
elif isinstance(node1, EmaneNet) and isinstance(node2, CoreNode):
|
||||||
|
iface2 = self._add_emane_link(node2, iface2_data, node1)
|
||||||
|
elif isinstance(node2, EmaneNet) and isinstance(node1, CoreNode):
|
||||||
|
iface1 = self._add_emane_link(node1, iface1_data, node2)
|
||||||
else:
|
else:
|
||||||
# peer to peer link
|
iface1, iface2 = self._add_wired_link(
|
||||||
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
|
node1, node2, iface1_data, iface2_data, options
|
||||||
logger.info("linking ptp: %s - %s", node1.name, node2.name)
|
)
|
||||||
start = self.state.should_start()
|
# configure tunnel nodes
|
||||||
ptp = self.create_node(PtpNet, start)
|
key = options.key
|
||||||
iface1 = node1.new_iface(ptp, iface1_data)
|
if isinstance(node1, TunnelNode):
|
||||||
iface2 = node2.new_iface(ptp, iface2_data)
|
logger.info("setting tunnel key for: %s", node1.name)
|
||||||
iface1.config(options)
|
node1.setkey(key, iface1_data)
|
||||||
if not options.unidirectional:
|
if isinstance(node2, TunnelNode):
|
||||||
iface2.config(options)
|
logger.info("setting tunnel key for: %s", node2.name)
|
||||||
# link node to net
|
node2.setkey(key, iface2_data)
|
||||||
elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase):
|
|
||||||
logger.info("linking node to net: %s - %s", node1.name, node2.name)
|
|
||||||
iface1 = node1.new_iface(node2, iface1_data)
|
|
||||||
if not isinstance(node2, (EmaneNet, WlanNode)):
|
|
||||||
iface1.config(options)
|
|
||||||
# link net to node
|
|
||||||
elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase):
|
|
||||||
logger.info("linking net to node: %s - %s", node1.name, node2.name)
|
|
||||||
iface2 = node2.new_iface(node1, iface2_data)
|
|
||||||
wireless_net = isinstance(node1, (EmaneNet, WlanNode))
|
|
||||||
if not options.unidirectional and not wireless_net:
|
|
||||||
iface2.config(options)
|
|
||||||
# network to network
|
|
||||||
elif isinstance(node1, CoreNetworkBase) and isinstance(
|
|
||||||
node2, CoreNetworkBase
|
|
||||||
):
|
|
||||||
logger.info(
|
|
||||||
"linking network to network: %s - %s", node1.name, node2.name
|
|
||||||
)
|
|
||||||
iface1 = node1.linknet(node2)
|
|
||||||
use_local = iface1.net == node1
|
|
||||||
iface1.config(options, use_local=use_local)
|
|
||||||
if not options.unidirectional:
|
|
||||||
iface1.config(options, use_local=not use_local)
|
|
||||||
else:
|
|
||||||
raise CoreError(
|
|
||||||
f"cannot link node1({type(node1)}) node2({type(node2)})"
|
|
||||||
)
|
|
||||||
|
|
||||||
# configure tunnel nodes
|
|
||||||
key = options.key
|
|
||||||
if isinstance(node1, TunnelNode):
|
|
||||||
logger.info("setting tunnel key for: %s", node1.name)
|
|
||||||
node1.setkey(key, iface1_data)
|
|
||||||
if isinstance(node2, TunnelNode):
|
|
||||||
logger.info("setting tunnel key for: %s", node2.name)
|
|
||||||
node2.setkey(key, iface2_data)
|
|
||||||
self.sdt.add_link(node1_id, node2_id)
|
self.sdt.add_link(node1_id, node2_id)
|
||||||
return iface1, iface2
|
return iface1, iface2
|
||||||
|
|
||||||
def delete_link(
|
def _add_wlan_link(
|
||||||
self,
|
self,
|
||||||
node1_id: int,
|
node: NodeBase,
|
||||||
node2_id: int,
|
iface_data: InterfaceData,
|
||||||
iface1_id: int = None,
|
net: Union[WlanNode, WirelessNode],
|
||||||
iface2_id: int = None,
|
) -> CoreInterface:
|
||||||
link_type: LinkTypes = LinkTypes.WIRED,
|
"""
|
||||||
|
Create a wlan link.
|
||||||
|
|
||||||
|
:param node: node to link to wlan network
|
||||||
|
:param iface_data: data to create interface with
|
||||||
|
:param net: wlan network to link to
|
||||||
|
:return: interface created for node
|
||||||
|
"""
|
||||||
|
# create interface
|
||||||
|
iface = node.create_iface(iface_data)
|
||||||
|
# attach to wlan
|
||||||
|
net.attach(iface)
|
||||||
|
# track link
|
||||||
|
core_link = CoreLink(node, iface, net, None)
|
||||||
|
self.link_manager.add(core_link)
|
||||||
|
return iface
|
||||||
|
|
||||||
|
def _add_emane_link(
|
||||||
|
self, node: CoreNode, iface_data: InterfaceData, net: EmaneNet
|
||||||
|
) -> CoreInterface:
|
||||||
|
"""
|
||||||
|
Create am emane link.
|
||||||
|
|
||||||
|
:param node: node to link to emane network
|
||||||
|
:param iface_data: data to create interface with
|
||||||
|
:param net: emane network to link to
|
||||||
|
:return: interface created for node
|
||||||
|
"""
|
||||||
|
# create iface tuntap
|
||||||
|
iface = net.create_tuntap(node, iface_data)
|
||||||
|
# track link
|
||||||
|
core_link = CoreLink(node, iface, net, None)
|
||||||
|
self.link_manager.add(core_link)
|
||||||
|
return iface
|
||||||
|
|
||||||
|
def _add_wired_link(
|
||||||
|
self,
|
||||||
|
node1: NodeBase,
|
||||||
|
node2: NodeBase,
|
||||||
|
iface1_data: InterfaceData = None,
|
||||||
|
iface2_data: InterfaceData = None,
|
||||||
|
options: LinkOptions = None,
|
||||||
|
) -> Tuple[CoreInterface, CoreInterface]:
|
||||||
|
"""
|
||||||
|
Create a wired link between two nodes.
|
||||||
|
|
||||||
|
:param node1: first node to be linked
|
||||||
|
:param node2: second node to be linked
|
||||||
|
:param iface1_data: data to create interface for node1
|
||||||
|
:param iface2_data: data to create interface for node2
|
||||||
|
:param options: options to configure interfaces with
|
||||||
|
:return: interfaces created for both nodes
|
||||||
|
"""
|
||||||
|
# create interfaces
|
||||||
|
iface1 = node1.create_iface(iface1_data, options)
|
||||||
|
iface2 = node2.create_iface(iface2_data, options)
|
||||||
|
# join and attach to ptp bridge
|
||||||
|
ptp = self.create_node(PtpNet, self.state.should_start())
|
||||||
|
ptp.attach(iface1)
|
||||||
|
ptp.attach(iface2)
|
||||||
|
# track link
|
||||||
|
core_link = CoreLink(node1, iface1, node2, iface2, ptp)
|
||||||
|
self.link_manager.add(core_link)
|
||||||
|
# setup link for gre tunnels if needed
|
||||||
|
if ptp.up:
|
||||||
|
self.distributed.create_gre_tunnels(core_link)
|
||||||
|
return iface1, iface2
|
||||||
|
|
||||||
|
def delete_link(
|
||||||
|
self, node1_id: int, node2_id: int, iface1_id: int = None, iface2_id: int = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Delete a link between nodes.
|
Delete a link between nodes.
|
||||||
|
@ -335,63 +387,38 @@ class Session:
|
||||||
:param node2_id: node two id
|
:param node2_id: node two id
|
||||||
:param iface1_id: interface id for node one
|
:param iface1_id: interface id for node one
|
||||||
:param iface2_id: interface id for node two
|
:param iface2_id: interface id for node two
|
||||||
:param link_type: link type to delete
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
:raises core.CoreError: when no common network is found for link being deleted
|
:raises core.CoreError: when no common network is found for link being deleted
|
||||||
"""
|
"""
|
||||||
node1 = self.get_node(node1_id, NodeBase)
|
node1 = self.get_node(node1_id, NodeBase)
|
||||||
node2 = self.get_node(node2_id, NodeBase)
|
node2 = self.get_node(node2_id, NodeBase)
|
||||||
logger.info(
|
logger.info(
|
||||||
"deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)",
|
"deleting link node(%s):interface(%s) node(%s):interface(%s)",
|
||||||
link_type.name,
|
|
||||||
node1.name,
|
node1.name,
|
||||||
iface1_id,
|
iface1_id,
|
||||||
node2.name,
|
node2.name,
|
||||||
iface2_id,
|
iface2_id,
|
||||||
)
|
)
|
||||||
|
iface1 = None
|
||||||
# wireless link
|
iface2 = None
|
||||||
if link_type == LinkTypes.WIRELESS:
|
if isinstance(node1, (WlanNode, WirelessNode)):
|
||||||
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
|
iface2 = node2.delete_iface(iface2_id)
|
||||||
self._link_wireless(node1, node2, connect=False)
|
node1.detach(iface2)
|
||||||
else:
|
elif isinstance(node2, (WlanNode, WirelessNode)):
|
||||||
raise CoreError(
|
iface1 = node1.delete_iface(iface1_id)
|
||||||
"cannot delete wireless link "
|
node2.detach(iface1)
|
||||||
f"node1({type(node1)}) node2({type(node2)})"
|
elif isinstance(node1, EmaneNet):
|
||||||
)
|
iface2 = node2.delete_iface(iface2_id)
|
||||||
# wired link
|
node1.detach(iface2)
|
||||||
|
elif isinstance(node2, EmaneNet):
|
||||||
|
iface1 = node1.delete_iface(iface1_id)
|
||||||
|
node2.detach(iface1)
|
||||||
else:
|
else:
|
||||||
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
|
iface1 = node1.delete_iface(iface1_id)
|
||||||
iface1 = node1.get_iface(iface1_id)
|
iface2 = node2.delete_iface(iface2_id)
|
||||||
iface2 = node2.get_iface(iface2_id)
|
core_link = self.link_manager.delete(node1, iface1, node2, iface2)
|
||||||
if iface1.net != iface2.net:
|
if core_link.ptp:
|
||||||
raise CoreError(
|
self.delete_node(core_link.ptp.id)
|
||||||
f"node1({node1.name}) node2({node2.name}) "
|
|
||||||
"not connected to same net"
|
|
||||||
)
|
|
||||||
ptp = iface1.net
|
|
||||||
node1.delete_iface(iface1_id)
|
|
||||||
node2.delete_iface(iface2_id)
|
|
||||||
self.delete_node(ptp.id)
|
|
||||||
elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase):
|
|
||||||
node1.delete_iface(iface1_id)
|
|
||||||
elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase):
|
|
||||||
node2.delete_iface(iface2_id)
|
|
||||||
elif isinstance(node1, CoreNetworkBase) and isinstance(
|
|
||||||
node2, CoreNetworkBase
|
|
||||||
):
|
|
||||||
iface1 = node1.get_linked_iface(node2)
|
|
||||||
if iface1:
|
|
||||||
node1.detach(iface1)
|
|
||||||
iface1.shutdown()
|
|
||||||
iface2 = node2.get_linked_iface(node1)
|
|
||||||
if iface2:
|
|
||||||
node2.detach(iface2)
|
|
||||||
iface2.shutdown()
|
|
||||||
if not iface1 and not iface2:
|
|
||||||
raise CoreError(
|
|
||||||
f"node1({node1.name}) and node2({node2.name}) are not connected"
|
|
||||||
)
|
|
||||||
self.sdt.delete_link(node1_id, node2_id)
|
self.sdt.delete_link(node1_id, node2_id)
|
||||||
|
|
||||||
def update_link(
|
def update_link(
|
||||||
|
@ -401,7 +428,6 @@ class Session:
|
||||||
iface1_id: int = None,
|
iface1_id: int = None,
|
||||||
iface2_id: int = None,
|
iface2_id: int = None,
|
||||||
options: LinkOptions = None,
|
options: LinkOptions = None,
|
||||||
link_type: LinkTypes = LinkTypes.WIRED,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Update link information between nodes.
|
Update link information between nodes.
|
||||||
|
@ -411,7 +437,6 @@ class Session:
|
||||||
:param iface1_id: interface id for node one
|
:param iface1_id: interface id for node one
|
||||||
:param iface2_id: interface id for node two
|
:param iface2_id: interface id for node two
|
||||||
:param options: data to update link with
|
:param options: data to update link with
|
||||||
:param link_type: type of link to update
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
:raises core.CoreError: when updating a wireless type link, when there is a
|
:raises core.CoreError: when updating a wireless type link, when there is a
|
||||||
unknown link between networks
|
unknown link between networks
|
||||||
|
@ -421,72 +446,26 @@ class Session:
|
||||||
node1 = self.get_node(node1_id, NodeBase)
|
node1 = self.get_node(node1_id, NodeBase)
|
||||||
node2 = self.get_node(node2_id, NodeBase)
|
node2 = self.get_node(node2_id, NodeBase)
|
||||||
logger.info(
|
logger.info(
|
||||||
"update link(%s) node(%s):interface(%s) node(%s):interface(%s)",
|
"update link node(%s):interface(%s) node(%s):interface(%s)",
|
||||||
link_type.name,
|
|
||||||
node1.name,
|
node1.name,
|
||||||
iface1_id,
|
iface1_id,
|
||||||
node2.name,
|
node2.name,
|
||||||
iface2_id,
|
iface2_id,
|
||||||
)
|
)
|
||||||
|
iface1 = node1.get_iface(iface1_id) if iface1_id is not None else None
|
||||||
# wireless link
|
iface2 = node2.get_iface(iface2_id) if iface2_id is not None else None
|
||||||
if link_type == LinkTypes.WIRELESS:
|
core_link = self.link_manager.get_link(node1, iface1, node2, iface2)
|
||||||
raise CoreError("cannot update wireless link")
|
if not core_link:
|
||||||
else:
|
raise CoreError(
|
||||||
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
|
f"there is no link for node({node1.name}):interface({iface1_id}) "
|
||||||
iface1 = node1.ifaces.get(iface1_id)
|
f"node({node2.name}):interface({iface2_id})"
|
||||||
if not iface1:
|
)
|
||||||
raise CoreError(
|
if iface1:
|
||||||
f"node({node1.name}) missing interface({iface1_id})"
|
iface1.options.update(options)
|
||||||
)
|
iface1.set_config()
|
||||||
iface2 = node2.ifaces.get(iface2_id)
|
if iface2 and not options.unidirectional:
|
||||||
if not iface2:
|
iface2.options.update(options)
|
||||||
raise CoreError(
|
iface2.set_config()
|
||||||
f"node({node2.name}) missing interface({iface2_id})"
|
|
||||||
)
|
|
||||||
if iface1.net != iface2.net:
|
|
||||||
raise CoreError(
|
|
||||||
f"node1({node1.name}) node2({node2.name}) "
|
|
||||||
"not connected to same net"
|
|
||||||
)
|
|
||||||
iface1.config(options)
|
|
||||||
if not options.unidirectional:
|
|
||||||
iface2.config(options)
|
|
||||||
elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase):
|
|
||||||
iface = node1.get_iface(iface1_id)
|
|
||||||
if iface.net != node2:
|
|
||||||
raise CoreError(
|
|
||||||
f"node1({node1.name}) iface1({iface1_id})"
|
|
||||||
f" is not linked to node1({node2.name})"
|
|
||||||
)
|
|
||||||
iface.config(options)
|
|
||||||
elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase):
|
|
||||||
iface = node2.get_iface(iface2_id)
|
|
||||||
if iface.net != node1:
|
|
||||||
raise CoreError(
|
|
||||||
f"node2({node2.name}) iface2({iface2_id})"
|
|
||||||
f" is not linked to node1({node1.name})"
|
|
||||||
)
|
|
||||||
iface.config(options)
|
|
||||||
elif isinstance(node1, CoreNetworkBase) and isinstance(
|
|
||||||
node2, CoreNetworkBase
|
|
||||||
):
|
|
||||||
iface = node1.get_linked_iface(node2)
|
|
||||||
if not iface:
|
|
||||||
iface = node2.get_linked_iface(node1)
|
|
||||||
if iface:
|
|
||||||
use_local = iface.net == node1
|
|
||||||
iface.config(options, use_local=use_local)
|
|
||||||
if not options.unidirectional:
|
|
||||||
iface.config(options, use_local=not use_local)
|
|
||||||
else:
|
|
||||||
raise CoreError(
|
|
||||||
f"node1({node1.name}) and node2({node2.name}) are not linked"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise CoreError(
|
|
||||||
f"cannot update link node1({type(node1)}) node2({type(node2)})"
|
|
||||||
)
|
|
||||||
|
|
||||||
def next_node_id(self) -> int:
|
def next_node_id(self) -> int:
|
||||||
"""
|
"""
|
||||||
|
@ -618,26 +597,6 @@ class Session:
|
||||||
node.position.set_geo(lon, lat, alt)
|
node.position.set_geo(lon, lat, alt)
|
||||||
self.sdt.edit_node(node, lon, lat, alt)
|
self.sdt.edit_node(node, lon, lat, alt)
|
||||||
|
|
||||||
def start_mobility(self, node_ids: List[int] = None) -> None:
|
|
||||||
"""
|
|
||||||
Start mobility for the provided node ids.
|
|
||||||
|
|
||||||
:param node_ids: nodes to start mobility for
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
self.mobility.startup(node_ids)
|
|
||||||
|
|
||||||
def is_active(self) -> bool:
|
|
||||||
"""
|
|
||||||
Determine if this session is considered to be active.
|
|
||||||
(Runtime or Data collect states)
|
|
||||||
|
|
||||||
:return: True if active, False otherwise
|
|
||||||
"""
|
|
||||||
result = self.state in {EventTypes.RUNTIME_STATE, EventTypes.DATACOLLECT_STATE}
|
|
||||||
logger.info("session(%s) checking if active: %s", self.id, result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def open_xml(self, file_path: Path, start: bool = False) -> None:
|
def open_xml(self, file_path: Path, start: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Import a session from the EmulationScript XML format.
|
Import a session from the EmulationScript XML format.
|
||||||
|
@ -723,6 +682,7 @@ class Session:
|
||||||
"""
|
"""
|
||||||
self.emane.shutdown()
|
self.emane.shutdown()
|
||||||
self.delete_nodes()
|
self.delete_nodes()
|
||||||
|
self.link_manager.reset()
|
||||||
self.distributed.shutdown()
|
self.distributed.shutdown()
|
||||||
self.hooks.clear()
|
self.hooks.clear()
|
||||||
self.emane.reset()
|
self.emane.reset()
|
||||||
|
@ -732,23 +692,6 @@ class Session:
|
||||||
self.mobility.config_reset()
|
self.mobility.config_reset()
|
||||||
self.link_colors.clear()
|
self.link_colors.clear()
|
||||||
|
|
||||||
def start_events(self) -> None:
|
|
||||||
"""
|
|
||||||
Start event loop.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
self.event_loop.run()
|
|
||||||
|
|
||||||
def mobility_event(self, event_data: EventData) -> None:
|
|
||||||
"""
|
|
||||||
Handle a mobility event.
|
|
||||||
|
|
||||||
:param event_data: event data to handle
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
self.mobility.handleevent(event_data)
|
|
||||||
|
|
||||||
def set_location(self, lat: float, lon: float, alt: float, scale: float) -> None:
|
def set_location(self, lat: float, lon: float, alt: float, scale: float) -> None:
|
||||||
"""
|
"""
|
||||||
Set session geospatial location.
|
Set session geospatial location.
|
||||||
|
@ -1007,15 +950,14 @@ class Session:
|
||||||
env["SESSION_STATE"] = str(self.state)
|
env["SESSION_STATE"] = str(self.state)
|
||||||
# try reading and merging optional environments from:
|
# try reading and merging optional environments from:
|
||||||
# /etc/core/environment
|
# /etc/core/environment
|
||||||
# /home/user/.core/environment
|
# /home/user/.coregui/environment
|
||||||
# /tmp/pycore.<session id>/environment
|
# /tmp/pycore.<session id>/environment
|
||||||
core_env_path = constants.CORE_CONF_DIR / "environment"
|
core_env_path = constants.CORE_CONF_DIR / "environment"
|
||||||
session_env_path = self.directory / "environment"
|
session_env_path = self.directory / "environment"
|
||||||
if self.user:
|
if self.user:
|
||||||
user_home_path = Path(f"~{self.user}").expanduser()
|
user_home_path = Path(f"~{self.user}").expanduser()
|
||||||
user_env1 = user_home_path / ".core" / "environment"
|
user_env = user_home_path / ".coregui" / "environment"
|
||||||
user_env2 = user_home_path / ".coregui" / "environment"
|
paths = [core_env_path, user_env, session_env_path]
|
||||||
paths = [core_env_path, user_env1, user_env2, session_env_path]
|
|
||||||
else:
|
else:
|
||||||
paths = [core_env_path, session_env_path]
|
paths = [core_env_path, session_env_path]
|
||||||
for path in paths:
|
for path in paths:
|
||||||
|
@ -1026,21 +968,6 @@ class Session:
|
||||||
logger.exception("error reading environment file: %s", path)
|
logger.exception("error reading environment file: %s", path)
|
||||||
return env
|
return env
|
||||||
|
|
||||||
def set_thumbnail(self, thumb_file: Path) -> None:
|
|
||||||
"""
|
|
||||||
Set the thumbnail filename. Move files from /tmp to session dir.
|
|
||||||
|
|
||||||
:param thumb_file: tumbnail file to set for session
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if not thumb_file.is_file():
|
|
||||||
logger.error("thumbnail file to set does not exist: %s", thumb_file)
|
|
||||||
self.thumbnail = None
|
|
||||||
return
|
|
||||||
dst_path = self.directory / thumb_file.name
|
|
||||||
shutil.copy(thumb_file, dst_path)
|
|
||||||
self.thumbnail = dst_path
|
|
||||||
|
|
||||||
def set_user(self, user: str) -> None:
|
def set_user(self, user: str) -> None:
|
||||||
"""
|
"""
|
||||||
Set the username for this session. Update the permissions of the
|
Set the username for this session. Update the permissions of the
|
||||||
|
@ -1049,14 +976,13 @@ class Session:
|
||||||
:param user: user to give write permissions to for the session directory
|
:param user: user to give write permissions to for the session directory
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
if user:
|
|
||||||
try:
|
|
||||||
uid = pwd.getpwnam(user).pw_uid
|
|
||||||
gid = self.directory.stat().st_gid
|
|
||||||
os.chown(self.directory, uid, gid)
|
|
||||||
except IOError:
|
|
||||||
logger.exception("failed to set permission on %s", self.directory)
|
|
||||||
self.user = user
|
self.user = user
|
||||||
|
try:
|
||||||
|
uid = pwd.getpwnam(user).pw_uid
|
||||||
|
gid = self.directory.stat().st_gid
|
||||||
|
os.chown(self.directory, uid, gid)
|
||||||
|
except IOError:
|
||||||
|
logger.exception("failed to set permission on %s", self.directory)
|
||||||
|
|
||||||
def create_node(
|
def create_node(
|
||||||
self, _class: Type[NT], start: bool, *args: Any, **kwargs: Any
|
self, _class: Type[NT], start: bool, *args: Any, **kwargs: Any
|
||||||
|
@ -1194,6 +1120,10 @@ class Session:
|
||||||
# boot node services and then start mobility
|
# boot node services and then start mobility
|
||||||
exceptions = self.boot_nodes()
|
exceptions = self.boot_nodes()
|
||||||
if not exceptions:
|
if not exceptions:
|
||||||
|
# complete wireless node
|
||||||
|
for node in self.nodes.values():
|
||||||
|
if isinstance(node, WirelessNode):
|
||||||
|
node.post_startup()
|
||||||
self.mobility.startup()
|
self.mobility.startup()
|
||||||
# notify listeners that instantiation is complete
|
# notify listeners that instantiation is complete
|
||||||
event = EventData(event_type=EventTypes.INSTANTIATION_COMPLETE)
|
event = EventData(event_type=EventTypes.INSTANTIATION_COMPLETE)
|
||||||
|
@ -1480,7 +1410,8 @@ class Session:
|
||||||
ip4_mask=ip4_mask,
|
ip4_mask=ip4_mask,
|
||||||
mtu=DEFAULT_MTU,
|
mtu=DEFAULT_MTU,
|
||||||
)
|
)
|
||||||
iface = node.new_iface(control_net, iface_data)
|
iface = node.create_iface(iface_data)
|
||||||
|
control_net.attach(iface)
|
||||||
iface.control = True
|
iface.control = True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
msg = f"Control interface not added to node {node.id}. "
|
msg = f"Control interface not added to node {node.id}. "
|
||||||
|
|
|
@ -404,9 +404,11 @@ class CoreClient:
|
||||||
for edge in self.links.values():
|
for edge in self.links.values():
|
||||||
link = edge.link
|
link = edge.link
|
||||||
if not definition:
|
if not definition:
|
||||||
if link.iface1 and not link.iface1.mac:
|
node1 = self.session.nodes[link.node1_id]
|
||||||
|
node2 = self.session.nodes[link.node2_id]
|
||||||
|
if nutils.is_container(node1) and link.iface1 and not link.iface1.mac:
|
||||||
link.iface1.mac = self.ifaces_manager.next_mac()
|
link.iface1.mac = self.ifaces_manager.next_mac()
|
||||||
if link.iface2 and not link.iface2.mac:
|
if nutils.is_container(node2) and link.iface2 and not link.iface2.mac:
|
||||||
link.iface2.mac = self.ifaces_manager.next_mac()
|
link.iface2.mac = self.ifaces_manager.next_mac()
|
||||||
links.append(link)
|
links.append(link)
|
||||||
if edge.asymmetric_link:
|
if edge.asymmetric_link:
|
||||||
|
|
BIN
daemon/core/gui/data/icons/wireless.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
|
@ -230,13 +230,8 @@ class NodeConfigDialog(Dialog):
|
||||||
if nutils.is_model(self.node):
|
if nutils.is_model(self.node):
|
||||||
label = ttk.Label(frame, text="Type")
|
label = ttk.Label(frame, text="Type")
|
||||||
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
|
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
|
||||||
combobox = ttk.Combobox(
|
entry = ttk.Entry(frame, textvariable=self.type, state=tk.DISABLED)
|
||||||
frame,
|
entry.grid(row=row, column=1, sticky=tk.EW)
|
||||||
textvariable=self.type,
|
|
||||||
values=list(nutils.NODE_MODELS),
|
|
||||||
state=combo_state,
|
|
||||||
)
|
|
||||||
combobox.grid(row=row, column=1, sticky=tk.EW)
|
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
# container image field
|
# container image field
|
||||||
|
@ -275,7 +270,7 @@ class NodeConfigDialog(Dialog):
|
||||||
ifaces_scroll.listbox.bind("<<ListboxSelect>>", self.iface_select)
|
ifaces_scroll.listbox.bind("<<ListboxSelect>>", self.iface_select)
|
||||||
|
|
||||||
# interfaces
|
# interfaces
|
||||||
if self.canvas_node.ifaces:
|
if nutils.is_container(self.node):
|
||||||
self.draw_ifaces()
|
self.draw_ifaces()
|
||||||
|
|
||||||
self.draw_spacer()
|
self.draw_spacer()
|
||||||
|
|
|
@ -298,7 +298,10 @@ class CanvasNode:
|
||||||
other_iface = edge.other_iface(self)
|
other_iface = edge.other_iface(self)
|
||||||
label = other_node.core_node.name
|
label = other_node.core_node.name
|
||||||
if other_iface:
|
if other_iface:
|
||||||
label = f"{label}:{other_iface.name}"
|
iface_label = other_iface.id
|
||||||
|
if other_iface.name:
|
||||||
|
iface_label = other_iface.name
|
||||||
|
label = f"{label}:{iface_label}"
|
||||||
func_unlink = functools.partial(self.click_unlink, edge)
|
func_unlink = functools.partial(self.click_unlink, edge)
|
||||||
unlink_menu.add_command(label=label, command=func_unlink)
|
unlink_menu.add_command(label=label, command=func_unlink)
|
||||||
themes.style_menu(unlink_menu)
|
themes.style_menu(unlink_menu)
|
||||||
|
|
|
@ -53,6 +53,7 @@ class ImageEnum(Enum):
|
||||||
LINK = "link"
|
LINK = "link"
|
||||||
HUB = "hub"
|
HUB = "hub"
|
||||||
WLAN = "wlan"
|
WLAN = "wlan"
|
||||||
|
WIRELESS = "wireless"
|
||||||
EMANE = "emane"
|
EMANE = "emane"
|
||||||
RJ45 = "rj45"
|
RJ45 = "rj45"
|
||||||
TUNNEL = "tunnel"
|
TUNNEL = "tunnel"
|
||||||
|
|
|
@ -241,10 +241,10 @@ class InterfaceManager:
|
||||||
dst_node = edge.dst.core_node
|
dst_node = edge.dst.core_node
|
||||||
self.determine_subnets(edge.src, edge.dst)
|
self.determine_subnets(edge.src, edge.dst)
|
||||||
src_iface = None
|
src_iface = None
|
||||||
if nutils.is_container(src_node):
|
if nutils.is_iface_node(src_node):
|
||||||
src_iface = self.create_iface(edge.src, edge.linked_wireless)
|
src_iface = self.create_iface(edge.src, edge.linked_wireless)
|
||||||
dst_iface = None
|
dst_iface = None
|
||||||
if nutils.is_container(dst_node):
|
if nutils.is_iface_node(dst_node):
|
||||||
dst_iface = self.create_iface(edge.dst, edge.linked_wireless)
|
dst_iface = self.create_iface(edge.dst, edge.linked_wireless)
|
||||||
link = Link(
|
link = Link(
|
||||||
type=LinkType.WIRED,
|
type=LinkType.WIRED,
|
||||||
|
@ -258,22 +258,26 @@ class InterfaceManager:
|
||||||
|
|
||||||
def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface:
|
def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface:
|
||||||
node = canvas_node.core_node
|
node = canvas_node.core_node
|
||||||
ip4, ip6 = self.get_ips(node)
|
if nutils.is_bridge(node):
|
||||||
if wireless_link:
|
iface_id = canvas_node.next_iface_id()
|
||||||
ip4_mask = WIRELESS_IP4_MASK
|
iface = Interface(id=iface_id)
|
||||||
ip6_mask = WIRELESS_IP6_MASK
|
|
||||||
else:
|
else:
|
||||||
ip4_mask = IP4_MASK
|
ip4, ip6 = self.get_ips(node)
|
||||||
ip6_mask = IP6_MASK
|
if wireless_link:
|
||||||
iface_id = canvas_node.next_iface_id()
|
ip4_mask = WIRELESS_IP4_MASK
|
||||||
name = f"eth{iface_id}"
|
ip6_mask = WIRELESS_IP6_MASK
|
||||||
iface = Interface(
|
else:
|
||||||
id=iface_id,
|
ip4_mask = IP4_MASK
|
||||||
name=name,
|
ip6_mask = IP6_MASK
|
||||||
ip4=ip4,
|
iface_id = canvas_node.next_iface_id()
|
||||||
ip4_mask=ip4_mask,
|
name = f"eth{iface_id}"
|
||||||
ip6=ip6,
|
iface = Interface(
|
||||||
ip6_mask=ip6_mask,
|
id=iface_id,
|
||||||
)
|
name=name,
|
||||||
|
ip4=ip4,
|
||||||
|
ip4_mask=ip4_mask,
|
||||||
|
ip6=ip6,
|
||||||
|
ip6_mask=ip6_mask,
|
||||||
|
)
|
||||||
logger.info("create node(%s) interface(%s)", node.name, iface)
|
logger.info("create node(%s) interface(%s)", node.name, iface)
|
||||||
return iface
|
return iface
|
||||||
|
|
|
@ -18,12 +18,16 @@ NETWORK_NODES: List["NodeDraw"] = []
|
||||||
NODE_ICONS = {}
|
NODE_ICONS = {}
|
||||||
CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC}
|
CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC}
|
||||||
IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC}
|
IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC}
|
||||||
WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
WIRELESS_NODES: Set[NodeType] = {
|
||||||
|
NodeType.WIRELESS_LAN,
|
||||||
|
NodeType.EMANE,
|
||||||
|
NodeType.WIRELESS,
|
||||||
|
}
|
||||||
RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
|
RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
|
||||||
BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH}
|
BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH}
|
||||||
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET}
|
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET}
|
||||||
MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
||||||
NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"}
|
NODE_MODELS: Set[str] = {"router", "PC", "mdr", "prouter"}
|
||||||
ROUTER_NODES: Set[str] = {"router", "mdr"}
|
ROUTER_NODES: Set[str] = {"router", "mdr"}
|
||||||
ANTENNA_ICON: Optional[PhotoImage] = None
|
ANTENNA_ICON: Optional[PhotoImage] = None
|
||||||
|
|
||||||
|
@ -46,6 +50,7 @@ def setup() -> None:
|
||||||
(ImageEnum.HUB, NodeType.HUB, "Hub"),
|
(ImageEnum.HUB, NodeType.HUB, "Hub"),
|
||||||
(ImageEnum.SWITCH, NodeType.SWITCH, "Switch"),
|
(ImageEnum.SWITCH, NodeType.SWITCH, "Switch"),
|
||||||
(ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"),
|
(ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"),
|
||||||
|
(ImageEnum.WIRELESS, NodeType.WIRELESS, "Wireless"),
|
||||||
(ImageEnum.EMANE, NodeType.EMANE, "EMANE"),
|
(ImageEnum.EMANE, NodeType.EMANE, "EMANE"),
|
||||||
(ImageEnum.RJ45, NodeType.RJ45, "RJ45"),
|
(ImageEnum.RJ45, NodeType.RJ45, "RJ45"),
|
||||||
(ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"),
|
(ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"),
|
||||||
|
@ -97,6 +102,10 @@ def is_custom(node: Node) -> bool:
|
||||||
return is_model(node) and node.model not in NODE_MODELS
|
return is_model(node) and node.model not in NODE_MODELS
|
||||||
|
|
||||||
|
|
||||||
|
def is_iface_node(node: Node) -> bool:
|
||||||
|
return is_container(node) or is_bridge(node)
|
||||||
|
|
||||||
|
|
||||||
def get_custom_services(gui_config: GuiConfig, name: str) -> List[str]:
|
def get_custom_services(gui_config: GuiConfig, name: str) -> List[str]:
|
||||||
for custom_node in gui_config.nodes:
|
for custom_node in gui_config.nodes:
|
||||||
if custom_node.name == name:
|
if custom_node.name == name:
|
||||||
|
|
|
@ -225,7 +225,6 @@ class WirelessModel(ConfigurableOptions):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config_type: RegisterTlvs = RegisterTlvs.WIRELESS
|
config_type: RegisterTlvs = RegisterTlvs.WIRELESS
|
||||||
bitmap: str = None
|
|
||||||
position_callback: Callable[[CoreInterface], None] = None
|
position_callback: Callable[[CoreInterface], None] = None
|
||||||
|
|
||||||
def __init__(self, session: "Session", _id: int) -> None:
|
def __init__(self, session: "Session", _id: int) -> None:
|
||||||
|
@ -321,7 +320,8 @@ class BasicRangeModel(WirelessModel):
|
||||||
loss=self.loss,
|
loss=self.loss,
|
||||||
jitter=self.jitter,
|
jitter=self.jitter,
|
||||||
)
|
)
|
||||||
iface.config(options)
|
iface.options.update(options)
|
||||||
|
iface.set_config()
|
||||||
|
|
||||||
def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]:
|
def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -13,11 +13,11 @@ import netaddr
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
from core.configservice.dependencies import ConfigServiceDependencies
|
from core.configservice.dependencies import ConfigServiceDependencies
|
||||||
from core.emulator.data import InterfaceData, LinkData
|
from core.emulator.data import InterfaceData, LinkOptions
|
||||||
from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes
|
from core.emulator.enumerations import LinkTypes, NodeTypes
|
||||||
from core.errors import CoreCommandError, CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.executables import BASH, MOUNT, TEST, VCMD, VNODED
|
from core.executables import BASH, MOUNT, TEST, VCMD, VNODED
|
||||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface, TunTap, Veth
|
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||||
from core.nodes.netclient import LinuxNetClient, get_net_client
|
from core.nodes.netclient import LinuxNetClient, get_net_client
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -74,6 +74,7 @@ class NodeBase(abc.ABC):
|
||||||
self.icon: Optional[str] = None
|
self.icon: Optional[str] = None
|
||||||
self.position: Position = Position()
|
self.position: Position = Position()
|
||||||
self.up: bool = False
|
self.up: bool = False
|
||||||
|
self.lock: RLock = RLock()
|
||||||
self.net_client: LinuxNetClient = get_net_client(
|
self.net_client: LinuxNetClient = get_net_client(
|
||||||
self.session.use_ovs(), self.host_cmd
|
self.session.use_ovs(), self.host_cmd
|
||||||
)
|
)
|
||||||
|
@ -96,6 +97,18 @@ class NodeBase(abc.ABC):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||||
|
"""
|
||||||
|
Adopt an interface, placing within network namespacing for containers
|
||||||
|
and setting to bridge masters for network like nodes.
|
||||||
|
|
||||||
|
:param iface: interface to adopt
|
||||||
|
:param name: proper name to use for interface
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def host_cmd(
|
def host_cmd(
|
||||||
self,
|
self,
|
||||||
args: str,
|
args: str,
|
||||||
|
@ -120,6 +133,19 @@ class NodeBase(abc.ABC):
|
||||||
else:
|
else:
|
||||||
return self.server.remote_cmd(args, env, cwd, wait)
|
return self.server.remote_cmd(args, env, cwd, wait)
|
||||||
|
|
||||||
|
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Runs a command that is in the context of a node, default is to run a standard
|
||||||
|
host command.
|
||||||
|
|
||||||
|
:param args: command to run
|
||||||
|
:param wait: True to wait for status, False otherwise
|
||||||
|
:param shell: True to use shell, False otherwise
|
||||||
|
:return: combined stdout and stderr
|
||||||
|
:raises CoreCommandError: when a non-zero exit status occurs
|
||||||
|
"""
|
||||||
|
return self.host_cmd(args, wait=wait, shell=shell)
|
||||||
|
|
||||||
def setposition(self, x: float = None, y: float = None, z: float = None) -> bool:
|
def setposition(self, x: float = None, y: float = None, z: float = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Set the (x,y,z) position of the object.
|
Set the (x,y,z) position of the object.
|
||||||
|
@ -139,6 +165,70 @@ class NodeBase(abc.ABC):
|
||||||
"""
|
"""
|
||||||
return self.position.get()
|
return self.position.get()
|
||||||
|
|
||||||
|
def create_iface(
|
||||||
|
self, iface_data: InterfaceData = None, options: LinkOptions = None
|
||||||
|
) -> CoreInterface:
|
||||||
|
"""
|
||||||
|
Creates an interface and adopts it to a node.
|
||||||
|
|
||||||
|
:param iface_data: data to create interface with
|
||||||
|
:param options: options to create interface with
|
||||||
|
:return: created interface
|
||||||
|
"""
|
||||||
|
with self.lock:
|
||||||
|
if iface_data and iface_data.id is not None:
|
||||||
|
if iface_data.id in self.ifaces:
|
||||||
|
raise CoreError(
|
||||||
|
f"node({self.id}) interface({iface_data.id}) already exists"
|
||||||
|
)
|
||||||
|
iface_id = iface_data.id
|
||||||
|
else:
|
||||||
|
iface_id = self.next_iface_id()
|
||||||
|
mtu = DEFAULT_MTU
|
||||||
|
if iface_data and iface_data.mtu is not None:
|
||||||
|
mtu = iface_data.mtu
|
||||||
|
name = f"veth{self.id}.{iface_id}.{self.session.short_session_id()}"
|
||||||
|
localname = f"{name}p"
|
||||||
|
iface = CoreInterface(
|
||||||
|
iface_id,
|
||||||
|
name,
|
||||||
|
localname,
|
||||||
|
self.session.use_ovs(),
|
||||||
|
mtu,
|
||||||
|
self,
|
||||||
|
self.server,
|
||||||
|
)
|
||||||
|
if iface_data:
|
||||||
|
if iface_data.mac:
|
||||||
|
iface.set_mac(iface_data.mac)
|
||||||
|
for ip in iface_data.get_ips():
|
||||||
|
iface.add_ip(ip)
|
||||||
|
if iface_data.name:
|
||||||
|
name = iface_data.name
|
||||||
|
if options:
|
||||||
|
iface.options.update(options)
|
||||||
|
self.ifaces[iface_id] = iface
|
||||||
|
if self.up:
|
||||||
|
iface.startup()
|
||||||
|
self.adopt_iface(iface, name)
|
||||||
|
else:
|
||||||
|
iface.name = name
|
||||||
|
return iface
|
||||||
|
|
||||||
|
def delete_iface(self, iface_id: int) -> CoreInterface:
|
||||||
|
"""
|
||||||
|
Delete an interface.
|
||||||
|
|
||||||
|
:param iface_id: interface id to delete
|
||||||
|
:return: the removed interface
|
||||||
|
"""
|
||||||
|
if iface_id not in self.ifaces:
|
||||||
|
raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
|
||||||
|
iface = self.ifaces.pop(iface_id)
|
||||||
|
logger.info("node(%s) removing interface(%s)", self.name, iface.name)
|
||||||
|
iface.shutdown()
|
||||||
|
return iface
|
||||||
|
|
||||||
def get_iface(self, iface_id: int) -> CoreInterface:
|
def get_iface(self, iface_id: int) -> CoreInterface:
|
||||||
"""
|
"""
|
||||||
Retrieve interface based on id.
|
Retrieve interface based on id.
|
||||||
|
@ -191,15 +281,6 @@ class NodeBase(abc.ABC):
|
||||||
self.iface_id += 1
|
self.iface_id += 1
|
||||||
return iface_id
|
return iface_id
|
||||||
|
|
||||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
|
||||||
"""
|
|
||||||
Build link data for this node.
|
|
||||||
|
|
||||||
:param flags: message flags
|
|
||||||
:return: list of link data
|
|
||||||
"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class CoreNodeBase(NodeBase):
|
class CoreNodeBase(NodeBase):
|
||||||
"""
|
"""
|
||||||
|
@ -227,14 +308,6 @@ class CoreNodeBase(NodeBase):
|
||||||
self.directory: Optional[Path] = None
|
self.directory: Optional[Path] = None
|
||||||
self.tmpnodedir: bool = False
|
self.tmpnodedir: bool = False
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def startup(self) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def create_dir(self, dir_path: Path) -> None:
|
def create_dir(self, dir_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -270,19 +343,6 @@ class CoreNodeBase(NodeBase):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
|
|
||||||
"""
|
|
||||||
Runs a command within a node container.
|
|
||||||
|
|
||||||
:param args: command to run
|
|
||||||
:param wait: True to wait for status, False otherwise
|
|
||||||
:param shell: True to use shell, False otherwise
|
|
||||||
:return: combined stdout and stderr
|
|
||||||
:raises CoreCommandError: when a non-zero exit status occurs
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def termcmdstring(self, sh: str) -> str:
|
def termcmdstring(self, sh: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -293,19 +353,6 @@ class CoreNodeBase(NodeBase):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def new_iface(
|
|
||||||
self, net: "CoreNetworkBase", iface_data: InterfaceData
|
|
||||||
) -> CoreInterface:
|
|
||||||
"""
|
|
||||||
Create a new interface.
|
|
||||||
|
|
||||||
:param net: network to associate with
|
|
||||||
:param iface_data: interface data for new interface
|
|
||||||
:return: interface index
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def path_exists(self, path: str) -> bool:
|
def path_exists(self, path: str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -318,7 +365,7 @@ class CoreNodeBase(NodeBase):
|
||||||
|
|
||||||
def host_path(self, path: Path, is_dir: bool = False) -> Path:
|
def host_path(self, path: Path, is_dir: bool = False) -> Path:
|
||||||
"""
|
"""
|
||||||
Return the name of a node"s file on the host filesystem.
|
Return the name of a node's file on the host filesystem.
|
||||||
|
|
||||||
:param path: path to translate to host path
|
:param path: path to translate to host path
|
||||||
:param is_dir: True if path is a directory path, False otherwise
|
:param is_dir: True if path is a directory path, False otherwise
|
||||||
|
@ -393,54 +440,6 @@ class CoreNodeBase(NodeBase):
|
||||||
if self.tmpnodedir:
|
if self.tmpnodedir:
|
||||||
self.host_cmd(f"rm -rf {self.directory}")
|
self.host_cmd(f"rm -rf {self.directory}")
|
||||||
|
|
||||||
def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
|
|
||||||
"""
|
|
||||||
Add network interface to node and set the network interface index if successful.
|
|
||||||
|
|
||||||
:param iface: network interface to add
|
|
||||||
:param iface_id: interface id
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if iface_id in self.ifaces:
|
|
||||||
raise CoreError(f"interface({iface_id}) already exists")
|
|
||||||
self.ifaces[iface_id] = iface
|
|
||||||
iface.node_id = iface_id
|
|
||||||
|
|
||||||
def delete_iface(self, iface_id: int) -> None:
|
|
||||||
"""
|
|
||||||
Delete a network interface
|
|
||||||
|
|
||||||
:param iface_id: interface index to delete
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if iface_id not in self.ifaces:
|
|
||||||
raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
|
|
||||||
iface = self.ifaces.pop(iface_id)
|
|
||||||
logger.info("node(%s) removing interface(%s)", self.name, iface.name)
|
|
||||||
iface.detachnet()
|
|
||||||
iface.shutdown()
|
|
||||||
|
|
||||||
def attachnet(self, iface_id: int, net: "CoreNetworkBase") -> None:
|
|
||||||
"""
|
|
||||||
Attach a network.
|
|
||||||
|
|
||||||
:param iface_id: interface of index to attach
|
|
||||||
:param net: network to attach
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
iface = self.get_iface(iface_id)
|
|
||||||
iface.attachnet(net)
|
|
||||||
|
|
||||||
def detachnet(self, iface_id: int) -> None:
|
|
||||||
"""
|
|
||||||
Detach network interface.
|
|
||||||
|
|
||||||
:param iface_id: interface id to detach
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
iface = self.get_iface(iface_id)
|
|
||||||
iface.detachnet()
|
|
||||||
|
|
||||||
def setposition(self, x: float = None, y: float = None, z: float = None) -> None:
|
def setposition(self, x: float = None, y: float = None, z: float = None) -> None:
|
||||||
"""
|
"""
|
||||||
Set position.
|
Set position.
|
||||||
|
@ -455,25 +454,6 @@ class CoreNodeBase(NodeBase):
|
||||||
for iface in self.get_ifaces():
|
for iface in self.get_ifaces():
|
||||||
iface.setposition()
|
iface.setposition()
|
||||||
|
|
||||||
def commonnets(
|
|
||||||
self, node: "CoreNodeBase", want_ctrl: bool = False
|
|
||||||
) -> List[Tuple["CoreNetworkBase", CoreInterface, CoreInterface]]:
|
|
||||||
"""
|
|
||||||
Given another node or net object, return common networks between
|
|
||||||
this node and that object. A list of tuples is returned, with each tuple
|
|
||||||
consisting of (network, interface1, interface2).
|
|
||||||
|
|
||||||
:param node: node to get common network with
|
|
||||||
:param want_ctrl: flag set to determine if control network are wanted
|
|
||||||
:return: tuples of common networks
|
|
||||||
"""
|
|
||||||
common = []
|
|
||||||
for iface1 in self.get_ifaces(control=want_ctrl):
|
|
||||||
for iface2 in node.get_ifaces():
|
|
||||||
if iface1.net == iface2.net:
|
|
||||||
common.append((iface1.net, iface1, iface2))
|
|
||||||
return common
|
|
||||||
|
|
||||||
|
|
||||||
class CoreNode(CoreNodeBase):
|
class CoreNode(CoreNodeBase):
|
||||||
"""
|
"""
|
||||||
|
@ -504,7 +484,6 @@ class CoreNode(CoreNodeBase):
|
||||||
self.directory: Optional[Path] = directory
|
self.directory: Optional[Path] = directory
|
||||||
self.ctrlchnlname: Path = self.session.directory / self.name
|
self.ctrlchnlname: Path = self.session.directory / self.name
|
||||||
self.pid: Optional[int] = None
|
self.pid: Optional[int] = None
|
||||||
self.lock: RLock = RLock()
|
|
||||||
self._mounts: List[Tuple[Path, Path]] = []
|
self._mounts: List[Tuple[Path, Path]] = []
|
||||||
self.node_net_client: LinuxNetClient = self.create_node_net_client(
|
self.node_net_client: LinuxNetClient = self.create_node_net_client(
|
||||||
self.session.use_ovs()
|
self.session.use_ovs()
|
||||||
|
@ -585,6 +564,10 @@ class CoreNode(CoreNodeBase):
|
||||||
self._mounts = []
|
self._mounts = []
|
||||||
# shutdown all interfaces
|
# shutdown all interfaces
|
||||||
for iface in self.get_ifaces():
|
for iface in self.get_ifaces():
|
||||||
|
try:
|
||||||
|
self.node_net_client.device_flush(iface.name)
|
||||||
|
except CoreCommandError:
|
||||||
|
pass
|
||||||
iface.shutdown()
|
iface.shutdown()
|
||||||
# kill node process if present
|
# kill node process if present
|
||||||
try:
|
try:
|
||||||
|
@ -691,150 +674,6 @@ class CoreNode(CoreNodeBase):
|
||||||
self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}")
|
self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}")
|
||||||
self._mounts.append((src_path, target_path))
|
self._mounts.append((src_path, target_path))
|
||||||
|
|
||||||
def next_iface_id(self) -> int:
|
|
||||||
"""
|
|
||||||
Retrieve a new interface index.
|
|
||||||
|
|
||||||
:return: new interface index
|
|
||||||
"""
|
|
||||||
with self.lock:
|
|
||||||
return super().next_iface_id()
|
|
||||||
|
|
||||||
def newveth(self, iface_id: int = None, ifname: str = None, mtu: int = None) -> int:
|
|
||||||
"""
|
|
||||||
Create a new interface.
|
|
||||||
|
|
||||||
:param iface_id: id for the new interface
|
|
||||||
:param ifname: name for the new interface
|
|
||||||
:param mtu: mtu for interface
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
with self.lock:
|
|
||||||
mtu = mtu if mtu is not None else DEFAULT_MTU
|
|
||||||
iface_id = iface_id if iface_id is not None else self.next_iface_id()
|
|
||||||
ifname = ifname if ifname is not None else f"eth{iface_id}"
|
|
||||||
sessionid = self.session.short_session_id()
|
|
||||||
try:
|
|
||||||
suffix = f"{self.id:x}.{iface_id}.{sessionid}"
|
|
||||||
except TypeError:
|
|
||||||
suffix = f"{self.id}.{iface_id}.{sessionid}"
|
|
||||||
localname = f"veth{suffix}"
|
|
||||||
name = f"{localname}p"
|
|
||||||
veth = Veth(self.session, name, localname, mtu, self.server, self)
|
|
||||||
veth.adopt_node(iface_id, ifname, self.up)
|
|
||||||
return iface_id
|
|
||||||
|
|
||||||
def newtuntap(self, iface_id: int = None, ifname: str = None) -> int:
|
|
||||||
"""
|
|
||||||
Create a new tunnel tap.
|
|
||||||
|
|
||||||
:param iface_id: interface id
|
|
||||||
:param ifname: interface name
|
|
||||||
:return: interface index
|
|
||||||
"""
|
|
||||||
with self.lock:
|
|
||||||
iface_id = iface_id if iface_id is not None else self.next_iface_id()
|
|
||||||
ifname = ifname if ifname is not None else f"eth{iface_id}"
|
|
||||||
sessionid = self.session.short_session_id()
|
|
||||||
localname = f"tap{self.id}.{iface_id}.{sessionid}"
|
|
||||||
name = ifname
|
|
||||||
tuntap = TunTap(self.session, name, localname, node=self)
|
|
||||||
if self.up:
|
|
||||||
tuntap.startup()
|
|
||||||
try:
|
|
||||||
self.add_iface(tuntap, iface_id)
|
|
||||||
except CoreError as e:
|
|
||||||
tuntap.shutdown()
|
|
||||||
raise e
|
|
||||||
return iface_id
|
|
||||||
|
|
||||||
def set_mac(self, iface_id: int, mac: str) -> None:
|
|
||||||
"""
|
|
||||||
Set hardware address for an interface.
|
|
||||||
|
|
||||||
:param iface_id: id of interface to set hardware address for
|
|
||||||
:param mac: mac address to set
|
|
||||||
:return: nothing
|
|
||||||
:raises CoreCommandError: when a non-zero exit status occurs
|
|
||||||
"""
|
|
||||||
iface = self.get_iface(iface_id)
|
|
||||||
iface.set_mac(mac)
|
|
||||||
if self.up:
|
|
||||||
self.node_net_client.device_mac(iface.name, str(iface.mac))
|
|
||||||
|
|
||||||
def add_ip(self, iface_id: int, ip: str) -> None:
|
|
||||||
"""
|
|
||||||
Add an ip address to an interface in the format "10.0.0.1/24".
|
|
||||||
|
|
||||||
:param iface_id: id of interface to add address to
|
|
||||||
:param ip: address to add to interface
|
|
||||||
:return: nothing
|
|
||||||
:raises CoreError: when ip address provided is invalid
|
|
||||||
:raises CoreCommandError: when a non-zero exit status occurs
|
|
||||||
"""
|
|
||||||
iface = self.get_iface(iface_id)
|
|
||||||
iface.add_ip(ip)
|
|
||||||
if self.up:
|
|
||||||
# ipv4 check
|
|
||||||
broadcast = None
|
|
||||||
if netaddr.valid_ipv4(ip):
|
|
||||||
broadcast = "+"
|
|
||||||
self.node_net_client.create_address(iface.name, ip, broadcast)
|
|
||||||
|
|
||||||
def remove_ip(self, iface_id: int, ip: str) -> None:
|
|
||||||
"""
|
|
||||||
Remove an ip address from an interface in the format "10.0.0.1/24".
|
|
||||||
|
|
||||||
:param iface_id: id of interface to delete address from
|
|
||||||
:param ip: ip address to remove from interface
|
|
||||||
:return: nothing
|
|
||||||
:raises CoreError: when ip address provided is invalid
|
|
||||||
:raises CoreCommandError: when a non-zero exit status occurs
|
|
||||||
"""
|
|
||||||
iface = self.get_iface(iface_id)
|
|
||||||
iface.remove_ip(ip)
|
|
||||||
if self.up:
|
|
||||||
self.node_net_client.delete_address(iface.name, ip)
|
|
||||||
|
|
||||||
def ifup(self, iface_id: int) -> None:
|
|
||||||
"""
|
|
||||||
Bring an interface up.
|
|
||||||
|
|
||||||
:param iface_id: index of interface to bring up
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if self.up:
|
|
||||||
iface = self.get_iface(iface_id)
|
|
||||||
self.node_net_client.device_up(iface.name)
|
|
||||||
|
|
||||||
def new_iface(
|
|
||||||
self, net: "CoreNetworkBase", iface_data: InterfaceData
|
|
||||||
) -> CoreInterface:
|
|
||||||
"""
|
|
||||||
Create a new network interface.
|
|
||||||
|
|
||||||
:param net: network to associate with
|
|
||||||
:param iface_data: interface data for new interface
|
|
||||||
:return: interface index
|
|
||||||
"""
|
|
||||||
with self.lock:
|
|
||||||
if net.has_custom_iface:
|
|
||||||
return net.custom_iface(self, iface_data)
|
|
||||||
else:
|
|
||||||
iface_id = iface_data.id
|
|
||||||
if iface_id is not None and iface_id in self.ifaces:
|
|
||||||
raise CoreError(
|
|
||||||
f"node({self.name}) already has interface({iface_id})"
|
|
||||||
)
|
|
||||||
iface_id = self.newveth(iface_id, iface_data.name, iface_data.mtu)
|
|
||||||
self.attachnet(iface_id, net)
|
|
||||||
if iface_data.mac:
|
|
||||||
self.set_mac(iface_id, iface_data.mac)
|
|
||||||
for ip in iface_data.get_ips():
|
|
||||||
self.add_ip(iface_id, ip)
|
|
||||||
self.ifup(iface_id)
|
|
||||||
return self.get_iface(iface_id)
|
|
||||||
|
|
||||||
def _find_parent_path(self, path: Path) -> Optional[Path]:
|
def _find_parent_path(self, path: Path) -> Optional[Path]:
|
||||||
"""
|
"""
|
||||||
Check if there is a mounted parent directory created for this node.
|
Check if there is a mounted parent directory created for this node.
|
||||||
|
@ -910,6 +749,49 @@ class CoreNode(CoreNodeBase):
|
||||||
if mode is not None:
|
if mode is not None:
|
||||||
self.host_cmd(f"chmod {mode:o} {host_path}")
|
self.host_cmd(f"chmod {mode:o} {host_path}")
|
||||||
|
|
||||||
|
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||||
|
"""
|
||||||
|
Adopt interface to the network namespace of the node and setting
|
||||||
|
the proper name provided.
|
||||||
|
|
||||||
|
:param iface: interface to adopt
|
||||||
|
:param name: proper name for interface
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
# TODO: container, checksums off (container only?)
|
||||||
|
# TODO: container, get flow id (container only?)
|
||||||
|
# validate iface belongs to node and get id
|
||||||
|
iface_id = self.get_iface_id(iface)
|
||||||
|
if iface_id == -1:
|
||||||
|
raise CoreError(f"adopting unknown iface({iface.name})")
|
||||||
|
# add iface to container namespace
|
||||||
|
self.net_client.device_ns(iface.name, str(self.pid))
|
||||||
|
# use default iface name for container, if a unique name was not provided
|
||||||
|
if iface.name == name:
|
||||||
|
name = f"eth{iface_id}"
|
||||||
|
self.node_net_client.device_name(iface.name, name)
|
||||||
|
iface.name = name
|
||||||
|
# turn checksums off
|
||||||
|
self.node_net_client.checksums_off(iface.name)
|
||||||
|
# retrieve flow id for container
|
||||||
|
iface.flow_id = self.node_net_client.get_ifindex(iface.name)
|
||||||
|
logger.debug("interface flow index: %s - %s", iface.name, iface.flow_id)
|
||||||
|
# set mac address
|
||||||
|
if iface.mac:
|
||||||
|
self.node_net_client.device_mac(iface.name, str(iface.mac))
|
||||||
|
logger.debug("interface mac: %s - %s", iface.name, iface.mac)
|
||||||
|
# set all addresses
|
||||||
|
for ip in iface.ips():
|
||||||
|
# ipv4 check
|
||||||
|
broadcast = None
|
||||||
|
if netaddr.valid_ipv4(ip):
|
||||||
|
broadcast = "+"
|
||||||
|
self.node_net_client.create_address(iface.name, str(ip), broadcast)
|
||||||
|
# configure iface options
|
||||||
|
iface.set_config()
|
||||||
|
# set iface up
|
||||||
|
self.node_net_client.device_up(iface.name)
|
||||||
|
|
||||||
|
|
||||||
class CoreNetworkBase(NodeBase):
|
class CoreNetworkBase(NodeBase):
|
||||||
"""
|
"""
|
||||||
|
@ -917,7 +799,6 @@ class CoreNetworkBase(NodeBase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
linktype: LinkTypes = LinkTypes.WIRED
|
linktype: LinkTypes = LinkTypes.WIRED
|
||||||
has_custom_iface: bool = False
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -941,57 +822,6 @@ class CoreNetworkBase(NodeBase):
|
||||||
self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {}
|
self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {}
|
||||||
self.linked_lock: threading.Lock = threading.Lock()
|
self.linked_lock: threading.Lock = threading.Lock()
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def startup(self) -> None:
|
|
||||||
"""
|
|
||||||
Each object implements its own startup method.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
"""
|
|
||||||
Each object implements its own shutdown method.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
|
|
||||||
"""
|
|
||||||
Link network to another.
|
|
||||||
|
|
||||||
:param net: network to link with
|
|
||||||
:return: created interface
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
|
||||||
"""
|
|
||||||
Defines custom logic for creating an interface, if required.
|
|
||||||
|
|
||||||
:param node: node to create interface for
|
|
||||||
:param iface_data: data for creating interface
|
|
||||||
:return: created interface
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def get_linked_iface(self, net: "CoreNetworkBase") -> Optional[CoreInterface]:
|
|
||||||
"""
|
|
||||||
Return the interface that links this net with another net.
|
|
||||||
|
|
||||||
:param net: interface to get link for
|
|
||||||
:return: interface the provided network is linked to
|
|
||||||
"""
|
|
||||||
for iface in self.get_ifaces():
|
|
||||||
if iface.othernet == net:
|
|
||||||
return iface
|
|
||||||
return None
|
|
||||||
|
|
||||||
def attach(self, iface: CoreInterface) -> None:
|
def attach(self, iface: CoreInterface) -> None:
|
||||||
"""
|
"""
|
||||||
Attach network interface.
|
Attach network interface.
|
||||||
|
@ -999,9 +829,10 @@ class CoreNetworkBase(NodeBase):
|
||||||
:param iface: network interface to attach
|
:param iface: network interface to attach
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
i = self.next_iface_id()
|
iface_id = self.next_iface_id()
|
||||||
self.ifaces[i] = iface
|
self.ifaces[iface_id] = iface
|
||||||
iface.net_id = i
|
iface.net = self
|
||||||
|
iface.net_id = iface_id
|
||||||
with self.linked_lock:
|
with self.linked_lock:
|
||||||
self.linked[iface] = {}
|
self.linked[iface] = {}
|
||||||
|
|
||||||
|
@ -1013,56 +844,11 @@ class CoreNetworkBase(NodeBase):
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
del self.ifaces[iface.net_id]
|
del self.ifaces[iface.net_id]
|
||||||
|
iface.net = None
|
||||||
iface.net_id = None
|
iface.net_id = None
|
||||||
with self.linked_lock:
|
with self.linked_lock:
|
||||||
del self.linked[iface]
|
del self.linked[iface]
|
||||||
|
|
||||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
|
||||||
"""
|
|
||||||
Build link data objects for this network. Each link object describes a link
|
|
||||||
between this network and a node.
|
|
||||||
|
|
||||||
:param flags: message type
|
|
||||||
:return: list of link data
|
|
||||||
"""
|
|
||||||
all_links = []
|
|
||||||
# build a link message from this network node to each node having a
|
|
||||||
# connected interface
|
|
||||||
for iface in self.get_ifaces():
|
|
||||||
unidirectional = 0
|
|
||||||
linked_node = iface.node
|
|
||||||
if linked_node is None:
|
|
||||||
# two layer-2 switches/hubs linked together
|
|
||||||
if not iface.othernet:
|
|
||||||
continue
|
|
||||||
linked_node = iface.othernet
|
|
||||||
if linked_node.id == self.id:
|
|
||||||
continue
|
|
||||||
if iface.local_options != iface.options:
|
|
||||||
unidirectional = 1
|
|
||||||
iface_data = iface.get_data()
|
|
||||||
link_data = LinkData(
|
|
||||||
message_type=flags,
|
|
||||||
type=self.linktype,
|
|
||||||
node1_id=self.id,
|
|
||||||
node2_id=linked_node.id,
|
|
||||||
iface2=iface_data,
|
|
||||||
options=iface.local_options,
|
|
||||||
)
|
|
||||||
link_data.options.unidirectional = unidirectional
|
|
||||||
all_links.append(link_data)
|
|
||||||
if unidirectional:
|
|
||||||
link_data = LinkData(
|
|
||||||
message_type=MessageFlags.NONE,
|
|
||||||
type=self.linktype,
|
|
||||||
node1_id=linked_node.id,
|
|
||||||
node2_id=self.id,
|
|
||||||
options=iface.options,
|
|
||||||
)
|
|
||||||
link_data.options.unidirectional = unidirectional
|
|
||||||
all_links.append(link_data)
|
|
||||||
return all_links
|
|
||||||
|
|
||||||
|
|
||||||
class Position:
|
class Position:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -93,10 +93,9 @@ class DockerNode(CoreNode):
|
||||||
will run on, default is None for localhost
|
will run on, default is None for localhost
|
||||||
:param image: image to start container with
|
:param image: image to start container with
|
||||||
"""
|
"""
|
||||||
if image is None:
|
|
||||||
image = "ubuntu"
|
|
||||||
self.image: str = image
|
|
||||||
super().__init__(session, _id, name, directory, server)
|
super().__init__(session, _id, name, directory, server)
|
||||||
|
self.image = image if image is not None else "ubuntu"
|
||||||
|
self.client: Optional[DockerClient] = None
|
||||||
|
|
||||||
def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient:
|
def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient:
|
||||||
"""
|
"""
|
||||||
|
@ -141,7 +140,6 @@ class DockerNode(CoreNode):
|
||||||
# nothing to do if node is not up
|
# nothing to do if node is not up
|
||||||
if not self.up:
|
if not self.up:
|
||||||
return
|
return
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.ifaces.clear()
|
self.ifaces.clear()
|
||||||
self.client.stop_container()
|
self.client.stop_container()
|
||||||
|
|
|
@ -4,7 +4,6 @@ virtual ethernet classes that implement the interfaces available under Linux.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import time
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Callable, Dict, List, Optional
|
from typing import TYPE_CHECKING, Callable, Dict, List, Optional
|
||||||
|
|
||||||
|
@ -20,11 +19,12 @@ from core.nodes.netclient import LinuxNetClient, get_net_client
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.emulator.distributed import DistributedServer
|
|
||||||
from core.emulator.session import Session
|
from core.emulator.session import Session
|
||||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
from core.emulator.distributed import DistributedServer
|
||||||
|
from core.nodes.base import CoreNetworkBase, CoreNode, NodeBase
|
||||||
|
|
||||||
DEFAULT_MTU: int = 1500
|
DEFAULT_MTU: int = 1500
|
||||||
|
IFACE_NAME_LENGTH: int = 15
|
||||||
|
|
||||||
|
|
||||||
def tc_clear_cmd(name: str) -> str:
|
def tc_clear_cmd(name: str) -> str:
|
||||||
|
@ -78,35 +78,42 @@ class CoreInterface:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: "Session",
|
_id: int,
|
||||||
name: str,
|
name: str,
|
||||||
localname: str,
|
localname: str,
|
||||||
|
use_ovs: bool,
|
||||||
mtu: int = DEFAULT_MTU,
|
mtu: int = DEFAULT_MTU,
|
||||||
|
node: "NodeBase" = None,
|
||||||
server: "DistributedServer" = None,
|
server: "DistributedServer" = None,
|
||||||
node: "CoreNode" = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Creates a CoreInterface instance.
|
Creates a CoreInterface instance.
|
||||||
|
|
||||||
:param session: core session instance
|
:param _id: interface id for associated node
|
||||||
:param name: interface name
|
:param name: interface name
|
||||||
:param localname: interface local name
|
:param localname: interface local name
|
||||||
|
:param use_ovs: True to use ovs, False otherwise
|
||||||
:param mtu: mtu value
|
:param mtu: mtu value
|
||||||
|
:param node: node associated with this interface
|
||||||
:param server: remote server node will run on, default is None for localhost
|
:param server: remote server node will run on, default is None for localhost
|
||||||
:param node: node for interface
|
|
||||||
"""
|
"""
|
||||||
if len(name) >= 16:
|
if len(name) >= IFACE_NAME_LENGTH:
|
||||||
raise CoreError(f"interface name ({name}) too long, max 16")
|
raise CoreError(
|
||||||
if len(localname) >= 16:
|
f"interface name ({name}) too long, max {IFACE_NAME_LENGTH}"
|
||||||
raise CoreError(f"interface local name ({localname}) too long, max 16")
|
)
|
||||||
self.session: "Session" = session
|
if len(localname) >= IFACE_NAME_LENGTH:
|
||||||
self.node: Optional["CoreNode"] = node
|
raise CoreError(
|
||||||
|
f"interface local name ({localname}) too long, max {IFACE_NAME_LENGTH}"
|
||||||
|
)
|
||||||
|
self.id: int = _id
|
||||||
|
self.node: Optional["NodeBase"] = node
|
||||||
|
# id of interface for network, used by wlan/emane
|
||||||
|
self.net_id: Optional[int] = None
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.localname: str = localname
|
self.localname: str = localname
|
||||||
self.up: bool = False
|
self.up: bool = False
|
||||||
self.mtu: int = mtu
|
self.mtu: int = mtu
|
||||||
self.net: Optional[CoreNetworkBase] = None
|
self.net: Optional[CoreNetworkBase] = None
|
||||||
self.othernet: Optional[CoreNetworkBase] = None
|
|
||||||
self.ip4s: List[netaddr.IPNetwork] = []
|
self.ip4s: List[netaddr.IPNetwork] = []
|
||||||
self.ip6s: List[netaddr.IPNetwork] = []
|
self.ip6s: List[netaddr.IPNetwork] = []
|
||||||
self.mac: Optional[netaddr.EUI] = None
|
self.mac: Optional[netaddr.EUI] = None
|
||||||
|
@ -114,20 +121,12 @@ class CoreInterface:
|
||||||
self.poshook: Callable[[CoreInterface], None] = lambda x: None
|
self.poshook: Callable[[CoreInterface], None] = lambda x: None
|
||||||
# used with EMANE
|
# used with EMANE
|
||||||
self.transport_type: TransportType = TransportType.VIRTUAL
|
self.transport_type: TransportType = TransportType.VIRTUAL
|
||||||
# id of interface for node
|
|
||||||
self.node_id: Optional[int] = None
|
|
||||||
# id of interface for network
|
|
||||||
self.net_id: Optional[int] = None
|
|
||||||
# id used to find flow data
|
# id used to find flow data
|
||||||
self.flow_id: Optional[int] = None
|
self.flow_id: Optional[int] = None
|
||||||
self.server: Optional["DistributedServer"] = server
|
self.server: Optional["DistributedServer"] = server
|
||||||
self.net_client: LinuxNetClient = get_net_client(
|
self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd)
|
||||||
self.session.use_ovs(), self.host_cmd
|
|
||||||
)
|
|
||||||
self.control: bool = False
|
self.control: bool = False
|
||||||
# configuration data
|
# configuration data
|
||||||
self.has_local_netem: bool = False
|
|
||||||
self.local_options: LinkOptions = LinkOptions()
|
|
||||||
self.has_netem: bool = False
|
self.has_netem: bool = False
|
||||||
self.options: LinkOptions = LinkOptions()
|
self.options: LinkOptions = LinkOptions()
|
||||||
|
|
||||||
|
@ -161,7 +160,13 @@ class CoreInterface:
|
||||||
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
pass
|
self.net_client.create_veth(self.localname, self.name)
|
||||||
|
if self.mtu > 0:
|
||||||
|
self.net_client.set_mtu(self.name, self.mtu)
|
||||||
|
self.net_client.set_mtu(self.localname, self.mtu)
|
||||||
|
self.net_client.device_up(self.name)
|
||||||
|
self.net_client.device_up(self.localname)
|
||||||
|
self.up = True
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -169,29 +174,14 @@ class CoreInterface:
|
||||||
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
pass
|
if not self.up:
|
||||||
|
return
|
||||||
def attachnet(self, net: "CoreNetworkBase") -> None:
|
if self.localname:
|
||||||
"""
|
try:
|
||||||
Attach network.
|
self.net_client.delete_device(self.localname)
|
||||||
|
except CoreCommandError:
|
||||||
:param net: network to attach
|
pass
|
||||||
:return: nothing
|
self.up = False
|
||||||
"""
|
|
||||||
if self.net:
|
|
||||||
self.detachnet()
|
|
||||||
self.net = None
|
|
||||||
net.attach(self)
|
|
||||||
self.net = net
|
|
||||||
|
|
||||||
def detachnet(self) -> None:
|
|
||||||
"""
|
|
||||||
Detach from a network.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if self.net is not None:
|
|
||||||
self.net.detach(self)
|
|
||||||
|
|
||||||
def add_ip(self, ip: str) -> None:
|
def add_ip(self, ip: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -303,41 +293,24 @@ class CoreInterface:
|
||||||
"""
|
"""
|
||||||
return self.transport_type == TransportType.VIRTUAL
|
return self.transport_type == TransportType.VIRTUAL
|
||||||
|
|
||||||
def config(self, options: LinkOptions, use_local: bool = True) -> None:
|
def set_config(self) -> None:
|
||||||
"""
|
|
||||||
Configure interface using tc based on existing state and provided
|
|
||||||
link options.
|
|
||||||
|
|
||||||
:param options: options to configure with
|
|
||||||
:param use_local: True to use localname for device, False for name
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
# determine name, options, and if anything has changed
|
|
||||||
name = self.localname if use_local else self.name
|
|
||||||
current_options = self.local_options if use_local else self.options
|
|
||||||
changed = current_options.update(options)
|
|
||||||
# nothing more to do when nothing has changed or not up
|
|
||||||
if not changed or not self.up:
|
|
||||||
return
|
|
||||||
# clear current settings
|
# clear current settings
|
||||||
if current_options.is_clear():
|
if self.options.is_clear():
|
||||||
clear_local_netem = use_local and self.has_local_netem
|
if self.has_netem:
|
||||||
clear_netem = not use_local and self.has_netem
|
cmd = tc_clear_cmd(self.name)
|
||||||
if clear_local_netem or clear_netem:
|
if self.node:
|
||||||
cmd = tc_clear_cmd(name)
|
self.node.cmd(cmd)
|
||||||
self.host_cmd(cmd)
|
|
||||||
if use_local:
|
|
||||||
self.has_local_netem = False
|
|
||||||
else:
|
else:
|
||||||
self.has_netem = False
|
self.host_cmd(cmd)
|
||||||
|
self.has_netem = False
|
||||||
# set updated settings
|
# set updated settings
|
||||||
else:
|
else:
|
||||||
cmd = tc_cmd(name, current_options, self.mtu)
|
cmd = tc_cmd(self.name, self.options, self.mtu)
|
||||||
self.host_cmd(cmd)
|
if self.node:
|
||||||
if use_local:
|
self.node.cmd(cmd)
|
||||||
self.has_local_netem = True
|
|
||||||
else:
|
else:
|
||||||
self.has_netem = True
|
self.host_cmd(cmd)
|
||||||
|
self.has_netem = True
|
||||||
|
|
||||||
def get_data(self) -> InterfaceData:
|
def get_data(self) -> InterfaceData:
|
||||||
"""
|
"""
|
||||||
|
@ -345,231 +318,22 @@ class CoreInterface:
|
||||||
|
|
||||||
:return: interface data
|
:return: interface data
|
||||||
"""
|
"""
|
||||||
if self.node:
|
|
||||||
iface_id = self.node.get_iface_id(self)
|
|
||||||
else:
|
|
||||||
iface_id = self.othernet.get_iface_id(self)
|
|
||||||
data = InterfaceData(
|
|
||||||
id=iface_id, name=self.name, mac=str(self.mac) if self.mac else None
|
|
||||||
)
|
|
||||||
ip4 = self.get_ip4()
|
ip4 = self.get_ip4()
|
||||||
if ip4:
|
ip4_addr = str(ip4.ip) if ip4 else None
|
||||||
data.ip4 = str(ip4.ip)
|
ip4_mask = ip4.prefixlen if ip4 else None
|
||||||
data.ip4_mask = ip4.prefixlen
|
|
||||||
ip6 = self.get_ip6()
|
ip6 = self.get_ip6()
|
||||||
if ip6:
|
ip6_addr = str(ip6.ip) if ip6 else None
|
||||||
data.ip6 = str(ip6.ip)
|
ip6_mask = ip6.prefixlen if ip6 else None
|
||||||
data.ip6_mask = ip6.prefixlen
|
mac = str(self.mac) if self.mac else None
|
||||||
return data
|
return InterfaceData(
|
||||||
|
id=self.id,
|
||||||
|
name=self.name,
|
||||||
class Veth(CoreInterface):
|
mac=mac,
|
||||||
"""
|
ip4=ip4_addr,
|
||||||
Provides virtual ethernet functionality for core nodes.
|
ip4_mask=ip4_mask,
|
||||||
"""
|
ip6=ip6_addr,
|
||||||
|
ip6_mask=ip6_mask,
|
||||||
def adopt_node(self, iface_id: int, name: str, start: bool) -> None:
|
)
|
||||||
"""
|
|
||||||
Adopt this interface to the provided node, configuring and associating
|
|
||||||
with the node as needed.
|
|
||||||
|
|
||||||
:param iface_id: interface id for node
|
|
||||||
:param name: name of interface fo rnode
|
|
||||||
:param start: True to start interface, False otherwise
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if start:
|
|
||||||
self.startup()
|
|
||||||
self.net_client.device_ns(self.name, str(self.node.pid))
|
|
||||||
self.node.node_net_client.checksums_off(self.name)
|
|
||||||
self.flow_id = self.node.node_net_client.get_ifindex(self.name)
|
|
||||||
logger.debug("interface flow index: %s - %s", self.name, self.flow_id)
|
|
||||||
mac = self.node.node_net_client.get_mac(self.name)
|
|
||||||
logger.debug("interface mac: %s - %s", self.name, mac)
|
|
||||||
self.set_mac(mac)
|
|
||||||
self.node.node_net_client.device_name(self.name, name)
|
|
||||||
self.name = name
|
|
||||||
try:
|
|
||||||
self.node.add_iface(self, iface_id)
|
|
||||||
except CoreError as e:
|
|
||||||
self.shutdown()
|
|
||||||
raise e
|
|
||||||
|
|
||||||
def startup(self) -> None:
|
|
||||||
"""
|
|
||||||
Interface startup logic.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
:raises CoreCommandError: when there is a command exception
|
|
||||||
"""
|
|
||||||
self.net_client.create_veth(self.localname, self.name)
|
|
||||||
if self.mtu > 0:
|
|
||||||
self.net_client.set_mtu(self.name, self.mtu)
|
|
||||||
self.net_client.set_mtu(self.localname, self.mtu)
|
|
||||||
self.net_client.device_up(self.localname)
|
|
||||||
self.up = True
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
"""
|
|
||||||
Interface shutdown logic.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if not self.up:
|
|
||||||
return
|
|
||||||
if self.node:
|
|
||||||
try:
|
|
||||||
self.node.node_net_client.device_flush(self.name)
|
|
||||||
except CoreCommandError:
|
|
||||||
pass
|
|
||||||
if self.localname:
|
|
||||||
try:
|
|
||||||
self.net_client.delete_device(self.localname)
|
|
||||||
except CoreCommandError:
|
|
||||||
pass
|
|
||||||
self.up = False
|
|
||||||
|
|
||||||
|
|
||||||
class TunTap(CoreInterface):
|
|
||||||
"""
|
|
||||||
TUN/TAP virtual device in TAP mode
|
|
||||||
"""
|
|
||||||
|
|
||||||
def startup(self) -> None:
|
|
||||||
"""
|
|
||||||
Startup logic for a tunnel tap.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
# TODO: more sophisticated TAP creation here
|
|
||||||
# Debian does not support -p (tap) option, RedHat does.
|
|
||||||
# For now, this is disabled to allow the TAP to be created by another
|
|
||||||
# system (e.g. EMANE"s emanetransportd)
|
|
||||||
# check_call(["tunctl", "-t", self.name])
|
|
||||||
# self.install()
|
|
||||||
self.up = True
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
"""
|
|
||||||
Shutdown functionality for a tunnel tap.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if not self.up:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self.node.node_net_client.device_flush(self.name)
|
|
||||||
except CoreCommandError:
|
|
||||||
logger.exception("error shutting down tunnel tap")
|
|
||||||
self.up = False
|
|
||||||
|
|
||||||
def waitfor(
|
|
||||||
self, func: Callable[[], int], attempts: int = 10, maxretrydelay: float = 0.25
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Wait for func() to return zero with exponential backoff.
|
|
||||||
|
|
||||||
:param func: function to wait for a result of zero
|
|
||||||
:param attempts: number of attempts to wait for a zero result
|
|
||||||
:param maxretrydelay: maximum retry delay
|
|
||||||
:return: True if wait succeeded, False otherwise
|
|
||||||
"""
|
|
||||||
delay = 0.01
|
|
||||||
result = False
|
|
||||||
for i in range(1, attempts + 1):
|
|
||||||
r = func()
|
|
||||||
if r == 0:
|
|
||||||
result = True
|
|
||||||
break
|
|
||||||
msg = f"attempt {i} failed with nonzero exit status {r}"
|
|
||||||
if i < attempts + 1:
|
|
||||||
msg += ", retrying..."
|
|
||||||
logger.info(msg)
|
|
||||||
time.sleep(delay)
|
|
||||||
delay += delay
|
|
||||||
if delay > maxretrydelay:
|
|
||||||
delay = maxretrydelay
|
|
||||||
else:
|
|
||||||
msg += ", giving up"
|
|
||||||
logger.info(msg)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def waitfordevicelocal(self) -> None:
|
|
||||||
"""
|
|
||||||
Check for presence of a local device - tap device may not
|
|
||||||
appear right away waits
|
|
||||||
|
|
||||||
:return: wait for device local response
|
|
||||||
"""
|
|
||||||
logger.debug("waiting for device local: %s", self.localname)
|
|
||||||
|
|
||||||
def localdevexists():
|
|
||||||
try:
|
|
||||||
self.net_client.device_show(self.localname)
|
|
||||||
return 0
|
|
||||||
except CoreCommandError:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
self.waitfor(localdevexists)
|
|
||||||
|
|
||||||
def waitfordevicenode(self) -> None:
|
|
||||||
"""
|
|
||||||
Check for presence of a node device - tap device may not appear right away waits.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
logger.debug("waiting for device node: %s", self.name)
|
|
||||||
|
|
||||||
def nodedevexists():
|
|
||||||
try:
|
|
||||||
self.node.node_net_client.device_show(self.name)
|
|
||||||
return 0
|
|
||||||
except CoreCommandError:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
while True:
|
|
||||||
result = self.waitfor(nodedevexists)
|
|
||||||
if result:
|
|
||||||
break
|
|
||||||
|
|
||||||
# TODO: emane specific code
|
|
||||||
# check if this is an EMANE interface; if so, continue
|
|
||||||
# waiting if EMANE is still running
|
|
||||||
should_retry = count < 5
|
|
||||||
is_emane = self.session.emane.is_emane_net(self.net)
|
|
||||||
is_emane_running = self.session.emane.emanerunning(self.node)
|
|
||||||
if all([should_retry, is_emane, is_emane_running]):
|
|
||||||
count += 1
|
|
||||||
else:
|
|
||||||
raise RuntimeError("node device failed to exist")
|
|
||||||
|
|
||||||
def install(self) -> None:
|
|
||||||
"""
|
|
||||||
Install this TAP into its namespace. This is not done from the
|
|
||||||
startup() method but called at a later time when a userspace
|
|
||||||
program (running on the host) has had a chance to open the socket
|
|
||||||
end of the TAP.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
:raises CoreCommandError: when there is a command exception
|
|
||||||
"""
|
|
||||||
self.waitfordevicelocal()
|
|
||||||
netns = str(self.node.pid)
|
|
||||||
self.net_client.device_ns(self.localname, netns)
|
|
||||||
self.node.node_net_client.device_name(self.localname, self.name)
|
|
||||||
self.node.node_net_client.device_up(self.name)
|
|
||||||
|
|
||||||
def set_ips(self) -> None:
|
|
||||||
"""
|
|
||||||
Set interface ip addresses.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
self.waitfordevicenode()
|
|
||||||
for ip in self.ips():
|
|
||||||
self.node.node_net_client.create_address(self.name, str(ip))
|
|
||||||
|
|
||||||
|
|
||||||
class GreTap(CoreInterface):
|
class GreTap(CoreInterface):
|
||||||
|
@ -594,7 +358,7 @@ class GreTap(CoreInterface):
|
||||||
"""
|
"""
|
||||||
Creates a GreTap instance.
|
Creates a GreTap instance.
|
||||||
|
|
||||||
:param session: core session instance
|
:param session: session for this gre tap
|
||||||
:param remoteip: remote address
|
:param remoteip: remote address
|
||||||
:param key: gre tap key
|
:param key: gre tap key
|
||||||
:param node: related core node
|
:param node: related core node
|
||||||
|
@ -612,7 +376,7 @@ class GreTap(CoreInterface):
|
||||||
sessionid = session.short_session_id()
|
sessionid = session.short_session_id()
|
||||||
localname = f"gt.{self.id}.{sessionid}"
|
localname = f"gt.{self.id}.{sessionid}"
|
||||||
name = f"{localname}p"
|
name = f"{localname}p"
|
||||||
super().__init__(session, name, localname, mtu, server, node)
|
super().__init__(0, name, localname, session.use_ovs(), mtu, node, server)
|
||||||
self.transport_type: TransportType = TransportType.RAW
|
self.transport_type: TransportType = TransportType.RAW
|
||||||
self.remote_ip: str = remoteip
|
self.remote_ip: str = remoteip
|
||||||
self.ttl: int = ttl
|
self.ttl: int = ttl
|
||||||
|
|
|
@ -6,6 +6,7 @@ from tempfile import NamedTemporaryFile
|
||||||
from typing import TYPE_CHECKING, Callable, Dict, Optional
|
from typing import TYPE_CHECKING, Callable, Dict, Optional
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
from core.emulator.data import InterfaceData, LinkOptions
|
||||||
from core.emulator.distributed import DistributedServer
|
from core.emulator.distributed import DistributedServer
|
||||||
from core.emulator.enumerations import NodeTypes
|
from core.emulator.enumerations import NodeTypes
|
||||||
from core.errors import CoreCommandError
|
from core.errors import CoreCommandError
|
||||||
|
@ -89,10 +90,9 @@ class LxcNode(CoreNode):
|
||||||
will run on, default is None for localhost
|
will run on, default is None for localhost
|
||||||
:param image: image to start container with
|
:param image: image to start container with
|
||||||
"""
|
"""
|
||||||
if image is None:
|
|
||||||
image = "ubuntu"
|
|
||||||
self.image: str = image
|
|
||||||
super().__init__(session, _id, name, directory, server)
|
super().__init__(session, _id, name, directory, server)
|
||||||
|
self.image: str = image if image is not None else "ubuntu"
|
||||||
|
self.client: Optional[LxdClient] = None
|
||||||
|
|
||||||
def alive(self) -> bool:
|
def alive(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -125,7 +125,6 @@ class LxcNode(CoreNode):
|
||||||
# nothing to do if node is not up
|
# nothing to do if node is not up
|
||||||
if not self.up:
|
if not self.up:
|
||||||
return
|
return
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.ifaces.clear()
|
self.ifaces.clear()
|
||||||
self.client.stop_container()
|
self.client.stop_container()
|
||||||
|
@ -212,7 +211,10 @@ class LxcNode(CoreNode):
|
||||||
if mode is not None:
|
if mode is not None:
|
||||||
self.cmd(f"chmod {mode:o} {dst_path}")
|
self.cmd(f"chmod {mode:o} {dst_path}")
|
||||||
|
|
||||||
def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
|
def create_iface(
|
||||||
super().add_iface(iface, iface_id)
|
self, iface_data: InterfaceData = None, options: LinkOptions = None
|
||||||
|
) -> CoreInterface:
|
||||||
|
iface = super().create_iface(iface_data, options)
|
||||||
# adding small delay to allow time for adding addresses to work correctly
|
# adding small delay to allow time for adding addresses to work correctly
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
return iface
|
||||||
|
|
|
@ -22,8 +22,8 @@ from core.emulator.enumerations import (
|
||||||
)
|
)
|
||||||
from core.errors import CoreCommandError, CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.executables import NFTABLES
|
from core.executables import NFTABLES
|
||||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
from core.nodes.base import CoreNetworkBase
|
||||||
from core.nodes.interface import CoreInterface, GreTap, Veth
|
from core.nodes.interface import CoreInterface, GreTap
|
||||||
from core.nodes.netclient import get_net_client
|
from core.nodes.netclient import get_net_client
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -222,7 +222,7 @@ class CoreNetwork(CoreNetworkBase):
|
||||||
policy: NetworkPolicy = None,
|
policy: NetworkPolicy = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Creates a LxBrNet instance.
|
Creates a CoreNetwork instance.
|
||||||
|
|
||||||
:param session: core session instance
|
:param session: core session instance
|
||||||
:param _id: object id
|
:param _id: object id
|
||||||
|
@ -280,6 +280,17 @@ class CoreNetwork(CoreNetworkBase):
|
||||||
self.up = True
|
self.up = True
|
||||||
nft_queue.start()
|
nft_queue.start()
|
||||||
|
|
||||||
|
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||||
|
"""
|
||||||
|
Adopt interface and set it to use this bridge as master.
|
||||||
|
|
||||||
|
:param iface: interface to adpopt
|
||||||
|
:param name: formal name for interface
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
iface.net_client.set_iface_master(self.brname, iface.name)
|
||||||
|
iface.set_config()
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
"""
|
"""
|
||||||
Linux bridge shutdown logic.
|
Linux bridge shutdown logic.
|
||||||
|
@ -309,9 +320,9 @@ class CoreNetwork(CoreNetworkBase):
|
||||||
:param iface: network interface to attach
|
:param iface: network interface to attach
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
|
super().attach(iface)
|
||||||
if self.up:
|
if self.up:
|
||||||
iface.net_client.set_iface_master(self.brname, iface.localname)
|
iface.net_client.set_iface_master(self.brname, iface.localname)
|
||||||
super().attach(iface)
|
|
||||||
|
|
||||||
def detach(self, iface: CoreInterface) -> None:
|
def detach(self, iface: CoreInterface) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -320,9 +331,9 @@ class CoreNetwork(CoreNetworkBase):
|
||||||
:param iface: network interface to detach
|
:param iface: network interface to detach
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
|
super().detach(iface)
|
||||||
if self.up:
|
if self.up:
|
||||||
iface.net_client.delete_iface(self.brname, iface.localname)
|
iface.net_client.delete_iface(self.brname, iface.localname)
|
||||||
super().detach(iface)
|
|
||||||
|
|
||||||
def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool:
|
def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -378,67 +389,6 @@ class CoreNetwork(CoreNetworkBase):
|
||||||
self.linked[iface1][iface2] = True
|
self.linked[iface1][iface2] = True
|
||||||
nft_queue.update(self)
|
nft_queue.update(self)
|
||||||
|
|
||||||
def linknet(self, net: CoreNetworkBase) -> CoreInterface:
|
|
||||||
"""
|
|
||||||
Link this bridge with another by creating a veth pair and installing
|
|
||||||
each device into each bridge.
|
|
||||||
|
|
||||||
:param net: network to link with
|
|
||||||
:return: created interface
|
|
||||||
"""
|
|
||||||
sessionid = self.session.short_session_id()
|
|
||||||
try:
|
|
||||||
_id = f"{self.id:x}"
|
|
||||||
except TypeError:
|
|
||||||
_id = str(self.id)
|
|
||||||
try:
|
|
||||||
net_id = f"{net.id:x}"
|
|
||||||
except TypeError:
|
|
||||||
net_id = str(net.id)
|
|
||||||
localname = f"veth{_id}.{net_id}.{sessionid}"
|
|
||||||
name = f"veth{net_id}.{_id}.{sessionid}"
|
|
||||||
iface = Veth(self.session, name, localname)
|
|
||||||
if self.up:
|
|
||||||
iface.startup()
|
|
||||||
self.attach(iface)
|
|
||||||
if net.up and net.brname:
|
|
||||||
iface.net_client.set_iface_master(net.brname, iface.name)
|
|
||||||
i = net.next_iface_id()
|
|
||||||
net.ifaces[i] = iface
|
|
||||||
with net.linked_lock:
|
|
||||||
net.linked[iface] = {}
|
|
||||||
iface.net = self
|
|
||||||
iface.othernet = net
|
|
||||||
return iface
|
|
||||||
|
|
||||||
def get_linked_iface(self, net: CoreNetworkBase) -> Optional[CoreInterface]:
|
|
||||||
"""
|
|
||||||
Return the interface of that links this net with another net
|
|
||||||
(that were linked using linknet()).
|
|
||||||
|
|
||||||
:param net: interface to get link for
|
|
||||||
:return: interface the provided network is linked to
|
|
||||||
"""
|
|
||||||
for iface in self.get_ifaces():
|
|
||||||
if iface.othernet == net:
|
|
||||||
return iface
|
|
||||||
return None
|
|
||||||
|
|
||||||
def add_ips(self, ips: List[str]) -> None:
|
|
||||||
"""
|
|
||||||
Add ip addresses on the bridge in the format "10.0.0.1/24".
|
|
||||||
|
|
||||||
:param ips: ip address to add
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if not self.up:
|
|
||||||
return
|
|
||||||
for ip in ips:
|
|
||||||
self.net_client.create_address(self.brname, ip)
|
|
||||||
|
|
||||||
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
|
||||||
raise CoreError(f"{type(self).__name__} does not support, custom interfaces")
|
|
||||||
|
|
||||||
|
|
||||||
class GreTapBridge(CoreNetwork):
|
class GreTapBridge(CoreNetwork):
|
||||||
"""
|
"""
|
||||||
|
@ -686,15 +636,6 @@ class CtrlNet(CoreNetwork):
|
||||||
|
|
||||||
super().shutdown()
|
super().shutdown()
|
||||||
|
|
||||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
|
||||||
"""
|
|
||||||
Do not include CtrlNet in link messages describing this session.
|
|
||||||
|
|
||||||
:param flags: message flags
|
|
||||||
:return: list of link data
|
|
||||||
"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class PtpNet(CoreNetwork):
|
class PtpNet(CoreNetwork):
|
||||||
"""
|
"""
|
||||||
|
@ -714,50 +655,6 @@ class PtpNet(CoreNetwork):
|
||||||
raise CoreError("ptp links support at most 2 network interfaces")
|
raise CoreError("ptp links support at most 2 network interfaces")
|
||||||
super().attach(iface)
|
super().attach(iface)
|
||||||
|
|
||||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
|
||||||
"""
|
|
||||||
Build CORE API TLVs for a point-to-point link. One Link message
|
|
||||||
describes this network.
|
|
||||||
|
|
||||||
:param flags: message flags
|
|
||||||
:return: list of link data
|
|
||||||
"""
|
|
||||||
all_links = []
|
|
||||||
if len(self.ifaces) != 2:
|
|
||||||
return all_links
|
|
||||||
ifaces = self.get_ifaces()
|
|
||||||
iface1 = ifaces[0]
|
|
||||||
iface2 = ifaces[1]
|
|
||||||
unidirectional = 0 if iface1.local_options == iface2.local_options else 1
|
|
||||||
iface1_data = iface1.get_data()
|
|
||||||
iface2_data = iface2.get_data()
|
|
||||||
link_data = LinkData(
|
|
||||||
message_type=flags,
|
|
||||||
type=self.linktype,
|
|
||||||
node1_id=iface1.node.id,
|
|
||||||
node2_id=iface2.node.id,
|
|
||||||
iface1=iface1_data,
|
|
||||||
iface2=iface2_data,
|
|
||||||
options=iface1.local_options,
|
|
||||||
)
|
|
||||||
link_data.options.unidirectional = unidirectional
|
|
||||||
all_links.append(link_data)
|
|
||||||
# build a 2nd link message for the upstream link parameters
|
|
||||||
# (swap if1 and if2)
|
|
||||||
if unidirectional:
|
|
||||||
link_data = LinkData(
|
|
||||||
message_type=MessageFlags.NONE,
|
|
||||||
type=self.linktype,
|
|
||||||
node1_id=iface2.node.id,
|
|
||||||
node2_id=iface1.node.id,
|
|
||||||
iface1=InterfaceData(id=iface2_data.id),
|
|
||||||
iface2=InterfaceData(id=iface1_data.id),
|
|
||||||
options=iface2.local_options,
|
|
||||||
)
|
|
||||||
link_data.options.unidirectional = unidirectional
|
|
||||||
all_links.append(link_data)
|
|
||||||
return all_links
|
|
||||||
|
|
||||||
|
|
||||||
class SwitchNode(CoreNetwork):
|
class SwitchNode(CoreNetwork):
|
||||||
"""
|
"""
|
||||||
|
@ -884,10 +781,10 @@ class WlanNode(CoreNetwork):
|
||||||
:param flags: message flags
|
:param flags: message flags
|
||||||
:return: list of link data
|
:return: list of link data
|
||||||
"""
|
"""
|
||||||
links = super().links(flags)
|
|
||||||
if self.model:
|
if self.model:
|
||||||
links.extend(self.model.links(flags))
|
return self.model.links(flags)
|
||||||
return links
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class TunnelNode(GreTapBridge):
|
class TunnelNode(GreTapBridge):
|
||||||
|
|
|
@ -3,17 +3,18 @@ PhysicalNode class for including real systems in the emulated network.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||||
|
|
||||||
from core.emulator.data import InterfaceData
|
import netaddr
|
||||||
|
|
||||||
|
from core.emulator.data import InterfaceData, LinkOptions
|
||||||
from core.emulator.distributed import DistributedServer
|
from core.emulator.distributed import DistributedServer
|
||||||
from core.emulator.enumerations import NodeTypes, TransportType
|
from core.emulator.enumerations import NodeTypes, TransportType
|
||||||
from core.errors import CoreCommandError, CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.executables import MOUNT, TEST, UMOUNT
|
from core.executables import BASH, TEST, UMOUNT
|
||||||
from core.nodes.base import CoreNetworkBase, CoreNodeBase
|
from core.nodes.base import CoreNode, CoreNodeBase
|
||||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
from core.nodes.interface import CoreInterface
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -21,185 +22,6 @@ if TYPE_CHECKING:
|
||||||
from core.emulator.session import Session
|
from core.emulator.session import Session
|
||||||
|
|
||||||
|
|
||||||
class PhysicalNode(CoreNodeBase):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
session: "Session",
|
|
||||||
_id: int = None,
|
|
||||||
name: str = None,
|
|
||||||
directory: Path = None,
|
|
||||||
server: DistributedServer = None,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(session, _id, name, server)
|
|
||||||
if not self.server:
|
|
||||||
raise CoreError("physical nodes must be assigned to a remote server")
|
|
||||||
self.directory: Optional[Path] = directory
|
|
||||||
self.lock: threading.RLock = threading.RLock()
|
|
||||||
self._mounts: List[Tuple[Path, Path]] = []
|
|
||||||
|
|
||||||
def startup(self) -> None:
|
|
||||||
with self.lock:
|
|
||||||
self.makenodedir()
|
|
||||||
self.up = True
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
if not self.up:
|
|
||||||
return
|
|
||||||
with self.lock:
|
|
||||||
while self._mounts:
|
|
||||||
_, target_path = self._mounts.pop(-1)
|
|
||||||
self.umount(target_path)
|
|
||||||
for iface in self.get_ifaces():
|
|
||||||
iface.shutdown()
|
|
||||||
self.rmnodedir()
|
|
||||||
|
|
||||||
def path_exists(self, path: str) -> bool:
|
|
||||||
"""
|
|
||||||
Determines if a file or directory path exists.
|
|
||||||
|
|
||||||
:param path: path to file or directory
|
|
||||||
:return: True if path exists, False otherwise
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.host_cmd(f"{TEST} -e {path}")
|
|
||||||
return True
|
|
||||||
except CoreCommandError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def termcmdstring(self, sh: str = "/bin/sh") -> str:
|
|
||||||
"""
|
|
||||||
Create a terminal command string.
|
|
||||||
|
|
||||||
:param sh: shell to execute command in
|
|
||||||
:return: str
|
|
||||||
"""
|
|
||||||
return sh
|
|
||||||
|
|
||||||
def set_mac(self, iface_id: int, mac: str) -> None:
|
|
||||||
"""
|
|
||||||
Set mac address for an interface.
|
|
||||||
|
|
||||||
:param iface_id: index of interface to set hardware address for
|
|
||||||
:param mac: mac address to set
|
|
||||||
:return: nothing
|
|
||||||
:raises CoreCommandError: when a non-zero exit status occurs
|
|
||||||
"""
|
|
||||||
iface = self.get_iface(iface_id)
|
|
||||||
iface.set_mac(mac)
|
|
||||||
if self.up:
|
|
||||||
self.net_client.device_mac(iface.name, str(iface.mac))
|
|
||||||
|
|
||||||
def add_ip(self, iface_id: int, ip: str) -> None:
|
|
||||||
"""
|
|
||||||
Add an ip address to an interface in the format "10.0.0.1/24".
|
|
||||||
|
|
||||||
:param iface_id: id of interface to add address to
|
|
||||||
:param ip: address to add to interface
|
|
||||||
:return: nothing
|
|
||||||
:raises CoreError: when ip address provided is invalid
|
|
||||||
:raises CoreCommandError: when a non-zero exit status occurs
|
|
||||||
"""
|
|
||||||
iface = self.get_iface(iface_id)
|
|
||||||
iface.add_ip(ip)
|
|
||||||
if self.up:
|
|
||||||
self.net_client.create_address(iface.name, ip)
|
|
||||||
|
|
||||||
def remove_ip(self, iface_id: int, ip: str) -> None:
|
|
||||||
"""
|
|
||||||
Remove an ip address from an interface in the format "10.0.0.1/24".
|
|
||||||
|
|
||||||
:param iface_id: id of interface to delete address from
|
|
||||||
:param ip: ip address to remove from interface
|
|
||||||
:return: nothing
|
|
||||||
:raises CoreError: when ip address provided is invalid
|
|
||||||
:raises CoreCommandError: when a non-zero exit status occurs
|
|
||||||
"""
|
|
||||||
iface = self.get_iface(iface_id)
|
|
||||||
iface.remove_ip(ip)
|
|
||||||
if self.up:
|
|
||||||
self.net_client.delete_address(iface.name, ip)
|
|
||||||
|
|
||||||
def adopt_iface(
|
|
||||||
self, iface: CoreInterface, iface_id: int, mac: str, ips: List[str]
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
When a link message is received linking this node to another part of
|
|
||||||
the emulation, no new interface is created; instead, adopt the
|
|
||||||
GreTap interface as the node interface.
|
|
||||||
"""
|
|
||||||
iface.name = f"gt{iface_id}"
|
|
||||||
iface.node = self
|
|
||||||
self.add_iface(iface, iface_id)
|
|
||||||
# use a more reasonable name, e.g. "gt0" instead of "gt.56286.150"
|
|
||||||
if self.up:
|
|
||||||
self.net_client.device_down(iface.localname)
|
|
||||||
self.net_client.device_name(iface.localname, iface.name)
|
|
||||||
iface.localname = iface.name
|
|
||||||
if mac:
|
|
||||||
self.set_mac(iface_id, mac)
|
|
||||||
for ip in ips:
|
|
||||||
self.add_ip(iface_id, ip)
|
|
||||||
if self.up:
|
|
||||||
self.net_client.device_up(iface.localname)
|
|
||||||
|
|
||||||
def next_iface_id(self) -> int:
|
|
||||||
with self.lock:
|
|
||||||
while self.iface_id in self.ifaces:
|
|
||||||
self.iface_id += 1
|
|
||||||
iface_id = self.iface_id
|
|
||||||
self.iface_id += 1
|
|
||||||
return iface_id
|
|
||||||
|
|
||||||
def new_iface(
|
|
||||||
self, net: CoreNetworkBase, iface_data: InterfaceData
|
|
||||||
) -> CoreInterface:
|
|
||||||
logger.info("creating interface")
|
|
||||||
ips = iface_data.get_ips()
|
|
||||||
iface_id = iface_data.id
|
|
||||||
if iface_id is None:
|
|
||||||
iface_id = self.next_iface_id()
|
|
||||||
name = iface_data.name
|
|
||||||
if name is None:
|
|
||||||
name = f"gt{iface_id}"
|
|
||||||
_, remote_tap = self.session.distributed.create_gre_tunnel(
|
|
||||||
net, self.server, iface_data.mtu, self.up
|
|
||||||
)
|
|
||||||
self.adopt_iface(remote_tap, iface_id, iface_data.mac, ips)
|
|
||||||
return remote_tap
|
|
||||||
|
|
||||||
def privatedir(self, dir_path: Path) -> None:
|
|
||||||
if not str(dir_path).startswith("/"):
|
|
||||||
raise CoreError(f"private directory path not fully qualified: {dir_path}")
|
|
||||||
host_path = self.host_path(dir_path, is_dir=True)
|
|
||||||
self.host_cmd(f"mkdir -p {host_path}")
|
|
||||||
self.mount(host_path, dir_path)
|
|
||||||
|
|
||||||
def mount(self, src_path: Path, target_path: Path) -> None:
|
|
||||||
logger.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path)
|
|
||||||
self.cmd(f"mkdir -p {target_path}")
|
|
||||||
self.host_cmd(f"{MOUNT} --bind {src_path} {target_path}", cwd=self.directory)
|
|
||||||
self._mounts.append((src_path, target_path))
|
|
||||||
|
|
||||||
def umount(self, target_path: Path) -> None:
|
|
||||||
logger.info("unmounting '%s'", target_path)
|
|
||||||
try:
|
|
||||||
self.host_cmd(f"{UMOUNT} -l {target_path}", cwd=self.directory)
|
|
||||||
except CoreCommandError:
|
|
||||||
logger.exception("unmounting failed for %s", target_path)
|
|
||||||
|
|
||||||
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
|
|
||||||
return self.host_cmd(args, wait=wait)
|
|
||||||
|
|
||||||
def create_dir(self, dir_path: Path) -> None:
|
|
||||||
raise CoreError("physical node does not support creating directories")
|
|
||||||
|
|
||||||
def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None:
|
|
||||||
raise CoreError("physical node does not support creating files")
|
|
||||||
|
|
||||||
def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
|
|
||||||
raise CoreError("physical node does not support copying files")
|
|
||||||
|
|
||||||
|
|
||||||
class Rj45Node(CoreNodeBase):
|
class Rj45Node(CoreNodeBase):
|
||||||
"""
|
"""
|
||||||
RJ45Node is a physical interface on the host linked to the emulated
|
RJ45Node is a physical interface on the host linked to the emulated
|
||||||
|
@ -214,7 +36,6 @@ class Rj45Node(CoreNodeBase):
|
||||||
session: "Session",
|
session: "Session",
|
||||||
_id: int = None,
|
_id: int = None,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
mtu: int = DEFAULT_MTU,
|
|
||||||
server: DistributedServer = None,
|
server: DistributedServer = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -223,17 +44,14 @@ class Rj45Node(CoreNodeBase):
|
||||||
:param session: core session instance
|
:param session: core session instance
|
||||||
:param _id: node id
|
:param _id: node id
|
||||||
:param name: node name
|
:param name: node name
|
||||||
:param mtu: rj45 mtu
|
|
||||||
:param server: remote server node
|
:param server: remote server node
|
||||||
will run on, default is None for localhost
|
will run on, default is None for localhost
|
||||||
"""
|
"""
|
||||||
super().__init__(session, _id, name, server)
|
super().__init__(session, _id, name, server)
|
||||||
self.iface: CoreInterface = CoreInterface(
|
self.iface: CoreInterface = CoreInterface(
|
||||||
session, name, name, mtu, server, self
|
self.iface_id, name, name, session.use_ovs(), node=self, server=server
|
||||||
)
|
)
|
||||||
self.iface.transport_type = TransportType.RAW
|
self.iface.transport_type = TransportType.RAW
|
||||||
self.lock: threading.RLock = threading.RLock()
|
|
||||||
self.iface_id: Optional[int] = None
|
|
||||||
self.old_up: bool = False
|
self.old_up: bool = False
|
||||||
self.old_addrs: List[Tuple[str, Optional[str]]] = []
|
self.old_addrs: List[Tuple[str, Optional[str]]] = []
|
||||||
|
|
||||||
|
@ -245,7 +63,7 @@ class Rj45Node(CoreNodeBase):
|
||||||
:raises CoreCommandError: when there is a command exception
|
:raises CoreCommandError: when there is a command exception
|
||||||
"""
|
"""
|
||||||
# interface will also be marked up during net.attach()
|
# interface will also be marked up during net.attach()
|
||||||
self.savestate()
|
self.save_state()
|
||||||
self.net_client.device_up(self.iface.localname)
|
self.net_client.device_up(self.iface.localname)
|
||||||
self.up = True
|
self.up = True
|
||||||
|
|
||||||
|
@ -266,7 +84,7 @@ class Rj45Node(CoreNodeBase):
|
||||||
except CoreCommandError:
|
except CoreCommandError:
|
||||||
pass
|
pass
|
||||||
self.up = False
|
self.up = False
|
||||||
self.restorestate()
|
self.restore_state()
|
||||||
|
|
||||||
def path_exists(self, path: str) -> bool:
|
def path_exists(self, path: str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -281,33 +99,28 @@ class Rj45Node(CoreNodeBase):
|
||||||
except CoreCommandError:
|
except CoreCommandError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def new_iface(
|
def create_iface(
|
||||||
self, net: CoreNetworkBase, iface_data: InterfaceData
|
self, iface_data: InterfaceData = None, options: LinkOptions = None
|
||||||
) -> CoreInterface:
|
) -> CoreInterface:
|
||||||
"""
|
|
||||||
This is called when linking with another node. Since this node
|
|
||||||
represents an interface, we do not create another object here,
|
|
||||||
but attach ourselves to the given network.
|
|
||||||
|
|
||||||
:param net: new network instance
|
|
||||||
:param iface_data: interface data for new interface
|
|
||||||
:return: interface index
|
|
||||||
:raises ValueError: when an interface has already been created, one max
|
|
||||||
"""
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
iface_id = iface_data.id
|
if self.iface.id in self.ifaces:
|
||||||
if iface_id is None:
|
|
||||||
iface_id = 0
|
|
||||||
if self.iface.net is not None:
|
|
||||||
raise CoreError(
|
raise CoreError(
|
||||||
f"RJ45({self.name}) nodes support at most 1 network interface"
|
f"rj45({self.name}) nodes support at most 1 network interface"
|
||||||
)
|
)
|
||||||
self.ifaces[iface_id] = self.iface
|
if iface_data and iface_data.mtu is not None:
|
||||||
self.iface_id = iface_id
|
self.iface.mtu = iface_data.mtu
|
||||||
self.iface.attachnet(net)
|
self.iface.ip4s.clear()
|
||||||
|
self.iface.ip6s.clear()
|
||||||
for ip in iface_data.get_ips():
|
for ip in iface_data.get_ips():
|
||||||
self.add_ip(ip)
|
self.iface.add_ip(ip)
|
||||||
return self.iface
|
self.ifaces[self.iface.id] = self.iface
|
||||||
|
if self.up:
|
||||||
|
for ip in self.iface.ips():
|
||||||
|
self.net_client.create_address(self.iface.name, str(ip))
|
||||||
|
return self.iface
|
||||||
|
|
||||||
|
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||||
|
raise CoreError(f"rj45({self.name}) does not support adopt interface")
|
||||||
|
|
||||||
def delete_iface(self, iface_id: int) -> None:
|
def delete_iface(self, iface_id: int) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -318,16 +131,10 @@ class Rj45Node(CoreNodeBase):
|
||||||
"""
|
"""
|
||||||
self.get_iface(iface_id)
|
self.get_iface(iface_id)
|
||||||
self.ifaces.pop(iface_id)
|
self.ifaces.pop(iface_id)
|
||||||
if self.iface.net is None:
|
|
||||||
raise CoreError(
|
|
||||||
f"RJ45({self.name}) is not currently connected to a network"
|
|
||||||
)
|
|
||||||
self.iface.detachnet()
|
|
||||||
self.iface.net = None
|
|
||||||
self.shutdown()
|
self.shutdown()
|
||||||
|
|
||||||
def get_iface(self, iface_id: int) -> CoreInterface:
|
def get_iface(self, iface_id: int) -> CoreInterface:
|
||||||
if iface_id != self.iface_id or iface_id not in self.ifaces:
|
if iface_id not in self.ifaces:
|
||||||
raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
|
raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
|
||||||
return self.iface
|
return self.iface
|
||||||
|
|
||||||
|
@ -341,42 +148,17 @@ class Rj45Node(CoreNodeBase):
|
||||||
"""
|
"""
|
||||||
if iface is not self.iface:
|
if iface is not self.iface:
|
||||||
raise CoreError(f"node({self.name}) does not have interface({iface.name})")
|
raise CoreError(f"node({self.name}) does not have interface({iface.name})")
|
||||||
return self.iface_id
|
return self.iface.id
|
||||||
|
|
||||||
def add_ip(self, ip: str) -> None:
|
def save_state(self) -> None:
|
||||||
"""
|
|
||||||
Add an ip address to an interface in the format "10.0.0.1/24".
|
|
||||||
|
|
||||||
:param ip: address to add to interface
|
|
||||||
:return: nothing
|
|
||||||
:raises CoreError: when ip address provided is invalid
|
|
||||||
:raises CoreCommandError: when a non-zero exit status occurs
|
|
||||||
"""
|
|
||||||
self.iface.add_ip(ip)
|
|
||||||
if self.up:
|
|
||||||
self.net_client.create_address(self.name, ip)
|
|
||||||
|
|
||||||
def remove_ip(self, ip: str) -> None:
|
|
||||||
"""
|
|
||||||
Remove an ip address from an interface in the format "10.0.0.1/24".
|
|
||||||
|
|
||||||
:param ip: ip address to remove from interface
|
|
||||||
:return: nothing
|
|
||||||
:raises CoreError: when ip address provided is invalid
|
|
||||||
:raises CoreCommandError: when a non-zero exit status occurs
|
|
||||||
"""
|
|
||||||
self.iface.remove_ip(ip)
|
|
||||||
if self.up:
|
|
||||||
self.net_client.delete_address(self.name, ip)
|
|
||||||
|
|
||||||
def savestate(self) -> None:
|
|
||||||
"""
|
"""
|
||||||
Save the addresses and other interface state before using the
|
Save the addresses and other interface state before using the
|
||||||
interface for emulation purposes. TODO: save/restore the PROMISC flag
|
interface for emulation purposes.
|
||||||
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
:raises CoreCommandError: when there is a command exception
|
:raises CoreCommandError: when there is a command exception
|
||||||
"""
|
"""
|
||||||
|
# TODO: save/restore the PROMISC flag
|
||||||
self.old_up = False
|
self.old_up = False
|
||||||
self.old_addrs: List[Tuple[str, Optional[str]]] = []
|
self.old_addrs: List[Tuple[str, Optional[str]]] = []
|
||||||
localname = self.iface.localname
|
localname = self.iface.localname
|
||||||
|
@ -397,7 +179,7 @@ class Rj45Node(CoreNodeBase):
|
||||||
self.old_addrs.append((items[1], None))
|
self.old_addrs.append((items[1], None))
|
||||||
logger.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up)
|
logger.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up)
|
||||||
|
|
||||||
def restorestate(self) -> None:
|
def restore_state(self) -> None:
|
||||||
"""
|
"""
|
||||||
Restore the addresses and other interface state after using it.
|
Restore the addresses and other interface state after using it.
|
||||||
|
|
||||||
|
@ -437,3 +219,69 @@ class Rj45Node(CoreNodeBase):
|
||||||
|
|
||||||
def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
|
def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
|
||||||
raise CoreError("rj45 does not support copying files")
|
raise CoreError("rj45 does not support copying files")
|
||||||
|
|
||||||
|
|
||||||
|
class PhysicalNode(CoreNode):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
session: "Session",
|
||||||
|
_id: int = None,
|
||||||
|
name: str = None,
|
||||||
|
directory: Path = None,
|
||||||
|
server: DistributedServer = None,
|
||||||
|
) -> None:
|
||||||
|
if not self.server:
|
||||||
|
raise CoreError("physical nodes must be assigned to a remote server")
|
||||||
|
super().__init__(session, _id, name, directory, server)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
240
daemon/core/nodes/wireless.py
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
"""
|
||||||
|
Defines a wireless node that allows programmatic link connectivity and
|
||||||
|
configuration between pairs of nodes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Dict, Tuple
|
||||||
|
|
||||||
|
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
|
||||||
|
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__)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WirelessLink:
|
||||||
|
bridge1: str
|
||||||
|
bridge2: str
|
||||||
|
iface: CoreInterface
|
||||||
|
|
||||||
|
|
||||||
|
class WirelessNode(CoreNetworkBase):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
session: "Session",
|
||||||
|
_id: int,
|
||||||
|
name: str,
|
||||||
|
server: "DistributedServer" = None,
|
||||||
|
):
|
||||||
|
super().__init__(session, _id, name, server)
|
||||||
|
self.bridges: Dict[int, Tuple[CoreInterface, str]] = {}
|
||||||
|
self.links: Dict[Tuple[int, int], WirelessLink] = {}
|
||||||
|
self.labels: Dict[Tuple[int, int], str] = {}
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
session_id = self.session.short_session_id()
|
||||||
|
name1 = f"we{self.id}.{node1.id}.{node2.id}.{session_id}"
|
||||||
|
name2 = f"we{self.id}.{node2.id}.{node1.id}.{session_id}"
|
||||||
|
link_iface = CoreInterface(0, name1, name2, self.session.use_ovs())
|
||||||
|
link_iface.startup()
|
||||||
|
link = WirelessLink(bridge1, bridge2, link_iface)
|
||||||
|
self.links[key] = link
|
||||||
|
# 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)
|
||||||
|
# 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)
|
||||||
|
# send link
|
||||||
|
self.send_link(node1.id, node2.id, MessageFlags.ADD)
|
||||||
|
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 = (node1_id, node2_id) if node1_id < node2_id else (node2_id, node1_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 linked:
|
||||||
|
self.net_client.set_iface_master(bridge1, iface.name)
|
||||||
|
self.net_client.set_iface_master(bridge2, iface.localname)
|
||||||
|
message_type = MessageFlags.ADD
|
||||||
|
else:
|
||||||
|
self.net_client.delete_iface(bridge1, iface.name)
|
||||||
|
self.net_client.delete_iface(bridge2, iface.localname)
|
||||||
|
message_type = MessageFlags.DELETE
|
||||||
|
label = self.labels.get(key)
|
||||||
|
self.send_link(key[0], key[1], message_type, label)
|
||||||
|
|
||||||
|
def link_config(
|
||||||
|
self, node1_id: int, node2_id: int, options1: LinkOptions, options2: LinkOptions
|
||||||
|
) -> None:
|
||||||
|
key = (node1_id, node2_id) if node1_id < node2_id else (node2_id, node1_id)
|
||||||
|
link = self.links.get(key)
|
||||||
|
if not link:
|
||||||
|
raise CoreError(f"invalid node links node1({node1_id}) node2({node2_id})")
|
||||||
|
iface = link.iface
|
||||||
|
iface.options.update(options1)
|
||||||
|
iface.set_config()
|
||||||
|
name, localname = iface.name, iface.localname
|
||||||
|
iface.name, iface.localname = localname, name
|
||||||
|
iface.options.update(options2)
|
||||||
|
iface.set_config()
|
||||||
|
iface.name, iface.localname = name, localname
|
||||||
|
if options1 == options2:
|
||||||
|
if options1.is_clear():
|
||||||
|
label = ""
|
||||||
|
else:
|
||||||
|
label = f"{options1.loss:.2f}%/{options1.delay}us"
|
||||||
|
else:
|
||||||
|
label = (
|
||||||
|
f"({options1.loss:.2f}%/{options1.delay}us) "
|
||||||
|
f"({options2.loss:.2f}%/{options2.delay}us)"
|
||||||
|
)
|
||||||
|
self.labels[key] = label
|
||||||
|
self.send_link(key[0], key[1], MessageFlags.NONE, 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
|
||||||
|
point1 = iface.node.position.get()
|
||||||
|
point2 = oiface.node.position.get()
|
||||||
|
distance = calc_distance(point1, point2) - 250
|
||||||
|
distance = max(distance, 0.0)
|
||||||
|
logger.info(
|
||||||
|
"n1(%s) n2(%s) d(%s)", iface.node.name, oiface.node.name, distance
|
||||||
|
)
|
||||||
|
loss = min((distance / 500) * 100.0, 100.0)
|
||||||
|
node1_id = iface.node.id
|
||||||
|
node2_id = oiface.node.id
|
||||||
|
options = LinkOptions(loss=loss, delay=0)
|
||||||
|
self.link_config(node1_id, node2_id, options, options)
|
||||||
|
|
||||||
|
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||||
|
raise CoreError(f"{type(self)} does not support adopt interface")
|
|
@ -4,7 +4,7 @@ sdt.py: Scripted Display Tool (SDT3D) helper
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
from typing import IO, TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from core.constants import CORE_CONF_DIR, CORE_DATA_DIR
|
from core.constants import CORE_CONF_DIR, CORE_DATA_DIR
|
||||||
|
@ -12,22 +12,15 @@ from core.emane.nodes import EmaneNet
|
||||||
from core.emulator.data import LinkData, NodeData
|
from core.emulator.data import LinkData, NodeData
|
||||||
from core.emulator.enumerations import EventTypes, MessageFlags
|
from core.emulator.enumerations import EventTypes, MessageFlags
|
||||||
from core.errors import CoreError
|
from core.errors import CoreError
|
||||||
from core.nodes.base import CoreNetworkBase, NodeBase
|
from core.nodes.base import NodeBase
|
||||||
from core.nodes.network import WlanNode
|
from core.nodes.network import WlanNode
|
||||||
|
from core.nodes.wireless import WirelessNode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.emulator.session import Session
|
from core.emulator.session import Session
|
||||||
|
|
||||||
|
|
||||||
def get_link_id(node1_id: int, node2_id: int, network_id: int) -> str:
|
|
||||||
link_id = f"{node1_id}-{node2_id}"
|
|
||||||
if network_id is not None:
|
|
||||||
link_id = f"{link_id}-{network_id}"
|
|
||||||
return link_id
|
|
||||||
|
|
||||||
|
|
||||||
CORE_LAYER: str = "CORE"
|
CORE_LAYER: str = "CORE"
|
||||||
NODE_LAYER: str = "CORE::Nodes"
|
NODE_LAYER: str = "CORE::Nodes"
|
||||||
LINK_LAYER: str = "CORE::Links"
|
LINK_LAYER: str = "CORE::Links"
|
||||||
|
@ -36,6 +29,17 @@ CORE_LAYERS: List[str] = [CORE_LAYER, LINK_LAYER, NODE_LAYER, WIRED_LINK_LAYER]
|
||||||
DEFAULT_LINK_COLOR: str = "red"
|
DEFAULT_LINK_COLOR: str = "red"
|
||||||
|
|
||||||
|
|
||||||
|
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}"
|
||||||
|
if network_id is not None:
|
||||||
|
link_id = f"{link_id}-{network_id}"
|
||||||
|
return link_id
|
||||||
|
|
||||||
|
|
||||||
class Sdt:
|
class Sdt:
|
||||||
"""
|
"""
|
||||||
Helper class for exporting session objects to NRL"s SDT3D.
|
Helper class for exporting session objects to NRL"s SDT3D.
|
||||||
|
@ -67,7 +71,7 @@ class Sdt:
|
||||||
:param session: session this manager is tied to
|
:param session: session this manager is tied to
|
||||||
"""
|
"""
|
||||||
self.session: "Session" = session
|
self.session: "Session" = session
|
||||||
self.sock: Optional[IO] = None
|
self.sock: Optional[socket.socket] = None
|
||||||
self.connected: bool = False
|
self.connected: bool = False
|
||||||
self.url: str = self.DEFAULT_SDT_URL
|
self.url: str = self.DEFAULT_SDT_URL
|
||||||
self.address: Optional[Tuple[Optional[str], Optional[int]]] = None
|
self.address: Optional[Tuple[Optional[str], Optional[int]]] = None
|
||||||
|
@ -210,26 +214,23 @@ class Sdt:
|
||||||
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
nets = []
|
|
||||||
# create layers
|
|
||||||
for layer in CORE_LAYERS:
|
for layer in CORE_LAYERS:
|
||||||
self.cmd(f"layer {layer}")
|
self.cmd(f"layer {layer}")
|
||||||
|
|
||||||
with self.session.nodes_lock:
|
with self.session.nodes_lock:
|
||||||
for node_id in self.session.nodes:
|
nets = []
|
||||||
node = self.session.nodes[node_id]
|
for node in self.session.nodes.values():
|
||||||
if isinstance(node, CoreNetworkBase):
|
if isinstance(node, (EmaneNet, WlanNode)):
|
||||||
nets.append(node)
|
nets.append(node)
|
||||||
if not isinstance(node, NodeBase):
|
if not isinstance(node, NodeBase):
|
||||||
continue
|
continue
|
||||||
self.add_node(node)
|
self.add_node(node)
|
||||||
|
for link in self.session.link_manager.links():
|
||||||
|
if is_wireless(link.node1) or is_wireless(link.node2):
|
||||||
|
continue
|
||||||
|
link_data = link.get_data(MessageFlags.ADD)
|
||||||
|
self.handle_link_update(link_data)
|
||||||
for net in nets:
|
for net in nets:
|
||||||
all_links = net.links(flags=MessageFlags.ADD)
|
for link_data in net.links(MessageFlags.ADD):
|
||||||
for link_data in all_links:
|
|
||||||
is_wireless = isinstance(net, (WlanNode, EmaneNet))
|
|
||||||
if is_wireless and link_data.node1_id == net.id:
|
|
||||||
continue
|
|
||||||
self.handle_link_update(link_data)
|
self.handle_link_update(link_data)
|
||||||
|
|
||||||
def get_node_position(self, node: NodeBase) -> Optional[str]:
|
def get_node_position(self, node: NodeBase) -> Optional[str]:
|
||||||
|
@ -341,7 +342,7 @@ class Sdt:
|
||||||
result = False
|
result = False
|
||||||
try:
|
try:
|
||||||
node = self.session.get_node(node_id, NodeBase)
|
node = self.session.get_node(node_id, NodeBase)
|
||||||
result = isinstance(node, (WlanNode, EmaneNet))
|
result = isinstance(node, (WlanNode, EmaneNet, WirelessNode))
|
||||||
except CoreError:
|
except CoreError:
|
||||||
pass
|
pass
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -109,114 +109,6 @@ class ServiceDependencies:
|
||||||
return self.boot_paths
|
return self.boot_paths
|
||||||
|
|
||||||
|
|
||||||
class ServiceShim:
|
|
||||||
keys: List[str] = [
|
|
||||||
"dirs",
|
|
||||||
"files",
|
|
||||||
"startidx",
|
|
||||||
"cmdup",
|
|
||||||
"cmddown",
|
|
||||||
"cmdval",
|
|
||||||
"meta",
|
|
||||||
"starttime",
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tovaluelist(cls, node: CoreNode, service: "CoreService") -> str:
|
|
||||||
"""
|
|
||||||
Convert service properties into a string list of key=value pairs,
|
|
||||||
separated by "|".
|
|
||||||
|
|
||||||
:param node: node to get value list for
|
|
||||||
:param service: service to get value list for
|
|
||||||
:return: value list string
|
|
||||||
"""
|
|
||||||
start_time = 0
|
|
||||||
start_index = 0
|
|
||||||
valmap = [
|
|
||||||
service.dirs,
|
|
||||||
service.configs,
|
|
||||||
start_index,
|
|
||||||
service.startup,
|
|
||||||
service.shutdown,
|
|
||||||
service.validate,
|
|
||||||
service.meta,
|
|
||||||
start_time,
|
|
||||||
]
|
|
||||||
if not service.custom:
|
|
||||||
valmap[1] = service.get_configs(node)
|
|
||||||
valmap[3] = service.get_startup(node)
|
|
||||||
vals = ["%s=%s" % (x, y) for x, y in zip(cls.keys, valmap)]
|
|
||||||
return "|".join(vals)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fromvaluelist(cls, service: "CoreService", values: List[str]) -> None:
|
|
||||||
"""
|
|
||||||
Convert list of values into properties for this instantiated
|
|
||||||
(customized) service.
|
|
||||||
|
|
||||||
:param service: service to get value list for
|
|
||||||
:param values: value list to set properties from
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
# TODO: support empty value? e.g. override default meta with ''
|
|
||||||
for key in cls.keys:
|
|
||||||
try:
|
|
||||||
cls.setvalue(service, key, values[cls.keys.index(key)])
|
|
||||||
except IndexError:
|
|
||||||
# old config does not need to have new keys
|
|
||||||
logger.exception("error indexing into key")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setvalue(cls, service: "CoreService", key: str, value: str) -> None:
|
|
||||||
"""
|
|
||||||
Set values for this service.
|
|
||||||
|
|
||||||
:param service: service to get value list for
|
|
||||||
:param key: key to set value for
|
|
||||||
:param value: value of key to set
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if key not in cls.keys:
|
|
||||||
raise ValueError("key `%s` not in `%s`" % (key, cls.keys))
|
|
||||||
# this handles data conversion to int, string, and tuples
|
|
||||||
if value:
|
|
||||||
if key == "startidx":
|
|
||||||
value = int(value)
|
|
||||||
elif key == "starttime":
|
|
||||||
value = float(value)
|
|
||||||
elif key == "meta":
|
|
||||||
value = str(value)
|
|
||||||
else:
|
|
||||||
value = utils.make_tuple_fromstr(value, str)
|
|
||||||
|
|
||||||
if key == "dirs":
|
|
||||||
service.dirs = value
|
|
||||||
elif key == "files":
|
|
||||||
service.configs = value
|
|
||||||
elif key == "cmdup":
|
|
||||||
service.startup = value
|
|
||||||
elif key == "cmddown":
|
|
||||||
service.shutdown = value
|
|
||||||
elif key == "cmdval":
|
|
||||||
service.validate = value
|
|
||||||
elif key == "meta":
|
|
||||||
service.meta = value
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def servicesfromopaque(cls, opaque: str) -> List[str]:
|
|
||||||
"""
|
|
||||||
Build a list of services from an opaque data string.
|
|
||||||
|
|
||||||
:param opaque: opaque data string
|
|
||||||
:return: services
|
|
||||||
"""
|
|
||||||
servicesstring = opaque.split(":")
|
|
||||||
if servicesstring[0] != "service":
|
|
||||||
return []
|
|
||||||
return servicesstring[1].split(",")
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceManager:
|
class ServiceManager:
|
||||||
"""
|
"""
|
||||||
Manages services available for CORE nodes to use.
|
Manages services available for CORE nodes to use.
|
||||||
|
|
|
@ -7,15 +7,26 @@ from typing import Optional, Tuple
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.nodes.base import CoreNode
|
from core.nodes.base import CoreNode, NodeBase
|
||||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||||
from core.nodes.network import PtpNet, WlanNode
|
from core.nodes.network import PtpNet, WlanNode
|
||||||
from core.nodes.physical import Rj45Node
|
from core.nodes.physical import Rj45Node
|
||||||
|
from core.nodes.wireless import WirelessNode
|
||||||
from core.services.coreservices import CoreService
|
from core.services.coreservices import CoreService
|
||||||
|
|
||||||
FRR_STATE_DIR: str = "/var/run/frr"
|
FRR_STATE_DIR: str = "/var/run/frr"
|
||||||
|
|
||||||
|
|
||||||
|
def is_wireless(node: NodeBase) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the node is a wireless type node.
|
||||||
|
|
||||||
|
:param node: node to check type for
|
||||||
|
:return: True if wireless type, False otherwise
|
||||||
|
"""
|
||||||
|
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
|
||||||
|
|
||||||
|
|
||||||
class FRRZebra(CoreService):
|
class FRRZebra(CoreService):
|
||||||
name: str = "FRRzebra"
|
name: str = "FRRzebra"
|
||||||
group: str = "FRR"
|
group: str = "FRR"
|
||||||
|
@ -593,7 +604,7 @@ class FRRBabel(FrrService):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
|
def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
|
||||||
if iface.net and isinstance(iface.net, (EmaneNet, WlanNode)):
|
if is_wireless(iface.net):
|
||||||
return " babel wireless\n no babel split-horizon\n"
|
return " babel wireless\n no babel split-horizon\n"
|
||||||
else:
|
else:
|
||||||
return " babel wired\n babel split-horizon\n"
|
return " babel wired\n babel split-horizon\n"
|
||||||
|
|
|
@ -7,15 +7,26 @@ import netaddr
|
||||||
|
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.emulator.enumerations import LinkTypes
|
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.interface import DEFAULT_MTU, CoreInterface
|
||||||
from core.nodes.network import PtpNet, WlanNode
|
from core.nodes.network import PtpNet, WlanNode
|
||||||
from core.nodes.physical import Rj45Node
|
from core.nodes.physical import Rj45Node
|
||||||
|
from core.nodes.wireless import WirelessNode
|
||||||
from core.services.coreservices import CoreService
|
from core.services.coreservices import CoreService
|
||||||
|
|
||||||
QUAGGA_STATE_DIR: str = "/var/run/quagga"
|
QUAGGA_STATE_DIR: str = "/var/run/quagga"
|
||||||
|
|
||||||
|
|
||||||
|
def is_wireless(node: NodeBase) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the node is a wireless type node.
|
||||||
|
|
||||||
|
:param node: node to check type for
|
||||||
|
:return: True if wireless type, False otherwise
|
||||||
|
"""
|
||||||
|
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
|
||||||
|
|
||||||
|
|
||||||
class Zebra(CoreService):
|
class Zebra(CoreService):
|
||||||
name: str = "zebra"
|
name: str = "zebra"
|
||||||
group: str = "Quagga"
|
group: str = "Quagga"
|
||||||
|
@ -431,7 +442,7 @@ class Ospfv3mdr(Ospfv3):
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
|
def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
|
||||||
cfg = cls.mtu_check(iface)
|
cfg = cls.mtu_check(iface)
|
||||||
if iface.net is not None and isinstance(iface.net, (WlanNode, EmaneNet)):
|
if is_wireless(iface.net):
|
||||||
return (
|
return (
|
||||||
cfg
|
cfg
|
||||||
+ """\
|
+ """\
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Type, TypeVar
|
from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
@ -8,13 +8,15 @@ import core.nodes.base
|
||||||
import core.nodes.physical
|
import core.nodes.physical
|
||||||
from core import utils
|
from core import utils
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
|
from core.emulator.data import InterfaceData, LinkOptions, NodeOptions
|
||||||
from core.emulator.enumerations import EventTypes, NodeTypes
|
from core.emulator.enumerations import EventTypes, NodeTypes
|
||||||
from core.errors import CoreXmlError
|
from core.errors import CoreXmlError
|
||||||
from core.nodes.base import CoreNodeBase, NodeBase
|
from core.nodes.base import CoreNodeBase, NodeBase
|
||||||
from core.nodes.docker import DockerNode
|
from core.nodes.docker import DockerNode
|
||||||
|
from core.nodes.interface import CoreInterface
|
||||||
from core.nodes.lxd import LxcNode
|
from core.nodes.lxd import LxcNode
|
||||||
from core.nodes.network import CtrlNet, GreTapBridge, WlanNode
|
from core.nodes.network import CtrlNet, GreTapBridge, WlanNode
|
||||||
|
from core.nodes.wireless import WirelessNode
|
||||||
from core.services.coreservices import CoreService
|
from core.services.coreservices import CoreService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -269,8 +271,8 @@ class CoreXmlWriter:
|
||||||
|
|
||||||
def write_session(self) -> None:
|
def write_session(self) -> None:
|
||||||
# generate xml content
|
# generate xml content
|
||||||
links = self.write_nodes()
|
self.write_nodes()
|
||||||
self.write_links(links)
|
self.write_links()
|
||||||
self.write_mobility_configs()
|
self.write_mobility_configs()
|
||||||
self.write_emane_configs()
|
self.write_emane_configs()
|
||||||
self.write_service_configs()
|
self.write_service_configs()
|
||||||
|
@ -449,8 +451,7 @@ class CoreXmlWriter:
|
||||||
if node_types.getchildren():
|
if node_types.getchildren():
|
||||||
self.scenario.append(node_types)
|
self.scenario.append(node_types)
|
||||||
|
|
||||||
def write_nodes(self) -> List[LinkData]:
|
def write_nodes(self) -> None:
|
||||||
links = []
|
|
||||||
for node_id in self.session.nodes:
|
for node_id in self.session.nodes:
|
||||||
node = self.session.nodes[node_id]
|
node = self.session.nodes[node_id]
|
||||||
# network node
|
# network node
|
||||||
|
@ -464,10 +465,6 @@ class CoreXmlWriter:
|
||||||
elif isinstance(node, core.nodes.base.CoreNodeBase):
|
elif isinstance(node, core.nodes.base.CoreNodeBase):
|
||||||
self.write_device(node)
|
self.write_device(node)
|
||||||
|
|
||||||
# add known links
|
|
||||||
links.extend(node.links())
|
|
||||||
return links
|
|
||||||
|
|
||||||
def write_network(self, node: NodeBase) -> None:
|
def write_network(self, node: NodeBase) -> None:
|
||||||
# ignore p2p and other nodes that are not part of the api
|
# ignore p2p and other nodes that are not part of the api
|
||||||
if not node.apitype:
|
if not node.apitype:
|
||||||
|
@ -476,15 +473,21 @@ class CoreXmlWriter:
|
||||||
network = NetworkElement(self.session, node)
|
network = NetworkElement(self.session, node)
|
||||||
self.networks.append(network.element)
|
self.networks.append(network.element)
|
||||||
|
|
||||||
def write_links(self, links: List[LinkData]) -> None:
|
def write_links(self) -> None:
|
||||||
link_elements = etree.Element("links")
|
link_elements = etree.Element("links")
|
||||||
# add link data
|
for core_link in self.session.link_manager.links():
|
||||||
for link_data in links:
|
node1, iface1 = core_link.node1, core_link.iface1
|
||||||
# skip basic range links
|
node2, iface2 = core_link.node2, core_link.iface2
|
||||||
if link_data.iface1 is None and link_data.iface2 is None:
|
unidirectional = core_link.is_unidirectional()
|
||||||
continue
|
link_element = self.create_link_element(
|
||||||
link_element = self.create_link_element(link_data)
|
node1, iface1, node2, iface2, core_link.options(), unidirectional
|
||||||
|
)
|
||||||
link_elements.append(link_element)
|
link_elements.append(link_element)
|
||||||
|
if unidirectional:
|
||||||
|
link_element = self.create_link_element(
|
||||||
|
node2, iface2, node1, iface1, iface2.options, unidirectional
|
||||||
|
)
|
||||||
|
link_elements.append(link_element)
|
||||||
if link_elements.getchildren():
|
if link_elements.getchildren():
|
||||||
self.scenario.append(link_elements)
|
self.scenario.append(link_elements)
|
||||||
|
|
||||||
|
@ -493,67 +496,71 @@ class CoreXmlWriter:
|
||||||
self.devices.append(device.element)
|
self.devices.append(device.element)
|
||||||
|
|
||||||
def create_iface_element(
|
def create_iface_element(
|
||||||
self, element_name: str, node_id: int, iface_data: InterfaceData
|
self, element_name: str, iface: CoreInterface
|
||||||
) -> etree.Element:
|
) -> etree.Element:
|
||||||
iface_element = etree.Element(element_name)
|
iface_element = etree.Element(element_name)
|
||||||
node = self.session.get_node(node_id, NodeBase)
|
# check if interface if connected to emane
|
||||||
if isinstance(node, CoreNodeBase):
|
if isinstance(iface.node, CoreNodeBase) and isinstance(iface.net, EmaneNet):
|
||||||
iface = node.get_iface(iface_data.id)
|
nem_id = self.session.emane.get_nem_id(iface)
|
||||||
# check if emane interface
|
add_attribute(iface_element, "nem", nem_id)
|
||||||
if isinstance(iface.net, EmaneNet):
|
ip4 = iface.get_ip4()
|
||||||
nem_id = self.session.emane.get_nem_id(iface)
|
ip4_mask = None
|
||||||
add_attribute(iface_element, "nem", nem_id)
|
if ip4:
|
||||||
add_attribute(iface_element, "id", iface_data.id)
|
ip4_mask = ip4.prefixlen
|
||||||
add_attribute(iface_element, "name", iface_data.name)
|
ip4 = str(ip4.ip)
|
||||||
add_attribute(iface_element, "mac", iface_data.mac)
|
ip6 = iface.get_ip6()
|
||||||
add_attribute(iface_element, "ip4", iface_data.ip4)
|
ip6_mask = None
|
||||||
add_attribute(iface_element, "ip4_mask", iface_data.ip4_mask)
|
if ip6:
|
||||||
add_attribute(iface_element, "ip6", iface_data.ip6)
|
ip6_mask = ip6.prefixlen
|
||||||
add_attribute(iface_element, "ip6_mask", iface_data.ip6_mask)
|
ip6 = str(ip6.ip)
|
||||||
|
add_attribute(iface_element, "id", iface.id)
|
||||||
|
add_attribute(iface_element, "name", iface.name)
|
||||||
|
add_attribute(iface_element, "mac", iface.mac)
|
||||||
|
add_attribute(iface_element, "ip4", ip4)
|
||||||
|
add_attribute(iface_element, "ip4_mask", ip4_mask)
|
||||||
|
add_attribute(iface_element, "ip6", ip6)
|
||||||
|
add_attribute(iface_element, "ip6_mask", ip6_mask)
|
||||||
return iface_element
|
return iface_element
|
||||||
|
|
||||||
def create_link_element(self, link_data: LinkData) -> etree.Element:
|
def create_link_element(
|
||||||
|
self,
|
||||||
|
node1: NodeBase,
|
||||||
|
iface1: Optional[CoreInterface],
|
||||||
|
node2: NodeBase,
|
||||||
|
iface2: Optional[CoreInterface],
|
||||||
|
options: LinkOptions,
|
||||||
|
unidirectional: bool,
|
||||||
|
) -> etree.Element:
|
||||||
link_element = etree.Element("link")
|
link_element = etree.Element("link")
|
||||||
add_attribute(link_element, "node1", link_data.node1_id)
|
add_attribute(link_element, "node1", node1.id)
|
||||||
add_attribute(link_element, "node2", link_data.node2_id)
|
add_attribute(link_element, "node2", node2.id)
|
||||||
|
|
||||||
# check for interface one
|
# check for interface one
|
||||||
if link_data.iface1 is not None:
|
if iface1 is not None:
|
||||||
iface1 = self.create_iface_element(
|
iface1 = self.create_iface_element("iface1", iface1)
|
||||||
"iface1", link_data.node1_id, link_data.iface1
|
|
||||||
)
|
|
||||||
link_element.append(iface1)
|
link_element.append(iface1)
|
||||||
|
|
||||||
# check for interface two
|
# check for interface two
|
||||||
if link_data.iface2 is not None:
|
if iface2 is not None:
|
||||||
iface2 = self.create_iface_element(
|
iface2 = self.create_iface_element("iface2", iface2)
|
||||||
"iface2", link_data.node2_id, link_data.iface2
|
|
||||||
)
|
|
||||||
link_element.append(iface2)
|
link_element.append(iface2)
|
||||||
|
|
||||||
# check for options, don't write for emane/wlan links
|
# check for options, don't write for emane/wlan links
|
||||||
node1 = self.session.get_node(link_data.node1_id, NodeBase)
|
is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet, WirelessNode))
|
||||||
node2 = self.session.get_node(link_data.node2_id, NodeBase)
|
is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet, WirelessNode))
|
||||||
is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet))
|
if not (is_node1_wireless or is_node2_wireless):
|
||||||
is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet))
|
unidirectional = 1 if unidirectional else 0
|
||||||
if not any([is_node1_wireless, is_node2_wireless]):
|
options_element = etree.Element("options")
|
||||||
options_data = link_data.options
|
add_attribute(options_element, "delay", options.delay)
|
||||||
options = etree.Element("options")
|
add_attribute(options_element, "bandwidth", options.bandwidth)
|
||||||
add_attribute(options, "delay", options_data.delay)
|
add_attribute(options_element, "loss", options.loss)
|
||||||
add_attribute(options, "bandwidth", options_data.bandwidth)
|
add_attribute(options_element, "dup", options.dup)
|
||||||
add_attribute(options, "loss", options_data.loss)
|
add_attribute(options_element, "jitter", options.jitter)
|
||||||
add_attribute(options, "dup", options_data.dup)
|
add_attribute(options_element, "mer", options.mer)
|
||||||
add_attribute(options, "jitter", options_data.jitter)
|
add_attribute(options_element, "burst", options.burst)
|
||||||
add_attribute(options, "mer", options_data.mer)
|
add_attribute(options_element, "mburst", options.mburst)
|
||||||
add_attribute(options, "burst", options_data.burst)
|
add_attribute(options_element, "unidirectional", unidirectional)
|
||||||
add_attribute(options, "mburst", options_data.mburst)
|
add_attribute(options_element, "key", options.key)
|
||||||
add_attribute(options, "unidirectional", options_data.unidirectional)
|
add_attribute(options_element, "buffer", options.buffer)
|
||||||
add_attribute(options, "network_id", link_data.network_id)
|
if options_element.items():
|
||||||
add_attribute(options, "key", options_data.key)
|
link_element.append(options_element)
|
||||||
add_attribute(options, "buffer", options_data.buffer)
|
|
||||||
if options.items():
|
|
||||||
link_element.append(options)
|
|
||||||
|
|
||||||
return link_element
|
return link_element
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
[core-daemon]
|
[core-daemon]
|
||||||
#distributed_address = 127.0.0.1
|
#distributed_address = 127.0.0.1
|
||||||
listenaddr = localhost
|
|
||||||
port = 4038
|
|
||||||
grpcaddress = localhost
|
grpcaddress = localhost
|
||||||
grpcport = 50051
|
grpcport = 50051
|
||||||
quagga_bin_search = "/usr/local/bin /usr/bin /usr/lib/quagga"
|
quagga_bin_search = "/usr/local/bin /usr/bin /usr/lib/quagga"
|
||||||
|
@ -12,8 +10,8 @@ frr_sbin_search = "/usr/local/sbin /usr/sbin /usr/lib/frr"
|
||||||
# uncomment the following line to load custom services from the specified dir
|
# uncomment the following line to load custom services from the specified dir
|
||||||
# this may be a comma-separated list, and directory names should be unique
|
# this may be a comma-separated list, and directory names should be unique
|
||||||
# and not named 'services'
|
# and not named 'services'
|
||||||
#custom_services_dir = /home/username/.core/myservices
|
#custom_services_dir = /home/<user>/.coregui/custom_services
|
||||||
#custom_config_services_dir = /home/username/.coregui/custom_services
|
#custom_config_services_dir = /home/<user>/.coregui/custom_services
|
||||||
|
|
||||||
# uncomment to establish a standalone control backchannel for accessing nodes
|
# uncomment to establish a standalone control backchannel for accessing nodes
|
||||||
# (overriden by the session option of the same name)
|
# (overriden by the session option of the same name)
|
||||||
|
@ -48,7 +46,7 @@ emane_platform_port = 8101
|
||||||
emane_transform_port = 8201
|
emane_transform_port = 8201
|
||||||
emane_event_generate = True
|
emane_event_generate = True
|
||||||
emane_event_monitor = False
|
emane_event_monitor = False
|
||||||
#emane_models_dir = /home/username/.core/myemane
|
#emane_models_dir = /home/<user>/.coregui/custom_emane
|
||||||
# EMANE log level range [0,4] default: 2
|
# EMANE log level range [0,4] default: 2
|
||||||
#emane_log_level = 2
|
#emane_log_level = 2
|
||||||
emane_realtime = True
|
emane_realtime = True
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
# CORE
|
# CORE
|
||||||
# (c)2012 the Boeing Company.
|
|
||||||
# See the LICENSE file included in this distribution.
|
|
||||||
#
|
|
||||||
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
|
|
||||||
#
|
#
|
||||||
# Builds html and pdf documentation using Sphinx.
|
# Builds html and pdf documentation using Sphinx.
|
||||||
#
|
#
|
||||||
|
|
29
daemon/examples/grpc/wireless.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from core.api.grpc import client
|
||||||
|
from core.api.grpc.wrappers import NodeType, Position
|
||||||
|
|
||||||
|
# interface helper
|
||||||
|
iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64")
|
||||||
|
|
||||||
|
# create grpc client and connect
|
||||||
|
core = client.CoreGrpcClient()
|
||||||
|
core.connect()
|
||||||
|
|
||||||
|
# add session
|
||||||
|
session = core.create_session()
|
||||||
|
|
||||||
|
# create nodes
|
||||||
|
position = Position(x=200, y=200)
|
||||||
|
wlan = session.add_node(1, _type=NodeType.WIRELESS, position=position)
|
||||||
|
position = Position(x=100, y=100)
|
||||||
|
node1 = session.add_node(2, model="mdr", position=position)
|
||||||
|
position = Position(x=300, y=100)
|
||||||
|
node2 = session.add_node(3, model="mdr", position=position)
|
||||||
|
|
||||||
|
# create links
|
||||||
|
iface1 = iface_helper.create_iface(node1.id, 0)
|
||||||
|
session.add_link(node1=node1, node2=wlan, iface1=iface1)
|
||||||
|
iface1 = iface_helper.create_iface(node2.id, 0)
|
||||||
|
session.add_link(node1=node2, node2=wlan, iface1=iface1)
|
||||||
|
|
||||||
|
# start session
|
||||||
|
core.start_session(session)
|
46
daemon/examples/python/wireless.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# required imports
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.emulator.coreemu import CoreEmu
|
||||||
|
from core.emulator.data import IpPrefixes, NodeOptions
|
||||||
|
from core.emulator.enumerations import EventTypes
|
||||||
|
from core.nodes.base import CoreNode
|
||||||
|
from core.nodes.network import WlanNode
|
||||||
|
|
||||||
|
# enable info logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
# ip nerator for example
|
||||||
|
ip_prefixes = IpPrefixes(ip4_prefix="10.0.0.0/24")
|
||||||
|
|
||||||
|
# create emulator instance for creating sessions and utility methods
|
||||||
|
coreemu = CoreEmu()
|
||||||
|
session = coreemu.create_session()
|
||||||
|
|
||||||
|
# must be in configuration state for nodes to start
|
||||||
|
session.set_state(EventTypes.CONFIGURATION_STATE)
|
||||||
|
|
||||||
|
# create wireless
|
||||||
|
options = NodeOptions(x=200, y=200)
|
||||||
|
wireless = session.add_node(WlanNode, options=options)
|
||||||
|
|
||||||
|
# create nodes
|
||||||
|
options = NodeOptions(model="mdr", x=100, y=100)
|
||||||
|
n1 = session.add_node(CoreNode, options=options)
|
||||||
|
options = NodeOptions(model="mdr", x=300, y=100)
|
||||||
|
n2 = session.add_node(CoreNode, options=options)
|
||||||
|
|
||||||
|
# link nodes to wireless
|
||||||
|
iface1 = ip_prefixes.create_iface(n1)
|
||||||
|
session.add_link(n1.id, wireless.id, iface1)
|
||||||
|
iface1 = ip_prefixes.create_iface(n2)
|
||||||
|
session.add_link(n2.id, wireless.id, iface1)
|
||||||
|
|
||||||
|
# start session
|
||||||
|
session.instantiate()
|
||||||
|
|
||||||
|
# do whatever you like here
|
||||||
|
input("press enter to shutdown")
|
||||||
|
|
||||||
|
# stop session
|
||||||
|
session.shutdown()
|
|
@ -61,6 +61,8 @@ service CoreApi {
|
||||||
}
|
}
|
||||||
rpc DeleteLink (DeleteLinkRequest) returns (DeleteLinkResponse) {
|
rpc DeleteLink (DeleteLinkRequest) returns (DeleteLinkResponse) {
|
||||||
}
|
}
|
||||||
|
rpc Linked (LinkedRequest) returns (LinkedResponse) {
|
||||||
|
}
|
||||||
|
|
||||||
// mobility rpc
|
// mobility rpc
|
||||||
rpc GetMobilityConfig (mobility.GetMobilityConfigRequest) returns (mobility.GetMobilityConfigResponse) {
|
rpc GetMobilityConfig (mobility.GetMobilityConfigRequest) returns (mobility.GetMobilityConfigResponse) {
|
||||||
|
@ -98,6 +100,12 @@ service CoreApi {
|
||||||
rpc WlanLink (wlan.WlanLinkRequest) returns (wlan.WlanLinkResponse) {
|
rpc WlanLink (wlan.WlanLinkRequest) returns (wlan.WlanLinkResponse) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wireless rpc
|
||||||
|
rpc WirelessLinked (WirelessLinkedRequest) returns (WirelessLinkedResponse) {
|
||||||
|
}
|
||||||
|
rpc WirelessConfig (WirelessConfigRequest) returns (WirelessConfigResponse) {
|
||||||
|
}
|
||||||
|
|
||||||
// emane rpc
|
// emane rpc
|
||||||
rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) {
|
rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) {
|
||||||
}
|
}
|
||||||
|
@ -280,12 +288,11 @@ message ConfigEvent {
|
||||||
repeated int32 data_types = 5;
|
repeated int32 data_types = 5;
|
||||||
string data_values = 6;
|
string data_values = 6;
|
||||||
string captions = 7;
|
string captions = 7;
|
||||||
string bitmap = 8;
|
string possible_values = 8;
|
||||||
string possible_values = 9;
|
string groups = 9;
|
||||||
string groups = 10;
|
int32 iface_id = 10;
|
||||||
int32 iface_id = 11;
|
int32 network_id = 11;
|
||||||
int32 network_id = 12;
|
string opaque = 12;
|
||||||
string opaque = 13;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ExceptionEvent {
|
message ExceptionEvent {
|
||||||
|
@ -684,3 +691,38 @@ message Server {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
string host = 2;
|
string host = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message LinkedRequest {
|
||||||
|
int32 session_id = 1;
|
||||||
|
int32 node1_id = 2;
|
||||||
|
int32 node2_id = 3;
|
||||||
|
int32 iface1_id = 4;
|
||||||
|
int32 iface2_id = 5;
|
||||||
|
bool linked = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LinkedResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
message WirelessLinkedRequest {
|
||||||
|
int32 session_id = 1;
|
||||||
|
int32 wireless_id = 2;
|
||||||
|
int32 node1_id = 3;
|
||||||
|
int32 node2_id = 4;
|
||||||
|
bool linked = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WirelessLinkedResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
message WirelessConfigRequest {
|
||||||
|
int32 session_id = 1;
|
||||||
|
int32 wireless_id = 2;
|
||||||
|
int32 node1_id = 3;
|
||||||
|
int32 node2_id = 4;
|
||||||
|
LinkOptions options1 = 5;
|
||||||
|
LinkOptions options2 = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WirelessConfigResponse {
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "core"
|
name = "core"
|
||||||
version = "8.1.0"
|
version = "8.2.0"
|
||||||
description = "CORE Common Open Research Emulator"
|
description = "CORE Common Open Research Emulator"
|
||||||
authors = ["Boeing Research and Technology"]
|
authors = ["Boeing Research and Technology"]
|
||||||
license = "BSD-2-Clause"
|
license = "BSD-2-Clause"
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
from argparse import (
|
from argparse import (
|
||||||
ArgumentDefaultsHelpFormatter,
|
ArgumentDefaultsHelpFormatter,
|
||||||
ArgumentParser,
|
ArgumentParser,
|
||||||
ArgumentTypeError,
|
ArgumentTypeError,
|
||||||
Namespace,
|
Namespace,
|
||||||
_SubParsersAction,
|
|
||||||
)
|
)
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
import netaddr
|
import netaddr
|
||||||
|
from google.protobuf.json_format import MessageToDict
|
||||||
from netaddr import EUI, AddrFormatError, IPNetwork
|
from netaddr import EUI, AddrFormatError, IPNetwork
|
||||||
|
|
||||||
from core.api.grpc.client import CoreGrpcClient
|
from core.api.grpc.client import CoreGrpcClient
|
||||||
from core.api.grpc.wrappers import (
|
from core.api.grpc.wrappers import (
|
||||||
|
ConfigOption,
|
||||||
Geo,
|
Geo,
|
||||||
Interface,
|
Interface,
|
||||||
Link,
|
Link,
|
||||||
|
@ -29,6 +31,15 @@ from core.api.grpc.wrappers import (
|
||||||
NODE_TYPES = [x for x in NodeType if x != NodeType.PEER_TO_PEER]
|
NODE_TYPES = [x 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)
|
||||||
|
|
||||||
|
|
||||||
|
def print_json(data: Any) -> None:
|
||||||
|
data = json.dumps(data, indent=2)
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
|
||||||
def coreclient(func):
|
def coreclient(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
|
@ -94,11 +105,11 @@ def geo_type(value: str) -> Tuple[float, float, float]:
|
||||||
return lon, lat, alt
|
return lon, lat, alt
|
||||||
|
|
||||||
|
|
||||||
def file_type(value: str) -> str:
|
def file_type(value: str) -> Path:
|
||||||
path = Path(value)
|
path = Path(value)
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
raise ArgumentTypeError(f"invalid file: {value}")
|
raise ArgumentTypeError(f"invalid file: {value}")
|
||||||
return str(path.absolute())
|
return path
|
||||||
|
|
||||||
|
|
||||||
def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int:
|
def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int:
|
||||||
|
@ -140,12 +151,15 @@ def print_iface(iface: Interface) -> None:
|
||||||
def get_wlan_config(core: CoreGrpcClient, args: Namespace) -> None:
|
def get_wlan_config(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
session_id = get_current_session(core, args.session)
|
session_id = get_current_session(core, args.session)
|
||||||
config = core.get_wlan_config(session_id, args.node)
|
config = core.get_wlan_config(session_id, args.node)
|
||||||
size = 0
|
if args.json:
|
||||||
for option in config.values():
|
print_json(ConfigOption.to_dict(config))
|
||||||
size = max(size, len(option.name))
|
else:
|
||||||
print(f"{'Name':<{size}.{size}} | Value")
|
size = 0
|
||||||
for option in config.values():
|
for option in config.values():
|
||||||
print(f"{option.name:<{size}.{size}} | {option.value}")
|
size = max(size, len(option.name))
|
||||||
|
print(f"{'Name':<{size}.{size}} | Value")
|
||||||
|
for option in config.values():
|
||||||
|
print(f"{option.name:<{size}.{size}} | {option.value}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
|
@ -163,80 +177,102 @@ def set_wlan_config(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
if args.range:
|
if args.range:
|
||||||
config["range"] = str(args.range)
|
config["range"] = str(args.range)
|
||||||
result = core.set_wlan_config(session_id, args.node, config)
|
result = core.set_wlan_config(session_id, args.node, config)
|
||||||
print(f"set wlan config: {result}")
|
if args.json:
|
||||||
|
print_json(dict(result=result))
|
||||||
|
else:
|
||||||
|
print(f"set wlan config: {result}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
def open_xml(core: CoreGrpcClient, args: Namespace) -> None:
|
def open_xml(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
result, session_id = core.open_xml(args.file, args.start)
|
result, session_id = core.open_xml(args.file, args.start)
|
||||||
print(f"opened xml: {result},{session_id}")
|
if args.json:
|
||||||
|
print_json(dict(result=result, session_id=session_id))
|
||||||
|
else:
|
||||||
|
print(f"opened xml: {result},{session_id}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
def query_sessions(core: CoreGrpcClient, args: Namespace) -> None:
|
def query_sessions(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
sessions = core.get_sessions()
|
sessions = core.get_sessions()
|
||||||
print("Session ID | Session State | Nodes")
|
if args.json:
|
||||||
for session in sessions:
|
sessions = [protobuf_to_json(x.to_proto()) for x in sessions]
|
||||||
print(f"{session.id:<10} | {session.state.name:<13} | {session.nodes}")
|
print_json(sessions)
|
||||||
|
else:
|
||||||
|
print("Session ID | Session State | Nodes")
|
||||||
|
for session in sessions:
|
||||||
|
print(f"{session.id:<10} | {session.state.name:<13} | {session.nodes}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
def query_session(core: CoreGrpcClient, args: Namespace) -> None:
|
def query_session(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
session = core.get_session(args.id)
|
session = core.get_session(args.id)
|
||||||
print("Nodes")
|
if args.json:
|
||||||
print("Node ID | Node Name | Node Type")
|
session = protobuf_to_json(session.to_proto())
|
||||||
for node in session.nodes.values():
|
print_json(session)
|
||||||
print(f"{node.id:<7} | {node.name:<9} | {node.type.name}")
|
else:
|
||||||
print("\nLinks")
|
print("Nodes")
|
||||||
for link in session.links:
|
print("ID | Name | Type | XY | Geo")
|
||||||
n1 = session.nodes[link.node1_id].name
|
for node in session.nodes.values():
|
||||||
n2 = session.nodes[link.node2_id].name
|
xy_pos = f"{int(node.position.x)},{int(node.position.y)}"
|
||||||
print(f"Node | ", end="")
|
geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}"
|
||||||
print_iface_header()
|
print(f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}")
|
||||||
print(f"{n1:<6} | ", end="")
|
print("\nLinks")
|
||||||
if link.iface1:
|
for link in session.links:
|
||||||
print_iface(link.iface1)
|
n1 = session.nodes[link.node1_id].name
|
||||||
else:
|
n2 = session.nodes[link.node2_id].name
|
||||||
|
print(f"Node | ", end="")
|
||||||
|
print_iface_header()
|
||||||
|
print(f"{n1:<6} | ", end="")
|
||||||
|
if link.iface1:
|
||||||
|
print_iface(link.iface1)
|
||||||
|
else:
|
||||||
|
print()
|
||||||
|
print(f"{n2:<6} | ", end="")
|
||||||
|
if link.iface2:
|
||||||
|
print_iface(link.iface2)
|
||||||
|
else:
|
||||||
|
print()
|
||||||
print()
|
print()
|
||||||
print(f"{n2:<6} | ", end="")
|
|
||||||
if link.iface2:
|
|
||||||
print_iface(link.iface2)
|
|
||||||
else:
|
|
||||||
print()
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
def query_node(core: CoreGrpcClient, args: Namespace) -> None:
|
def query_node(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
session = core.get_session(args.id)
|
session = core.get_session(args.id)
|
||||||
node, ifaces, _ = core.get_node(args.id, args.node)
|
node, ifaces, _ = core.get_node(args.id, args.node)
|
||||||
print("ID | Name | Type | XY")
|
if args.json:
|
||||||
xy_pos = f"{int(node.position.x)},{int(node.position.y)}"
|
node = protobuf_to_json(node.to_proto())
|
||||||
print(f"{node.id:<4} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos}")
|
ifaces = [protobuf_to_json(x.to_proto()) for x in ifaces]
|
||||||
if node.geo:
|
print_json(dict(node=node, ifaces=ifaces))
|
||||||
print("Geo")
|
else:
|
||||||
print(f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}")
|
print("ID | Name | Type | XY | Geo")
|
||||||
if ifaces:
|
xy_pos = f"{int(node.position.x)},{int(node.position.y)}"
|
||||||
print("Interfaces")
|
geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}"
|
||||||
print("Connected To | ", end="")
|
print(f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}")
|
||||||
print_iface_header()
|
if ifaces:
|
||||||
for iface in ifaces:
|
print("Interfaces")
|
||||||
if iface.net_id == node.id:
|
print("Connected To | ", end="")
|
||||||
if iface.node_id:
|
print_iface_header()
|
||||||
name = session.nodes[iface.node_id].name
|
for iface in ifaces:
|
||||||
|
if iface.net_id == node.id:
|
||||||
|
if iface.node_id:
|
||||||
|
name = session.nodes[iface.node_id].name
|
||||||
|
else:
|
||||||
|
name = session.nodes[iface.net2_id].name
|
||||||
else:
|
else:
|
||||||
name = session.nodes[iface.net2_id].name
|
net_node = session.nodes.get(iface.net_id)
|
||||||
else:
|
name = net_node.name if net_node else ""
|
||||||
net_node = session.nodes.get(iface.net_id)
|
print(f"{name:<12} | ", end="")
|
||||||
name = net_node.name if net_node else ""
|
print_iface(iface)
|
||||||
print(f"{name:<12} | ", end="")
|
|
||||||
print_iface(iface)
|
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
def delete_session(core: CoreGrpcClient, args: Namespace) -> None:
|
def delete_session(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
result = core.delete_session(args.id)
|
result = core.delete_session(args.id)
|
||||||
print(f"delete session({args.id}): {result}")
|
if args.json:
|
||||||
|
print_json(dict(result=result))
|
||||||
|
else:
|
||||||
|
print(f"delete session({args.id}): {result}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
|
@ -263,14 +299,20 @@ def add_node(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
geo=geo,
|
geo=geo,
|
||||||
)
|
)
|
||||||
node_id = core.add_node(session_id, node)
|
node_id = core.add_node(session_id, node)
|
||||||
print(f"created node: {node_id}")
|
if args.json:
|
||||||
|
print_json(dict(node_id=node_id))
|
||||||
|
else:
|
||||||
|
print(f"created node: {node_id}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
def edit_node(core: CoreGrpcClient, args: Namespace) -> None:
|
def edit_node(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
session_id = get_current_session(core, args.session)
|
session_id = get_current_session(core, args.session)
|
||||||
result = core.edit_node(session_id, args.id, args.icon)
|
result = core.edit_node(session_id, args.id, args.icon)
|
||||||
print(f"edit node: {result}")
|
if args.json:
|
||||||
|
print_json(dict(result=result))
|
||||||
|
else:
|
||||||
|
print(f"edit node: {result}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
|
@ -285,14 +327,20 @@ def move_node(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
lon, lat, alt = args.geo
|
lon, lat, alt = args.geo
|
||||||
geo = Geo(lon=lon, lat=lat, alt=alt)
|
geo = Geo(lon=lon, lat=lat, alt=alt)
|
||||||
result = core.move_node(session_id, args.id, pos, geo)
|
result = core.move_node(session_id, args.id, pos, geo)
|
||||||
print(f"move node: {result}")
|
if args.json:
|
||||||
|
print_json(dict(result=result))
|
||||||
|
else:
|
||||||
|
print(f"move node: {result}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
def delete_node(core: CoreGrpcClient, args: Namespace) -> None:
|
def delete_node(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
session_id = get_current_session(core, args.session)
|
session_id = get_current_session(core, args.session)
|
||||||
result = core.delete_node(session_id, args.id)
|
result = core.delete_node(session_id, args.id)
|
||||||
print(f"deleted node: {result}")
|
if args.json:
|
||||||
|
print_json(dict(result=result))
|
||||||
|
else:
|
||||||
|
print(f"deleted node: {result}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
|
@ -313,8 +361,13 @@ def add_link(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
unidirectional=args.uni,
|
unidirectional=args.uni,
|
||||||
)
|
)
|
||||||
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options)
|
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options)
|
||||||
result, _, _ = core.add_link(session_id, link)
|
result, iface1, iface2 = core.add_link(session_id, link)
|
||||||
print(f"add link: {result}")
|
if args.json:
|
||||||
|
iface1 = protobuf_to_json(iface1.to_proto())
|
||||||
|
iface2 = protobuf_to_json(iface2.to_proto())
|
||||||
|
print_json(dict(result=result, iface1=iface1, iface2=iface2))
|
||||||
|
else:
|
||||||
|
print(f"add link: {result}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
|
@ -332,7 +385,10 @@ def edit_link(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
iface2 = Interface(args.iface2)
|
iface2 = Interface(args.iface2)
|
||||||
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options)
|
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options)
|
||||||
result = core.edit_link(session_id, link)
|
result = core.edit_link(session_id, link)
|
||||||
print(f"edit link: {result}")
|
if args.json:
|
||||||
|
print_json(dict(result=result))
|
||||||
|
else:
|
||||||
|
print(f"edit link: {result}")
|
||||||
|
|
||||||
|
|
||||||
@coreclient
|
@coreclient
|
||||||
|
@ -342,10 +398,13 @@ def delete_link(core: CoreGrpcClient, args: Namespace) -> None:
|
||||||
iface2 = Interface(args.iface2)
|
iface2 = Interface(args.iface2)
|
||||||
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2)
|
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2)
|
||||||
result = core.delete_link(session_id, link)
|
result = core.delete_link(session_id, link)
|
||||||
print(f"delete link: {result}")
|
if args.json:
|
||||||
|
print_json(dict(result=result))
|
||||||
|
else:
|
||||||
|
print(f"delete link: {result}")
|
||||||
|
|
||||||
|
|
||||||
def setup_sessions_parser(parent: _SubParsersAction) -> None:
|
def setup_sessions_parser(parent) -> None:
|
||||||
parser = parent.add_parser("session", help="session interactions")
|
parser = parent.add_parser("session", help="session interactions")
|
||||||
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
||||||
parser.add_argument("-i", "--id", type=int, help="session id to use", required=True)
|
parser.add_argument("-i", "--id", type=int, help="session id to use", required=True)
|
||||||
|
@ -358,7 +417,7 @@ def setup_sessions_parser(parent: _SubParsersAction) -> None:
|
||||||
delete_parser.set_defaults(func=delete_session)
|
delete_parser.set_defaults(func=delete_session)
|
||||||
|
|
||||||
|
|
||||||
def setup_node_parser(parent: _SubParsersAction) -> None:
|
def setup_node_parser(parent) -> None:
|
||||||
parser = parent.add_parser("node", help="node interactions")
|
parser = parent.add_parser("node", help="node interactions")
|
||||||
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
||||||
parser.add_argument("-s", "--session", type=int, help="session to interact with")
|
parser.add_argument("-s", "--session", type=int, help="session to interact with")
|
||||||
|
@ -402,7 +461,7 @@ def setup_node_parser(parent: _SubParsersAction) -> None:
|
||||||
delete_parser.set_defaults(func=delete_node)
|
delete_parser.set_defaults(func=delete_node)
|
||||||
|
|
||||||
|
|
||||||
def setup_link_parser(parent: _SubParsersAction) -> None:
|
def setup_link_parser(parent) -> None:
|
||||||
parser = parent.add_parser("link", help="link interactions")
|
parser = parent.add_parser("link", help="link interactions")
|
||||||
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
||||||
parser.add_argument("-s", "--session", type=int, help="session to interact with")
|
parser.add_argument("-s", "--session", type=int, help="session to interact with")
|
||||||
|
@ -455,7 +514,7 @@ def setup_link_parser(parent: _SubParsersAction) -> None:
|
||||||
delete_parser.set_defaults(func=delete_link)
|
delete_parser.set_defaults(func=delete_link)
|
||||||
|
|
||||||
|
|
||||||
def setup_query_parser(parent: _SubParsersAction) -> None:
|
def setup_query_parser(parent) -> None:
|
||||||
parser = parent.add_parser("query", help="query interactions")
|
parser = parent.add_parser("query", help="query interactions")
|
||||||
subparsers = parser.add_subparsers(help="query commands")
|
subparsers = parser.add_subparsers(help="query commands")
|
||||||
subparsers.required = True
|
subparsers.required = True
|
||||||
|
@ -477,7 +536,7 @@ def setup_query_parser(parent: _SubParsersAction) -> None:
|
||||||
node_parser.set_defaults(func=query_node)
|
node_parser.set_defaults(func=query_node)
|
||||||
|
|
||||||
|
|
||||||
def setup_xml_parser(parent: _SubParsersAction) -> None:
|
def setup_xml_parser(parent) -> None:
|
||||||
parser = parent.add_parser("xml", help="open session xml")
|
parser = parent.add_parser("xml", help="open session xml")
|
||||||
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
||||||
parser.add_argument("-f", "--file", type=file_type, help="xml file to open", required=True)
|
parser.add_argument("-f", "--file", type=file_type, help="xml file to open", required=True)
|
||||||
|
@ -485,7 +544,7 @@ def setup_xml_parser(parent: _SubParsersAction) -> None:
|
||||||
parser.set_defaults(func=open_xml)
|
parser.set_defaults(func=open_xml)
|
||||||
|
|
||||||
|
|
||||||
def setup_wlan_parser(parent: _SubParsersAction) -> None:
|
def setup_wlan_parser(parent) -> None:
|
||||||
parser = parent.add_parser("wlan", help="wlan specific interactions")
|
parser = parent.add_parser("wlan", help="wlan specific interactions")
|
||||||
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
||||||
parser.add_argument("-s", "--session", type=int, help="session to interact with")
|
parser.add_argument("-s", "--session", type=int, help="session to interact with")
|
||||||
|
@ -511,6 +570,9 @@ def setup_wlan_parser(parent: _SubParsersAction) -> None:
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument(
|
||||||
|
"-js", "--json", action="store_true", help="print responses to terminal as json"
|
||||||
|
)
|
||||||
subparsers = parser.add_subparsers(help="supported commands")
|
subparsers = parser.add_subparsers(help="supported commands")
|
||||||
subparsers.required = True
|
subparsers.required = True
|
||||||
subparsers.dest = "command"
|
subparsers.dest = "command"
|
||||||
|
|
|
@ -8,19 +8,15 @@ message handlers are defined and some support for sending messages.
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from core import constants
|
from core import constants
|
||||||
from core.api.grpc.server import CoreGrpcServer
|
from core.api.grpc.server import CoreGrpcServer
|
||||||
from core.api.tlv.corehandlers import CoreHandler, CoreUdpHandler
|
|
||||||
from core.api.tlv.coreserver import CoreServer, CoreUdpServer
|
|
||||||
from core.api.tlv.enumerations import CORE_API_PORT
|
|
||||||
from core.constants import CORE_CONF_DIR, COREDPY_VERSION
|
from core.constants import CORE_CONF_DIR, COREDPY_VERSION
|
||||||
from core.utils import close_onexec, load_logging_config
|
from core.emulator.coreemu import CoreEmu
|
||||||
|
from core.utils import load_logging_config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -34,20 +30,6 @@ def banner():
|
||||||
logger.info("CORE daemon v.%s started %s", constants.COREDPY_VERSION, time.ctime())
|
logger.info("CORE daemon v.%s started %s", constants.COREDPY_VERSION, time.ctime())
|
||||||
|
|
||||||
|
|
||||||
def start_udp(mainserver, server_address):
|
|
||||||
"""
|
|
||||||
Start a thread running a UDP server on the same host,port for
|
|
||||||
connectionless requests.
|
|
||||||
|
|
||||||
:param CoreServer mainserver: main core tcp server to piggy back off of
|
|
||||||
:param server_address:
|
|
||||||
:return: CoreUdpServer
|
|
||||||
"""
|
|
||||||
mainserver.udpserver = CoreUdpServer(server_address, CoreUdpHandler, mainserver)
|
|
||||||
mainserver.udpthread = threading.Thread(target=mainserver.udpserver.start, daemon=True)
|
|
||||||
mainserver.udpthread.start()
|
|
||||||
|
|
||||||
|
|
||||||
def cored(cfg):
|
def cored(cfg):
|
||||||
"""
|
"""
|
||||||
Start the CoreServer object and enter the server loop.
|
Start the CoreServer object and enter the server loop.
|
||||||
|
@ -55,34 +37,13 @@ def cored(cfg):
|
||||||
:param dict cfg: core configuration
|
:param dict cfg: core configuration
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
host = cfg["listenaddr"]
|
|
||||||
port = int(cfg["port"])
|
|
||||||
if host == "" or host is None:
|
|
||||||
host = "localhost"
|
|
||||||
|
|
||||||
try:
|
|
||||||
address = (host, port)
|
|
||||||
server = CoreServer(address, CoreHandler, cfg)
|
|
||||||
except:
|
|
||||||
logger.exception("error starting main server on: %s:%s", host, port)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# initialize grpc api
|
# initialize grpc api
|
||||||
grpc_server = CoreGrpcServer(server.coreemu)
|
coreemu = CoreEmu(cfg)
|
||||||
|
grpc_server = CoreGrpcServer(coreemu)
|
||||||
address_config = cfg["grpcaddress"]
|
address_config = cfg["grpcaddress"]
|
||||||
port_config = cfg["grpcport"]
|
port_config = cfg["grpcport"]
|
||||||
grpc_address = f"{address_config}:{port_config}"
|
grpc_address = f"{address_config}:{port_config}"
|
||||||
grpc_thread = threading.Thread(target=grpc_server.listen, args=(grpc_address,), daemon=True)
|
grpc_server.listen(grpc_address)
|
||||||
grpc_thread.start()
|
|
||||||
|
|
||||||
# start udp server
|
|
||||||
start_udp(server, address)
|
|
||||||
|
|
||||||
# close handlers
|
|
||||||
close_onexec(server.fileno())
|
|
||||||
|
|
||||||
logger.info("CORE TLV API TCP/UDP listening on: %s:%s", host, port)
|
|
||||||
server.serve_forever()
|
|
||||||
|
|
||||||
|
|
||||||
def get_merged_config(filename):
|
def get_merged_config(filename):
|
||||||
|
@ -98,49 +59,38 @@ def get_merged_config(filename):
|
||||||
default_grpc_port = "50051"
|
default_grpc_port = "50051"
|
||||||
default_address = "localhost"
|
default_address = "localhost"
|
||||||
defaults = {
|
defaults = {
|
||||||
"port": str(CORE_API_PORT),
|
|
||||||
"listenaddr": default_address,
|
|
||||||
"grpcport": default_grpc_port,
|
"grpcport": default_grpc_port,
|
||||||
"grpcaddress": default_address,
|
"grpcaddress": default_address,
|
||||||
"logfile": default_log
|
"logfile": default_log
|
||||||
}
|
}
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=f"CORE daemon v.{COREDPY_VERSION} instantiates Linux network namespace nodes.")
|
description=f"CORE daemon v.{COREDPY_VERSION} instantiates Linux network namespace nodes.")
|
||||||
parser.add_argument("-f", "--configfile", dest="configfile",
|
parser.add_argument("-f", "--configfile", dest="configfile",
|
||||||
help=f"read config from specified file; default = {filename}")
|
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("--ovs", action="store_true", help="enable experimental ovs mode, default is false")
|
||||||
parser.add_argument("--grpc-port", dest="grpcport",
|
parser.add_argument("--grpc-port", dest="grpcport",
|
||||||
help=f"grpc port to listen on; default {default_grpc_port}")
|
help=f"grpc port to listen on; default {default_grpc_port}")
|
||||||
parser.add_argument("--grpc-address", dest="grpcaddress",
|
parser.add_argument("--grpc-address", dest="grpcaddress",
|
||||||
help=f"grpc address to listen on; default {default_address}")
|
help=f"grpc address to listen on; default {default_address}")
|
||||||
parser.add_argument("-l", "--logfile", help=f"core logging configuration; default {default_log}")
|
parser.add_argument("-l", "--logfile", help=f"core logging configuration; default {default_log}")
|
||||||
|
|
||||||
# parse command line options
|
# parse command line options
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# convert ovs to internal format
|
# convert ovs to internal format
|
||||||
args.ovs = "1" if args.ovs else "0"
|
args.ovs = "1" if args.ovs else "0"
|
||||||
|
|
||||||
# read the config file
|
# read the config file
|
||||||
if args.configfile is not None:
|
if args.configfile is not None:
|
||||||
filename = args.configfile
|
filename = args.configfile
|
||||||
del args.configfile
|
del args.configfile
|
||||||
cfg = ConfigParser(defaults)
|
cfg = ConfigParser(defaults)
|
||||||
cfg.read(filename)
|
cfg.read(filename)
|
||||||
|
|
||||||
section = "core-daemon"
|
section = "core-daemon"
|
||||||
if not cfg.has_section(section):
|
if not cfg.has_section(section):
|
||||||
cfg.add_section(section)
|
cfg.add_section(section)
|
||||||
|
|
||||||
# merge argparse with configparser
|
# merge argparse with configparser
|
||||||
for opt in vars(args):
|
for opt in vars(args):
|
||||||
val = getattr(args, opt)
|
val = getattr(args, opt)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
cfg.set(section, opt, str(val))
|
cfg.set(section, opt, str(val))
|
||||||
|
|
||||||
return dict(cfg.items(section))
|
return dict(cfg.items(section))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 mock
|
||||||
import pytest
|
import pytest
|
||||||
from mock.mock import MagicMock
|
|
||||||
|
|
||||||
from core.api.grpc.client import InterfaceHelper
|
from core.api.grpc.client import InterfaceHelper
|
||||||
from core.api.grpc.server import CoreGrpcServer
|
from core.api.grpc.server import CoreGrpcServer
|
||||||
from core.api.tlv.corehandlers import CoreHandler
|
|
||||||
from core.emulator.coreemu import CoreEmu
|
from core.emulator.coreemu import CoreEmu
|
||||||
from core.emulator.data import IpPrefixes
|
from core.emulator.data import IpPrefixes
|
||||||
from core.emulator.distributed import DistributedServer
|
from core.emulator.distributed import DistributedServer
|
||||||
|
@ -104,17 +102,6 @@ def module_grpc(global_coreemu):
|
||||||
grpc_server.server.stop(None)
|
grpc_server.server.stop(None)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def module_coretlv(patcher, global_coreemu, global_session):
|
|
||||||
request_mock = MagicMock()
|
|
||||||
request_mock.fileno = MagicMock(return_value=1)
|
|
||||||
server = MockServer(global_coreemu)
|
|
||||||
request_handler = CoreHandler(request_mock, "", server)
|
|
||||||
request_handler.session = global_session
|
|
||||||
request_handler.add_session_handlers()
|
|
||||||
yield request_handler
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def grpc_server(module_grpc):
|
def grpc_server(module_grpc):
|
||||||
yield module_grpc
|
yield module_grpc
|
||||||
|
@ -130,16 +117,6 @@ def session(global_session):
|
||||||
global_session.clear()
|
global_session.clear()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def coretlv(module_coretlv):
|
|
||||||
session = module_coretlv.session
|
|
||||||
session.set_state(EventTypes.CONFIGURATION_STATE)
|
|
||||||
coreemu = module_coretlv.coreemu
|
|
||||||
coreemu.sessions[session.id] = session
|
|
||||||
yield module_coretlv
|
|
||||||
coreemu.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
parser.addoption("--distributed", help="distributed server address")
|
parser.addoption("--distributed", help="distributed server address")
|
||||||
parser.addoption("--mock", action="store_true", help="run without mocking")
|
parser.addoption("--mock", action="store_true", help="run without mocking")
|
||||||
|
|
|
@ -9,7 +9,6 @@ from typing import List, Type
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.emulator.data import IpPrefixes, NodeOptions
|
from core.emulator.data import IpPrefixes, NodeOptions
|
||||||
from core.emulator.enumerations import MessageFlags
|
|
||||||
from core.emulator.session import Session
|
from core.emulator.session import Session
|
||||||
from core.errors import CoreCommandError
|
from core.errors import CoreCommandError
|
||||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||||
|
@ -63,44 +62,6 @@ class TestCore:
|
||||||
status = ping(node1, node2, ip_prefixes)
|
status = ping(node1, node2, ip_prefixes)
|
||||||
assert not status
|
assert not status
|
||||||
|
|
||||||
def test_iface(self, session: Session, ip_prefixes: IpPrefixes):
|
|
||||||
"""
|
|
||||||
Test interface methods.
|
|
||||||
|
|
||||||
:param session: session for test
|
|
||||||
:param ip_prefixes: generates ip addresses for nodes
|
|
||||||
"""
|
|
||||||
|
|
||||||
# create ptp
|
|
||||||
ptp_node = session.add_node(PtpNet)
|
|
||||||
|
|
||||||
# create nodes
|
|
||||||
node1 = session.add_node(CoreNode)
|
|
||||||
node2 = session.add_node(CoreNode)
|
|
||||||
|
|
||||||
# link nodes to ptp net
|
|
||||||
for node in [node1, node2]:
|
|
||||||
iface = ip_prefixes.create_iface(node)
|
|
||||||
session.add_link(node.id, ptp_node.id, iface1_data=iface)
|
|
||||||
|
|
||||||
# instantiate session
|
|
||||||
session.instantiate()
|
|
||||||
|
|
||||||
# check link data gets generated
|
|
||||||
assert ptp_node.links(MessageFlags.ADD)
|
|
||||||
|
|
||||||
# check common nets exist between linked nodes
|
|
||||||
assert node1.commonnets(node2)
|
|
||||||
assert node2.commonnets(node1)
|
|
||||||
|
|
||||||
# check we can retrieve interface id
|
|
||||||
assert 0 in node1.ifaces
|
|
||||||
assert 0 in node2.ifaces
|
|
||||||
|
|
||||||
# delete interface and test that if no longer exists
|
|
||||||
node1.delete_iface(0)
|
|
||||||
assert 0 not in node1.ifaces
|
|
||||||
|
|
||||||
def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
"""
|
"""
|
||||||
Test basic wlan network.
|
Test basic wlan network.
|
||||||
|
|
|
@ -29,12 +29,14 @@ class TestDistributed:
|
||||||
|
|
||||||
# when
|
# when
|
||||||
session.distributed.add_server(server_name, host)
|
session.distributed.add_server(server_name, host)
|
||||||
|
node1 = session.add_node(HubNode)
|
||||||
options = NodeOptions(server=server_name)
|
options = NodeOptions(server=server_name)
|
||||||
node = session.add_node(HubNode, options=options)
|
node2 = session.add_node(HubNode, options=options)
|
||||||
|
session.add_link(node1.id, node2.id)
|
||||||
session.instantiate()
|
session.instantiate()
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert node.server is not None
|
assert node2.server is not None
|
||||||
assert node.server.name == server_name
|
assert node2.server.name == server_name
|
||||||
assert node.server.host == host
|
assert node2.server.host == host
|
||||||
assert len(session.distributed.tunnels) > 0
|
assert len(session.distributed.tunnels) == 1
|
||||||
|
|
|
@ -31,12 +31,10 @@ from core.api.grpc.wrappers import (
|
||||||
SessionLocation,
|
SessionLocation,
|
||||||
SessionState,
|
SessionState,
|
||||||
)
|
)
|
||||||
from core.api.tlv.dataconversion import ConfigShim
|
|
||||||
from core.api.tlv.enumerations import ConfigFlags
|
|
||||||
from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
|
from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.emulator.data import EventData, IpPrefixes, NodeData, NodeOptions
|
from core.emulator.data import EventData, IpPrefixes, NodeData, NodeOptions
|
||||||
from core.emulator.enumerations import EventTypes, ExceptionLevels
|
from core.emulator.enumerations import EventTypes, ExceptionLevels, MessageFlags
|
||||||
from core.errors import CoreError
|
from core.errors import CoreError
|
||||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||||
from core.nodes.base import CoreNode
|
from core.nodes.base import CoreNode
|
||||||
|
@ -415,7 +413,7 @@ class TestGrpc:
|
||||||
session = grpc_server.coreemu.create_session()
|
session = grpc_server.coreemu.create_session()
|
||||||
switch = session.add_node(SwitchNode)
|
switch = session.add_node(SwitchNode)
|
||||||
node = session.add_node(CoreNode)
|
node = session.add_node(CoreNode)
|
||||||
assert len(switch.links()) == 0
|
assert len(session.link_manager.links()) == 0
|
||||||
iface = InterfaceHelper("10.0.0.0/24").create_iface(node.id, 0)
|
iface = InterfaceHelper("10.0.0.0/24").create_iface(node.id, 0)
|
||||||
link = Link(node.id, switch.id, iface1=iface)
|
link = Link(node.id, switch.id, iface1=iface)
|
||||||
|
|
||||||
|
@ -425,7 +423,7 @@ class TestGrpc:
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert result is True
|
assert result is True
|
||||||
assert len(switch.links()) == 1
|
assert len(session.link_manager.links()) == 1
|
||||||
assert iface1.id == iface.id
|
assert iface1.id == iface.id
|
||||||
assert iface1.ip4 == iface.ip4
|
assert iface1.ip4 == iface.ip4
|
||||||
|
|
||||||
|
@ -447,11 +445,10 @@ class TestGrpc:
|
||||||
session = grpc_server.coreemu.create_session()
|
session = grpc_server.coreemu.create_session()
|
||||||
switch = session.add_node(SwitchNode)
|
switch = session.add_node(SwitchNode)
|
||||||
node = session.add_node(CoreNode)
|
node = session.add_node(CoreNode)
|
||||||
iface = ip_prefixes.create_iface(node)
|
iface_data = ip_prefixes.create_iface(node)
|
||||||
session.add_link(node.id, switch.id, iface)
|
iface, _ = session.add_link(node.id, switch.id, iface_data)
|
||||||
options = LinkOptions(bandwidth=30000)
|
options = LinkOptions(bandwidth=30000)
|
||||||
link = switch.links()[0]
|
assert iface.options.bandwidth != options.bandwidth
|
||||||
assert options.bandwidth != link.options.bandwidth
|
|
||||||
link = Link(node.id, switch.id, iface1=Interface(id=iface.id), options=options)
|
link = Link(node.id, switch.id, iface1=Interface(id=iface.id), options=options)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
|
@ -460,8 +457,7 @@ class TestGrpc:
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert result is True
|
assert result is True
|
||||||
link = switch.links()[0]
|
assert options.bandwidth == iface.options.bandwidth
|
||||||
assert options.bandwidth == link.options.bandwidth
|
|
||||||
|
|
||||||
def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes):
|
def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
|
@ -472,13 +468,7 @@ class TestGrpc:
|
||||||
node2 = session.add_node(CoreNode)
|
node2 = session.add_node(CoreNode)
|
||||||
iface2 = ip_prefixes.create_iface(node2)
|
iface2 = ip_prefixes.create_iface(node2)
|
||||||
session.add_link(node1.id, node2.id, iface1, iface2)
|
session.add_link(node1.id, node2.id, iface1, iface2)
|
||||||
link_node = None
|
assert len(session.link_manager.links()) == 1
|
||||||
for node_id in session.nodes:
|
|
||||||
node = session.nodes[node_id]
|
|
||||||
if node.id not in {node1.id, node2.id}:
|
|
||||||
link_node = node
|
|
||||||
break
|
|
||||||
assert len(link_node.links()) == 1
|
|
||||||
link = Link(
|
link = Link(
|
||||||
node1.id,
|
node1.id,
|
||||||
node2.id,
|
node2.id,
|
||||||
|
@ -492,7 +482,7 @@ class TestGrpc:
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert result is True
|
assert result is True
|
||||||
assert len(link_node.links()) == 0
|
assert len(session.link_manager.links()) == 0
|
||||||
|
|
||||||
def test_get_wlan_config(self, grpc_server: CoreGrpcServer):
|
def test_get_wlan_config(self, grpc_server: CoreGrpcServer):
|
||||||
# given
|
# given
|
||||||
|
@ -757,9 +747,11 @@ class TestGrpc:
|
||||||
session = grpc_server.coreemu.create_session()
|
session = grpc_server.coreemu.create_session()
|
||||||
wlan = session.add_node(WlanNode)
|
wlan = session.add_node(WlanNode)
|
||||||
node = session.add_node(CoreNode)
|
node = session.add_node(CoreNode)
|
||||||
iface = ip_prefixes.create_iface(node)
|
iface_data = ip_prefixes.create_iface(node)
|
||||||
session.add_link(node.id, wlan.id, iface)
|
session.add_link(node.id, wlan.id, iface_data)
|
||||||
link_data = wlan.links()[0]
|
core_link = list(session.link_manager.links())[0]
|
||||||
|
link_data = core_link.get_data(MessageFlags.ADD)
|
||||||
|
|
||||||
queue = Queue()
|
queue = Queue()
|
||||||
|
|
||||||
def handle_event(event: Event) -> None:
|
def handle_event(event: Event) -> None:
|
||||||
|
@ -820,30 +812,6 @@ class TestGrpc:
|
||||||
# then
|
# then
|
||||||
queue.get(timeout=5)
|
queue.get(timeout=5)
|
||||||
|
|
||||||
def test_config_events(self, grpc_server: CoreGrpcServer):
|
|
||||||
# given
|
|
||||||
client = CoreGrpcClient()
|
|
||||||
session = grpc_server.coreemu.create_session()
|
|
||||||
queue = Queue()
|
|
||||||
|
|
||||||
def handle_event(event: Event) -> None:
|
|
||||||
assert event.session_id == session.id
|
|
||||||
assert event.config_event is not None
|
|
||||||
queue.put(event)
|
|
||||||
|
|
||||||
# then
|
|
||||||
with client.context_connect():
|
|
||||||
client.events(session.id, handle_event)
|
|
||||||
time.sleep(0.1)
|
|
||||||
session_config = session.options.get_configs()
|
|
||||||
config_data = ConfigShim.config_data(
|
|
||||||
0, None, ConfigFlags.UPDATE.value, session.options, session_config
|
|
||||||
)
|
|
||||||
session.broadcast_config(config_data)
|
|
||||||
|
|
||||||
# then
|
|
||||||
queue.get(timeout=5)
|
|
||||||
|
|
||||||
def test_exception_events(self, grpc_server: CoreGrpcServer):
|
def test_exception_events(self, grpc_server: CoreGrpcServer):
|
||||||
# given
|
# given
|
||||||
client = CoreGrpcClient()
|
client = CoreGrpcClient()
|
||||||
|
@ -958,3 +926,26 @@ class TestGrpc:
|
||||||
with pytest.raises(grpc.RpcError):
|
with pytest.raises(grpc.RpcError):
|
||||||
with client.context_connect():
|
with client.context_connect():
|
||||||
client.move_nodes(streamer)
|
client.move_nodes(streamer)
|
||||||
|
|
||||||
|
def test_wlan_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes):
|
||||||
|
# given
|
||||||
|
client = CoreGrpcClient()
|
||||||
|
session = grpc_server.coreemu.create_session()
|
||||||
|
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
|
# then
|
||||||
|
assert len(session.link_manager.links()) == 1
|
||||||
assert node1.get_iface(iface1_data.id)
|
assert node1.get_iface(iface1_data.id)
|
||||||
assert node2.get_iface(iface2_data.id)
|
assert node2.get_iface(iface2_data.id)
|
||||||
assert iface1 is not None
|
assert iface1 is not None
|
||||||
|
assert iface1.options == LINK_OPTIONS
|
||||||
|
assert iface1.has_netem
|
||||||
|
assert node1.get_iface(iface1_data.id)
|
||||||
assert iface2 is not None
|
assert iface2 is not None
|
||||||
assert iface1.local_options == LINK_OPTIONS
|
assert iface2.options == LINK_OPTIONS
|
||||||
assert iface1.has_local_netem
|
assert iface2.has_netem
|
||||||
assert iface2.local_options == LINK_OPTIONS
|
assert node1.get_iface(iface1_data.id)
|
||||||
assert iface2.has_local_netem
|
|
||||||
|
|
||||||
def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
|
@ -62,16 +65,20 @@ class TestLinks:
|
||||||
iface1_data = ip_prefixes.create_iface(node1)
|
iface1_data = ip_prefixes.create_iface(node1)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
iface, _ = session.add_link(
|
iface1, iface2 = session.add_link(
|
||||||
node1.id, node2.id, iface1_data=iface1_data, options=LINK_OPTIONS
|
node1.id, node2.id, iface1_data=iface1_data, options=LINK_OPTIONS
|
||||||
)
|
)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert node2.links()
|
assert len(session.link_manager.links()) == 1
|
||||||
|
assert iface1 is not None
|
||||||
|
assert iface1.options == LINK_OPTIONS
|
||||||
|
assert iface1.has_netem
|
||||||
assert node1.get_iface(iface1_data.id)
|
assert node1.get_iface(iface1_data.id)
|
||||||
assert iface is not None
|
assert iface2 is not None
|
||||||
assert iface.local_options == LINK_OPTIONS
|
assert iface2.options == LINK_OPTIONS
|
||||||
assert iface.has_local_netem
|
assert iface2.has_netem
|
||||||
|
assert node2.get_iface(iface1_data.id)
|
||||||
|
|
||||||
def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
|
@ -80,32 +87,37 @@ class TestLinks:
|
||||||
iface2_data = ip_prefixes.create_iface(node2)
|
iface2_data = ip_prefixes.create_iface(node2)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
_, iface = session.add_link(
|
iface1, iface2 = session.add_link(
|
||||||
node1.id, node2.id, iface2_data=iface2_data, options=LINK_OPTIONS
|
node1.id, node2.id, iface2_data=iface2_data, options=LINK_OPTIONS
|
||||||
)
|
)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert node1.links()
|
assert len(session.link_manager.links()) == 1
|
||||||
assert node2.get_iface(iface2_data.id)
|
assert iface1 is not None
|
||||||
assert iface is not None
|
assert iface1.options == LINK_OPTIONS
|
||||||
assert iface.local_options == LINK_OPTIONS
|
assert iface1.has_netem
|
||||||
assert iface.has_local_netem
|
assert node1.get_iface(iface1.id)
|
||||||
|
assert iface2 is not None
|
||||||
|
assert iface2.options == LINK_OPTIONS
|
||||||
|
assert iface2.has_netem
|
||||||
|
assert node2.get_iface(iface2.id)
|
||||||
|
|
||||||
def test_add_net_to_net(self, session):
|
def test_add_net_to_net(self, session: Session):
|
||||||
# given
|
# given
|
||||||
node1 = session.add_node(SwitchNode)
|
node1 = session.add_node(SwitchNode)
|
||||||
node2 = session.add_node(SwitchNode)
|
node2 = session.add_node(SwitchNode)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
iface, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS)
|
iface1, iface2 = session.add_link(node1.id, node2.id, options=LINK_OPTIONS)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert node1.links()
|
assert len(session.link_manager.links()) == 1
|
||||||
assert iface is not None
|
assert iface1 is not None
|
||||||
assert iface.local_options == LINK_OPTIONS
|
assert iface1.options == LINK_OPTIONS
|
||||||
assert iface.options == LINK_OPTIONS
|
assert iface1.has_netem
|
||||||
assert iface.has_local_netem
|
assert iface2 is not None
|
||||||
assert iface.has_netem
|
assert iface2.options == LINK_OPTIONS
|
||||||
|
assert iface2.has_netem
|
||||||
|
|
||||||
def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
|
@ -141,48 +153,52 @@ class TestLinks:
|
||||||
)
|
)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
|
assert len(session.link_manager.links()) == 1
|
||||||
assert node1.get_iface(iface1_data.id)
|
assert node1.get_iface(iface1_data.id)
|
||||||
assert node2.get_iface(iface2_data.id)
|
assert node2.get_iface(iface2_data.id)
|
||||||
assert iface1 is not None
|
assert iface1 is not None
|
||||||
|
assert iface1.options == link_options1
|
||||||
|
assert iface1.has_netem
|
||||||
assert iface2 is not None
|
assert iface2 is not None
|
||||||
assert iface1.local_options == link_options1
|
assert iface2.options == link_options2
|
||||||
assert iface1.has_local_netem
|
assert iface2.has_netem
|
||||||
assert iface2.local_options == link_options2
|
|
||||||
assert iface2.has_local_netem
|
|
||||||
|
|
||||||
def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
node1 = session.add_node(CoreNode)
|
node1 = session.add_node(CoreNode)
|
||||||
node2 = session.add_node(SwitchNode)
|
node2 = session.add_node(SwitchNode)
|
||||||
iface1_data = ip_prefixes.create_iface(node1)
|
iface1_data = ip_prefixes.create_iface(node1)
|
||||||
iface1, _ = session.add_link(node1.id, node2.id, iface1_data)
|
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
|
||||||
assert iface1.local_options != LINK_OPTIONS
|
assert len(session.link_manager.links()) == 1
|
||||||
|
assert iface1.options != LINK_OPTIONS
|
||||||
|
assert iface2.options != LINK_OPTIONS
|
||||||
|
|
||||||
# when
|
# when
|
||||||
session.update_link(
|
session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
|
||||||
node1.id, node2.id, iface1_id=iface1_data.id, options=LINK_OPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert iface1.local_options == LINK_OPTIONS
|
assert iface1.options == LINK_OPTIONS
|
||||||
assert iface1.has_local_netem
|
assert iface1.has_netem
|
||||||
|
assert iface2.options == LINK_OPTIONS
|
||||||
|
assert iface2.has_netem
|
||||||
|
|
||||||
def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
node1 = session.add_node(SwitchNode)
|
node1 = session.add_node(SwitchNode)
|
||||||
node2 = session.add_node(CoreNode)
|
node2 = session.add_node(CoreNode)
|
||||||
iface2_data = ip_prefixes.create_iface(node2)
|
iface2_data = ip_prefixes.create_iface(node2)
|
||||||
_, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
||||||
assert iface2.local_options != LINK_OPTIONS
|
assert iface1.options != LINK_OPTIONS
|
||||||
|
assert iface2.options != LINK_OPTIONS
|
||||||
|
|
||||||
# when
|
# when
|
||||||
session.update_link(
|
session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
|
||||||
node1.id, node2.id, iface2_id=iface2_data.id, options=LINK_OPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert iface2.local_options == LINK_OPTIONS
|
assert iface1.options == LINK_OPTIONS
|
||||||
assert iface2.has_local_netem
|
assert iface1.has_netem
|
||||||
|
assert iface2.options == LINK_OPTIONS
|
||||||
|
assert iface2.has_netem
|
||||||
|
|
||||||
def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
|
@ -191,55 +207,68 @@ class TestLinks:
|
||||||
iface1_data = ip_prefixes.create_iface(node1)
|
iface1_data = ip_prefixes.create_iface(node1)
|
||||||
iface2_data = ip_prefixes.create_iface(node2)
|
iface2_data = ip_prefixes.create_iface(node2)
|
||||||
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
||||||
assert iface1.local_options != LINK_OPTIONS
|
assert iface1.options != LINK_OPTIONS
|
||||||
assert iface2.local_options != LINK_OPTIONS
|
assert iface2.options != LINK_OPTIONS
|
||||||
|
|
||||||
# when
|
# when
|
||||||
session.update_link(
|
session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
|
||||||
node1.id, node2.id, iface1_data.id, iface2_data.id, LINK_OPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert iface1.local_options == LINK_OPTIONS
|
assert iface1.options == LINK_OPTIONS
|
||||||
assert iface1.has_local_netem
|
assert iface1.has_netem
|
||||||
assert iface2.local_options == LINK_OPTIONS
|
assert iface2.options == LINK_OPTIONS
|
||||||
assert iface2.has_local_netem
|
assert iface2.has_netem
|
||||||
|
|
||||||
def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
node1 = session.add_node(SwitchNode)
|
node1 = session.add_node(SwitchNode)
|
||||||
node2 = session.add_node(SwitchNode)
|
node2 = session.add_node(SwitchNode)
|
||||||
iface1, _ = session.add_link(node1.id, node2.id)
|
iface1, iface2 = session.add_link(node1.id, node2.id)
|
||||||
assert iface1.local_options != LINK_OPTIONS
|
assert iface1.options != LINK_OPTIONS
|
||||||
|
assert iface2.options != LINK_OPTIONS
|
||||||
|
|
||||||
# when
|
# when
|
||||||
session.update_link(node1.id, node2.id, options=LINK_OPTIONS)
|
session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert iface1.local_options == LINK_OPTIONS
|
|
||||||
assert iface1.has_local_netem
|
|
||||||
assert iface1.options == LINK_OPTIONS
|
assert iface1.options == LINK_OPTIONS
|
||||||
assert iface1.has_netem
|
assert iface1.has_netem
|
||||||
|
assert iface2.options == LINK_OPTIONS
|
||||||
|
assert iface2.has_netem
|
||||||
|
|
||||||
|
def test_update_error(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
|
# given
|
||||||
|
node1 = session.add_node(CoreNode)
|
||||||
|
node2 = session.add_node(CoreNode)
|
||||||
|
iface1_data = ip_prefixes.create_iface(node1)
|
||||||
|
iface2_data = ip_prefixes.create_iface(node2)
|
||||||
|
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
||||||
|
assert iface1.options != LINK_OPTIONS
|
||||||
|
assert iface2.options != LINK_OPTIONS
|
||||||
|
|
||||||
|
# when
|
||||||
|
with pytest.raises(CoreError):
|
||||||
|
session.delete_link(node1.id, INVALID_ID, iface1.id, iface2.id)
|
||||||
|
|
||||||
def test_clear_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_clear_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
node1 = session.add_node(SwitchNode)
|
node1 = session.add_node(SwitchNode)
|
||||||
node2 = session.add_node(SwitchNode)
|
node2 = session.add_node(SwitchNode)
|
||||||
iface1, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS)
|
iface1, iface2 = session.add_link(node1.id, node2.id, options=LINK_OPTIONS)
|
||||||
assert iface1.local_options == LINK_OPTIONS
|
|
||||||
assert iface1.has_local_netem
|
|
||||||
assert iface1.options == LINK_OPTIONS
|
assert iface1.options == LINK_OPTIONS
|
||||||
assert iface1.has_netem
|
assert iface1.has_netem
|
||||||
|
assert iface2.options == LINK_OPTIONS
|
||||||
|
assert iface2.has_netem
|
||||||
|
|
||||||
# when
|
# when
|
||||||
options = LinkOptions(delay=0, bandwidth=0, loss=0.0, dup=0, jitter=0, buffer=0)
|
options = LinkOptions(delay=0, bandwidth=0, loss=0.0, dup=0, jitter=0, buffer=0)
|
||||||
session.update_link(node1.id, node2.id, options=options)
|
session.update_link(node1.id, node2.id, iface1.id, iface2.id, options)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert iface1.local_options.is_clear()
|
|
||||||
assert not iface1.has_local_netem
|
|
||||||
assert iface1.options.is_clear()
|
assert iface1.options.is_clear()
|
||||||
assert not iface1.has_netem
|
assert not iface1.has_netem
|
||||||
|
assert iface2.options.is_clear()
|
||||||
|
assert not iface2.has_netem
|
||||||
|
|
||||||
def test_delete_node_to_node(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_delete_node_to_node(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
|
@ -247,82 +276,100 @@ class TestLinks:
|
||||||
node2 = session.add_node(CoreNode)
|
node2 = session.add_node(CoreNode)
|
||||||
iface1_data = ip_prefixes.create_iface(node1)
|
iface1_data = ip_prefixes.create_iface(node1)
|
||||||
iface2_data = ip_prefixes.create_iface(node2)
|
iface2_data = ip_prefixes.create_iface(node2)
|
||||||
session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
||||||
assert node1.get_iface(iface1_data.id)
|
assert len(session.link_manager.links()) == 1
|
||||||
assert node2.get_iface(iface2_data.id)
|
assert node1.get_iface(iface1.id)
|
||||||
|
assert node2.get_iface(iface2.id)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
session.delete_link(node1.id, node2.id, iface1_data.id, iface2_data.id)
|
session.delete_link(node1.id, node2.id, iface1.id, iface2.id)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert iface1_data.id not in node1.ifaces
|
assert len(session.link_manager.links()) == 0
|
||||||
assert iface2_data.id not in node2.ifaces
|
assert iface1.id not in node1.ifaces
|
||||||
|
assert iface2.id not in node2.ifaces
|
||||||
|
|
||||||
def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
node1 = session.add_node(CoreNode)
|
node1 = session.add_node(CoreNode)
|
||||||
node2 = session.add_node(SwitchNode)
|
node2 = session.add_node(SwitchNode)
|
||||||
iface1_data = ip_prefixes.create_iface(node1)
|
iface1_data = ip_prefixes.create_iface(node1)
|
||||||
session.add_link(node1.id, node2.id, iface1_data)
|
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
|
||||||
assert node1.get_iface(iface1_data.id)
|
assert len(session.link_manager.links()) == 1
|
||||||
|
assert node1.get_iface(iface1.id)
|
||||||
|
assert node2.get_iface(iface2.id)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
session.delete_link(node1.id, node2.id, iface1_id=iface1_data.id)
|
session.delete_link(node1.id, node2.id, iface1.id, iface2.id)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert iface1_data.id not in node1.ifaces
|
assert len(session.link_manager.links()) == 0
|
||||||
|
assert iface1.id not in node1.ifaces
|
||||||
|
assert iface2.id not in node2.ifaces
|
||||||
|
|
||||||
def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
node1 = session.add_node(SwitchNode)
|
node1 = session.add_node(SwitchNode)
|
||||||
node2 = session.add_node(CoreNode)
|
node2 = session.add_node(CoreNode)
|
||||||
iface2_data = ip_prefixes.create_iface(node2)
|
iface2_data = ip_prefixes.create_iface(node2)
|
||||||
session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
||||||
assert node2.get_iface(iface2_data.id)
|
assert len(session.link_manager.links()) == 1
|
||||||
|
assert node1.get_iface(iface1.id)
|
||||||
|
assert node2.get_iface(iface2.id)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
session.delete_link(node1.id, node2.id, iface2_id=iface2_data.id)
|
session.delete_link(node1.id, node2.id, iface1.id, iface2.id)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert iface2_data.id not in node2.ifaces
|
assert len(session.link_manager.links()) == 0
|
||||||
|
assert iface1.id not in node1.ifaces
|
||||||
|
assert iface2.id not in node2.ifaces
|
||||||
|
|
||||||
def test_delete_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_delete_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
node1 = session.add_node(SwitchNode)
|
node1 = session.add_node(SwitchNode)
|
||||||
node2 = session.add_node(SwitchNode)
|
node2 = session.add_node(SwitchNode)
|
||||||
session.add_link(node1.id, node2.id)
|
iface1, iface2 = session.add_link(node1.id, node2.id)
|
||||||
assert node1.get_linked_iface(node2)
|
assert len(session.link_manager.links()) == 1
|
||||||
|
assert node1.get_iface(iface1.id)
|
||||||
|
assert node2.get_iface(iface2.id)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
session.delete_link(node1.id, node2.id)
|
session.delete_link(node1.id, node2.id, iface1.id, iface2.id)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert not node1.get_linked_iface(node2)
|
assert len(session.link_manager.links()) == 0
|
||||||
|
assert iface1.id not in node1.ifaces
|
||||||
|
assert iface2.id not in node2.ifaces
|
||||||
|
|
||||||
def test_delete_node_error(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_delete_node_error(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
node1 = session.add_node(SwitchNode)
|
node1 = session.add_node(SwitchNode)
|
||||||
node2 = session.add_node(SwitchNode)
|
node2 = session.add_node(SwitchNode)
|
||||||
session.add_link(node1.id, node2.id)
|
iface1, iface2 = session.add_link(node1.id, node2.id)
|
||||||
assert node1.get_linked_iface(node2)
|
assert len(session.link_manager.links()) == 1
|
||||||
|
assert node1.get_iface(iface1.id)
|
||||||
|
assert node2.get_iface(iface2.id)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
session.delete_link(node1.id, INVALID_ID)
|
session.delete_link(node1.id, INVALID_ID, iface1.id, iface2.id)
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
session.delete_link(INVALID_ID, node2.id)
|
session.delete_link(INVALID_ID, node2.id, iface1.id, iface2.id)
|
||||||
|
|
||||||
def test_delete_net_to_net_error(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_delete_net_to_net_error(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
node1 = session.add_node(SwitchNode)
|
node1 = session.add_node(SwitchNode)
|
||||||
node2 = session.add_node(SwitchNode)
|
node2 = session.add_node(SwitchNode)
|
||||||
node3 = session.add_node(SwitchNode)
|
node3 = session.add_node(SwitchNode)
|
||||||
session.add_link(node1.id, node2.id)
|
iface1, iface2 = session.add_link(node1.id, node2.id)
|
||||||
assert node1.get_linked_iface(node2)
|
assert len(session.link_manager.links()) == 1
|
||||||
|
assert node1.get_iface(iface1.id)
|
||||||
|
assert node2.get_iface(iface2.id)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
session.delete_link(node1.id, node3.id)
|
session.delete_link(node1.id, node3.id, iface1.id, iface2.id)
|
||||||
|
|
||||||
def test_delete_node_to_net_error(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_delete_node_to_net_error(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
|
@ -330,12 +377,14 @@ class TestLinks:
|
||||||
node2 = session.add_node(SwitchNode)
|
node2 = session.add_node(SwitchNode)
|
||||||
node3 = session.add_node(SwitchNode)
|
node3 = session.add_node(SwitchNode)
|
||||||
iface1_data = ip_prefixes.create_iface(node1)
|
iface1_data = ip_prefixes.create_iface(node1)
|
||||||
iface1, _ = session.add_link(node1.id, node2.id, iface1_data)
|
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
|
||||||
assert iface1
|
assert len(session.link_manager.links()) == 1
|
||||||
|
assert node1.get_iface(iface1.id)
|
||||||
|
assert node2.get_iface(iface2.id)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
session.delete_link(node1.id, node3.id)
|
session.delete_link(node1.id, node3.id, iface1.id, iface2.id)
|
||||||
|
|
||||||
def test_delete_net_to_node_error(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_delete_net_to_node_error(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
|
@ -343,12 +392,14 @@ class TestLinks:
|
||||||
node2 = session.add_node(CoreNode)
|
node2 = session.add_node(CoreNode)
|
||||||
node3 = session.add_node(SwitchNode)
|
node3 = session.add_node(SwitchNode)
|
||||||
iface2_data = ip_prefixes.create_iface(node2)
|
iface2_data = ip_prefixes.create_iface(node2)
|
||||||
_, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
|
||||||
assert iface2
|
assert len(session.link_manager.links()) == 1
|
||||||
|
assert node1.get_iface(iface1.id)
|
||||||
|
assert node2.get_iface(iface2.id)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
session.delete_link(node1.id, node3.id)
|
session.delete_link(node1.id, node3.id, iface1.id, iface2.id)
|
||||||
|
|
||||||
def test_delete_node_to_node_error(self, session: Session, ip_prefixes: IpPrefixes):
|
def test_delete_node_to_node_error(self, session: Session, ip_prefixes: IpPrefixes):
|
||||||
# given
|
# given
|
||||||
|
@ -358,9 +409,10 @@ class TestLinks:
|
||||||
iface1_data = ip_prefixes.create_iface(node1)
|
iface1_data = ip_prefixes.create_iface(node1)
|
||||||
iface2_data = ip_prefixes.create_iface(node2)
|
iface2_data = ip_prefixes.create_iface(node2)
|
||||||
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
||||||
assert iface1
|
assert len(session.link_manager.links()) == 1
|
||||||
assert iface2
|
assert node1.get_iface(iface1.id)
|
||||||
|
assert node2.get_iface(iface2.id)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
session.delete_link(node1.id, node3.id)
|
session.delete_link(node1.id, node3.id, iface1.id, iface2.id)
|
||||||
|
|
|
@ -60,6 +60,40 @@ class TestNodes:
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
session.get_node(node.id, CoreNode)
|
session.get_node(node.id, CoreNode)
|
||||||
|
|
||||||
|
def test_node_add_iface(self, session: Session):
|
||||||
|
# given
|
||||||
|
node = session.add_node(CoreNode)
|
||||||
|
|
||||||
|
# when
|
||||||
|
iface = node.create_iface()
|
||||||
|
|
||||||
|
# then
|
||||||
|
assert iface.id in node.ifaces
|
||||||
|
|
||||||
|
def test_node_get_iface(self, session: Session):
|
||||||
|
# given
|
||||||
|
node = session.add_node(CoreNode)
|
||||||
|
iface = node.create_iface()
|
||||||
|
assert iface.id in node.ifaces
|
||||||
|
|
||||||
|
# when
|
||||||
|
iface2 = node.get_iface(iface.id)
|
||||||
|
|
||||||
|
# then
|
||||||
|
assert iface == iface2
|
||||||
|
|
||||||
|
def test_node_delete_iface(self, session: Session):
|
||||||
|
# given
|
||||||
|
node = session.add_node(CoreNode)
|
||||||
|
iface = node.create_iface()
|
||||||
|
assert iface.id in node.ifaces
|
||||||
|
|
||||||
|
# when
|
||||||
|
node.delete_iface(iface.id)
|
||||||
|
|
||||||
|
# then
|
||||||
|
assert iface.id not in node.ifaces
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"mac,expected",
|
"mac,expected",
|
||||||
[
|
[
|
||||||
|
@ -70,12 +104,11 @@ class TestNodes:
|
||||||
def test_node_set_mac(self, session: Session, mac: str, expected: str):
|
def test_node_set_mac(self, session: Session, mac: str, expected: str):
|
||||||
# given
|
# given
|
||||||
node = session.add_node(CoreNode)
|
node = session.add_node(CoreNode)
|
||||||
switch = session.add_node(SwitchNode)
|
|
||||||
iface_data = InterfaceData()
|
iface_data = InterfaceData()
|
||||||
iface = node.new_iface(switch, iface_data)
|
iface = node.create_iface(iface_data)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
node.set_mac(iface.node_id, mac)
|
iface.set_mac(mac)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert str(iface.mac) == expected
|
assert str(iface.mac) == expected
|
||||||
|
@ -86,13 +119,12 @@ class TestNodes:
|
||||||
def test_node_set_mac_exception(self, session: Session, mac: str):
|
def test_node_set_mac_exception(self, session: Session, mac: str):
|
||||||
# given
|
# given
|
||||||
node = session.add_node(CoreNode)
|
node = session.add_node(CoreNode)
|
||||||
switch = session.add_node(SwitchNode)
|
|
||||||
iface_data = InterfaceData()
|
iface_data = InterfaceData()
|
||||||
iface = node.new_iface(switch, iface_data)
|
iface = node.create_iface(iface_data)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
node.set_mac(iface.node_id, mac)
|
iface.set_mac(mac)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"ip,expected,is_ip6",
|
"ip,expected,is_ip6",
|
||||||
|
@ -106,12 +138,11 @@ class TestNodes:
|
||||||
def test_node_add_ip(self, session: Session, ip: str, expected: str, is_ip6: bool):
|
def test_node_add_ip(self, session: Session, ip: str, expected: str, is_ip6: bool):
|
||||||
# given
|
# given
|
||||||
node = session.add_node(CoreNode)
|
node = session.add_node(CoreNode)
|
||||||
switch = session.add_node(SwitchNode)
|
|
||||||
iface_data = InterfaceData()
|
iface_data = InterfaceData()
|
||||||
iface = node.new_iface(switch, iface_data)
|
iface = node.create_iface(iface_data)
|
||||||
|
|
||||||
# when
|
# when
|
||||||
node.add_ip(iface.node_id, ip)
|
iface.add_ip(ip)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
if is_ip6:
|
if is_ip6:
|
||||||
|
@ -122,14 +153,13 @@ class TestNodes:
|
||||||
def test_node_add_ip_exception(self, session):
|
def test_node_add_ip_exception(self, session):
|
||||||
# given
|
# given
|
||||||
node = session.add_node(CoreNode)
|
node = session.add_node(CoreNode)
|
||||||
switch = session.add_node(SwitchNode)
|
|
||||||
iface_data = InterfaceData()
|
iface_data = InterfaceData()
|
||||||
iface = node.new_iface(switch, iface_data)
|
iface = node.create_iface(iface_data)
|
||||||
ip = "256.168.0.1/24"
|
ip = "256.168.0.1/24"
|
||||||
|
|
||||||
# when
|
# when
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
node.add_ip(iface.node_id, ip)
|
iface.add_ip(ip)
|
||||||
|
|
||||||
@pytest.mark.parametrize("net_type", NET_TYPES)
|
@pytest.mark.parametrize("net_type", NET_TYPES)
|
||||||
def test_net(self, session, net_type):
|
def test_net(self, session, net_type):
|
||||||
|
|
|
@ -10,7 +10,7 @@ from core.emulator.session import Session
|
||||||
from core.errors import CoreError
|
from core.errors import CoreError
|
||||||
from core.location.mobility import BasicRangeModel
|
from core.location.mobility import BasicRangeModel
|
||||||
from core.nodes.base import CoreNode
|
from core.nodes.base import CoreNode
|
||||||
from core.nodes.network import PtpNet, SwitchNode, WlanNode
|
from core.nodes.network import SwitchNode, WlanNode
|
||||||
from core.services.utility import SshService
|
from core.services.utility import SshService
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,25 +65,18 @@ class TestXml:
|
||||||
:param tmpdir: tmpdir to create data in
|
:param tmpdir: tmpdir to create data in
|
||||||
:param ip_prefixes: generates ip addresses for nodes
|
:param ip_prefixes: generates ip addresses for nodes
|
||||||
"""
|
"""
|
||||||
# create ptp
|
|
||||||
ptp_node = session.add_node(PtpNet)
|
|
||||||
|
|
||||||
# create nodes
|
# create nodes
|
||||||
node1 = session.add_node(CoreNode)
|
node1 = session.add_node(CoreNode)
|
||||||
node2 = session.add_node(CoreNode)
|
node2 = session.add_node(CoreNode)
|
||||||
|
|
||||||
# link nodes to ptp net
|
# link nodes
|
||||||
for node in [node1, node2]:
|
iface1_data = ip_prefixes.create_iface(node1)
|
||||||
iface_data = ip_prefixes.create_iface(node)
|
iface2_data = ip_prefixes.create_iface(node2)
|
||||||
session.add_link(node.id, ptp_node.id, iface1_data=iface_data)
|
session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
||||||
|
|
||||||
# instantiate session
|
# instantiate session
|
||||||
session.instantiate()
|
session.instantiate()
|
||||||
|
|
||||||
# get ids for nodes
|
|
||||||
node1_id = node1.id
|
|
||||||
node2_id = node2.id
|
|
||||||
|
|
||||||
# save xml
|
# save xml
|
||||||
xml_file = tmpdir.join("session.xml")
|
xml_file = tmpdir.join("session.xml")
|
||||||
file_path = Path(xml_file.strpath)
|
file_path = Path(xml_file.strpath)
|
||||||
|
@ -98,16 +91,19 @@ class TestXml:
|
||||||
|
|
||||||
# verify nodes have been removed from session
|
# verify nodes have been removed from session
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node1_id, CoreNode)
|
assert not session.get_node(node1.id, CoreNode)
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node2_id, CoreNode)
|
assert not session.get_node(node2.id, CoreNode)
|
||||||
|
# verify no links are known
|
||||||
|
assert len(session.link_manager.links()) == 0
|
||||||
|
|
||||||
# load saved xml
|
# load saved xml
|
||||||
session.open_xml(file_path, start=True)
|
session.open_xml(file_path, start=True)
|
||||||
|
|
||||||
# verify nodes have been recreated
|
# verify nodes have been recreated
|
||||||
assert session.get_node(node1_id, CoreNode)
|
assert session.get_node(node1.id, CoreNode)
|
||||||
assert session.get_node(node2_id, CoreNode)
|
assert session.get_node(node2.id, CoreNode)
|
||||||
|
assert len(session.link_manager.links()) == 1
|
||||||
|
|
||||||
def test_xml_ptp_services(
|
def test_xml_ptp_services(
|
||||||
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
||||||
|
@ -119,18 +115,15 @@ class TestXml:
|
||||||
:param tmpdir: tmpdir to create data in
|
:param tmpdir: tmpdir to create data in
|
||||||
:param ip_prefixes: generates ip addresses for nodes
|
:param ip_prefixes: generates ip addresses for nodes
|
||||||
"""
|
"""
|
||||||
# create ptp
|
|
||||||
ptp_node = session.add_node(PtpNet)
|
|
||||||
|
|
||||||
# create nodes
|
# create nodes
|
||||||
options = NodeOptions(model="host")
|
options = NodeOptions(model="host")
|
||||||
node1 = session.add_node(CoreNode, options=options)
|
node1 = session.add_node(CoreNode, options=options)
|
||||||
node2 = session.add_node(CoreNode)
|
node2 = session.add_node(CoreNode)
|
||||||
|
|
||||||
# link nodes to ptp net
|
# link nodes to ptp net
|
||||||
for node in [node1, node2]:
|
iface1_data = ip_prefixes.create_iface(node1)
|
||||||
iface_data = ip_prefixes.create_iface(node)
|
iface2_data = ip_prefixes.create_iface(node2)
|
||||||
session.add_link(node.id, ptp_node.id, iface1_data=iface_data)
|
session.add_link(node1.id, node2.id, iface1_data, iface2_data)
|
||||||
|
|
||||||
# set custom values for node service
|
# set custom values for node service
|
||||||
session.services.set_service(node1.id, SshService.name)
|
session.services.set_service(node1.id, SshService.name)
|
||||||
|
@ -143,10 +136,6 @@ class TestXml:
|
||||||
# instantiate session
|
# instantiate session
|
||||||
session.instantiate()
|
session.instantiate()
|
||||||
|
|
||||||
# get ids for nodes
|
|
||||||
node1_id = node1.id
|
|
||||||
node2_id = node2.id
|
|
||||||
|
|
||||||
# save xml
|
# save xml
|
||||||
xml_file = tmpdir.join("session.xml")
|
xml_file = tmpdir.join("session.xml")
|
||||||
file_path = Path(xml_file.strpath)
|
file_path = Path(xml_file.strpath)
|
||||||
|
@ -161,9 +150,9 @@ class TestXml:
|
||||||
|
|
||||||
# verify nodes have been removed from session
|
# verify nodes have been removed from session
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node1_id, CoreNode)
|
assert not session.get_node(node1.id, CoreNode)
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node2_id, CoreNode)
|
assert not session.get_node(node2.id, CoreNode)
|
||||||
|
|
||||||
# load saved xml
|
# load saved xml
|
||||||
session.open_xml(file_path, start=True)
|
session.open_xml(file_path, start=True)
|
||||||
|
@ -172,8 +161,8 @@ class TestXml:
|
||||||
service = session.services.get_service(node1.id, SshService.name)
|
service = session.services.get_service(node1.id, SshService.name)
|
||||||
|
|
||||||
# verify nodes have been recreated
|
# verify nodes have been recreated
|
||||||
assert session.get_node(node1_id, CoreNode)
|
assert session.get_node(node1.id, CoreNode)
|
||||||
assert session.get_node(node2_id, CoreNode)
|
assert session.get_node(node2.id, CoreNode)
|
||||||
assert service.config_data.get(service_file) == file_data
|
assert service.config_data.get(service_file) == file_data
|
||||||
|
|
||||||
def test_xml_mobility(
|
def test_xml_mobility(
|
||||||
|
@ -187,8 +176,8 @@ class TestXml:
|
||||||
:param ip_prefixes: generates ip addresses for nodes
|
:param ip_prefixes: generates ip addresses for nodes
|
||||||
"""
|
"""
|
||||||
# create wlan
|
# create wlan
|
||||||
wlan_node = session.add_node(WlanNode)
|
wlan = session.add_node(WlanNode)
|
||||||
session.mobility.set_model(wlan_node, BasicRangeModel, {"test": "1"})
|
session.mobility.set_model(wlan, BasicRangeModel, {"test": "1"})
|
||||||
|
|
||||||
# create nodes
|
# create nodes
|
||||||
options = NodeOptions(model="mdr")
|
options = NodeOptions(model="mdr")
|
||||||
|
@ -199,16 +188,11 @@ class TestXml:
|
||||||
# link nodes
|
# link nodes
|
||||||
for node in [node1, node2]:
|
for node in [node1, node2]:
|
||||||
iface_data = ip_prefixes.create_iface(node)
|
iface_data = ip_prefixes.create_iface(node)
|
||||||
session.add_link(node.id, wlan_node.id, iface1_data=iface_data)
|
session.add_link(node.id, wlan.id, iface1_data=iface_data)
|
||||||
|
|
||||||
# instantiate session
|
# instantiate session
|
||||||
session.instantiate()
|
session.instantiate()
|
||||||
|
|
||||||
# get ids for nodes
|
|
||||||
wlan_id = wlan_node.id
|
|
||||||
node1_id = node1.id
|
|
||||||
node2_id = node2.id
|
|
||||||
|
|
||||||
# save xml
|
# save xml
|
||||||
xml_file = tmpdir.join("session.xml")
|
xml_file = tmpdir.join("session.xml")
|
||||||
file_path = Path(xml_file.strpath)
|
file_path = Path(xml_file.strpath)
|
||||||
|
@ -223,20 +207,20 @@ class TestXml:
|
||||||
|
|
||||||
# verify nodes have been removed from session
|
# verify nodes have been removed from session
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node1_id, CoreNode)
|
assert not session.get_node(node1.id, CoreNode)
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node2_id, CoreNode)
|
assert not session.get_node(node2.id, CoreNode)
|
||||||
|
|
||||||
# load saved xml
|
# load saved xml
|
||||||
session.open_xml(file_path, start=True)
|
session.open_xml(file_path, start=True)
|
||||||
|
|
||||||
# retrieve configuration we set originally
|
# retrieve configuration we set originally
|
||||||
value = str(session.mobility.get_config("test", wlan_id, BasicRangeModel.name))
|
value = str(session.mobility.get_config("test", wlan.id, BasicRangeModel.name))
|
||||||
|
|
||||||
# verify nodes and configuration were restored
|
# verify nodes and configuration were restored
|
||||||
assert session.get_node(node1_id, CoreNode)
|
assert session.get_node(node1.id, CoreNode)
|
||||||
assert session.get_node(node2_id, CoreNode)
|
assert session.get_node(node2.id, CoreNode)
|
||||||
assert session.get_node(wlan_id, WlanNode)
|
assert session.get_node(wlan.id, WlanNode)
|
||||||
assert value == "1"
|
assert value == "1"
|
||||||
|
|
||||||
def test_network_to_network(self, session: Session, tmpdir: TemporaryFile):
|
def test_network_to_network(self, session: Session, tmpdir: TemporaryFile):
|
||||||
|
@ -256,10 +240,6 @@ class TestXml:
|
||||||
# instantiate session
|
# instantiate session
|
||||||
session.instantiate()
|
session.instantiate()
|
||||||
|
|
||||||
# get ids for nodes
|
|
||||||
node1_id = switch1.id
|
|
||||||
node2_id = switch2.id
|
|
||||||
|
|
||||||
# save xml
|
# save xml
|
||||||
xml_file = tmpdir.join("session.xml")
|
xml_file = tmpdir.join("session.xml")
|
||||||
file_path = Path(xml_file.strpath)
|
file_path = Path(xml_file.strpath)
|
||||||
|
@ -274,19 +254,19 @@ class TestXml:
|
||||||
|
|
||||||
# verify nodes have been removed from session
|
# verify nodes have been removed from session
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node1_id, SwitchNode)
|
assert not session.get_node(switch1.id, SwitchNode)
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node2_id, SwitchNode)
|
assert not session.get_node(switch2.id, SwitchNode)
|
||||||
|
|
||||||
# load saved xml
|
# load saved xml
|
||||||
session.open_xml(file_path, start=True)
|
session.open_xml(file_path, start=True)
|
||||||
|
|
||||||
# verify nodes have been recreated
|
# verify nodes have been recreated
|
||||||
switch1 = session.get_node(node1_id, SwitchNode)
|
switch1 = session.get_node(switch1.id, SwitchNode)
|
||||||
switch2 = session.get_node(node2_id, SwitchNode)
|
switch2 = session.get_node(switch2.id, SwitchNode)
|
||||||
assert switch1
|
assert switch1
|
||||||
assert switch2
|
assert switch2
|
||||||
assert len(switch1.links() + switch2.links()) == 1
|
assert len(session.link_manager.links()) == 1
|
||||||
|
|
||||||
def test_link_options(
|
def test_link_options(
|
||||||
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
||||||
|
@ -316,10 +296,6 @@ class TestXml:
|
||||||
# instantiate session
|
# instantiate session
|
||||||
session.instantiate()
|
session.instantiate()
|
||||||
|
|
||||||
# get ids for nodes
|
|
||||||
node1_id = node1.id
|
|
||||||
node2_id = switch.id
|
|
||||||
|
|
||||||
# save xml
|
# save xml
|
||||||
xml_file = tmpdir.join("session.xml")
|
xml_file = tmpdir.join("session.xml")
|
||||||
file_path = Path(xml_file.strpath)
|
file_path = Path(xml_file.strpath)
|
||||||
|
@ -334,27 +310,25 @@ class TestXml:
|
||||||
|
|
||||||
# verify nodes have been removed from session
|
# verify nodes have been removed from session
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node1_id, CoreNode)
|
assert not session.get_node(node1.id, CoreNode)
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node2_id, SwitchNode)
|
assert not session.get_node(switch.id, SwitchNode)
|
||||||
|
|
||||||
# load saved xml
|
# load saved xml
|
||||||
session.open_xml(file_path, start=True)
|
session.open_xml(file_path, start=True)
|
||||||
|
|
||||||
# verify nodes have been recreated
|
# verify nodes have been recreated
|
||||||
assert session.get_node(node1_id, CoreNode)
|
assert session.get_node(node1.id, CoreNode)
|
||||||
assert session.get_node(node2_id, SwitchNode)
|
assert session.get_node(switch.id, SwitchNode)
|
||||||
links = []
|
assert len(session.link_manager.links()) == 1
|
||||||
for node_id in session.nodes:
|
link = list(session.link_manager.links())[0]
|
||||||
node = session.nodes[node_id]
|
link_options = link.options()
|
||||||
links += node.links()
|
assert options.loss == link_options.loss
|
||||||
link = links[0]
|
assert options.bandwidth == link_options.bandwidth
|
||||||
assert options.loss == link.options.loss
|
assert options.jitter == link_options.jitter
|
||||||
assert options.bandwidth == link.options.bandwidth
|
assert options.delay == link_options.delay
|
||||||
assert options.jitter == link.options.jitter
|
assert options.dup == link_options.dup
|
||||||
assert options.delay == link.options.delay
|
assert options.buffer == link_options.buffer
|
||||||
assert options.dup == link.options.dup
|
|
||||||
assert options.buffer == link.options.buffer
|
|
||||||
|
|
||||||
def test_link_options_ptp(
|
def test_link_options_ptp(
|
||||||
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
||||||
|
@ -385,10 +359,6 @@ class TestXml:
|
||||||
# instantiate session
|
# instantiate session
|
||||||
session.instantiate()
|
session.instantiate()
|
||||||
|
|
||||||
# get ids for nodes
|
|
||||||
node1_id = node1.id
|
|
||||||
node2_id = node2.id
|
|
||||||
|
|
||||||
# save xml
|
# save xml
|
||||||
xml_file = tmpdir.join("session.xml")
|
xml_file = tmpdir.join("session.xml")
|
||||||
file_path = Path(xml_file.strpath)
|
file_path = Path(xml_file.strpath)
|
||||||
|
@ -403,27 +373,25 @@ class TestXml:
|
||||||
|
|
||||||
# verify nodes have been removed from session
|
# verify nodes have been removed from session
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node1_id, CoreNode)
|
assert not session.get_node(node1.id, CoreNode)
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node2_id, CoreNode)
|
assert not session.get_node(node2.id, CoreNode)
|
||||||
|
|
||||||
# load saved xml
|
# load saved xml
|
||||||
session.open_xml(file_path, start=True)
|
session.open_xml(file_path, start=True)
|
||||||
|
|
||||||
# verify nodes have been recreated
|
# verify nodes have been recreated
|
||||||
assert session.get_node(node1_id, CoreNode)
|
assert session.get_node(node1.id, CoreNode)
|
||||||
assert session.get_node(node2_id, CoreNode)
|
assert session.get_node(node2.id, CoreNode)
|
||||||
links = []
|
assert len(session.link_manager.links()) == 1
|
||||||
for node_id in session.nodes:
|
link = list(session.link_manager.links())[0]
|
||||||
node = session.nodes[node_id]
|
link_options = link.options()
|
||||||
links += node.links()
|
assert options.loss == link_options.loss
|
||||||
link = links[0]
|
assert options.bandwidth == link_options.bandwidth
|
||||||
assert options.loss == link.options.loss
|
assert options.jitter == link_options.jitter
|
||||||
assert options.bandwidth == link.options.bandwidth
|
assert options.delay == link_options.delay
|
||||||
assert options.jitter == link.options.jitter
|
assert options.dup == link_options.dup
|
||||||
assert options.delay == link.options.delay
|
assert options.buffer == link_options.buffer
|
||||||
assert options.dup == link.options.dup
|
|
||||||
assert options.buffer == link.options.buffer
|
|
||||||
|
|
||||||
def test_link_options_bidirectional(
|
def test_link_options_bidirectional(
|
||||||
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
|
||||||
|
@ -450,7 +418,9 @@ class TestXml:
|
||||||
options1.dup = 5
|
options1.dup = 5
|
||||||
options1.jitter = 5
|
options1.jitter = 5
|
||||||
options1.buffer = 50
|
options1.buffer = 50
|
||||||
session.add_link(node1.id, node2.id, iface1_data, iface2_data, options1)
|
iface1, iface2 = session.add_link(
|
||||||
|
node1.id, node2.id, iface1_data, iface2_data, options1
|
||||||
|
)
|
||||||
options2 = LinkOptions()
|
options2 = LinkOptions()
|
||||||
options2.unidirectional = 1
|
options2.unidirectional = 1
|
||||||
options2.bandwidth = 10000
|
options2.bandwidth = 10000
|
||||||
|
@ -459,17 +429,11 @@ class TestXml:
|
||||||
options2.dup = 10
|
options2.dup = 10
|
||||||
options2.jitter = 10
|
options2.jitter = 10
|
||||||
options2.buffer = 100
|
options2.buffer = 100
|
||||||
session.update_link(
|
session.update_link(node2.id, node1.id, iface2.id, iface1.id, options2)
|
||||||
node2.id, node1.id, iface2_data.id, iface1_data.id, options2
|
|
||||||
)
|
|
||||||
|
|
||||||
# instantiate session
|
# instantiate session
|
||||||
session.instantiate()
|
session.instantiate()
|
||||||
|
|
||||||
# get ids for nodes
|
|
||||||
node1_id = node1.id
|
|
||||||
node2_id = node2.id
|
|
||||||
|
|
||||||
# save xml
|
# save xml
|
||||||
xml_file = tmpdir.join("session.xml")
|
xml_file = tmpdir.join("session.xml")
|
||||||
file_path = Path(xml_file.strpath)
|
file_path = Path(xml_file.strpath)
|
||||||
|
@ -484,32 +448,26 @@ class TestXml:
|
||||||
|
|
||||||
# verify nodes have been removed from session
|
# verify nodes have been removed from session
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node1_id, CoreNode)
|
assert not session.get_node(node1.id, CoreNode)
|
||||||
with pytest.raises(CoreError):
|
with pytest.raises(CoreError):
|
||||||
assert not session.get_node(node2_id, CoreNode)
|
assert not session.get_node(node2.id, CoreNode)
|
||||||
|
|
||||||
# load saved xml
|
# load saved xml
|
||||||
session.open_xml(file_path, start=True)
|
session.open_xml(file_path, start=True)
|
||||||
|
|
||||||
# verify nodes have been recreated
|
# verify nodes have been recreated
|
||||||
assert session.get_node(node1_id, CoreNode)
|
assert session.get_node(node1.id, CoreNode)
|
||||||
assert session.get_node(node2_id, CoreNode)
|
assert session.get_node(node2.id, CoreNode)
|
||||||
links = []
|
assert len(session.link_manager.links()) == 1
|
||||||
for node_id in session.nodes:
|
assert options1.bandwidth == iface1.options.bandwidth
|
||||||
node = session.nodes[node_id]
|
assert options1.delay == iface1.options.delay
|
||||||
links += node.links()
|
assert options1.loss == iface1.options.loss
|
||||||
assert len(links) == 2
|
assert options1.dup == iface1.options.dup
|
||||||
link1 = links[0]
|
assert options1.jitter == iface1.options.jitter
|
||||||
link2 = links[1]
|
assert options1.buffer == iface1.options.buffer
|
||||||
assert options1.bandwidth == link1.options.bandwidth
|
assert options2.bandwidth == iface2.options.bandwidth
|
||||||
assert options1.delay == link1.options.delay
|
assert options2.delay == iface2.options.delay
|
||||||
assert options1.loss == link1.options.loss
|
assert options2.loss == iface2.options.loss
|
||||||
assert options1.dup == link1.options.dup
|
assert options2.dup == iface2.options.dup
|
||||||
assert options1.jitter == link1.options.jitter
|
assert options2.jitter == iface2.options.jitter
|
||||||
assert options1.buffer == link1.options.buffer
|
assert options2.buffer == iface2.options.buffer
|
||||||
assert options2.bandwidth == link2.options.bandwidth
|
|
||||||
assert options2.delay == link2.options.delay
|
|
||||||
assert options2.loss == link2.options.loss
|
|
||||||
assert options2.dup == link2.options.dup
|
|
||||||
assert options2.jitter == link2.options.jitter
|
|
||||||
assert options2.buffer == link2.options.buffer
|
|
||||||
|
|
|
@ -10,17 +10,14 @@
|
||||||
* Nodes are created using Linux namespaces
|
* Nodes are created using Linux namespaces
|
||||||
* Links are created using Linux bridges and virtual ethernet peers
|
* Links are created using Linux bridges and virtual ethernet peers
|
||||||
* Packets sent over links are manipulated using traffic control
|
* Packets sent over links are manipulated using traffic control
|
||||||
* Controlled via the CORE GUI
|
* Provides gRPC API
|
||||||
* Provides both a custom TLV API and gRPC API
|
|
||||||
* Python program that leverages a small C binary for node creation
|
|
||||||
* core-gui
|
* core-gui
|
||||||
* GUI and daemon communicate over the custom TLV API
|
* GUI and daemon communicate over gRPC API
|
||||||
* Drag and drop creation for nodes and links
|
* Drag and drop creation for nodes and links
|
||||||
* Can launch terminals for emulated nodes in running sessions
|
* Can launch terminals for emulated nodes in running sessions
|
||||||
* Can save/open scenario files to recreate previous sessions
|
* Can save/open scenario files to recreate previous sessions
|
||||||
* TCL/TK program
|
* vnoded
|
||||||
* coresendmsg
|
* Command line utility for creating CORE node namespaces
|
||||||
* Command line utility for sending TLV API messages to the core-daemon
|
|
||||||
* vcmd
|
* vcmd
|
||||||
* Command line utility for sending shell commands to nodes
|
* Command line utility for sending shell commands to nodes
|
||||||
|
|
||||||
|
@ -55,21 +52,10 @@ Nftables provides Ethernet frame filtering on Linux bridges. Wireless networks a
|
||||||
emulated by controlling which interfaces can send and receive with nftables
|
emulated by controlling which interfaces can send and receive with nftables
|
||||||
rules.
|
rules.
|
||||||
|
|
||||||
## Prior Work
|
|
||||||
|
|
||||||
The Tcl/Tk CORE GUI was originally derived from the open source
|
|
||||||
[IMUNES](http://imunes.net) project from the University of Zagreb as a custom
|
|
||||||
project within Boeing Research and Technology's Network Technology research
|
|
||||||
group in 2004. Since then they have developed the CORE framework to use Linux
|
|
||||||
namespacing, have developed a Python framework, and made numerous user and
|
|
||||||
kernel-space developments, such as support for wireless networks, IPsec,
|
|
||||||
distribute emulation, simulation integration, and more. The IMUNES project
|
|
||||||
also consists of userspace and kernel components.
|
|
||||||
|
|
||||||
## Open Source Project and Resources
|
## Open Source Project and Resources
|
||||||
|
|
||||||
CORE has been released by Boeing to the open source community under the BSD
|
CORE has been released by Boeing to the open source community under the BSD
|
||||||
license. If you find CORE useful for your work, please contribute back to the
|
license. If you find CORE useful for your work, please contribute back to the
|
||||||
project. Contributions can be as simple as reporting a bug, dropping a line of
|
project. Contributions can be as simple as reporting a bug, dropping a line of
|
||||||
encouragement or technical suggestions to the mailing lists, or can also
|
encouragement, or can also include submitting patches or maintaining aspects
|
||||||
include submitting patches or maintaining aspects of the tool.
|
of the tool.
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
## Repository Overview
|
## Repository Overview
|
||||||
|
|
||||||
The CORE source consists of several different programming languages for
|
The CORE source consists of several programming languages for
|
||||||
historical reasons. Current development focuses on the Python modules and
|
historical reasons. Current development focuses on the Python modules and
|
||||||
daemon. Here is a brief description of the source directories.
|
daemon. Here is a brief description of the source directories.
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ daemon. Here is a brief description of the source directories.
|
||||||
|-----------|--------------------------------------------------------------------------------------|
|
|-----------|--------------------------------------------------------------------------------------|
|
||||||
| daemon | Python CORE daemon/gui code that handles receiving API calls and creating containers |
|
| daemon | Python CORE daemon/gui code that handles receiving API calls and creating containers |
|
||||||
| docs | Markdown Documentation currently hosted on GitHub |
|
| docs | Markdown Documentation currently hosted on GitHub |
|
||||||
| gui | Tcl/Tk GUI |
|
|
||||||
| man | Template files for creating man pages for various CORE command line utilities |
|
| man | Template files for creating man pages for various CORE command line utilities |
|
||||||
| netns | C program for creating CORE containers |
|
| netns | C program for creating CORE containers |
|
||||||
|
|
||||||
|
@ -58,7 +57,7 @@ sudo core-daemon
|
||||||
# run python gui
|
# run python gui
|
||||||
core-pygui
|
core-pygui
|
||||||
|
|
||||||
# run tcl gui
|
# run gui
|
||||||
core-gui
|
core-gui
|
||||||
|
|
||||||
# run mocked unit tests
|
# run mocked unit tests
|
||||||
|
|
|
@ -9,7 +9,6 @@ skinparam {
|
||||||
|
|
||||||
package User {
|
package User {
|
||||||
component "core-gui" as gui #DeepSkyBlue
|
component "core-gui" as gui #DeepSkyBlue
|
||||||
component "coresendmsg" #DeepSkyBlue
|
|
||||||
component "python scripts" as scripts #DeepSkyBlue
|
component "python scripts" as scripts #DeepSkyBlue
|
||||||
component vcmd #DeepSkyBlue
|
component vcmd #DeepSkyBlue
|
||||||
}
|
}
|
||||||
|
@ -31,15 +30,11 @@ package "Linux System" {
|
||||||
}
|
}
|
||||||
|
|
||||||
package API {
|
package API {
|
||||||
interface TLV as tlv
|
|
||||||
interface gRPC as grpc
|
interface gRPC as grpc
|
||||||
}
|
}
|
||||||
|
|
||||||
gui <..> tlv
|
gui <..> grpc
|
||||||
coresendmsg <..> tlv
|
|
||||||
scripts <..> tlv
|
|
||||||
scripts <..> grpc
|
scripts <..> grpc
|
||||||
tlv -- daemon
|
|
||||||
grpc -- daemon
|
grpc -- daemon
|
||||||
scripts -- core
|
scripts -- core
|
||||||
daemon - core
|
daemon - core
|
||||||
|
|
|
@ -5,7 +5,7 @@ skinparam {
|
||||||
StateBackgroundColor #LightSteelBlue
|
StateBackgroundColor #LightSteelBlue
|
||||||
}
|
}
|
||||||
|
|
||||||
Definition: Session XML/IMN
|
Definition: Session XML
|
||||||
Definition: GUI Drawing
|
Definition: GUI Drawing
|
||||||
Definition: Scripts
|
Definition: Scripts
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,7 @@ connect_kwargs: {"key_filename": "/home/user/.ssh/core"}
|
||||||
|
|
||||||
Within the core-gui navigate to menu option:
|
Within the core-gui navigate to menu option:
|
||||||
|
|
||||||
**Session -> Emulation servers...**
|
**Session -> Servers...**
|
||||||
|
|
||||||
Within the dialog box presented, add or modify an existing server if present
|
Within the dialog box presented, add or modify an existing server if present
|
||||||
to use the name, address, and port for the a server you plan to use.
|
to use the name, address, and port for the a server you plan to use.
|
||||||
|
@ -132,12 +132,6 @@ to use the name, address, and port for the a server you plan to use.
|
||||||
Server configurations are loaded and written to in a configuration file for
|
Server configurations are loaded and written to in a configuration file for
|
||||||
the GUI.
|
the GUI.
|
||||||
|
|
||||||
**~/.core/servers.conf**
|
|
||||||
```conf
|
|
||||||
# name address port
|
|
||||||
server2 192.168.0.2 4038
|
|
||||||
```
|
|
||||||
|
|
||||||
## Assigning Nodes
|
## Assigning Nodes
|
||||||
|
|
||||||
The user needs to assign nodes to emulation servers in the scenario. Making no
|
The user needs to assign nodes to emulation servers in the scenario. Making no
|
||||||
|
|
|
@ -80,7 +80,7 @@ EMANE. An example emane section from the **core.conf** file is shown below:
|
||||||
emane_platform_port = 8101
|
emane_platform_port = 8101
|
||||||
emane_transform_port = 8201
|
emane_transform_port = 8201
|
||||||
emane_event_monitor = False
|
emane_event_monitor = False
|
||||||
#emane_models_dir = /home/username/.core/myemane
|
#emane_models_dir = /home/<user>/.coregui/custom_emane
|
||||||
# EMANE log level range [0,4] default: 2
|
# EMANE log level range [0,4] default: 2
|
||||||
emane_log_level = 2
|
emane_log_level = 2
|
||||||
emane_realtime = True
|
emane_realtime = True
|
||||||
|
@ -242,7 +242,7 @@ directory to find the generated EMANE xml files. One easy way to view
|
||||||
this information is by double-clicking one of the virtual nodes and listing the files
|
this information is by double-clicking one of the virtual nodes and listing the files
|
||||||
in the shell.
|
in the shell.
|
||||||
|
|
||||||
![](static/single-pc-emane.png)
|
![](static/emane-single-pc.png)
|
||||||
|
|
||||||
## Distributed EMANE
|
## Distributed EMANE
|
||||||
|
|
||||||
|
@ -277,7 +277,7 @@ it will be emulated locally.
|
||||||
Using the EMANE node configuration dialog. You can change the EMANE model
|
Using the EMANE node configuration dialog. You can change the EMANE model
|
||||||
being used, along with changing any configuration setting from their defaults.
|
being used, along with changing any configuration setting from their defaults.
|
||||||
|
|
||||||
![](static/distributed-emane-configuration.png)
|
![](static/emane-configuration.png)
|
||||||
|
|
||||||
> **NOTE:** Here is a quick checklist for distributed emulation with EMANE.
|
> **NOTE:** Here is a quick checklist for distributed emulation with EMANE.
|
||||||
|
|
||||||
|
@ -304,5 +304,3 @@ Double-clicking on a node during runtime will cause the GUI to attempt to SSH
|
||||||
to the emulation server for that node and run an interactive shell. The public
|
to the emulation server for that node and run an interactive shell. The public
|
||||||
key SSH configuration should be tested with all emulation servers prior to
|
key SSH configuration should be tested with all emulation servers prior to
|
||||||
starting the emulation.
|
starting the emulation.
|
||||||
|
|
||||||
![](static/distributed-emane-network.png)
|
|
||||||
|
|
|
@ -335,7 +335,7 @@ Create `/tmp/emane/blockageaft.xml` with the following contents.
|
||||||
## Run Demo
|
## Run Demo
|
||||||
1. Select `Open...` within the GUI
|
1. Select `Open...` within the GUI
|
||||||
1. Load `emane-demo-antenna.xml`
|
1. Load `emane-demo-antenna.xml`
|
||||||
1. Click ![Start Button](../static/gui/start.gif)
|
1. Click ![Start Button](../static/gui/start.png)
|
||||||
1. After startup completes, double click n1 to bring up the nodes terminal
|
1. After startup completes, double click n1 to bring up the nodes terminal
|
||||||
|
|
||||||
## Example Demo
|
## Example Demo
|
||||||
|
|
|
@ -11,7 +11,7 @@ for more specifics.
|
||||||
## Run Demo
|
## Run Demo
|
||||||
1. Select `Open...` within the GUI
|
1. Select `Open...` within the GUI
|
||||||
1. Load `emane-demo-eel.xml`
|
1. Load `emane-demo-eel.xml`
|
||||||
1. Click ![Start Button](../static/gui/start.gif)
|
1. Click ![Start Button](../static/gui/start.png)
|
||||||
1. After startup completes, double click n1 to bring up the nodes terminal
|
1. After startup completes, double click n1 to bring up the nodes terminal
|
||||||
|
|
||||||
## Example Demo
|
## Example Demo
|
||||||
|
|
|
@ -12,7 +12,7 @@ may provide more helpful details.
|
||||||
## Run Demo
|
## Run Demo
|
||||||
1. Select `Open...` within the GUI
|
1. Select `Open...` within the GUI
|
||||||
1. Load `emane-demo-files.xml`
|
1. Load `emane-demo-files.xml`
|
||||||
1. Click ![Start Button](../static/gui/start.gif)
|
1. Click ![Start Button](../static/gui/start.png)
|
||||||
1. After startup completes, double click n1 to bring up the nodes terminal
|
1. After startup completes, double click n1 to bring up the nodes terminal
|
||||||
|
|
||||||
## Example Demo
|
## Example Demo
|
||||||
|
@ -21,14 +21,14 @@ case we are running the RF Pipe model.
|
||||||
|
|
||||||
### Generated Files
|
### Generated Files
|
||||||
|
|
||||||
|Name|Description|
|
| Name | Description |
|
||||||
|---|---|
|
|-------------------------------------|------------------------------------------------------|
|
||||||
|\<node name>-platform.xml|configuration file for the emulator instances|
|
| \<node name>-platform.xml | configuration file for the emulator instances |
|
||||||
|\<interface name>-nem.xml|configuration for creating a NEM|
|
| \<interface name>-nem.xml | configuration for creating a NEM |
|
||||||
|\<interface name>-mac.xml|configuration for defining a NEMs MAC layer|
|
| \<interface name>-mac.xml | configuration for defining a NEMs MAC layer |
|
||||||
|\<interface name>-phy.xml|configuration for defining a NEMs PHY layer|
|
| \<interface name>-phy.xml | configuration for defining a NEMs PHY layer |
|
||||||
|\<interface name>-trans-virtual.xml|configuration when a virtual transport is being used|
|
| \<interface name>-trans-virtual.xml | configuration when a virtual transport is being used |
|
||||||
|\<interface name>-trans.xml|configuration when a raw transport is being used|
|
| \<interface name>-trans.xml | configuration when a raw transport is being used |
|
||||||
|
|
||||||
### Listing File
|
### Listing File
|
||||||
Below are the files within n1 after starting the demo session.
|
Below are the files within n1 after starting the demo session.
|
||||||
|
|
|
@ -13,7 +13,7 @@ may provide more helpful details.
|
||||||
## Run Demo
|
## Run Demo
|
||||||
1. Select `Open...` within the GUI
|
1. Select `Open...` within the GUI
|
||||||
1. Load `emane-demo-gpsd.xml`
|
1. Load `emane-demo-gpsd.xml`
|
||||||
1. Click ![Start Button](../static/gui/start.gif)
|
1. Click ![Start Button](../static/gui/start.png)
|
||||||
1. After startup completes, double click n1 to bring up the nodes terminal
|
1. After startup completes, double click n1 to bring up the nodes terminal
|
||||||
|
|
||||||
## Example Demo
|
## Example Demo
|
||||||
|
|
|
@ -11,7 +11,7 @@ for more specifics.
|
||||||
## Run Demo
|
## Run Demo
|
||||||
1. Select `Open...` within the GUI
|
1. Select `Open...` within the GUI
|
||||||
1. Load `emane-demo-precomputed.xml`
|
1. Load `emane-demo-precomputed.xml`
|
||||||
1. Click ![Start Button](../static/gui/start.gif)
|
1. Click ![Start Button](../static/gui/start.png)
|
||||||
1. After startup completes, double click n1 to bring up the nodes terminal
|
1. After startup completes, double click n1 to bring up the nodes terminal
|
||||||
|
|
||||||
## Example Demo
|
## Example Demo
|
||||||
|
|
440
docs/gui.md
|
@ -1,12 +1,10 @@
|
||||||
|
|
||||||
# Using the CORE GUI
|
# CORE GUI
|
||||||
|
|
||||||
* Table of Contents
|
* Table of Contents
|
||||||
{:toc}
|
{:toc}
|
||||||
|
|
||||||
The following image shows the CORE GUI:
|
![](static/core-gui.png)
|
||||||
![](static/core_screenshot.png)
|
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
@ -28,45 +26,54 @@ Beyond installing CORE, you must have the CORE daemon running. This is done
|
||||||
on the command line with either systemd or sysv.
|
on the command line with either systemd or sysv.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# systemd
|
# systemd service
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl start core-daemon
|
sudo systemctl start core-daemon
|
||||||
|
|
||||||
# sysv
|
|
||||||
sudo service core-daemon start
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also invoke the daemon directly from the command line, which can be
|
|
||||||
useful if you'd like to see the logging output directly.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# direct invocation
|
# direct invocation
|
||||||
sudo core-daemon
|
sudo core-daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## GUI Files
|
||||||
|
|
||||||
|
The GUI will create a directory in your home directory on first run called
|
||||||
|
~/.coregui. This directory will help layout various files that the GUI may use.
|
||||||
|
|
||||||
|
* .coregui/
|
||||||
|
* backgrounds/
|
||||||
|
* place backgrounds used for display in the GUI
|
||||||
|
* custom_emane/
|
||||||
|
* place to keep custom emane models to use with the core-daemon
|
||||||
|
* custom_services/
|
||||||
|
* place to keep custom services to use with the core-daemon
|
||||||
|
* icons/
|
||||||
|
* icons the GUI uses along with customs icons desired
|
||||||
|
* mobility/
|
||||||
|
* place to keep custom mobility files
|
||||||
|
* scripts/
|
||||||
|
* place to keep core related scripts
|
||||||
|
* xmls/
|
||||||
|
* place to keep saved session xml files
|
||||||
|
* gui.log
|
||||||
|
* log file when running the gui, look here when issues occur for exceptions etc
|
||||||
|
* config.yaml
|
||||||
|
* configuration file used to save/load various gui related settings (custom nodes, layouts, addresses, etc)
|
||||||
|
|
||||||
## Modes of Operation
|
## Modes of Operation
|
||||||
|
|
||||||
The CORE GUI has two primary modes of operation, **Edit** and **Execute**
|
The CORE GUI has two primary modes of operation, **Edit** and **Execute**
|
||||||
modes. Running the GUI, by typing **core-gui** with no options, starts in
|
modes. Running the GUI, by typing **core-pygui** with no options, starts in
|
||||||
Edit mode. Nodes are drawn on a blank canvas using the toolbar on the left
|
Edit mode. Nodes are drawn on a blank canvas using the toolbar on the left
|
||||||
and configured from right-click menus or by double-clicking them. The GUI
|
and configured from right-click menus or by double-clicking them. The GUI
|
||||||
does not need to be run as root.
|
does not need to be run as root.
|
||||||
|
|
||||||
Once editing is complete, pressing the green **Start** button (or choosing
|
Once editing is complete, pressing the green **Start** button instantiates
|
||||||
**Execute** from the **Session** menu) instantiates the topology within the
|
the topology and enters Execute mode. In execute mode,
|
||||||
Linux kernel and enters Execute mode. In execute mode, the user can interact
|
the user can interact with the running emulated machines by double-clicking or
|
||||||
with the running emulated machines by double-clicking or right-clicking on
|
right-clicking on them. The editing toolbar disappears and is replaced by an
|
||||||
them. The editing toolbar disappears and is replaced by an execute toolbar,
|
execute toolbar, which provides tools while running the emulation. Pressing
|
||||||
which provides tools while running the emulation. Pressing the red **Stop**
|
the red **Stop** button will destroy the running emulation and return CORE
|
||||||
button (or choosing **Terminate** from the **Session** menu) will destroy
|
to Edit mode.
|
||||||
the running emulation and return CORE to Edit mode.
|
|
||||||
|
|
||||||
CORE can be started directly in Execute mode by specifying **--start** and a
|
|
||||||
topology file on the command line:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
core-gui --start ~/.core/configs/myfile.imn
|
|
||||||
```
|
|
||||||
|
|
||||||
Once the emulation is running, the GUI can be closed, and a prompt will appear
|
Once the emulation is running, the GUI can be closed, and a prompt will appear
|
||||||
asking if the emulation should be terminated. The emulation may be left
|
asking if the emulation should be terminated. The emulation may be left
|
||||||
|
@ -74,11 +81,22 @@ running and the GUI can reconnect to an existing session at a later time.
|
||||||
|
|
||||||
The GUI can be run as a normal user on Linux.
|
The GUI can be run as a normal user on Linux.
|
||||||
|
|
||||||
The GUI can be connected to a different address or TCP port using the
|
The GUI currently provides the following options on startup.
|
||||||
**--address** and/or **--port** options. The defaults are shown below.
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
core-gui --address 127.0.0.1 --port 4038
|
usage: core-gui [-h] [-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [-p]
|
||||||
|
[-s SESSION] [--create-dir]
|
||||||
|
|
||||||
|
CORE Python GUI
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
||||||
|
logging level
|
||||||
|
-p, --proxy enable proxy
|
||||||
|
-s SESSION, --session SESSION
|
||||||
|
session id to join
|
||||||
|
--create-dir create gui directory and exit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Toolbar
|
## Toolbar
|
||||||
|
@ -95,22 +113,21 @@ sub-menus, which appear when you click on their group icon.
|
||||||
|
|
||||||
| Icon | Name | Description |
|
| Icon | Name | Description |
|
||||||
|----------------------------|----------------|----------------------------------------------------------------------------------------|
|
|----------------------------|----------------|----------------------------------------------------------------------------------------|
|
||||||
| ![](static/gui/select.gif) | Selection Tool | Tool for selecting, moving, configuring nodes. |
|
| ![](static/gui/select.png) | Selection Tool | Tool for selecting, moving, configuring nodes. |
|
||||||
| ![](static/gui/start.gif) | Start Button | Starts Execute mode, instantiates the emulation. |
|
| ![](static/gui/start.png) | Start Button | Starts Execute mode, instantiates the emulation. |
|
||||||
| ![](static/gui/link.gif) | Link | Allows network links to be drawn between two nodes by clicking and dragging the mouse. |
|
| ![](static/gui/link.png) | Link | Allows network links to be drawn between two nodes by clicking and dragging the mouse. |
|
||||||
|
|
||||||
### CORE Nodes
|
### CORE Nodes
|
||||||
|
|
||||||
These nodes will create a new node container and run associated services.
|
These nodes will create a new node container and run associated services.
|
||||||
|
|
||||||
| Icon | Name | Description |
|
| Icon | Name | Description |
|
||||||
|-----------------------------------------|---------|------------------------------------------------------------------------------|
|
|----------------------------|---------|------------------------------------------------------------------------------|
|
||||||
| ![](static/gui/router.gif) | Router | Runs Quagga OSPFv2 and OSPFv3 routing to forward packets. |
|
| ![](static/gui/router.png) | Router | Runs Quagga OSPFv2 and OSPFv3 routing to forward packets. |
|
||||||
| ![](static/gui/host.gif) | Host | Emulated server machine having a default route, runs SSH server. |
|
| ![](static/gui/host.png) | Host | Emulated server machine having a default route, runs SSH server. |
|
||||||
| ![](static/gui/pc.gif) | PC | Basic emulated machine having a default route, runs no processes by default. |
|
| ![](static/gui/pc.png) | PC | Basic emulated machine having a default route, runs no processes by default. |
|
||||||
| ![](static/gui/mdr.gif) | MDR | Runs Quagga OSPFv3 MDR routing for MANET-optimized routing. |
|
| ![](static/gui/mdr.png) | MDR | Runs Quagga OSPFv3 MDR routing for MANET-optimized routing. |
|
||||||
| ![](static/gui/router_green.gif) | PRouter | Physical router represents a real testbed machine. |
|
| ![](static/gui/router.png) | PRouter | Physical router represents a real testbed machine. |
|
||||||
| ![](static/gui/document-properties.gif) | Edit | Bring up the custom node dialog. |
|
|
||||||
|
|
||||||
### Network Nodes
|
### Network Nodes
|
||||||
|
|
||||||
|
@ -119,20 +136,20 @@ purpose described below.
|
||||||
|
|
||||||
| Icon | Name | Description |
|
| Icon | Name | Description |
|
||||||
|-------------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-------------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| ![](static/gui/hub.gif) | Hub | Ethernet hub forwards incoming packets to every connected node. |
|
| ![](static/gui/hub.png) | Hub | Ethernet hub forwards incoming packets to every connected node. |
|
||||||
| ![](static/gui/lanswitch.gif) | Switch | Ethernet switch intelligently forwards incoming packets to attached hosts using an Ethernet address hash table. |
|
| ![](static/gui/lanswitch.png) | Switch | Ethernet switch intelligently forwards incoming packets to attached hosts using an Ethernet address hash table. |
|
||||||
| ![](static/gui/wlan.gif) | Wireless LAN | When routers are connected to this WLAN node, they join a wireless network and an antenna is drawn instead of a connecting line; the WLAN node typically controls connectivity between attached wireless nodes based on the distance between them. |
|
| ![](static/gui/wlan.png) | Wireless LAN | When routers are connected to this WLAN node, they join a wireless network and an antenna is drawn instead of a connecting line; the WLAN node typically controls connectivity between attached wireless nodes based on the distance between them. |
|
||||||
| ![](static/gui/rj45.gif) | RJ45 | RJ45 Physical Interface Tool, emulated nodes can be linked to real physical interfaces; using this tool, real networks and devices can be physically connected to the live-running emulation. |
|
| ![](static/gui/rj45.png) | RJ45 | RJ45 Physical Interface Tool, emulated nodes can be linked to real physical interfaces; using this tool, real networks and devices can be physically connected to the live-running emulation. |
|
||||||
| ![](static/gui/tunnel.gif) | Tunnel | Tool allows connecting together more than one CORE emulation using GRE tunnels. |
|
| ![](static/gui/tunnel.png) | Tunnel | Tool allows connecting together more than one CORE emulation using GRE tunnels. |
|
||||||
|
|
||||||
### Annotation Tools
|
### Annotation Tools
|
||||||
|
|
||||||
| Icon | Name | Description |
|
| Icon | Name | Description |
|
||||||
|-------------------------------|-----------|---------------------------------------------------------------------|
|
|-------------------------------|-----------|---------------------------------------------------------------------|
|
||||||
| ![](static/gui/marker.gif) | Marker | For drawing marks on the canvas. |
|
| ![](static/gui/marker.png) | Marker | For drawing marks on the canvas. |
|
||||||
| ![](static/gui/oval.gif) | Oval | For drawing circles on the canvas that appear in the background. |
|
| ![](static/gui/oval.png) | Oval | For drawing circles on the canvas that appear in the background. |
|
||||||
| ![](static/gui/rectangle.gif) | Rectangle | For drawing rectangles on the canvas that appear in the background. |
|
| ![](static/gui/rectangle.png) | Rectangle | For drawing rectangles on the canvas that appear in the background. |
|
||||||
| ![](static/gui/text.gif) | Text | For placing text captions on the canvas. |
|
| ![](static/gui/text.png) | Text | For placing text captions on the canvas. |
|
||||||
|
|
||||||
### Execution Toolbar
|
### Execution Toolbar
|
||||||
|
|
||||||
|
@ -140,14 +157,12 @@ When the Start button is pressed, CORE switches to Execute mode, and the Edit
|
||||||
toolbar on the left of the CORE window is replaced with the Execution toolbar
|
toolbar on the left of the CORE window is replaced with the Execution toolbar
|
||||||
Below are the items on this toolbar, starting from the top.
|
Below are the items on this toolbar, starting from the top.
|
||||||
|
|
||||||
| Icon | Name | Description |
|
| Icon | Name | Description |
|
||||||
|-----------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| ![](static/gui/select.gif) | Selection Tool | In Execute mode, the Selection Tool can be used for moving nodes around the canvas, and double-clicking on a node will open a shell window for that node; right-clicking on a node invokes a pop-up menu of run-time options for that node. |
|
| ![](static/gui/stop.png) | Stop Button | Stops Execute mode, terminates the emulation, returns CORE to edit mode. |
|
||||||
| ![](static/gui/stop.gif) | Stop Button | Stops Execute mode, terminates the emulation, returns CORE to edit mode. |
|
| ![](static/gui/select.png) | Selection Tool | In Execute mode, the Selection Tool can be used for moving nodes around the canvas, and double-clicking on a node will open a shell window for that node; right-clicking on a node invokes a pop-up menu of run-time options for that node. |
|
||||||
| ![](static/gui/observe.gif) | Observer Widgets Tool | Clicking on this magnifying glass icon invokes a menu for easily selecting an Observer Widget. The icon has a darker gray background when an Observer Widget is active, during which time moving the mouse over a node will pop up an information display for that node. |
|
| ![](static/gui/marker.png) | Marker | For drawing freehand lines on the canvas, useful during demonstrations; markings are not saved. |
|
||||||
| ![](static/gui/marker.gif) | Marker | For drawing freehand lines on the canvas, useful during demonstrations; markings are not saved. |
|
| ![](static/gui/run.png) | Run Tool | This tool allows easily running a command on all or a subset of all nodes. A list box allows selecting any of the nodes. A text entry box allows entering any command. The command should return immediately, otherwise the display will block awaiting response. The *ping* command, for example, with no parameters, is not a good idea. The result of each command is displayed in a results box. The first occurrence of the special text "NODE" will be replaced with the node name. The command will not be attempted to run on nodes that are not routers, PCs, or hosts, even if they are selected. |
|
||||||
| ![](static/gui/twonode.gif) | Two-node Tool | Click to choose a starting and ending node, and run a one-time *traceroute* between those nodes or a continuous *ping -R* between nodes. The output is displayed in real time in a results box, while the IP addresses are parsed and the complete network path is highlighted on the CORE display. |
|
|
||||||
| ![](static/gui/run.gif) | Run Tool | This tool allows easily running a command on all or a subset of all nodes. A list box allows selecting any of the nodes. A text entry box allows entering any command. The command should return immediately, otherwise the display will block awaiting response. The *ping* command, for example, with no parameters, is not a good idea. The result of each command is displayed in a results box. The first occurrence of the special text "NODE" will be replaced with the node name. The command will not be attempted to run on nodes that are not routers, PCs, or hosts, even if they are selected. |
|
|
||||||
|
|
||||||
## Menu
|
## Menu
|
||||||
|
|
||||||
|
@ -157,98 +172,61 @@ menu, by clicking the dashed line at the top.
|
||||||
|
|
||||||
### File Menu
|
### File Menu
|
||||||
|
|
||||||
The File menu contains options for manipulating the **.imn** Configuration
|
The File menu contains options for saving and opening saved sessions.
|
||||||
Files. Generally, these menu items should not be used in Execute mode.
|
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| New | This starts a new file with an empty canvas. |
|
| New Session | This starts a new session with an empty canvas. |
|
||||||
| Open | Invokes the File Open dialog box for selecting a new **.imn** or XML file to open. You can change the default path used for this dialog in the Preferences Dialog. |
|
| Save | Saves the current topology. If you have not yet specified a file name, the Save As dialog box is invoked. |
|
||||||
| Save | Saves the current topology. If you have not yet specified a file name, the Save As dialog box is invoked. |
|
| Save As | Invokes the Save As dialog box for selecting a new **.xml** file for saving the current configuration in the XML file. |
|
||||||
| Save As XML | Invokes the Save As dialog box for selecting a new **.xml** file for saving the current configuration in the XML file. |
|
| Open | Invokes the File Open dialog box for selecting a new XML file to open. |
|
||||||
| Save As imn | Invokes the Save As dialog box for selecting a new **.imn** topology file for saving the current configuration. Files are saved in the *IMUNES network configuration* file. |
|
| Recently used files | Above the Quit menu command is a list of recently use files, if any have been opened. You can clear this list in the Preferences dialog box. You can specify the number of files to keep in this list from the Preferences dialog. Click on one of the file names listed to open that configuration file. |
|
||||||
| Export Python script | Prints Python snippets to the console, for inclusion in a CORE Python script. |
|
| Execute Python Script | Invokes a File Open dialog box for selecting a Python script to run and automatically connect to. After a selection is made, a Python Script Options dialog box is invoked to allow for command-line options to be added. The Python script must create a new CORE Session and add this session to the daemon's list of sessions in order for this to work. |
|
||||||
| Execute XML or Python script | Invokes a File Open dialog box for selecting an XML file to run or a Python script to run and automatically connect to. If a Python script, the script must create a new CORE Session and add this session to the daemon's list of sessions in order for this to work. |
|
| Quit | The Quit command should be used to exit the CORE GUI. CORE may prompt for termination if you are currently in Execute mode. Preferences and the recently-used files list are saved. |
|
||||||
| Execute Python script with options | Invokes a File Open dialog box for selecting a Python script to run and automatically connect to. After a selection is made, a Python Script Options dialog box is invoked to allow for command-line options to be added. The Python script must create a new CORE Session and add this session to the daemon's list of sessions in order for this to work. |
|
|
||||||
| Open current file in editor | This opens the current topology file in the **vim** text editor. First you need to save the file. Once the file has been edited with a text editor, you will need to reload the file to see your changes. The text editor can be changed from the Preferences Dialog. |
|
|
||||||
| Print | This uses the Tcl/Tk postscript command to print the current canvas to a printer. A dialog is invoked where you can specify a printing command, the default being **lpr**. The postscript output is piped to the print command. |
|
|
||||||
| Save screenshot | Saves the current canvas as a postscript graphic file. |
|
|
||||||
| Recently used files | Above the Quit menu command is a list of recently use files, if any have been opened. You can clear this list in the Preferences dialog box. You can specify the number of files to keep in this list from the Preferences dialog. Click on one of the file names listed to open that configuration file. |
|
|
||||||
| Quit | The Quit command should be used to exit the CORE GUI. CORE may prompt for termination if you are currently in Execute mode. Preferences and the recently-used files list are saved. |
|
|
||||||
|
|
||||||
### Edit Menu
|
### Edit Menu
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|--------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| Undo | Attempts to undo the last edit in edit mode. |
|
| Preferences | Invokes the Preferences dialog box. |
|
||||||
| Redo | Attempts to redo an edit that has been undone. |
|
| Custom Nodes | Custom node creation dialog box. |
|
||||||
| Cut, Copy, Paste | Used to cut, copy, and paste a selection. When nodes are pasted, their node numbers are automatically incremented, and existing links are preserved with new IP addresses assigned. Services and their customizations are copied to the new node, but care should be taken as node IP addresses have changed with possibly old addresses remaining in any custom service configurations. Annotations may also be copied and pasted. |
|
| Undo | (Disabled) Attempts to undo the last edit in edit mode. |
|
||||||
| Select All | Selects all items on the canvas. Selected items can be moved as a group. |
|
| Redo | (Disabled) Attempts to redo an edit that has been undone. |
|
||||||
| Select Adjacent | Select all nodes that are linked to the already selected node(s). For wireless nodes this simply selects the WLAN node(s) that the wireless node belongs to. You can use this by clicking on a node and pressing CTRL+N to select the adjacent nodes. |
|
| Cut, Copy, Paste, Delete | Used to cut, copy, paste, and delete a selection. When nodes are pasted, their node numbers are automatically incremented, and existing links are preserved with new IP addresses assigned. Services and their customizations are copied to the new node, but care should be taken as node IP addresses have changed with possibly old addresses remaining in any custom service configurations. Annotations may also be copied and pasted. |
|
||||||
| Find... | Invokes the *Find* dialog box. The Find dialog can be used to search for nodes by name or number. Results are listed in a table that includes the node or link location and details such as IP addresses or link parameters. Clicking on a result will focus the canvas on that node or link, switching canvases if necessary. |
|
|
||||||
| Clear marker | Clears any annotations drawn with the marker tool. Also clears any markings used to indicate a node's status. |
|
|
||||||
| Preferences... | Invokes the Preferences dialog box. |
|
|
||||||
|
|
||||||
### Canvas Menu
|
### Canvas Menu
|
||||||
|
|
||||||
The canvas menu provides commands for adding, removing, changing, and switching
|
The canvas menu provides commands related to the editing canvas.
|
||||||
to different editing canvases.
|
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| New | Creates a new empty canvas at the right of all existing canvases. |
|
| Size/scale | Invokes a Canvas Size and Scale dialog that allows configuring the canvas size, scale, and geographic reference point. The size controls allow changing the width and height of the current canvas, in pixels or meters. The scale allows specifying how many meters are equivalent to 100 pixels. The reference point controls specify the latitude, longitude, and altitude reference point used to convert between geographic and Cartesian coordinate systems. By clicking the *Save as default* option, all new canvases will be created with these properties. The default canvas size can also be changed in the Preferences dialog box. |
|
||||||
| Manage... | Invokes the *Manage Canvases* dialog box, where canvases may be renamed and reordered, and you can easily switch to one of the canvases by selecting it. |
|
| Wallpaper | Used for setting the canvas background image. |
|
||||||
| Delete | Deletes the current canvas and all items that it contains. |
|
|
||||||
| Size/scale... | Invokes a Canvas Size and Scale dialog that allows configuring the canvas size, scale, and geographic reference point. The size controls allow changing the width and height of the current canvas, in pixels or meters. The scale allows specifying how many meters are equivalent to 100 pixels. The reference point controls specify the latitude, longitude, and altitude reference point used to convert between geographic and Cartesian coordinate systems. By clicking the *Save as default* option, all new canvases will be created with these properties. The default canvas size can also be changed in the Preferences dialog box. |
|
|
||||||
| Wallpaper... | Used for setting the canvas background image. |
|
|
||||||
| Previous, Next, First, Last | Used for switching the active canvas to the first, last, or adjacent canvas. |
|
|
||||||
|
|
||||||
### View Menu
|
### View Menu
|
||||||
|
|
||||||
The View menu features items for controlling what is displayed on the drawing
|
The View menu features items for toggling on and off their display on the canvas.
|
||||||
canvas.
|
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------|-----------------------------------|
|
||||||
| Show | Opens a submenu of items that can be displayed or hidden, such as interface names, addresses, and labels. Use these options to help declutter the display. These options are generally saved in the topology files, so scenarios have a more consistent look when copied from one computer to another. |
|
| Interface Names | Display interface names on links. |
|
||||||
| Show hidden nodes | Reveal nodes that have been hidden. Nodes are hidden by selecting one or more nodes, right-clicking one and choosing *hide*. |
|
| IPv4 Addresses | Display IPv4 addresses on links. |
|
||||||
| Locked | Toggles locked view; when the view is locked, nodes cannot be moved around on the canvas with the mouse. This could be useful when sharing the topology with someone and you do not expect them to change things. |
|
| IPv6 Addresses | Display IPv6 addresses on links. |
|
||||||
| 3D GUI... | Launches a 3D GUI by running the command defined under Preferences, *3D GUI command*. This is typically a script that runs the SDT3D display. SDT is the Scripted Display Tool from NRL that is based on NASA's Java-based WorldWind virtual globe software. |
|
| Node Labels | Display node names. |
|
||||||
| Zoom In | Magnifies the display. You can also zoom in by clicking *zoom 100%* label in the status bar, or by pressing the **+** (plus) key. |
|
| Link Labels | Display link labels. |
|
||||||
| Zoom Out | Reduces the size of the display. You can also zoom out by right-clicking *zoom 100%* label in the status bar or by pressing the **-** (minus) key. |
|
| Annotations | Display annotations. |
|
||||||
|
| Canvas Grid | Display the canvas grid. |
|
||||||
|
|
||||||
### Tools Menu
|
### Tools Menu
|
||||||
|
|
||||||
The tools menu lists different utility functions.
|
The tools menu lists different utility functions.
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| Autorearrange all | Automatically arranges all nodes on the canvas. Nodes having a greater number of links are moved to the center. This mode can continue to run while placing nodes. To turn off this autorearrange mode, click on a blank area of the canvas with the select tool, or choose this menu option again. |
|
| Find | Display find dialog used for highlighting a node on the canvas. |
|
||||||
| Autorearrange selected | Automatically arranges the selected nodes on the canvas. |
|
| Auto Grid | Automatically layout nodes in a grid. |
|
||||||
| Align to grid | Moves nodes into a grid formation, starting with the smallest-numbered node in the upper-left corner of the canvas, arranging nodes in vertical columns. |
|
| IP addresses | Invokes the IP Addresses dialog box for configuring which IPv4/IPv6 prefixes are used when automatically addressing new interfaces. |
|
||||||
| Traffic... | Invokes the CORE Traffic Flows dialog box, which allows configuring, starting, and stopping MGEN traffic flows for the emulation. |
|
| MAC addresses | Invokes the MAC Addresses dialog box for configuring the starting number used as the lowest byte when generating each interface MAC address. This value should be changed when tunneling between CORE emulations to prevent MAC address conflicts. |
|
||||||
| IP addresses... | Invokes the IP Addresses dialog box for configuring which IPv4/IPv6 prefixes are used when automatically addressing new interfaces. |
|
|
||||||
| MAC addresses... | Invokes the MAC Addresses dialog box for configuring the starting number used as the lowest byte when generating each interface MAC address. This value should be changed when tunneling between CORE emulations to prevent MAC address conflicts. |
|
|
||||||
| Build hosts file... | Invokes the Build hosts File dialog box for generating **/etc/hosts** file entries based on IP addresses used in the emulation. |
|
|
||||||
| Renumber nodes... | Invokes the Renumber Nodes dialog box, which allows swapping one node number with another in a few clicks. |
|
|
||||||
| Experimental... | Menu of experimental options, such as a tool to convert ns-2 scripts to IMUNES imn topologies, supporting only basic ns-2 functionality, and a tool for automatically dividing up a topology into partitions. |
|
|
||||||
| Topology generator | Opens a submenu of topologies to generate. You can first select the type of node that the topology should consist of, or routers will be chosen by default. Nodes may be randomly placed, aligned in grids, or various other topology patterns. All of the supported patterns are listed in the table below. |
|
|
||||||
| Debugger... | Opens the CORE Debugger window for executing arbitrary Tcl/Tk commands. |
|
|
||||||
|
|
||||||
#### Topology Generator
|
|
||||||
|
|
||||||
| Pattern | Description |
|
|
||||||
|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| Random | Nodes are randomly placed about the canvas, but are not linked together. This can be used in conjunction with a WLAN node to quickly create a wireless network. |
|
|
||||||
| Grid | Nodes are placed in horizontal rows starting in the upper-left corner, evenly spaced to the right; nodes are not linked to each other. |
|
|
||||||
| Connected Grid | Nodes are placed in an N x M (width and height) rectangular grid, and each node is linked to the node above, below, left and right of itself. |
|
|
||||||
| Chain | Nodes are linked together one after the other in a chain. |
|
|
||||||
| Star | One node is placed in the center with N nodes surrounding it in a circular pattern, with each node linked to the center node. |
|
|
||||||
| Cycle | Nodes are arranged in a circular pattern with every node connected to its neighbor to form a closed circular path. |
|
|
||||||
| Wheel | The wheel pattern links nodes in a combination of both Star and Cycle patterns. |
|
|
||||||
| Cube | Generate a cube graph of nodes. |
|
|
||||||
| Clique | Creates a clique graph of nodes, where every node is connected to every other node. |
|
|
||||||
| Bipartite | Creates a bipartite graph of nodes, having two disjoint sets of vertices. |
|
|
||||||
|
|
||||||
### Widgets Menu
|
### Widgets Menu
|
||||||
|
|
||||||
|
@ -275,30 +253,29 @@ Here are some standard widgets:
|
||||||
Only half of the line is drawn because each
|
Only half of the line is drawn because each
|
||||||
router may be in a different adjacency state with respect to the other.
|
router may be in a different adjacency state with respect to the other.
|
||||||
* **Throughput** - displays the kilobits-per-second throughput above each link,
|
* **Throughput** - displays the kilobits-per-second throughput above each link,
|
||||||
using statistics gathered from the ng_pipe Netgraph node that implements each
|
using statistics gathered from each link. If the throughput exceeds a certain
|
||||||
link. If the throughput exceeds a certain threshold, the link will become
|
threshold, the link will become highlighted. For wireless nodes which broadcast
|
||||||
highlighted. For wireless nodes which broadcast data to all nodes in range,
|
data to all nodes in range, the throughput rate is displayed next to the node and
|
||||||
the throughput rate is displayed next to the node and the node will become
|
the node will become circled if the threshold is exceeded.
|
||||||
circled if the threshold is exceeded.
|
|
||||||
|
|
||||||
#### Observer Widgets
|
#### Observer Widgets
|
||||||
|
|
||||||
These Widgets are available from the *Observer Widgets* submenu of the
|
These Widgets are available from the **Observer Widgets** submenu of the
|
||||||
*Widgets* menu, and from the Widgets Tool on the toolbar. Only one Observer Widget may
|
**Widgets** menu, and from the Widgets Tool on the toolbar. Only one Observer Widget may
|
||||||
be used at a time. Mouse over a node while the session is running to pop up
|
be used at a time. Mouse over a node while the session is running to pop up
|
||||||
an informational display about that node.
|
an informational display about that node.
|
||||||
|
|
||||||
Available Observer Widgets include IPv4 and IPv6 routing tables, socket
|
Available Observer Widgets include IPv4 and IPv6 routing tables, socket
|
||||||
information, list of running processes, and OSPFv2/v3 neighbor information.
|
information, list of running processes, and OSPFv2/v3 neighbor information.
|
||||||
|
|
||||||
Observer Widgets may be edited by the user and rearranged. Choosing *Edit...*
|
Observer Widgets may be edited by the user and rearranged. Choosing
|
||||||
from the Observer Widget menu will invoke the Observer Widgets dialog. A list
|
**Widgets->Observer Widgets->Edit Observers** from the Observer Widget menu will
|
||||||
of Observer Widgets is displayed along with up and down arrows for rearranging
|
invoke the Observer Widgets dialog. A list of Observer Widgets is displayed along
|
||||||
the list. Controls are available for renaming each widget, for changing the
|
with up and down arrows for rearranging the list. Controls are available for
|
||||||
command that is run during mouse over, and for adding and deleting items from
|
renaming each widget, for changing the command that is run during mouse over, and
|
||||||
the list. Note that specified commands should return immediately to avoid
|
for adding and deleting items from the list. Note that specified commands should
|
||||||
delays in the GUI display. Changes are saved to a **widgets.conf** file in
|
return immediately to avoid delays in the GUI display. Changes are saved to a
|
||||||
the CORE configuration directory.
|
**config.yaml** file in the CORE configuration directory.
|
||||||
|
|
||||||
### Session Menu
|
### Session Menu
|
||||||
|
|
||||||
|
@ -306,28 +283,23 @@ The Session Menu has entries for starting, stopping, and managing sessions,
|
||||||
in addition to global options such as node types, comments, hooks, servers,
|
in addition to global options such as node types, comments, hooks, servers,
|
||||||
and options.
|
and options.
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| Start or Stop | This starts or stops the emulation, performing the same function as the green Start or red Stop button. |
|
| Sessions | Invokes the CORE Sessions dialog box containing a list of active CORE sessions in the daemon. Basic session information such as name, node count, start time, and a thumbnail are displayed. This dialog allows connecting to different sessions, shutting them down, or starting a new session. |
|
||||||
| Change sessions... | Invokes the CORE Sessions dialog box containing a list of active CORE sessions in the daemon. Basic session information such as name, node count, start time, and a thumbnail are displayed. This dialog allows connecting to different sessions, shutting them down, or starting a new session. |
|
| Servers | Invokes the CORE emulation servers dialog for configuring. |
|
||||||
| Node types... | Invokes the CORE Node Types dialog, performing the same function as the Edit button on the Network-Layer Nodes toolbar. |
|
| Options | Presents per-session options, such as the IPv4 prefix to be used, if any, for a control network the ability to preserve the session directory; and an on/off switch for SDT3D support. |
|
||||||
| Comments... | Invokes the CORE Session Comments window where optional text comments may be specified. These comments are saved at the top of the configuration file, and can be useful for describing the topology or how to use the network. |
|
| Hooks | Invokes the CORE Session Hooks window where scripts may be configured for a particular session state. The session states are defined in the [table](#session-states) below. The top of the window has a list of configured hooks, and buttons on the bottom left allow adding, editing, and removing hook scripts. The new or edit button will open a hook script editing window. A hook script is a shell script invoked on the host (not within a virtual node). |
|
||||||
| Hooks... | Invokes the CORE Session Hooks window where scripts may be configured for a particular session state. The session states are defined in the [table](#session-states) below. The top of the window has a list of configured hooks, and buttons on the bottom left allow adding, editing, and removing hook scripts. The new or edit button will open a hook script editing window. A hook script is a shell script invoked on the host (not within a virtual node). |
|
|
||||||
| Reset node positions | If you have moved nodes around using the mouse or by using a mobility module, choosing this item will reset all nodes to their original position on the canvas. The node locations are remembered when you first press the Start button. |
|
|
||||||
| Emulation servers... | Invokes the CORE emulation servers dialog for configuring. |
|
|
||||||
| Change Sessions... | Invokes the Sessions dialog for switching between different running sessions. This dialog is presented during startup when one or more sessions are already running. |
|
|
||||||
| Options... | Presents per-session options, such as the IPv4 prefix to be used, if any, for a control network the ability to preserve the session directory; and an on/off switch for SDT3D support. |
|
|
||||||
|
|
||||||
#### Session States
|
#### Session States
|
||||||
|
|
||||||
| State | Description |
|
| State | Description |
|
||||||
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| definition | Used by the GUI to tell the backend to clear any state. |
|
| Definition | Used by the GUI to tell the backend to clear any state. |
|
||||||
| configuration | When the user presses the *Start* button, node, link, and other configuration data is sent to the backend. This state is also reached when the user customizes a service. |
|
| Configuration | When the user presses the *Start* button, node, link, and other configuration data is sent to the backend. This state is also reached when the user customizes a service. |
|
||||||
| instantiation | After configuration data has been sent, just before the nodes are created. |
|
| Instantiation | After configuration data has been sent, just before the nodes are created. |
|
||||||
| runtime | All nodes and networks have been built and are running. (This is the same state at which the previously-named *global experiment script* was run.) |
|
| Runtime | All nodes and networks have been built and are running. (This is the same state at which the previously-named *global experiment script* was run.) |
|
||||||
| datacollect | The user has pressed the *Stop* button, but before services have been stopped and nodes have been shut down. This is a good time to collect log files and other data from the nodes. |
|
| Datacollect | The user has pressed the *Stop* button, but before services have been stopped and nodes have been shut down. This is a good time to collect log files and other data from the nodes. |
|
||||||
| shutdown | All nodes and networks have been shut down and destroyed. |
|
| Shutdown | All nodes and networks have been shut down and destroyed. |
|
||||||
|
|
||||||
### Help Menu
|
### Help Menu
|
||||||
|
|
||||||
|
@ -341,13 +313,13 @@ and options.
|
||||||
|
|
||||||
CORE's emulated networks run in real time, so they can be connected to live
|
CORE's emulated networks run in real time, so they can be connected to live
|
||||||
physical networks. The RJ45 tool and the Tunnel tool help with connecting to
|
physical networks. The RJ45 tool and the Tunnel tool help with connecting to
|
||||||
the real world. These tools are available from the *Link-layer nodes* menu.
|
the real world. These tools are available from the **Link-layer nodes** menu.
|
||||||
|
|
||||||
When connecting two or more CORE emulations together, MAC address collisions
|
When connecting two or more CORE emulations together, MAC address collisions
|
||||||
should be avoided. CORE automatically assigns MAC addresses to interfaces when
|
should be avoided. CORE automatically assigns MAC addresses to interfaces when
|
||||||
the emulation is started, starting with **00:00:00:aa:00:00** and incrementing
|
the emulation is started, starting with **00:00:00:aa:00:00** and incrementing
|
||||||
the bottom byte. The starting byte should be changed on the second CORE machine
|
the bottom byte. The starting byte should be changed on the second CORE machine
|
||||||
using the *MAC addresses...* option from the *Tools* menu.
|
using the **Tools->MAC Addresses** option the menu.
|
||||||
|
|
||||||
### RJ45 Tool
|
### RJ45 Tool
|
||||||
|
|
||||||
|
@ -360,8 +332,8 @@ connection. When the physical interface is assigned to CORE, it may not be used
|
||||||
for anything else. Another consideration is that the computer or network that
|
for anything else. Another consideration is that the computer or network that
|
||||||
you are connecting to must be co-located with the CORE machine.
|
you are connecting to must be co-located with the CORE machine.
|
||||||
|
|
||||||
To place an RJ45 connection, click on the *Link-layer nodes* toolbar and select
|
To place an RJ45 connection, click on the **Link-layer nodes** toolbar and select
|
||||||
the *RJ45 Tool* from the submenu. Click on the canvas near the node you want to
|
the **RJ45 Tool** from the submenu. Click on the canvas near the node you want to
|
||||||
connect to. This could be a router, hub, switch, or WLAN, for example. Now
|
connect to. This could be a router, hub, switch, or WLAN, for example. Now
|
||||||
click on the *Link Tool* and draw a link between the RJ45 and the other node.
|
click on the *Link Tool* and draw a link between the RJ45 and the other node.
|
||||||
The RJ45 node will display "UNASSIGNED". Double-click the RJ45 node to assign a
|
The RJ45 node will display "UNASSIGNED". Double-click the RJ45 node to assign a
|
||||||
|
@ -459,7 +431,7 @@ firewall is not blocking the GRE traffic.
|
||||||
The host machine that runs the CORE GUI and/or daemon is not necessarily
|
The host machine that runs the CORE GUI and/or daemon is not necessarily
|
||||||
accessible from a node. Running an X11 application on a node, for example,
|
accessible from a node. Running an X11 application on a node, for example,
|
||||||
requires some channel of communication for the application to connect with
|
requires some channel of communication for the application to connect with
|
||||||
the X server for graphical display. There are several different ways to
|
the X server for graphical display. There are different ways to
|
||||||
connect from the node to the host and vice versa.
|
connect from the node to the host and vice versa.
|
||||||
|
|
||||||
#### Control Network
|
#### Control Network
|
||||||
|
@ -476,12 +448,6 @@ the node, and SSH with X11 forwarding can be used from the host to the node.
|
||||||
ssh -X 172.16.0.5 xclock
|
ssh -X 172.16.0.5 xclock
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the **coresendmsg** utility can be used for a node to send
|
|
||||||
messages to the CORE daemon running on the host (if the **listenaddr = 0.0.0.0**
|
|
||||||
is set in the **/etc/core/core.conf** file) to interact with the running
|
|
||||||
emulation. For example, a node may move itself or other nodes, or change
|
|
||||||
its icon based on some node state.
|
|
||||||
|
|
||||||
#### Other Methods
|
#### Other Methods
|
||||||
|
|
||||||
There are still other ways to connect a host with a node. The RJ45 Tool
|
There are still other ways to connect a host with a node. The RJ45 Tool
|
||||||
|
@ -509,11 +475,11 @@ the node linked to the RJ45 may have the address **10.0.1.1**.
|
||||||
|
|
||||||
### Wired Networks
|
### Wired Networks
|
||||||
|
|
||||||
Wired networks are created using the *Link Tool* to draw a link between two
|
Wired networks are created using the **Link Tool** to draw a link between two
|
||||||
nodes. This automatically draws a red line representing an Ethernet link and
|
nodes. This automatically draws a red line representing an Ethernet link and
|
||||||
creates new interfaces on network-layer nodes.
|
creates new interfaces on network-layer nodes.
|
||||||
|
|
||||||
Double-click on the link to invoke the *link configuration* dialog box. Here
|
Double-click on the link to invoke the **link configuration** dialog box. Here
|
||||||
you can change the Bandwidth, Delay, Loss, and Duplicate
|
you can change the Bandwidth, Delay, Loss, and Duplicate
|
||||||
rate parameters for that link. You can also modify the color and width of the
|
rate parameters for that link. You can also modify the color and width of the
|
||||||
link, affecting its display.
|
link, affecting its display.
|
||||||
|
@ -550,11 +516,10 @@ on platform. See the table below for a brief overview of wireless model types.
|
||||||
To quickly build a wireless network, you can first place several router nodes
|
To quickly build a wireless network, you can first place several router nodes
|
||||||
onto the canvas. If you have the
|
onto the canvas. If you have the
|
||||||
Quagga MDR software installed, it is
|
Quagga MDR software installed, it is
|
||||||
recommended that you use the *mdr* node type for reduced routing overhead. Next
|
recommended that you use the **mdr** node type for reduced routing overhead. Next
|
||||||
choose the *wireless LAN* from the *Link-layer nodes* submenu. First set the
|
choose the **WLAN** from the **Link-layer nodes** submenu. First set the
|
||||||
desired WLAN parameters by double-clicking the cloud icon. Then you can link
|
desired WLAN parameters by double-clicking the cloud icon. Then you can link
|
||||||
all of the routers by right-clicking on the WLAN and choosing *Link to all
|
all selected right-clicking on the WLAN and choosing **Link to Selected**.
|
||||||
routers*.
|
|
||||||
|
|
||||||
Linking a router to the WLAN causes a small antenna to appear, but no red link
|
Linking a router to the WLAN causes a small antenna to appear, but no red link
|
||||||
line is drawn. Routers can have multiple wireless links and both wireless and
|
line is drawn. Routers can have multiple wireless links and both wireless and
|
||||||
|
@ -564,28 +529,27 @@ enables OSPFv3 with MANET extensions. This is a Boeing-developed extension to
|
||||||
Quagga's OSPFv3 that reduces flooding overhead and optimizes the flooding
|
Quagga's OSPFv3 that reduces flooding overhead and optimizes the flooding
|
||||||
procedure for mobile ad-hoc (MANET) networks.
|
procedure for mobile ad-hoc (MANET) networks.
|
||||||
|
|
||||||
The default configuration of the WLAN is set to use the basic range model,
|
The default configuration of the WLAN is set to use the basic range model. Having this model
|
||||||
using the *Basic* tab in the WLAN configuration dialog. Having this model
|
|
||||||
selected causes **core-daemon** to calculate the distance between nodes based
|
selected causes **core-daemon** to calculate the distance between nodes based
|
||||||
on screen pixels. A numeric range in screen pixels is set for the wireless
|
on screen pixels. A numeric range in screen pixels is set for the wireless
|
||||||
network using the *Range* slider. When two wireless nodes are within range of
|
network using the **Range** slider. When two wireless nodes are within range of
|
||||||
each other, a green line is drawn between them and they are linked. Two
|
each other, a green line is drawn between them and they are linked. Two
|
||||||
wireless nodes that are farther than the range pixels apart are not linked.
|
wireless nodes that are farther than the range pixels apart are not linked.
|
||||||
During Execute mode, users may move wireless nodes around by clicking and
|
During Execute mode, users may move wireless nodes around by clicking and
|
||||||
dragging them, and wireless links will be dynamically made or broken.
|
dragging them, and wireless links will be dynamically made or broken.
|
||||||
|
|
||||||
The *EMANE* tab lists available EMANE models to use for wireless networking.
|
The **EMANE Nodes** leverage available EMANE models to use for wireless networking.
|
||||||
See the [EMANE](emane.md) chapter for details on using EMANE.
|
See the [EMANE](emane.md) chapter for details on using EMANE.
|
||||||
|
|
||||||
### Mobility Scripting
|
### Mobility Scripting
|
||||||
|
|
||||||
CORE has a few ways to script mobility.
|
CORE has a few ways to script mobility.
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| ns-2 script | The script specifies either absolute positions or waypoints with a velocity. Locations are given with Cartesian coordinates. |
|
| ns-2 script | The script specifies either absolute positions or waypoints with a velocity. Locations are given with Cartesian coordinates. |
|
||||||
| CORE API | An external entity can move nodes by sending CORE API Node messages with updated X,Y coordinates; the **coresendmsg** utility allows a shell script to generate these messages. |
|
| gRPC API | An external entity can move nodes by leveraging the gRPC API |
|
||||||
| EMANE events | See [EMANE](emane.md) for details on using EMANE scripts to move nodes around. Location information is typically given as latitude, longitude, and altitude. |
|
| EMANE events | See [EMANE](emane.md) for details on using EMANE scripts to move nodes around. Location information is typically given as latitude, longitude, and altitude. |
|
||||||
|
|
||||||
For the first method, you can create a mobility script using a text
|
For the first method, you can create a mobility script using a text
|
||||||
editor, or using a tool such as [BonnMotion](http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/), and associate the script with one of the wireless
|
editor, or using a tool such as [BonnMotion](http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/), and associate the script with one of the wireless
|
||||||
|
@ -604,7 +568,7 @@ bm NSFile -f sample
|
||||||
When the Execute mode is started and one of the WLAN nodes has a mobility
|
When the Execute mode is started and one of the WLAN nodes has a mobility
|
||||||
script, a mobility script window will appear. This window contains controls for
|
script, a mobility script window will appear. This window contains controls for
|
||||||
starting, stopping, and resetting the running time for the mobility script. The
|
starting, stopping, and resetting the running time for the mobility script. The
|
||||||
*loop* checkbox causes the script to play continuously. The *resolution* text
|
**loop** checkbox causes the script to play continuously. The **resolution** text
|
||||||
box contains the number of milliseconds between each timer event; lower values
|
box contains the number of milliseconds between each timer event; lower values
|
||||||
cause the mobility to appear smoother but consumes greater CPU time.
|
cause the mobility to appear smoother but consumes greater CPU time.
|
||||||
|
|
||||||
|
@ -628,79 +592,28 @@ accurate.
|
||||||
Examples mobility scripts (and their associated topology files) can be found
|
Examples mobility scripts (and their associated topology files) can be found
|
||||||
in the **configs/** directory.
|
in the **configs/** directory.
|
||||||
|
|
||||||
## Multiple Canvases
|
## Alerts
|
||||||
|
|
||||||
CORE supports multiple canvases for organizing emulated nodes. Nodes running on
|
The alerts button is located in the bottom right-hand corner
|
||||||
different canvases may be linked together.
|
of the status bar in the CORE GUI. This will change colors to indicate one or
|
||||||
|
more problems with the running emulation. Clicking on the alerts button will invoke the
|
||||||
|
alerts dialog.
|
||||||
|
|
||||||
To create a new canvas, choose *New* from the *Canvas* menu. A new canvas tab
|
The alerts dialog contains a list of alerts received from
|
||||||
appears in the bottom left corner. Clicking on a canvas tab switches to that
|
the CORE daemon. An alert has a time, severity level, optional node number,
|
||||||
canvas. Double-click on one of the tabs to invoke the *Manage Canvases* dialog
|
and source. When the alerts button is red, this indicates one or more fatal
|
||||||
box. Here, canvases may be renamed and reordered, and you can easily switch to
|
exceptions. An alert with a fatal severity level indicates that one or more
|
||||||
one of the canvases by selecting it.
|
|
||||||
|
|
||||||
Each canvas maintains its own set of nodes and annotations. To link between
|
|
||||||
canvases, select a node and right-click on it, choose *Create link to*, choose
|
|
||||||
the target canvas from the list, and from that submenu the desired node. A
|
|
||||||
pseudo-link will be drawn, representing the link between the two nodes on
|
|
||||||
different canvases. Double-clicking on the label at the end of the arrow will
|
|
||||||
jump to the canvas that it links.
|
|
||||||
|
|
||||||
## Check Emulation Light (CEL)
|
|
||||||
|
|
||||||
The |cel| Check Emulation Light, or CEL, is located in the bottom right-hand corner
|
|
||||||
of the status bar in the CORE GUI. This is a yellow icon that indicates one or
|
|
||||||
more problems with the running emulation. Clicking on the CEL will invoke the
|
|
||||||
CEL dialog.
|
|
||||||
|
|
||||||
The Check Emulation Light dialog contains a list of exceptions received from
|
|
||||||
the CORE daemon. An exception has a time, severity level, optional node number,
|
|
||||||
and source. When the CEL is blinking, this indicates one or more fatal
|
|
||||||
exceptions. An exception with a fatal severity level indicates that one or more
|
|
||||||
of the basic pieces of emulation could not be created, such as failure to
|
of the basic pieces of emulation could not be created, such as failure to
|
||||||
create a bridge or namespace, or the failure to launch EMANE processes for an
|
create a bridge or namespace, or the failure to launch EMANE processes for an
|
||||||
EMANE-based network.
|
EMANE-based network.
|
||||||
|
|
||||||
Clicking on an exception displays details for that
|
Clicking on an alert displays details for that
|
||||||
exception. If a node number is specified, that node is highlighted on the
|
exceptio. The exception source is a text string
|
||||||
canvas when the exception is selected. The exception source is a text string
|
|
||||||
to help trace where the exception occurred; "service:UserDefined" for example,
|
to help trace where the exception occurred; "service:UserDefined" for example,
|
||||||
would appear for a failed validation command with the UserDefined service.
|
would appear for a failed validation command with the UserDefined service.
|
||||||
|
|
||||||
Buttons are available at the bottom of the dialog for clearing the exception
|
A button is available at the bottom of the dialog for clearing the exception
|
||||||
list and for viewing the CORE daemon and node log files.
|
list.
|
||||||
|
|
||||||
> **NOTE:** In batch mode, exceptions received from the CORE daemon are displayed on
|
|
||||||
the console.
|
|
||||||
|
|
||||||
## Configuration Files
|
|
||||||
|
|
||||||
Configurations are saved to **xml** or **.imn** topology files using
|
|
||||||
the *File* menu. You
|
|
||||||
can easily edit these files with a text editor.
|
|
||||||
Any time you edit the topology
|
|
||||||
file, you will need to stop the emulation if it were running and reload the
|
|
||||||
file.
|
|
||||||
|
|
||||||
The **.imn** file format comes from IMUNES, and is
|
|
||||||
basically Tcl lists of nodes, links, etc.
|
|
||||||
Tabs and spacing in the topology files are important. The file starts by
|
|
||||||
listing every node, then links, annotations, canvases, and options. Each entity
|
|
||||||
has a block contained in braces. The first block is indented by four spaces.
|
|
||||||
Within the **network-config** block (and any *custom-*-config* block), the
|
|
||||||
indentation is one tab character.
|
|
||||||
|
|
||||||
> **NOTE:** There are several topology examples included with CORE in
|
|
||||||
the **configs/** directory.
|
|
||||||
This directory can be found in **~/.core/configs**, or
|
|
||||||
installed to the filesystem
|
|
||||||
under **/usr[/local]/share/examples/configs**.
|
|
||||||
|
|
||||||
> **NOTE:** When using the **.imn** file format, file paths for things like custom
|
|
||||||
icons may contain the special variables **$CORE_DATA_DIR** or **$CONFDIR** which
|
|
||||||
will be substituted with **/usr/share/core** or **~/.core/configs**.
|
|
||||||
|
|
||||||
> **NOTE:** Feel free to edit the files directly using your favorite text editor.
|
|
||||||
|
|
||||||
## Customizing your Topology's Look
|
## Customizing your Topology's Look
|
||||||
|
|
||||||
|
@ -723,12 +636,3 @@ A background image for the canvas may be set using the *Wallpaper...* option
|
||||||
from the *Canvas* menu. The image may be centered, tiled, or scaled to fit the
|
from the *Canvas* menu. The image may be centered, tiled, or scaled to fit the
|
||||||
canvas size. An existing terrain, map, or network diagram could be used as a
|
canvas size. An existing terrain, map, or network diagram could be used as a
|
||||||
background, for example, with CORE nodes drawn on top.
|
background, for example, with CORE nodes drawn on top.
|
||||||
|
|
||||||
## Preferences
|
|
||||||
|
|
||||||
The *Preferences* Dialog can be accessed from the **Edit_Menu**. There are
|
|
||||||
numerous defaults that can be set with this dialog, which are stored in the
|
|
||||||
**~/.core/prefs.conf** preferences file.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,7 @@ networking scenarios, security studies, and increasing the size of physical test
|
||||||
| [Installation](install.md) | How to install CORE and its requirements |
|
| [Installation](install.md) | How to install CORE and its requirements |
|
||||||
| [Architecture](architecture.md) | Overview of the architecture |
|
| [Architecture](architecture.md) | Overview of the architecture |
|
||||||
| [Node Types](nodetypes.md) | Overview of node types supported within CORE |
|
| [Node Types](nodetypes.md) | Overview of node types supported within CORE |
|
||||||
| [Python GUI](pygui.md) | How to use the default python based GUI |
|
| [GUI](gui.md) | How to use the GUI |
|
||||||
| [Legacy GUI (deprecated)](gui.md) | How to use the deprecated Tcl based GUI |
|
|
||||||
| [Python API](python.md) | Covers how to control core directly using python |
|
| [Python API](python.md) | Covers how to control core directly using python |
|
||||||
| [gRPC API](grpc.md) | Covers how control core using gRPC |
|
| [gRPC API](grpc.md) | Covers how control core using gRPC |
|
||||||
| [Distributed](distributed.md) | Details for running CORE across multiple servers |
|
| [Distributed](distributed.md) | Details for running CORE across multiple servers |
|
||||||
|
@ -34,17 +33,3 @@ networking scenarios, security studies, and increasing the size of physical test
|
||||||
| [EMANE](emane.md) | Overview of EMANE integration and integrating custom EMANE models |
|
| [EMANE](emane.md) | Overview of EMANE integration and integrating custom EMANE models |
|
||||||
| [Performance](performance.md) | Notes on performance when using CORE |
|
| [Performance](performance.md) | Notes on performance when using CORE |
|
||||||
| [Developers Guide](devguide.md) | Overview on how to contribute to CORE |
|
| [Developers Guide](devguide.md) | Overview on how to contribute to CORE |
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
The CORE project was derived from the open source IMUNES project from the University of Zagreb in 2004. In 2006,
|
|
||||||
changes for CORE were released back to that project, some items of which were adopted. Marko Zec <zec@fer.hr> is the
|
|
||||||
primary developer from the University of Zagreb responsible for the IMUNES (GUI) and VirtNet (kernel) projects. Ana
|
|
||||||
Kukec and Miljenko Mikuc are known contributors.
|
|
||||||
|
|
||||||
Jeff Ahrenholz has been the primary Boeing developer of CORE, and has written this manual. Tom Goff designed the
|
|
||||||
Python framework and has made significant contributions. Claudiu Danilov, Rod Santiago, Kevin Larson, Gary Pei,
|
|
||||||
Phil Spagnolo, and Ian Chakeres have contributed code to CORE. Dan Mackley helped develop the CORE API, originally to
|
|
||||||
interface with a simulator. Jae Kim and Tom Henderson have supervised the project and provided direction.
|
|
||||||
|
|
||||||
Copyright (c) 2005-2020, the Boeing Company.
|
|
||||||
|
|
|
@ -51,11 +51,6 @@ The following is a list of files that would be installed after running the autom
|
||||||
|
|
||||||
* executable files
|
* executable files
|
||||||
* `<prefix>/bin/{core-daemon, core-gui, vcmd, vnoded, etc}`
|
* `<prefix>/bin/{core-daemon, core-gui, vcmd, vnoded, etc}`
|
||||||
* tcl/tk gui files
|
|
||||||
* `<prefix>/lib/core`
|
|
||||||
* `<prefix>/share/core/icons`
|
|
||||||
* example imn files
|
|
||||||
* `<prefix>/share/core/examples`
|
|
||||||
* python files
|
* python files
|
||||||
* poetry virtual env
|
* poetry virtual env
|
||||||
* `cd <repo>/daemon && poetry env info`
|
* `cd <repo>/daemon && poetry env info`
|
||||||
|
@ -77,15 +72,11 @@ After the installation complete it will have installed the following scripts.
|
||||||
|---------------------|------------------------------------------------------------------------------|
|
|---------------------|------------------------------------------------------------------------------|
|
||||||
| core-cleanup | tool to help removed lingering core created containers, bridges, directories |
|
| core-cleanup | tool to help removed lingering core created containers, bridges, directories |
|
||||||
| core-cli | tool to query, open xml files, and send commands using gRPC |
|
| core-cli | tool to query, open xml files, and send commands using gRPC |
|
||||||
| core-daemon | runs the backed core server providing TLV and gRPC APIs |
|
| core-daemon | runs the backed core server providing a gRPC API |
|
||||||
| core-gui | runs the legacy tcl/tk based GUI |
|
| core-gui | starts GUI |
|
||||||
| core-imn-to-xml | tool to help automate converting a .imn file to .xml format |
|
|
||||||
| core-manage | tool to add, remove, or check for services, models, and node types |
|
|
||||||
| core-pygui | runs the new python/tk based GUI |
|
|
||||||
| core-python | provides a convenience for running the core python virtual environment |
|
| core-python | provides a convenience for running the core python virtual environment |
|
||||||
| core-route-monitor | tool to help monitor traffic across nodes and feed that to SDT |
|
| core-route-monitor | tool to help monitor traffic across nodes and feed that to SDT |
|
||||||
| core-service-update | tool to update automate modifying a legacy service to match current naming |
|
| core-service-update | tool to update automate modifying a legacy service to match current naming |
|
||||||
| coresendmsg | tool to send TLV API commands from command line |
|
|
||||||
|
|
||||||
## Upgrading from Older Release
|
## Upgrading from Older Release
|
||||||
Please make sure to uninstall any previous installations of CORE cleanly
|
Please make sure to uninstall any previous installations of CORE cleanly
|
||||||
|
@ -237,7 +228,7 @@ sudo docker exec -it core core-gui
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running User Scripts
|
## Running User Scripts
|
||||||
If you create your own python scripts to run CORE directly or using the gRPC/TLV
|
If you create your own python scripts to run CORE directly or using the gRPC
|
||||||
APIs you will need to make sure you are running them within context of the
|
APIs you will need to make sure you are running them within context of the
|
||||||
installed virtual environment. To help support this CORE provides the `core-python`
|
installed virtual environment. To help support this CORE provides the `core-python`
|
||||||
executable. This executable will allow you to enter CORE's python virtual
|
executable. This executable will allow you to enter CORE's python virtual
|
||||||
|
|
649
docs/pygui.md
|
@ -1,649 +0,0 @@
|
||||||
|
|
||||||
# (BETA) Python GUI
|
|
||||||
|
|
||||||
* Table of Contents
|
|
||||||
{:toc}
|
|
||||||
|
|
||||||
![](static/core-pygui.png)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The GUI is used to draw nodes and network devices on a canvas, linking them
|
|
||||||
together to create an emulated network session.
|
|
||||||
|
|
||||||
After pressing the start button, CORE will proceed through these phases,
|
|
||||||
staying in the **runtime** phase. After the session is stopped, CORE will
|
|
||||||
proceed to the **data collection** phase before tearing down the emulated
|
|
||||||
state.
|
|
||||||
|
|
||||||
CORE can be customized to perform any action at each state. See the
|
|
||||||
**Hooks...** entry on the [Session Menu](#session-menu) for details about
|
|
||||||
when these session states are reached.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Beyond installing CORE, you must have the CORE daemon running. This is done
|
|
||||||
on the command line with either systemd or sysv.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# systemd service
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl start core-daemon
|
|
||||||
|
|
||||||
# sysv service
|
|
||||||
sudo service core-daemon start
|
|
||||||
|
|
||||||
# direct invocation
|
|
||||||
sudo core-daemon
|
|
||||||
```
|
|
||||||
|
|
||||||
## GUI Files
|
|
||||||
|
|
||||||
> **NOTE:** Previously the BETA GUI placed files under ~/.coretk, this has been
|
|
||||||
> updated to be ~/.coregui. The prior config file named ~/.coretk/gui.yaml is
|
|
||||||
> also now known as ~/.coregui/config.yaml and has a slightly different format
|
|
||||||
|
|
||||||
The GUI will create a directory in your home directory on first run called
|
|
||||||
~/.coregui. This directory will help layout various files that the GUI may use.
|
|
||||||
|
|
||||||
* .coregui/
|
|
||||||
* backgrounds/
|
|
||||||
* place backgrounds used for display in the GUI
|
|
||||||
* custom_emane/
|
|
||||||
* place to keep custom emane models to use with the core-daemon
|
|
||||||
* custom_services/
|
|
||||||
* place to keep custom services to use with the core-daemon
|
|
||||||
* icons/
|
|
||||||
* icons the GUI uses along with customs icons desired
|
|
||||||
* mobility/
|
|
||||||
* place to keep custom mobility files
|
|
||||||
* scripts/
|
|
||||||
* place to keep core related scripts
|
|
||||||
* xmls/
|
|
||||||
* place to keep saved session xml files
|
|
||||||
* gui.log
|
|
||||||
* log file when running the gui, look here when issues occur for exceptions etc
|
|
||||||
* config.yaml
|
|
||||||
* configuration file used to save/load various gui related settings (custom nodes, layouts, addresses, etc)
|
|
||||||
|
|
||||||
## Modes of Operation
|
|
||||||
|
|
||||||
The CORE GUI has two primary modes of operation, **Edit** and **Execute**
|
|
||||||
modes. Running the GUI, by typing **core-pygui** with no options, starts in
|
|
||||||
Edit mode. Nodes are drawn on a blank canvas using the toolbar on the left
|
|
||||||
and configured from right-click menus or by double-clicking them. The GUI
|
|
||||||
does not need to be run as root.
|
|
||||||
|
|
||||||
Once editing is complete, pressing the green **Start** button instantiates
|
|
||||||
the topology and enters Execute mode. In execute mode,
|
|
||||||
the user can interact with the running emulated machines by double-clicking or
|
|
||||||
right-clicking on them. The editing toolbar disappears and is replaced by an
|
|
||||||
execute toolbar, which provides tools while running the emulation. Pressing
|
|
||||||
the red **Stop** button will destroy the running emulation and return CORE
|
|
||||||
to Edit mode.
|
|
||||||
|
|
||||||
Once the emulation is running, the GUI can be closed, and a prompt will appear
|
|
||||||
asking if the emulation should be terminated. The emulation may be left
|
|
||||||
running and the GUI can reconnect to an existing session at a later time.
|
|
||||||
|
|
||||||
The GUI can be run as a normal user on Linux.
|
|
||||||
|
|
||||||
The python GUI currently provides the following options on startup.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
usage: core-pygui [-h] [-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [-p]
|
|
||||||
|
|
||||||
CORE Python GUI
|
|
||||||
|
|
||||||
optional arguments:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
|
||||||
logging level
|
|
||||||
-p, --proxy enable proxy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Toolbar
|
|
||||||
|
|
||||||
The toolbar is a row of buttons that runs vertically along the left side of the
|
|
||||||
CORE GUI window. The toolbar changes depending on the mode of operation.
|
|
||||||
|
|
||||||
### Editing Toolbar
|
|
||||||
|
|
||||||
When CORE is in Edit mode (the default), the vertical Editing Toolbar exists on
|
|
||||||
the left side of the CORE window. Below are brief descriptions for each toolbar
|
|
||||||
item, starting from the top. Most of the tools are grouped into related
|
|
||||||
sub-menus, which appear when you click on their group icon.
|
|
||||||
|
|
||||||
| Icon | Name | Description |
|
|
||||||
|------------------------------|----------------|----------------------------------------------------------------------------------------|
|
|
||||||
| ![](static/pygui/select.png) | Selection Tool | Tool for selecting, moving, configuring nodes. |
|
|
||||||
| ![](static/pygui/start.png) | Start Button | Starts Execute mode, instantiates the emulation. |
|
|
||||||
| ![](static/pygui/link.png) | Link | Allows network links to be drawn between two nodes by clicking and dragging the mouse. |
|
|
||||||
|
|
||||||
### CORE Nodes
|
|
||||||
|
|
||||||
These nodes will create a new node container and run associated services.
|
|
||||||
|
|
||||||
| Icon | Name | Description |
|
|
||||||
|------------------------------|---------|------------------------------------------------------------------------------|
|
|
||||||
| ![](static/pygui/router.png) | Router | Runs Quagga OSPFv2 and OSPFv3 routing to forward packets. |
|
|
||||||
| ![](static/pygui/host.png) | Host | Emulated server machine having a default route, runs SSH server. |
|
|
||||||
| ![](static/pygui/pc.png) | PC | Basic emulated machine having a default route, runs no processes by default. |
|
|
||||||
| ![](static/pygui/mdr.png) | MDR | Runs Quagga OSPFv3 MDR routing for MANET-optimized routing. |
|
|
||||||
| ![](static/pygui/router.png) | PRouter | Physical router represents a real testbed machine. |
|
|
||||||
|
|
||||||
### Network Nodes
|
|
||||||
|
|
||||||
These nodes are mostly used to create a Linux bridge that serves the
|
|
||||||
purpose described below.
|
|
||||||
|
|
||||||
| Icon | Name | Description |
|
|
||||||
|---------------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| ![](static/pygui/hub.png) | Hub | Ethernet hub forwards incoming packets to every connected node. |
|
|
||||||
| ![](static/pygui/lanswitch.png) | Switch | Ethernet switch intelligently forwards incoming packets to attached hosts using an Ethernet address hash table. |
|
|
||||||
| ![](static/pygui/wlan.png) | Wireless LAN | When routers are connected to this WLAN node, they join a wireless network and an antenna is drawn instead of a connecting line; the WLAN node typically controls connectivity between attached wireless nodes based on the distance between them. |
|
|
||||||
| ![](static/pygui/rj45.png) | RJ45 | RJ45 Physical Interface Tool, emulated nodes can be linked to real physical interfaces; using this tool, real networks and devices can be physically connected to the live-running emulation. |
|
|
||||||
| ![](static/pygui/tunnel.png) | Tunnel | Tool allows connecting together more than one CORE emulation using GRE tunnels. |
|
|
||||||
|
|
||||||
### Annotation Tools
|
|
||||||
|
|
||||||
| Icon | Name | Description |
|
|
||||||
|---------------------------------|-----------|---------------------------------------------------------------------|
|
|
||||||
| ![](static/pygui/marker.png) | Marker | For drawing marks on the canvas. |
|
|
||||||
| ![](static/pygui/oval.png) | Oval | For drawing circles on the canvas that appear in the background. |
|
|
||||||
| ![](static/pygui/rectangle.png) | Rectangle | For drawing rectangles on the canvas that appear in the background. |
|
|
||||||
| ![](static/pygui/text.png) | Text | For placing text captions on the canvas. |
|
|
||||||
|
|
||||||
### Execution Toolbar
|
|
||||||
|
|
||||||
When the Start button is pressed, CORE switches to Execute mode, and the Edit
|
|
||||||
toolbar on the left of the CORE window is replaced with the Execution toolbar
|
|
||||||
Below are the items on this toolbar, starting from the top.
|
|
||||||
|
|
||||||
| Icon | Name | Description |
|
|
||||||
|------------------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| ![](static/pygui/stop.png) | Stop Button | Stops Execute mode, terminates the emulation, returns CORE to edit mode. |
|
|
||||||
| ![](static/pygui/select.png) | Selection Tool | In Execute mode, the Selection Tool can be used for moving nodes around the canvas, and double-clicking on a node will open a shell window for that node; right-clicking on a node invokes a pop-up menu of run-time options for that node. |
|
|
||||||
| ![](static/pygui/marker.png) | Marker | For drawing freehand lines on the canvas, useful during demonstrations; markings are not saved. |
|
|
||||||
| ![](static/pygui/run.png) | Run Tool | This tool allows easily running a command on all or a subset of all nodes. A list box allows selecting any of the nodes. A text entry box allows entering any command. The command should return immediately, otherwise the display will block awaiting response. The *ping* command, for example, with no parameters, is not a good idea. The result of each command is displayed in a results box. The first occurrence of the special text "NODE" will be replaced with the node name. The command will not be attempted to run on nodes that are not routers, PCs, or hosts, even if they are selected. |
|
|
||||||
|
|
||||||
## Menu
|
|
||||||
|
|
||||||
The menubar runs along the top of the CORE GUI window and provides access to a
|
|
||||||
variety of features. Some of the menus are detachable, such as the *Widgets*
|
|
||||||
menu, by clicking the dashed line at the top.
|
|
||||||
|
|
||||||
### File Menu
|
|
||||||
|
|
||||||
The File menu contains options for manipulating the **.imn** Configuration
|
|
||||||
Files. Generally, these menu items should not be used in Execute mode.
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| New Session | This starts a new session with an empty canvas. |
|
|
||||||
| Save | Saves the current topology. If you have not yet specified a file name, the Save As dialog box is invoked. |
|
|
||||||
| Save As | Invokes the Save As dialog box for selecting a new **.xml** file for saving the current configuration in the XML file. |
|
|
||||||
| Open | Invokes the File Open dialog box for selecting a new XML file to open. |
|
|
||||||
| Recently used files | Above the Quit menu command is a list of recently use files, if any have been opened. You can clear this list in the Preferences dialog box. You can specify the number of files to keep in this list from the Preferences dialog. Click on one of the file names listed to open that configuration file. |
|
|
||||||
| Execute Python Script | Invokes a File Open dialog box for selecting a Python script to run and automatically connect to. After a selection is made, a Python Script Options dialog box is invoked to allow for command-line options to be added. The Python script must create a new CORE Session and add this session to the daemon's list of sessions in order for this to work. |
|
|
||||||
| Quit | The Quit command should be used to exit the CORE GUI. CORE may prompt for termination if you are currently in Execute mode. Preferences and the recently-used files list are saved. |
|
|
||||||
|
|
||||||
### Edit Menu
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|--------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| Preferences | Invokes the Preferences dialog box. |
|
|
||||||
| Custom Nodes | Custom node creation dialog box. |
|
|
||||||
| Undo | (Disabled) Attempts to undo the last edit in edit mode. |
|
|
||||||
| Redo | (Disabled) Attempts to redo an edit that has been undone. |
|
|
||||||
| Cut, Copy, Paste, Delete | Used to cut, copy, paste, and delete a selection. When nodes are pasted, their node numbers are automatically incremented, and existing links are preserved with new IP addresses assigned. Services and their customizations are copied to the new node, but care should be taken as node IP addresses have changed with possibly old addresses remaining in any custom service configurations. Annotations may also be copied and pasted. |
|
|
||||||
|
|
||||||
### Canvas Menu
|
|
||||||
|
|
||||||
The canvas menu provides commands related to the editing canvas.
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| Size/scale | Invokes a Canvas Size and Scale dialog that allows configuring the canvas size, scale, and geographic reference point. The size controls allow changing the width and height of the current canvas, in pixels or meters. The scale allows specifying how many meters are equivalent to 100 pixels. The reference point controls specify the latitude, longitude, and altitude reference point used to convert between geographic and Cartesian coordinate systems. By clicking the *Save as default* option, all new canvases will be created with these properties. The default canvas size can also be changed in the Preferences dialog box. |
|
|
||||||
| Wallpaper | Used for setting the canvas background image. |
|
|
||||||
|
|
||||||
### View Menu
|
|
||||||
|
|
||||||
The View menu features items for toggling on and off their display on the canvas.
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|-----------------|-----------------------------------|
|
|
||||||
| Interface Names | Display interface names on links. |
|
|
||||||
| IPv4 Addresses | Display IPv4 addresses on links. |
|
|
||||||
| IPv6 Addresses | Display IPv6 addresses on links. |
|
|
||||||
| Node Labels | Display node names. |
|
|
||||||
| Link Labels | Display link labels. |
|
|
||||||
| Annotations | Display annotations. |
|
|
||||||
| Canvas Grid | Display the canvas grid. |
|
|
||||||
|
|
||||||
### Tools Menu
|
|
||||||
|
|
||||||
The tools menu lists different utility functions.
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| Find | Display find dialog used for highlighting a node on the canvas. |
|
|
||||||
| Auto Grid | Automatically layout nodes in a grid. |
|
|
||||||
| IP addresses | Invokes the IP Addresses dialog box for configuring which IPv4/IPv6 prefixes are used when automatically addressing new interfaces. |
|
|
||||||
| MAC addresses | Invokes the MAC Addresses dialog box for configuring the starting number used as the lowest byte when generating each interface MAC address. This value should be changed when tunneling between CORE emulations to prevent MAC address conflicts. |
|
|
||||||
|
|
||||||
### Widgets Menu
|
|
||||||
|
|
||||||
Widgets are GUI elements that allow interaction with a running emulation.
|
|
||||||
Widgets typically automate the running of commands on emulated nodes to report
|
|
||||||
status information of some type and display this on screen.
|
|
||||||
|
|
||||||
#### Periodic Widgets
|
|
||||||
|
|
||||||
These Widgets are those available from the main *Widgets* menu. More than one
|
|
||||||
of these Widgets may be run concurrently. An event loop fires once every second
|
|
||||||
that the emulation is running. If one of these Widgets is enabled, its periodic
|
|
||||||
routine will be invoked at this time. Each Widget may have a configuration
|
|
||||||
dialog box which is also accessible from the *Widgets* menu.
|
|
||||||
|
|
||||||
Here are some standard widgets:
|
|
||||||
|
|
||||||
* **Adjacency** - displays router adjacency states for Quagga's OSPFv2 and OSPFv3
|
|
||||||
routing protocols. A line is drawn from each router halfway to the router ID
|
|
||||||
of an adjacent router. The color of the line is based on the OSPF adjacency
|
|
||||||
state such as Two-way or Full. To learn about the different colors, see the
|
|
||||||
*Configure Adjacency...* menu item. The **vtysh** command is used to
|
|
||||||
dump OSPF neighbor information.
|
|
||||||
Only half of the line is drawn because each
|
|
||||||
router may be in a different adjacency state with respect to the other.
|
|
||||||
* **Throughput** - displays the kilobits-per-second throughput above each link,
|
|
||||||
using statistics gathered from the ng_pipe Netgraph node that implements each
|
|
||||||
link. If the throughput exceeds a certain threshold, the link will become
|
|
||||||
highlighted. For wireless nodes which broadcast data to all nodes in range,
|
|
||||||
the throughput rate is displayed next to the node and the node will become
|
|
||||||
circled if the threshold is exceeded.
|
|
||||||
|
|
||||||
#### Observer Widgets
|
|
||||||
|
|
||||||
These Widgets are available from the **Observer Widgets** submenu of the
|
|
||||||
**Widgets** menu, and from the Widgets Tool on the toolbar. Only one Observer Widget may
|
|
||||||
be used at a time. Mouse over a node while the session is running to pop up
|
|
||||||
an informational display about that node.
|
|
||||||
|
|
||||||
Available Observer Widgets include IPv4 and IPv6 routing tables, socket
|
|
||||||
information, list of running processes, and OSPFv2/v3 neighbor information.
|
|
||||||
|
|
||||||
Observer Widgets may be edited by the user and rearranged. Choosing
|
|
||||||
**Widgets->Observer Widgets->Edit Observers** from the Observer Widget menu will
|
|
||||||
invoke the Observer Widgets dialog. A list of Observer Widgets is displayed along
|
|
||||||
with up and down arrows for rearranging the list. Controls are available for
|
|
||||||
renaming each widget, for changing the command that is run during mouse over, and
|
|
||||||
for adding and deleting items from the list. Note that specified commands should
|
|
||||||
return immediately to avoid delays in the GUI display. Changes are saved to a
|
|
||||||
**config.yaml** file in the CORE configuration directory.
|
|
||||||
|
|
||||||
### Session Menu
|
|
||||||
|
|
||||||
The Session Menu has entries for starting, stopping, and managing sessions,
|
|
||||||
in addition to global options such as node types, comments, hooks, servers,
|
|
||||||
and options.
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| Sessions | Invokes the CORE Sessions dialog box containing a list of active CORE sessions in the daemon. Basic session information such as name, node count, start time, and a thumbnail are displayed. This dialog allows connecting to different sessions, shutting them down, or starting a new session. |
|
|
||||||
| Servers | Invokes the CORE emulation servers dialog for configuring. |
|
|
||||||
| Options | Presents per-session options, such as the IPv4 prefix to be used, if any, for a control network the ability to preserve the session directory; and an on/off switch for SDT3D support. |
|
|
||||||
| Hooks | Invokes the CORE Session Hooks window where scripts may be configured for a particular session state. The session states are defined in the [table](#session-states) below. The top of the window has a list of configured hooks, and buttons on the bottom left allow adding, editing, and removing hook scripts. The new or edit button will open a hook script editing window. A hook script is a shell script invoked on the host (not within a virtual node). |
|
|
||||||
|
|
||||||
#### Session States
|
|
||||||
|
|
||||||
| State | Description |
|
|
||||||
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| Definition | Used by the GUI to tell the backend to clear any state. |
|
|
||||||
| Configuration | When the user presses the *Start* button, node, link, and other configuration data is sent to the backend. This state is also reached when the user customizes a service. |
|
|
||||||
| Instantiation | After configuration data has been sent, just before the nodes are created. |
|
|
||||||
| Runtime | All nodes and networks have been built and are running. (This is the same state at which the previously-named *global experiment script* was run.) |
|
|
||||||
| Datacollect | The user has pressed the *Stop* button, but before services have been stopped and nodes have been shut down. This is a good time to collect log files and other data from the nodes. |
|
|
||||||
| Shutdown | All nodes and networks have been shut down and destroyed. |
|
|
||||||
|
|
||||||
### Help Menu
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|--------------------------|---------------------------------------------------------------|
|
|
||||||
| CORE Github (www) | Link to the CORE GitHub page. |
|
|
||||||
| CORE Documentation (www) | Lnk to the CORE Documentation page. |
|
|
||||||
| About | Invokes the About dialog box for viewing version information. |
|
|
||||||
|
|
||||||
## Connecting with Physical Networks
|
|
||||||
|
|
||||||
CORE's emulated networks run in real time, so they can be connected to live
|
|
||||||
physical networks. The RJ45 tool and the Tunnel tool help with connecting to
|
|
||||||
the real world. These tools are available from the **Link-layer nodes** menu.
|
|
||||||
|
|
||||||
When connecting two or more CORE emulations together, MAC address collisions
|
|
||||||
should be avoided. CORE automatically assigns MAC addresses to interfaces when
|
|
||||||
the emulation is started, starting with **00:00:00:aa:00:00** and incrementing
|
|
||||||
the bottom byte. The starting byte should be changed on the second CORE machine
|
|
||||||
using the **Tools->MAC Addresses** option the menu.
|
|
||||||
|
|
||||||
### RJ45 Tool
|
|
||||||
|
|
||||||
The RJ45 node in CORE represents a physical interface on the real CORE machine.
|
|
||||||
Any real-world network device can be connected to the interface and communicate
|
|
||||||
with the CORE nodes in real time.
|
|
||||||
|
|
||||||
The main drawback is that one physical interface is required for each
|
|
||||||
connection. When the physical interface is assigned to CORE, it may not be used
|
|
||||||
for anything else. Another consideration is that the computer or network that
|
|
||||||
you are connecting to must be co-located with the CORE machine.
|
|
||||||
|
|
||||||
To place an RJ45 connection, click on the **Link-layer nodes** toolbar and select
|
|
||||||
the **RJ45 Tool** from the submenu. Click on the canvas near the node you want to
|
|
||||||
connect to. This could be a router, hub, switch, or WLAN, for example. Now
|
|
||||||
click on the *Link Tool* and draw a link between the RJ45 and the other node.
|
|
||||||
The RJ45 node will display "UNASSIGNED". Double-click the RJ45 node to assign a
|
|
||||||
physical interface. A list of available interfaces will be shown, and one may
|
|
||||||
be selected by double-clicking its name in the list, or an interface name may
|
|
||||||
be entered into the text box.
|
|
||||||
|
|
||||||
> **NOTE:** When you press the Start button to instantiate your topology, the
|
|
||||||
interface assigned to the RJ45 will be connected to the CORE topology. The
|
|
||||||
interface can no longer be used by the system.
|
|
||||||
|
|
||||||
Multiple RJ45 nodes can be used within CORE and assigned to the same physical
|
|
||||||
interface if 802.1x VLANs are used. This allows for more RJ45 nodes than
|
|
||||||
physical ports are available, but the (e.g. switching) hardware connected to
|
|
||||||
the physical port must support the VLAN tagging, and the available bandwidth
|
|
||||||
will be shared.
|
|
||||||
|
|
||||||
You need to create separate VLAN virtual devices on the Linux host,
|
|
||||||
and then assign these devices to RJ45 nodes inside of CORE. The VLANning is
|
|
||||||
actually performed outside of CORE, so when the CORE emulated node receives a
|
|
||||||
packet, the VLAN tag will already be removed.
|
|
||||||
|
|
||||||
Here are example commands for creating VLAN devices under Linux:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
ip link add link eth0 name eth0.1 type vlan id 1
|
|
||||||
ip link add link eth0 name eth0.2 type vlan id 2
|
|
||||||
ip link add link eth0 name eth0.3 type vlan id 3
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tunnel Tool
|
|
||||||
|
|
||||||
The tunnel tool builds GRE tunnels between CORE emulations or other hosts.
|
|
||||||
Tunneling can be helpful when the number of physical interfaces is limited or
|
|
||||||
when the peer is located on a different network. Also a physical interface does
|
|
||||||
not need to be dedicated to CORE as with the RJ45 tool.
|
|
||||||
|
|
||||||
The peer GRE tunnel endpoint may be another CORE machine or another
|
|
||||||
host that supports GRE tunneling. When placing a Tunnel node, initially
|
|
||||||
the node will display "UNASSIGNED". This text should be replaced with the IP
|
|
||||||
address of the tunnel peer. This is the IP address of the other CORE machine or
|
|
||||||
physical machine, not an IP address of another virtual node.
|
|
||||||
|
|
||||||
> **NOTE:** Be aware of possible MTU (Maximum Transmission Unit) issues with GRE devices. The *gretap* device
|
|
||||||
has an interface MTU of 1,458 bytes; when joined to a Linux bridge, the
|
|
||||||
bridge's MTU
|
|
||||||
becomes 1,458 bytes. The Linux bridge will not perform fragmentation for
|
|
||||||
large packets if other bridge ports have a higher MTU such as 1,500 bytes.
|
|
||||||
|
|
||||||
The GRE key is used to identify flows with GRE tunneling. This allows multiple
|
|
||||||
GRE tunnels to exist between that same pair of tunnel peers. A unique number
|
|
||||||
should be used when multiple tunnels are used with the same peer. When
|
|
||||||
configuring the peer side of the tunnel, ensure that the matching keys are
|
|
||||||
used.
|
|
||||||
|
|
||||||
Here are example commands for building the other end of a tunnel on a Linux
|
|
||||||
machine. In this example, a router in CORE has the virtual address
|
|
||||||
**10.0.0.1/24** and the CORE host machine has the (real) address
|
|
||||||
**198.51.100.34/24**. The Linux box
|
|
||||||
that will connect with the CORE machine is reachable over the (real) network
|
|
||||||
at **198.51.100.76/24**.
|
|
||||||
The emulated router is linked with the Tunnel Node. In the
|
|
||||||
Tunnel Node configuration dialog, the address **198.51.100.76** is entered, with
|
|
||||||
the key set to **1**. The gretap interface on the Linux box will be assigned
|
|
||||||
an address from the subnet of the virtual router node,
|
|
||||||
**10.0.0.2/24**.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# these commands are run on the tunnel peer
|
|
||||||
sudo ip link add gt0 type gretap remote 198.51.100.34 local 198.51.100.76 key 1
|
|
||||||
sudo ip addr add 10.0.0.2/24 dev gt0
|
|
||||||
sudo ip link set dev gt0 up
|
|
||||||
```
|
|
||||||
|
|
||||||
Now the virtual router should be able to ping the Linux machine:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# from the CORE router node
|
|
||||||
ping 10.0.0.2
|
|
||||||
```
|
|
||||||
|
|
||||||
And the Linux machine should be able to ping inside the CORE emulation:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# from the tunnel peer
|
|
||||||
ping 10.0.0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
To debug this configuration, **tcpdump** can be run on the gretap devices, or
|
|
||||||
on the physical interfaces on the CORE or Linux machines. Make sure that a
|
|
||||||
firewall is not blocking the GRE traffic.
|
|
||||||
|
|
||||||
### Communicating with the Host Machine
|
|
||||||
|
|
||||||
The host machine that runs the CORE GUI and/or daemon is not necessarily
|
|
||||||
accessible from a node. Running an X11 application on a node, for example,
|
|
||||||
requires some channel of communication for the application to connect with
|
|
||||||
the X server for graphical display. There are several different ways to
|
|
||||||
connect from the node to the host and vice versa.
|
|
||||||
|
|
||||||
#### Control Network
|
|
||||||
|
|
||||||
The quickest way to connect with the host machine through the primary control
|
|
||||||
network.
|
|
||||||
|
|
||||||
With a control network, the host can launch an X11 application on a node.
|
|
||||||
To run an X11 application on the node, the **SSH** service can be enabled on
|
|
||||||
the node, and SSH with X11 forwarding can be used from the host to the node.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# SSH from host to node n5 to run an X11 app
|
|
||||||
ssh -X 172.16.0.5 xclock
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that the **coresendmsg** utility can be used for a node to send
|
|
||||||
messages to the CORE daemon running on the host (if the **listenaddr = 0.0.0.0**
|
|
||||||
is set in the **/etc/core/core.conf** file) to interact with the running
|
|
||||||
emulation. For example, a node may move itself or other nodes, or change
|
|
||||||
its icon based on some node state.
|
|
||||||
|
|
||||||
#### Other Methods
|
|
||||||
|
|
||||||
There are still other ways to connect a host with a node. The RJ45 Tool
|
|
||||||
can be used in conjunction with a dummy interface to access a node:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo modprobe dummy numdummies=1
|
|
||||||
```
|
|
||||||
|
|
||||||
A **dummy0** interface should appear on the host. Use the RJ45 tool assigned
|
|
||||||
to **dummy0**, and link this to a node in your scenario. After starting the
|
|
||||||
session, configure an address on the host.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo ip link show type bridge
|
|
||||||
# determine bridge name from the above command
|
|
||||||
# assign an IP address on the same network as the linked node
|
|
||||||
sudo ip addr add 10.0.1.2/24 dev b.48304.34658
|
|
||||||
```
|
|
||||||
|
|
||||||
In the example shown above, the host will have the address **10.0.1.2** and
|
|
||||||
the node linked to the RJ45 may have the address **10.0.1.1**.
|
|
||||||
|
|
||||||
## Building Sample Networks
|
|
||||||
|
|
||||||
### Wired Networks
|
|
||||||
|
|
||||||
Wired networks are created using the **Link Tool** to draw a link between two
|
|
||||||
nodes. This automatically draws a red line representing an Ethernet link and
|
|
||||||
creates new interfaces on network-layer nodes.
|
|
||||||
|
|
||||||
Double-click on the link to invoke the **link configuration** dialog box. Here
|
|
||||||
you can change the Bandwidth, Delay, Loss, and Duplicate
|
|
||||||
rate parameters for that link. You can also modify the color and width of the
|
|
||||||
link, affecting its display.
|
|
||||||
|
|
||||||
Link-layer nodes are provided for modeling wired networks. These do not create
|
|
||||||
a separate network stack when instantiated, but are implemented using Linux bridging.
|
|
||||||
These are the hub, switch, and wireless LAN nodes. The hub copies each packet from
|
|
||||||
the incoming link to every connected link, while the switch behaves more like an
|
|
||||||
Ethernet switch and keeps track of the Ethernet address of the connected peer,
|
|
||||||
forwarding unicast traffic only to the appropriate ports.
|
|
||||||
|
|
||||||
The wireless LAN (WLAN) is covered in the next section.
|
|
||||||
|
|
||||||
### Wireless Networks
|
|
||||||
|
|
||||||
The wireless LAN node allows you to build wireless networks where moving nodes
|
|
||||||
around affects the connectivity between them. Connection between a pair of nodes is stronger
|
|
||||||
when the nodes are closer while connection is weaker when the nodes are further away.
|
|
||||||
The wireless LAN, or WLAN, node appears as a small cloud. The WLAN offers
|
|
||||||
several levels of wireless emulation fidelity, depending on your modeling needs.
|
|
||||||
|
|
||||||
The WLAN tool can be extended with plug-ins for different levels of wireless
|
|
||||||
fidelity. The basic on/off range is the default setting available on all
|
|
||||||
platforms. Other plug-ins offer higher fidelity at the expense of greater
|
|
||||||
complexity and CPU usage. The availability of certain plug-ins varies depending
|
|
||||||
on platform. See the table below for a brief overview of wireless model types.
|
|
||||||
|
|
||||||
|
|
||||||
| Model | Type | Supported Platform(s) | Fidelity | Description |
|
|
||||||
|-------|---------|-----------------------|----------|-------------------------------------------------------------------------------|
|
|
||||||
| Basic | on/off | Linux | Low | Ethernet bridging with nftables |
|
|
||||||
| EMANE | Plug-in | Linux | High | TAP device connected to EMANE emulator with pluggable MAC and PHY radio types |
|
|
||||||
|
|
||||||
To quickly build a wireless network, you can first place several router nodes
|
|
||||||
onto the canvas. If you have the
|
|
||||||
Quagga MDR software installed, it is
|
|
||||||
recommended that you use the **mdr** node type for reduced routing overhead. Next
|
|
||||||
choose the **WLAN** from the **Link-layer nodes** submenu. First set the
|
|
||||||
desired WLAN parameters by double-clicking the cloud icon. Then you can link
|
|
||||||
all selected right-clicking on the WLAN and choosing **Link to Selected**.
|
|
||||||
|
|
||||||
Linking a router to the WLAN causes a small antenna to appear, but no red link
|
|
||||||
line is drawn. Routers can have multiple wireless links and both wireless and
|
|
||||||
wired links (however, you will need to manually configure route
|
|
||||||
redistribution.) The mdr node type will generate a routing configuration that
|
|
||||||
enables OSPFv3 with MANET extensions. This is a Boeing-developed extension to
|
|
||||||
Quagga's OSPFv3 that reduces flooding overhead and optimizes the flooding
|
|
||||||
procedure for mobile ad-hoc (MANET) networks.
|
|
||||||
|
|
||||||
The default configuration of the WLAN is set to use the basic range model. Having this model
|
|
||||||
selected causes **core-daemon** to calculate the distance between nodes based
|
|
||||||
on screen pixels. A numeric range in screen pixels is set for the wireless
|
|
||||||
network using the **Range** slider. When two wireless nodes are within range of
|
|
||||||
each other, a green line is drawn between them and they are linked. Two
|
|
||||||
wireless nodes that are farther than the range pixels apart are not linked.
|
|
||||||
During Execute mode, users may move wireless nodes around by clicking and
|
|
||||||
dragging them, and wireless links will be dynamically made or broken.
|
|
||||||
|
|
||||||
The **EMANE Nodes** leverage available EMANE models to use for wireless networking.
|
|
||||||
See the [EMANE](emane.md) chapter for details on using EMANE.
|
|
||||||
|
|
||||||
### Mobility Scripting
|
|
||||||
|
|
||||||
CORE has a few ways to script mobility.
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| ns-2 script | The script specifies either absolute positions or waypoints with a velocity. Locations are given with Cartesian coordinates. |
|
|
||||||
| CORE API | An external entity can move nodes by sending CORE API Node messages with updated X,Y coordinates; the **coresendmsg** utility allows a shell script to generate these messages. |
|
|
||||||
| EMANE events | See [EMANE](emane.md) for details on using EMANE scripts to move nodes around. Location information is typically given as latitude, longitude, and altitude. |
|
|
||||||
|
|
||||||
For the first method, you can create a mobility script using a text
|
|
||||||
editor, or using a tool such as [BonnMotion](http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/), and associate the script with one of the wireless
|
|
||||||
using the WLAN configuration dialog box. Click the *ns-2 mobility script...*
|
|
||||||
button, and set the *mobility script file* field in the resulting *ns2script*
|
|
||||||
configuration dialog.
|
|
||||||
|
|
||||||
Here is an example for creating a BonnMotion script for 10 nodes:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
bm -f sample RandomWaypoint -n 10 -d 60 -x 1000 -y 750
|
|
||||||
bm NSFile -f sample
|
|
||||||
# use the resulting 'sample.ns_movements' file in CORE
|
|
||||||
```
|
|
||||||
|
|
||||||
When the Execute mode is started and one of the WLAN nodes has a mobility
|
|
||||||
script, a mobility script window will appear. This window contains controls for
|
|
||||||
starting, stopping, and resetting the running time for the mobility script. The
|
|
||||||
**loop** checkbox causes the script to play continuously. The **resolution** text
|
|
||||||
box contains the number of milliseconds between each timer event; lower values
|
|
||||||
cause the mobility to appear smoother but consumes greater CPU time.
|
|
||||||
|
|
||||||
The format of an ns-2 mobility script looks like:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# nodes: 3, max time: 35.000000, max x: 600.00, max y: 600.00
|
|
||||||
$node_(2) set X_ 144.0
|
|
||||||
$node_(2) set Y_ 240.0
|
|
||||||
$node_(2) set Z_ 0.00
|
|
||||||
$ns_ at 1.00 "$node_(2) setdest 130.0 280.0 15.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
The first three lines set an initial position for node 2. The last line in the
|
|
||||||
above example causes node 2 to move towards the destination **(130, 280)** at
|
|
||||||
speed **15**. All units are screen coordinates, with speed in units per second.
|
|
||||||
The total script time is learned after all nodes have reached their waypoints.
|
|
||||||
Initially, the time slider in the mobility script dialog will not be
|
|
||||||
accurate.
|
|
||||||
|
|
||||||
Examples mobility scripts (and their associated topology files) can be found
|
|
||||||
in the **configs/** directory.
|
|
||||||
|
|
||||||
## Alerts
|
|
||||||
|
|
||||||
The alerts button is located in the bottom right-hand corner
|
|
||||||
of the status bar in the CORE GUI. This will change colors to indicate one or
|
|
||||||
more problems with the running emulation. Clicking on the alerts button will invoke the
|
|
||||||
alerts dialog.
|
|
||||||
|
|
||||||
The alerts dialog contains a list of alerts received from
|
|
||||||
the CORE daemon. An alert has a time, severity level, optional node number,
|
|
||||||
and source. When the alerts button is red, this indicates one or more fatal
|
|
||||||
exceptions. An alert with a fatal severity level indicates that one or more
|
|
||||||
of the basic pieces of emulation could not be created, such as failure to
|
|
||||||
create a bridge or namespace, or the failure to launch EMANE processes for an
|
|
||||||
EMANE-based network.
|
|
||||||
|
|
||||||
Clicking on an alert displays details for that
|
|
||||||
exceptio. The exception source is a text string
|
|
||||||
to help trace where the exception occurred; "service:UserDefined" for example,
|
|
||||||
would appear for a failed validation command with the UserDefined service.
|
|
||||||
|
|
||||||
A button is available at the bottom of the dialog for clearing the exception
|
|
||||||
list.
|
|
||||||
|
|
||||||
## Customizing your Topology's Look
|
|
||||||
|
|
||||||
Several annotation tools are provided for changing the way your topology is
|
|
||||||
presented. Captions may be added with the Text tool. Ovals and rectangles may
|
|
||||||
be drawn in the background, helpful for visually grouping nodes together.
|
|
||||||
|
|
||||||
During live demonstrations the marker tool may be helpful for drawing temporary
|
|
||||||
annotations on the canvas that may be quickly erased. A size and color palette
|
|
||||||
appears at the bottom of the toolbar when the marker tool is selected. Markings
|
|
||||||
are only temporary and are not saved in the topology file.
|
|
||||||
|
|
||||||
The basic node icons can be replaced with a custom image of your choice. Icons
|
|
||||||
appear best when they use the GIF or PNG format with a transparent background.
|
|
||||||
To change a node's icon, double-click the node to invoke its configuration
|
|
||||||
dialog and click on the button to the right of the node name that shows the
|
|
||||||
node's current icon.
|
|
||||||
|
|
||||||
A background image for the canvas may be set using the *Wallpaper...* option
|
|
||||||
from the *Canvas* menu. The image may be centered, tiled, or scaled to fit the
|
|
||||||
canvas size. An existing terrain, map, or network diagram could be used as a
|
|
||||||
background, for example, with CORE nodes drawn on top.
|
|
|
@ -63,13 +63,6 @@ toolbar, or choose *Node types...* from the *Session* menu. Note that
|
||||||
any new services selected are not applied to existing nodes if the nodes have
|
any new services selected are not applied to existing nodes if the nodes have
|
||||||
been customized.
|
been customized.
|
||||||
|
|
||||||
The node types are saved in a **~/.core/nodes.conf** file, not with the
|
|
||||||
**.imn** file. Keep this in mind when changing the default services for
|
|
||||||
existing node types; it may be better to simply create a new node type. It is
|
|
||||||
recommended that you do not change the default built-in node types. The
|
|
||||||
**nodes.conf** file can be copied between CORE machines to save your custom
|
|
||||||
types.
|
|
||||||
|
|
||||||
## Customizing a Service
|
## Customizing a Service
|
||||||
|
|
||||||
A service can be fully customized for a particular node. From the node's
|
A service can be fully customized for a particular node. From the node's
|
||||||
|
@ -151,11 +144,11 @@ ideas for a service before adding a new service type.
|
||||||
defines one or more classes to be imported. You can create multiple Python
|
defines one or more classes to be imported. You can create multiple Python
|
||||||
files that will be imported.
|
files that will be imported.
|
||||||
|
|
||||||
2. Put these files in a directory such as /home/username/.core/myservices
|
2. Put these files in a directory such as `/home/<user>/.coregui/custom_services`
|
||||||
Note that the last component of this directory name **myservices** should not
|
Note that the last component of this directory name **custom_services** should not
|
||||||
be named something like **services** which conflicts with an existing module.
|
be named the same as any python module, due to naming conflicts.
|
||||||
|
|
||||||
3. Add a **custom_services_dir = /home/username/.core/myservices** entry to the
|
3. Add a **custom_services_dir = `/home/<user>/.coregui/custom_services`** entry to the
|
||||||
/etc/core/core.conf file.
|
/etc/core/core.conf file.
|
||||||
|
|
||||||
**NOTE:**
|
**NOTE:**
|
||||||
|
@ -165,7 +158,7 @@ ideas for a service before adding a new service type.
|
||||||
or **services**.
|
or **services**.
|
||||||
|
|
||||||
4. Restart the CORE daemon (core-daemon). Any import errors (Python syntax)
|
4. Restart the CORE daemon (core-daemon). Any import errors (Python syntax)
|
||||||
should be displayed in the /var/log/core-daemon.log log file (or on screen).
|
should be displayed in the daemon output.
|
||||||
|
|
||||||
5. Start using your custom service on your nodes. You can create a new node
|
5. Start using your custom service on your nodes. You can create a new node
|
||||||
type that uses your service, or change the default services for an existing
|
type that uses your service, or change the default services for an existing
|
||||||
|
|
BIN
docs/static/architecture.png
vendored
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 637 KiB After Width: | Height: | Size: 637 KiB |
BIN
docs/static/core_screenshot.png
vendored
Before Width: | Height: | Size: 753 KiB |
BIN
docs/static/distributed-controlnetwork.png
vendored
Before Width: | Height: | Size: 45 KiB |
BIN
docs/static/distributed-emane-network.png
vendored
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
BIN
docs/static/gui/document-properties.gif
vendored
Before Width: | Height: | Size: 635 B |
BIN
docs/static/gui/host.gif
vendored
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 642 B After Width: | Height: | Size: 642 B |
BIN
docs/static/gui/hub.gif
vendored
Before Width: | Height: | Size: 719 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
docs/static/gui/lanswitch.gif
vendored
Before Width: | Height: | Size: 744 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
docs/static/gui/link.gif
vendored
Before Width: | Height: | Size: 86 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
docs/static/gui/marker.gif
vendored
Before Width: | Height: | Size: 375 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
docs/static/gui/mdr.gif
vendored
Before Width: | Height: | Size: 1.2 KiB |