Merge pull request #498 from coreemu/develop

7.0.0
This commit is contained in:
bharnden 2020-07-23 21:30:18 -07:00 committed by GitHub
commit eb70386238
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
209 changed files with 10343 additions and 9561 deletions

View file

@ -11,32 +11,31 @@ jobs:
uses: actions/setup-python@v1 uses: actions/setup-python@v1
with: with:
python-version: 3.6 python-version: 3.6
- name: Install pipenv - name: install poetry
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install pipenv pip install poetry
cd daemon cd daemon
cp setup.py.in setup.py
cp core/constants.py.in core/constants.py cp core/constants.py.in core/constants.py
sed -i 's/True/False/g' core/constants.py sed -i 's/required=True/required=False/g' core/emulator/coreemu.py
pipenv sync --dev poetry install
- name: isort - name: isort
run: | run: |
cd daemon cd daemon
pipenv run isort -c -df poetry run isort -c -df
- name: black - name: black
run: | run: |
cd daemon cd daemon
pipenv run black --check --exclude ".+_pb2.*.py|doc|build|utm\.py|setup\.py" . poetry run black --check .
- name: flake8 - name: flake8
run: | run: |
cd daemon cd daemon
pipenv run flake8 poetry run flake8
- name: grpc - name: grpc
run: | run: |
cd daemon/proto cd daemon/proto
pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto poetry run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto
- name: test - name: test
run: | run: |
cd daemon cd daemon
pipenv run test --mock poetry run pytest --mock tests

4
.gitignore vendored
View file

@ -39,6 +39,7 @@ coverage.xml
# python files # python files
*.egg-info *.egg-info
*.pyc
# ignore package files # ignore package files
*.rpm *.rpm
@ -55,8 +56,5 @@ coverage.xml
netns/setup.py netns/setup.py
daemon/setup.py daemon/setup.py
# ignore corefx build
corefx/target
# python # python
__pycache__ __pycache__

View file

@ -1,3 +1,53 @@
## 2020-07-23 CORE 7.0.0
* Breaking Changes
* core.emudata and core.data combined and cleaned up into core.data
* updates to consistently use mac instead of hwaddr/mac
* \#468 - code related to adding/editing/deleting links cleaned up
* \#469 - usages of per all changed to loss to be consistent
* \#470 - variables with numbered names now use numbers directly
* \#471 - node startup is no longer embedded within its constructor
* \#472 - code updated to refer to interfaces consistently as iface
* \#475 - code updates changing how ip addresses are stored on interfaces
* \#476 - executables to check for moved into own module core.executables
* \#486 - core will now install into its own python virtual environment managed by poetry
* core-daemon
* updates to properly save/load distributed servers to xml
* \#474 - added type hinting to all service files
* \#478 - fixed typo in config service directory
* \#479 - opening an xml file will now cycle through states like a normal session
* \#480 - ovs configuration will now save/load from xml and display in guis
* \#484 - changes to support adding emane links during runtime
* core-pygui
* fixed issue not displaying services for the default group in service dialogs
* fixed issue starting a session when the daemon is not present
* fixed issue attempting to open terminals for invalid nodes
* fixed issue syncing session location
* fixed issue joining a session with mobility, not in runtime
* added cpu usage monitor to status bar
* emane configurations can now be seen during runtime
* rj45 nodes can only have one link
* disabling throughputs will clear labels
* improvements to custom service copy
* link options will now be drawn on as a label
* updates to handle runtime link events
* \#477 - added optional details pane for a quick view of node/link details
* \#485 - pygui fixed observer widget for invalid nodes
* \#496 - improved alert handling
* core-gui
* \#493 - increased frame size to show all emane configuration options
* gRPC API
* added set session user rpc
* added cpu usage stream
* interface objects returned from get_node will now provide node_id, net_id, and net2_id data
* peer to peer nodes will not be included in get_session calls
* pathloss events will now throw an error when nem id not found
* \#481 - link rpc calls will broadcast out
* \#496 - added alert rpc call
* Services
* fixed issue reading files in security services
* \#494 - add staticd to daemons list for frr services
## 2020-06-11 CORE 6.5.0 ## 2020-06-11 CORE 6.5.0
* Breaking Changes * Breaking Changes
* CoreNode.newnetif - both parameters are required and now takes an InterfaceData object as its second parameter * CoreNode.newnetif - both parameters are required and now takes an InterfaceData object as its second parameter

View file

@ -11,7 +11,7 @@ if WANT_GUI
endif endif
if WANT_DAEMON if WANT_DAEMON
DAEMON = scripts daemon DAEMON = daemon
endif endif
if WANT_NETNS if WANT_NETNS
@ -44,58 +44,6 @@ DISTCLEANFILES = aclocal.m4 \
MAINTAINERCLEANFILES = .version \ MAINTAINERCLEANFILES = .version \
.version.date .version.date
define fpm-rpm =
fpm -s dir -t rpm -n core \
-m "$(PACKAGE_MAINTAINERS)" \
--license "BSD" \
--description "Common Open Research Emulator" \
--url https://github.com/coreemu/core \
--vendor "$(PACKAGE_VENDOR)" \
-p core_VERSION_ARCH.rpm \
-v $(PACKAGE_VERSION) \
--rpm-init scripts/core-daemon \
--config-files "/etc/core" \
-d "ethtool" \
-d "tcl" \
-d "tk" \
-d "procps-ng" \
-d "bash >= 3.0" \
-d "ebtables" \
-d "iproute" \
-d "libev" \
-d "net-tools" \
-d "python3 >= 3.6" \
-d "python3-tkinter" \
-C $(DESTDIR)
endef
define fpm-deb =
fpm -s dir -t deb -n core \
-m "$(PACKAGE_MAINTAINERS)" \
--license "BSD" \
--description "Common Open Research Emulator" \
--url https://github.com/coreemu/core \
--vendor "$(PACKAGE_VENDOR)" \
-p core_VERSION_ARCH.deb \
-v $(PACKAGE_VERSION) \
--deb-systemd scripts/core-daemon.service \
--deb-no-default-config-files \
--config-files "/etc/core" \
-d "ethtool" \
-d "tcl" \
-d "tk" \
-d "libtk-img" \
-d "procps" \
-d "libc6 >= 2.14" \
-d "bash >= 3.0" \
-d "ebtables" \
-d "iproute2" \
-d "libev4" \
-d "python3 >= 3.6" \
-d "python3-tk" \
-C $(DESTDIR)
endef
define fpm-distributed-deb = define fpm-distributed-deb =
fpm -s dir -t deb -n core-distributed \ fpm -s dir -t deb -n core-distributed \
-m "$(PACKAGE_MAINTAINERS)" \ -m "$(PACKAGE_MAINTAINERS)" \
@ -138,12 +86,6 @@ fpm -s dir -t rpm -n core-distributed \
-C $(DESTDIR) -C $(DESTDIR)
endef endef
.PHONY: fpm
fpm: clean-local-fpm
$(MAKE) install DESTDIR=$(DESTDIR)
$(call fpm-deb)
$(call fpm-rpm)
.PHONY: fpm-distributed .PHONY: fpm-distributed
fpm-distributed: clean-local-fpm fpm-distributed: clean-local-fpm
$(MAKE) -C netns install DESTDIR=$(DESTDIR) $(MAKE) -C netns install DESTDIR=$(DESTDIR)
@ -182,11 +124,8 @@ all: change-files
.PHONY: change-files .PHONY: change-files
change-files: change-files:
$(call change-files,gui/core-gui) $(call change-files,gui/core-gui)
$(call change-files,scripts/core-daemon.service)
$(call change-files,scripts/core-daemon)
$(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)
$(call change-files,daemon/setup.py)
CORE_DOC_SRC = core-python-$(PACKAGE_VERSION) CORE_DOC_SRC = core-python-$(PACKAGE_VERSION)
.PHONY: doc .PHONY: doc

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 6.5.0) AC_INIT(core, 7.0.0)
# autoconf and automake initialization # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) AC_CONFIG_SRCDIR([netns/version.h.in])
@ -167,18 +167,6 @@ if test "x$enable_daemon" = "xyes"; then
if test "x$ovs_of_path" = "xno" ; then if test "x$ovs_of_path" = "xno" ; then
AC_MSG_WARN([Could not locate ovs-ofctl cannot use OVS mode]) AC_MSG_WARN([Could not locate ovs-ofctl cannot use OVS mode])
fi fi
CFLAGS_save=$CFLAGS
CPPFLAGS_save=$CPPFLAGS
if test "x$PYTHON_INCLUDE_DIR" = "x"; then
PYTHON_INCLUDE_DIR=`$PYTHON -c "import distutils.sysconfig; print(distutils.sysconfig.get_python_inc())"`
fi
CFLAGS="-I$PYTHON_INCLUDE_DIR"
CPPFLAGS="-I$PYTHON_INCLUDE_DIR"
AC_CHECK_HEADERS([Python.h], [],
AC_MSG_ERROR([Python bindings require Python development headers (try installing your 'python-devel' or 'python-dev' package)]))
CFLAGS=$CFLAGS_save
CPPFLAGS=$CPPFLAGS_save
fi 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
@ -220,22 +208,12 @@ if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then
AS_IF([$PYTHON -c "import sphinx_rtd_theme" &> /dev/null], [], [AC_MSG_ERROR([doc dependency missing, please install python3 -m pip install sphinx-rtd-theme])]) AS_IF([$PYTHON -c "import sphinx_rtd_theme" &> /dev/null], [], [AC_MSG_ERROR([doc dependency missing, please install python3 -m pip install sphinx-rtd-theme])])
fi fi
AC_ARG_WITH([startup],
[AS_HELP_STRING([--with-startup=option],
[option=systemd,suse,none to install systemd/SUSE init scripts])],
[with_startup=$with_startup],
[with_startup=initd])
AC_SUBST(with_startup)
AC_MSG_RESULT([using startup option $with_startup])
# Variable substitutions # Variable substitutions
AM_CONDITIONAL(WANT_GUI, test x$enable_gui = xyes) 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)
AM_CONDITIONAL(WANT_NETNS, test x$want_linux_netns = xyes) AM_CONDITIONAL(WANT_NETNS, test x$want_linux_netns = xyes)
AM_CONDITIONAL(WANT_INITD, test x$with_startup = xinitd)
AM_CONDITIONAL(WANT_SYSTEMD, test x$with_startup = xsystemd)
AM_CONDITIONAL(WANT_VNODEDONLY, test x$enable_vnodedonly = xyes) AM_CONDITIONAL(WANT_VNODEDONLY, test x$enable_vnodedonly = xyes)
if test $cross_compiling = no; then if test $cross_compiling = no; then
@ -249,7 +227,6 @@ AC_CONFIG_FILES([Makefile
gui/version.tcl gui/version.tcl
gui/Makefile gui/Makefile
gui/icons/Makefile gui/icons/Makefile
scripts/Makefile
man/Makefile man/Makefile
docs/Makefile docs/Makefile
daemon/Makefile daemon/Makefile
@ -279,9 +256,6 @@ Daemon:
Daemon path: ${bindir} Daemon path: ${bindir}
Daemon config: ${CORE_CONF_DIR} Daemon config: ${CORE_CONF_DIR}
Python: ${PYTHON} Python: ${PYTHON}
Logs: ${CORE_STATE_DIR}/log
Startup: ${with_startup}
Features to build: Features to build:
Build GUI: ${enable_gui} Build GUI: ${enable_gui}

2
daemon/.gitignore vendored
View file

@ -1,2 +0,0 @@
*.pyc
build

View file

@ -5,19 +5,19 @@ repos:
name: isort name: isort
stages: [commit] stages: [commit]
language: system language: system
entry: bash -c 'cd daemon && pipenv run isort --atomic -y' entry: bash -c 'cd daemon && poetry run isort --atomic -y'
types: [python] types: [python]
- id: black - id: black
name: black name: black
stages: [commit] stages: [commit]
language: system language: system
entry: bash -c 'cd daemon && pipenv run black --exclude ".+_pb2.*.py|doc|build|utm\.py" .' entry: bash -c 'cd daemon && poetry run black .'
types: [python] types: [python]
- id: flake8 - id: flake8
name: flake8 name: flake8
stages: [commit] stages: [commit]
language: system language: system
entry: bash -c 'cd daemon && pipenv run flake8' entry: bash -c 'cd daemon && poetry run flake8'
types: [python] types: [python]

View file

@ -1,2 +0,0 @@
graft core/gui/data
graft core/configservices/*/templates

View file

@ -7,43 +7,12 @@
# Makefile for building netns components. # Makefile for building netns components.
# #
SETUPPY = setup.py
SETUPPYFLAGS = -v
if WANT_DOCS if WANT_DOCS
DOCS = doc DOCS = doc
endif endif
SUBDIRS = proto $(DOCS) SUBDIRS = proto $(DOCS)
SCRIPT_FILES := $(notdir $(wildcard scripts/*))
MAN_FILES := $(notdir $(wildcard ../man/*.1))
# Python package build
noinst_SCRIPTS = build
build:
$(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) build
# Python package install
install-exec-hook:
$(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) install \
--root=/$(DESTDIR) \
--prefix=$(prefix) \
--single-version-externally-managed
# Python package uninstall
uninstall-hook:
rm -rf $(DESTDIR)/etc/core
rm -rf $(DESTDIR)/$(datadir)/core
rm -f $(addprefix $(DESTDIR)/$(datarootdir)/man/man1/, $(MAN_FILES))
rm -f $(addprefix $(DESTDIR)/$(bindir)/,$(SCRIPT_FILES))
rm -rf $(DESTDIR)/$(pythondir)/core-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg-info
rm -rf $(DESTDIR)/$(pythondir)/core
# Python package cleanup
clean-local:
-rm -rf build
# because we include entire directories with EXTRA_DIST, we need to clean up # because we include entire directories with EXTRA_DIST, we need to clean up
# the source control files # the source control files
dist-hook: dist-hook:
@ -52,17 +21,15 @@ dist-hook:
distclean-local: distclean-local:
-rm -rf core.egg-info -rm -rf core.egg-info
DISTCLEANFILES = Makefile.in DISTCLEANFILES = Makefile.in
# files to include with distribution tarball # files to include with distribution tarball
EXTRA_DIST = $(SETUPPY) \ EXTRA_DIST = core \
core \
data \ data \
doc/conf.py.in \ doc/conf.py.in \
examples \ examples \
scripts \ scripts \
tests \ tests \
test.py \
setup.cfg \ setup.cfg \
requirements.txt poetry.lock \
pyproject.toml

View file

@ -1,23 +0,0 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[scripts]
core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf"
core-pygui = "python scripts/core-pygui"
test = "pytest -v tests"
test-mock = "pytest -v --mock tests"
test-emane = "pytest -v tests/emane"
[dev-packages]
grpcio-tools = "*"
isort = "*"
pre-commit = "*"
flake8 = "*"
black = "==19.3b0"
pytest = "*"
mock = "*"
[packages]
core = {editable = true,path = "."}

732
daemon/Pipfile.lock generated
View file

@ -1,732 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "199897f713f6f338316b33fcbbe0001e9e55fcd5e5e24b2245a89454ce13321f"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"bcrypt": {
"hashes": [
"sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89",
"sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42",
"sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294",
"sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161",
"sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752",
"sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31",
"sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5",
"sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c",
"sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0",
"sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de",
"sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e",
"sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052",
"sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09",
"sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105",
"sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133",
"sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1",
"sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7",
"sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"
],
"version": "==3.1.7"
},
"cffi": {
"hashes": [
"sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
"sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
"sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
"sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
"sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
"sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
"sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
"sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
"sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
"sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
"sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
"sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
"sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
"sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
"sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
"sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
"sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
"sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
"sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
"sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
"sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
"sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
"sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
"sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
"sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
"sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
"sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
"sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
],
"version": "==1.14.0"
},
"core": {
"editable": true,
"path": "."
},
"cryptography": {
"hashes": [
"sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
"sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
"sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
"sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
"sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
"sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
"sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
"sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
"sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
"sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
"sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
"sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
"sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
"sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
"sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
"sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
"sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
"sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
"sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
"sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
"sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
],
"version": "==2.8"
},
"dataclasses": {
"hashes": [
"sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836",
"sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"
],
"index": "pypi",
"markers": "python_version == '3.6'",
"version": "==0.7"
},
"fabric": {
"hashes": [
"sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389",
"sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"
],
"version": "==2.5.0"
},
"grpcio": {
"hashes": [
"sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6",
"sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045",
"sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1",
"sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00",
"sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942",
"sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8",
"sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a",
"sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c",
"sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6",
"sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a",
"sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1",
"sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961",
"sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7",
"sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386",
"sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e",
"sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866",
"sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d",
"sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371",
"sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6",
"sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb",
"sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3",
"sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6",
"sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47",
"sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454",
"sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e",
"sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8",
"sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c",
"sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4",
"sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7",
"sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6",
"sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85",
"sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571",
"sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345",
"sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb",
"sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49",
"sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8",
"sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844",
"sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54",
"sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca",
"sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5",
"sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3",
"sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed",
"sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b"
],
"version": "==1.27.2"
},
"invoke": {
"hashes": [
"sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132",
"sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134",
"sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d"
],
"version": "==1.4.1"
},
"lxml": {
"hashes": [
"sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd",
"sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c",
"sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081",
"sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f",
"sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261",
"sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a",
"sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9",
"sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a",
"sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb",
"sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60",
"sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128",
"sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a",
"sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717",
"sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89",
"sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72",
"sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8",
"sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3",
"sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7",
"sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8",
"sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77",
"sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1",
"sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15",
"sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679",
"sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012",
"sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6",
"sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc",
"sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca"
],
"version": "==4.5.0"
},
"mako": {
"hashes": [
"sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d",
"sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9"
],
"version": "==1.1.2"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"version": "==1.1.1"
},
"netaddr": {
"hashes": [
"sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd",
"sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"
],
"version": "==0.7.19"
},
"paramiko": {
"hashes": [
"sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f",
"sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f"
],
"version": "==2.7.1"
},
"pillow": {
"hashes": [
"sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be",
"sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946",
"sha256:54ebae163e8412aff0b9df1e88adab65788f5f5b58e625dc5c7f51eaf14a6837",
"sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f",
"sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00",
"sha256:5f7ae9126d16194f114435ebb79cc536b5682002a4fa57fa7bb2cbcde65f2f4d",
"sha256:62a889aeb0a79e50ecf5af272e9e3c164148f4bd9636cc6bcfa182a52c8b0533",
"sha256:7406f5a9b2fd966e79e6abdaf700585a4522e98d6559ce37fc52e5c955fade0a",
"sha256:8453f914f4e5a3d828281a6628cf517832abfa13ff50679a4848926dac7c0358",
"sha256:87269cc6ce1e3dee11f23fa515e4249ae678dbbe2704598a51cee76c52e19cda",
"sha256:875358310ed7abd5320f21dd97351d62de4929b0426cdb1eaa904b64ac36b435",
"sha256:8ac6ce7ff3892e5deaab7abaec763538ffd011f74dc1801d93d3c5fc541feee2",
"sha256:91b710e3353aea6fc758cdb7136d9bbdcb26b53cefe43e2cba953ac3ee1d3313",
"sha256:9d2ba4ed13af381233e2d810ff3bab84ef9f18430a9b336ab69eaf3cd24299ff",
"sha256:a62ec5e13e227399be73303ff301f2865bf68657d15ea50b038d25fc41097317",
"sha256:ab76e5580b0ed647a8d8d2d2daee170e8e9f8aad225ede314f684e297e3643c2",
"sha256:bf4003aa538af3f4205c5fac56eacaa67a6dd81e454ffd9e9f055fff9f1bc614",
"sha256:bf598d2e37cf8edb1a2f26ed3fb255191f5232badea4003c16301cb94ac5bdd0",
"sha256:c18f70dc27cc5d236f10e7834236aff60aadc71346a5bc1f4f83a4b3abee6386",
"sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9",
"sha256:dc058b7833184970d1248135b8b0ab702e6daa833be14035179f2acb78ff5636",
"sha256:ff3797f2f16bf9d17d53257612da84dd0758db33935777149b3334c01ff68865"
],
"version": "==7.0.0"
},
"pycparser": {
"hashes": [
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
"version": "==2.20"
},
"pynacl": {
"hashes": [
"sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255",
"sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c",
"sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e",
"sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae",
"sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621",
"sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56",
"sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39",
"sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310",
"sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1",
"sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5",
"sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a",
"sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786",
"sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b",
"sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b",
"sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f",
"sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20",
"sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415",
"sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715",
"sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92",
"sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1",
"sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"
],
"version": "==1.3.0"
},
"pyproj": {
"hashes": [
"sha256:0d8196a5ac75fee2cf71c21066b3344427abfa8ad69b536d3404d5c7c9c0b886",
"sha256:12e378a0a21c73f96177f6cf64520f17e6b7aa02fc9cb27bd5c2d5b06ce170af",
"sha256:17738836128704d8f80b771572d77b8733841f0cb0ca42620549236ea62c4663",
"sha256:1a39175944710b225fd1943cb3b8ea0c8e059d3016360022ca10bbb7a6bfc9ae",
"sha256:2566bffb5395c9fbdb02077a0bc3e3ed0b2e4e3cadf65019e3139a8dfe27dd1d",
"sha256:3f43277f21ddaabed93b9885a4e494b785dca56e31fd37a935519d99b07807f0",
"sha256:424304beca6e0b0bc12aa46fc6d14a481ea47b1a4edec4854bb281656de38948",
"sha256:48128d794c8f52fcff2433a481e3aa2ccb0e0b3ccd51d3ad7cc10cc488c3f547",
"sha256:4a16b650722982cddedd45dfc36435b96e0ba83a2aebd4a4c247e5a68c852442",
"sha256:5161f1b5ece8a5263b64d97a32fbc473a4c6fdca5c95478e58e519ef1e97528e",
"sha256:6839ce14635ebfb01c67e456148f4f1fa04b03ef9645551b89d36593f2a3e57d",
"sha256:80e9f85ab81da75289308f23a62e1426a38411a07b0da738958d65ae8cc6c59c",
"sha256:881b44e94c781d02ecf1d9314fc7f44c09e6d54a8eac281869365999ac4db7a1",
"sha256:977542d2f8cf2981cf3ad72cedfebcd6ac56977c7aa830d9b49fa7888b56e83d",
"sha256:9bba6cbff7e23bb6d9062786d516602681b4414e9e423c138a7360e4d2a193e8",
"sha256:9bf64bba03ddc534ed3c6271ba8f9d31040f40cf8e9e7e458b6b1524a6f59082",
"sha256:9c712ceaa01488ebe6e357e1dfa2434c2304aad8a810e5d4c3d2abe21def6d58",
"sha256:b7da17e5a5c6039f85843e88c2f1ca8606d1a4cc13a87e7b68b9f51a54ef201a",
"sha256:bcdf81b3f13d2cc0354a4c3f7a567b71fcf6fe8098e519aaaee8e61f05c9de10",
"sha256:bebd3f987b7196e9d2ccfe55911b0c76ba9ce309bcabfb629ef205cbaaad37c5",
"sha256:c244e923073cd0bab74ba861ba31724aab90efda35b47a9676603c1a8e80b3ba",
"sha256:dacb94a9d570f4d9fc9369a22d44d7b3071cfe4d57d0ff2f57abd7ef6127fe41"
],
"version": "==2.6.0"
},
"pyyaml": {
"hashes": [
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
],
"version": "==5.3.1"
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
],
"version": "==1.14.0"
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"black": {
"hashes": [
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
],
"index": "pypi",
"version": "==19.3b0"
},
"cfgv": {
"hashes": [
"sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53",
"sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513"
],
"version": "==3.1.0"
},
"click": {
"hashes": [
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
],
"version": "==7.1.1"
},
"distlib": {
"hashes": [
"sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"
],
"version": "==0.3.0"
},
"entrypoints": {
"hashes": [
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
],
"version": "==0.3"
},
"filelock": {
"hashes": [
"sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
"sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
],
"version": "==3.0.12"
},
"flake8": {
"hashes": [
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
"sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
],
"index": "pypi",
"version": "==3.7.9"
},
"grpcio": {
"hashes": [
"sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6",
"sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045",
"sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1",
"sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00",
"sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942",
"sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8",
"sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a",
"sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c",
"sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6",
"sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a",
"sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1",
"sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961",
"sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7",
"sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386",
"sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e",
"sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866",
"sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d",
"sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371",
"sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6",
"sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb",
"sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3",
"sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6",
"sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47",
"sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454",
"sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e",
"sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8",
"sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c",
"sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4",
"sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7",
"sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6",
"sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85",
"sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571",
"sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345",
"sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb",
"sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49",
"sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8",
"sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844",
"sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54",
"sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca",
"sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5",
"sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3",
"sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed",
"sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b"
],
"version": "==1.27.2"
},
"grpcio-tools": {
"hashes": [
"sha256:00c5080cfb197ed20ecf0d0ff2d07f1fc9c42c724cad21c40ff2d048de5712b1",
"sha256:069826dd02ce1886444cf4519c4fe1b05ac9ef41491f26e97400640531db47f6",
"sha256:1266b577abe7c720fd16a83d0a4999a192e87c4a98fc9f97e0b99b106b3e155f",
"sha256:16dc3fad04fe18d50777c56af7b2d9b9984cd1cfc71184646eb431196d1645c6",
"sha256:1de5a273eaffeb3d126a63345e9e848ea7db740762f700eb8b5d84c5e3e7687d",
"sha256:2ca280af2cae1a014a238057bd3c0a254527569a6a9169a01c07f0590081d530",
"sha256:43a1573400527a23e4174d88604fde7a9d9a69bf9473c21936b7f409858f8ebb",
"sha256:4698c6b6a57f73b14d91a542c69ff33a2da8729691b7060a5d7f6383624d045e",
"sha256:520b7dafddd0f82cb7e4f6e9c6ba1049aa804d0e207870def9fe7f94d1e14090",
"sha256:57f8b9e2c7f55cd45f6dd930d6de61deb42d3eb7f9788137fbc7155cf724132a",
"sha256:59fbeb5bb9a7b94eb61642ac2cee1db5233b8094ca76fc56d4e0c6c20b5dd85f",
"sha256:5fd7efc2fd3370bd2c72dc58f31a407a5dff5498befa145da211b2e8c6a52c63",
"sha256:6016c07d6566e3109a3c032cf3861902d66501ecc08a5a84c47e43027302f367",
"sha256:627c91923df75091d8c4d244af38d5ab7ed8d786d480751d6c2b9267fbb92fe0",
"sha256:69c4a63919b9007e845d9f8980becd2f89d808a4a431ca32b9723ee37b521cb1",
"sha256:77e25c241e33b75612f2aa62985f746c6f6803ec4e452da508bb7f8d90a69db4",
"sha256:7a2d5fb558ac153a326e742ebfd7020eb781c43d3ffd920abd42b2e6c6fdfb37",
"sha256:7b54b283ec83190680903a9037376dc915e1f03852a2d574ba4d981b7a1fd3d0",
"sha256:845a51305af9fc7f9e2078edaec9a759153195f6cf1fbb12b1fa6f077e56b260",
"sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88",
"sha256:87e8ca2c2d2d3e09b2a2bed5d740d7b3e64028dafb7d6be543b77eec85590736",
"sha256:8e7738a4b93842bca1158cde81a3587c9b7111823e40a1ddf73292ca9d58e08b",
"sha256:915a695bc112517af48126ee0ecdb6aff05ed33f3eeef28f0d076f1f6b52ef5e",
"sha256:99961156a36aae4a402d6b14c1e7efde642794b3ddbf32c51db0cb3a199e8b11",
"sha256:9ba88c2d99bcaf7b9cb720925e3290d73b2367d238c5779363fd5598b2dc98c7",
"sha256:a140bf853edb2b5e8692fe94869e3e34077d7599170c113d07a58286c604f4fe",
"sha256:a14dc7a36c845991d908a7179502ca47bcba5ae1817c4426ce68cf2c97b20ad9",
"sha256:a3d2aec4b09c8e59fee8b0d1ed668d09e8c48b738f03f5d8401d7eb409111c47",
"sha256:a8f892378b0b02526635b806f59141abbb429d19bec56e869e04f396502c9651",
"sha256:aaa5ae26883c3d58d1a4323981f96b941fa09bb8f0f368d97c6225585280cf04",
"sha256:b56caecc16307b088a431a4038c3b3bb7d0e7f9988cbd0e9fa04ac937455ea38",
"sha256:bd7f59ff1252a3db8a143b13ea1c1e93d4b8cf4b852eb48b22ef1e6942f62a84",
"sha256:c1bb8f47d58e9f7c4825abfe01e6b85eda53c8b31d2267ca4cddf3c4d0829b80",
"sha256:d1a5e5fa47ba9557a7d3b31605631805adc66cdba9d95b5d10dfc52cca1fed53",
"sha256:dcbc06556f3713a9348c4fce02d05d91e678fc320fb2bcf0ddf8e4bb11d17867",
"sha256:e17b2e0936b04ced99769e26111e1e86ba81619d1b2691b1364f795e45560953",
"sha256:e6932518db389ede8bf06b4119bbd3e17f42d4626e72dec2b8955b20ec732cb6",
"sha256:ea4b3ad696d976d5eac74ec8df9a2c692113e455446ee38d5b3bd87f8e034fa6",
"sha256:ee50b0cf0d28748ef9f941894eb50fc464bd61b8e96aaf80c5056bea9b80d580",
"sha256:ef624b6134aef737b3daa4fb7e806cb8c5749efecd0b1fa9ce4f7e060c7a0221",
"sha256:f5450aa904e720f9c6407b59e96a8951ed6a95463f49444b6d2594b067d39588",
"sha256:f8514453411d72cc3cf7d481f2b6057e5b7436736d0cd39ee2b2f72088bbf497",
"sha256:fae91f30dc050a8d0b32d20dc700e6092f0bd2138d83e9570fff3f0372c1b27e"
],
"index": "pypi",
"version": "==1.27.2"
},
"identify": {
"hashes": [
"sha256:a7577a1f55cee1d21953a5cf11a3c839ab87f5ef909a4cba6cf52ed72b4c6059",
"sha256:ab246293e6585a1c6361a505b68d5b501a0409310932b7de2c2ead667b564d89"
],
"version": "==1.4.13"
},
"importlib-metadata": {
"hashes": [
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
],
"markers": "python_version < '3.8'",
"version": "==1.6.0"
},
"importlib-resources": {
"hashes": [
"sha256:4019b6a9082d8ada9def02bece4a76b131518866790d58fdda0b5f8c603b36c2",
"sha256:dd98ceeef3f5ad2ef4cc287b8586da4ebad15877f351e9688987ad663a0a29b8"
],
"markers": "python_version < '3.7'",
"version": "==1.4.0"
},
"isort": {
"hashes": [
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
],
"index": "pypi",
"version": "==4.3.21"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"mock": {
"hashes": [
"sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0",
"sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72"
],
"index": "pypi",
"version": "==4.0.2"
},
"more-itertools": {
"hashes": [
"sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
"sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
],
"version": "==8.2.0"
},
"nodeenv": {
"hashes": [
"sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212"
],
"version": "==1.3.5"
},
"packaging": {
"hashes": [
"sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
"sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
],
"version": "==20.3"
},
"pluggy": {
"hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"version": "==0.13.1"
},
"pre-commit": {
"hashes": [
"sha256:487c675916e6f99d355ec5595ad77b325689d423ef4839db1ed2f02f639c9522",
"sha256:c0aa11bce04a7b46c5544723aedf4e81a4d5f64ad1205a30a9ea12d5e81969e1"
],
"index": "pypi",
"version": "==2.2.0"
},
"protobuf": {
"hashes": [
"sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab",
"sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f",
"sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a",
"sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0",
"sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4",
"sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2",
"sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee",
"sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07",
"sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151",
"sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a",
"sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f",
"sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7",
"sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956",
"sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306",
"sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961",
"sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481",
"sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a",
"sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80"
],
"version": "==3.11.3"
},
"py": {
"hashes": [
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
],
"version": "==1.8.1"
},
"pycodestyle": {
"hashes": [
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
],
"version": "==2.5.0"
},
"pyflakes": {
"hashes": [
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
],
"version": "==2.1.1"
},
"pyparsing": {
"hashes": [
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
],
"version": "==2.4.6"
},
"pytest": {
"hashes": [
"sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172",
"sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"
],
"index": "pypi",
"version": "==5.4.1"
},
"pyyaml": {
"hashes": [
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
],
"version": "==5.3.1"
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
],
"version": "==1.14.0"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"virtualenv": {
"hashes": [
"sha256:4e399f48c6b71228bf79f5febd27e3bbb753d9d5905776a86667bc61ab628a25",
"sha256:9e81279f4a9d16d1c0654a127c2c86e5bca2073585341691882c1e66e31ef8a5"
],
"version": "==20.0.15"
},
"wcwidth": {
"hashes": [
"sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1",
"sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"
],
"version": "==0.1.9"
},
"zipp": {
"hashes": [
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
],
"markers": "python_version < '3.8'",
"version": "==3.1.0"
}
}
}

View file

@ -5,7 +5,7 @@ gRpc client for interfacing with CORE.
import logging import logging
import threading import threading
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, Callable, Dict, Generator, Iterable, List from typing import Any, Callable, Dict, Generator, Iterable, List, Optional
import grpc import grpc
@ -92,7 +92,7 @@ from core.api.grpc.wlan_pb2 import (
WlanLinkRequest, WlanLinkRequest,
WlanLinkResponse, WlanLinkResponse,
) )
from core.emulator.emudata import IpPrefixes from core.emulator.data import IpPrefixes
class InterfaceHelper: class InterfaceHelper:
@ -108,29 +108,29 @@ class InterfaceHelper:
:param ip6_prefix: ip6 prefix to use for generation :param ip6_prefix: ip6 prefix to use for generation
:raises ValueError: when both ip4 and ip6 prefixes have not been provided :raises ValueError: when both ip4 and ip6 prefixes have not been provided
""" """
self.prefixes = IpPrefixes(ip4_prefix, ip6_prefix) self.prefixes: IpPrefixes = IpPrefixes(ip4_prefix, ip6_prefix)
def create_interface( def create_iface(
self, node_id: int, interface_id: int, name: str = None, mac: str = None self, node_id: int, iface_id: int, name: str = None, mac: str = None
) -> core_pb2.Interface: ) -> core_pb2.Interface:
""" """
Create an interface protobuf object. Create an interface protobuf object.
:param node_id: node id to create interface for :param node_id: node id to create interface for
:param interface_id: interface id :param iface_id: interface id
:param name: name of interface :param name: name of interface
:param mac: mac address for interface :param mac: mac address for interface
:return: interface protobuf :return: interface protobuf
""" """
interface_data = self.prefixes.gen_interface(node_id, name, mac) iface_data = self.prefixes.gen_iface(node_id, name, mac)
return core_pb2.Interface( return core_pb2.Interface(
id=interface_id, id=iface_id,
name=interface_data.name, name=iface_data.name,
ip4=interface_data.ip4, ip4=iface_data.ip4,
ip4mask=interface_data.ip4_mask, ip4_mask=iface_data.ip4_mask,
ip6=interface_data.ip6, ip6=iface_data.ip6,
ip6mask=interface_data.ip6_mask, ip6_mask=iface_data.ip6_mask,
mac=interface_data.mac, mac=iface_data.mac,
) )
@ -177,10 +177,10 @@ class CoreGrpcClient:
:param address: grpc server address to connect to :param address: grpc server address to connect to
""" """
self.address = address self.address: str = address
self.stub = None self.stub: Optional[core_pb2_grpc.CoreApiStub] = None
self.channel = None self.channel: Optional[grpc.Channel] = None
self.proxy = proxy self.proxy: bool = proxy
def start_session( def start_session(
self, self,
@ -414,6 +414,20 @@ class CoreGrpcClient:
request = core_pb2.SetSessionStateRequest(session_id=session_id, state=state) request = core_pb2.SetSessionStateRequest(session_id=session_id, state=state)
return self.stub.SetSessionState(request) return self.stub.SetSessionState(request)
def set_session_user(
self, session_id: int, user: str
) -> core_pb2.SetSessionUserResponse:
"""
Set session user, used for helping to find files without full paths.
:param session_id: id of session
:param user: user to set for session
:return: response with result of success or failure
:raises grpc.RpcError: when session doesn't exist
"""
request = core_pb2.SetSessionUserRequest(session_id=session_id, user=user)
return self.stub.SetSessionUser(request)
def add_session_server( def add_session_server(
self, session_id: int, name: str, host: str self, session_id: int, name: str, host: str
) -> core_pb2.AddSessionServerResponse: ) -> core_pb2.AddSessionServerResponse:
@ -431,12 +445,29 @@ class CoreGrpcClient:
) )
return self.stub.AddSessionServer(request) return self.stub.AddSessionServer(request)
def alert(
self,
session_id: int,
level: core_pb2.ExceptionLevel,
source: str,
text: str,
node_id: int = None,
) -> core_pb2.SessionAlertResponse:
request = core_pb2.SessionAlertRequest(
session_id=session_id,
level=level,
source=source,
text=text,
node_id=node_id,
)
return self.stub.SessionAlert(request)
def events( def events(
self, self,
session_id: int, session_id: int,
handler: Callable[[core_pb2.Event], None], handler: Callable[[core_pb2.Event], None],
events: List[core_pb2.Event] = None, events: List[core_pb2.Event] = None,
) -> Any: ) -> grpc.Future:
""" """
Listen for session events. Listen for session events.
@ -453,7 +484,7 @@ class CoreGrpcClient:
def throughputs( def throughputs(
self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None] self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None]
) -> Any: ) -> grpc.Future:
""" """
Listen for throughput events with information for interfaces and bridges. Listen for throughput events with information for interfaces and bridges.
@ -467,18 +498,36 @@ class CoreGrpcClient:
start_streamer(stream, handler) start_streamer(stream, handler)
return stream return stream
def cpu_usage(
self, delay: int, handler: Callable[[core_pb2.CpuUsageEvent], None]
) -> grpc.Future:
"""
Listen for cpu usage events with the given repeat delay.
:param delay: delay between receiving events
:param handler: handler for every event
:return: stream processing events, can be used to cancel stream
"""
request = core_pb2.CpuUsageRequest(delay=delay)
stream = self.stub.CpuUsage(request)
start_streamer(stream, handler)
return stream
def add_node( def add_node(
self, session_id: int, node: core_pb2.Node self, session_id: int, node: core_pb2.Node, source: str = None
) -> core_pb2.AddNodeResponse: ) -> core_pb2.AddNodeResponse:
""" """
Add node to session. Add node to session.
:param session_id: session id :param session_id: session id
:param node: node to add :param node: node to add
:param source: source application
:return: response with node id :return: response with node id
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
request = core_pb2.AddNodeRequest(session_id=session_id, node=node) request = core_pb2.AddNodeRequest(
session_id=session_id, node=node, source=source
)
return self.stub.AddNode(request) return self.stub.AddNode(request)
def get_node(self, session_id: int, node_id: int) -> core_pb2.GetNodeResponse: def get_node(self, session_id: int, node_id: int) -> core_pb2.GetNodeResponse:
@ -499,8 +548,8 @@ class CoreGrpcClient:
node_id: int, node_id: int,
position: core_pb2.Position = None, position: core_pb2.Position = None,
icon: str = None, icon: str = None,
source: str = None,
geo: core_pb2.Geo = None, geo: core_pb2.Geo = None,
source: str = None,
) -> core_pb2.EditNodeResponse: ) -> core_pb2.EditNodeResponse:
""" """
Edit a node, currently only changes position. Edit a node, currently only changes position.
@ -509,8 +558,8 @@ class CoreGrpcClient:
:param node_id: node id :param node_id: node id
:param position: position to set node to :param position: position to set node to
:param icon: path to icon for gui to use for node :param icon: path to icon for gui to use for node
:param source: application source editing node
:param geo: lon,lat,alt location for node :param geo: lon,lat,alt location for node
:param source: application source
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session or node doesn't exist :raises grpc.RpcError: when session or node doesn't exist
""" """
@ -536,16 +585,21 @@ class CoreGrpcClient:
""" """
return self.stub.MoveNodes(move_iterator) return self.stub.MoveNodes(move_iterator)
def delete_node(self, session_id: int, node_id: int) -> core_pb2.DeleteNodeResponse: def delete_node(
self, session_id: int, node_id: int, source: str = None
) -> core_pb2.DeleteNodeResponse:
""" """
Delete node from session. Delete node from session.
:param session_id: session id :param session_id: session id
:param node_id: node id :param node_id: node id
:param source: application source
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
request = core_pb2.DeleteNodeRequest(session_id=session_id, node_id=node_id) request = core_pb2.DeleteNodeRequest(
session_id=session_id, node_id=node_id, source=source
)
return self.stub.DeleteNode(request) return self.stub.DeleteNode(request)
def node_command( def node_command(
@ -609,91 +663,101 @@ class CoreGrpcClient:
def add_link( def add_link(
self, self,
session_id: int, session_id: int,
node_one_id: int, node1_id: int,
node_two_id: int, node2_id: int,
interface_one: core_pb2.Interface = None, iface1: core_pb2.Interface = None,
interface_two: core_pb2.Interface = None, iface2: core_pb2.Interface = None,
options: core_pb2.LinkOptions = None, options: core_pb2.LinkOptions = None,
source: str = None,
) -> core_pb2.AddLinkResponse: ) -> core_pb2.AddLinkResponse:
""" """
Add a link between nodes. Add a link between nodes.
:param session_id: session id :param session_id: session id
:param node_one_id: node one id :param node1_id: node one id
:param node_two_id: node two id :param node2_id: node two id
:param interface_one: node one interface data :param iface1: node one interface data
:param interface_two: node two interface data :param iface2: node two interface data
:param options: options for link (jitter, bandwidth, etc) :param options: options for link (jitter, bandwidth, etc)
:param source: application source
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session or one of the nodes don't exist :raises grpc.RpcError: when session or one of the nodes don't exist
""" """
link = core_pb2.Link( link = core_pb2.Link(
node_one_id=node_one_id, node1_id=node1_id,
node_two_id=node_two_id, node2_id=node2_id,
type=core_pb2.LinkType.WIRED, type=core_pb2.LinkType.WIRED,
interface_one=interface_one, iface1=iface1,
interface_two=interface_two, iface2=iface2,
options=options, options=options,
) )
request = core_pb2.AddLinkRequest(session_id=session_id, link=link) request = core_pb2.AddLinkRequest(
session_id=session_id, link=link, source=source
)
return self.stub.AddLink(request) return self.stub.AddLink(request)
def edit_link( def edit_link(
self, self,
session_id: int, session_id: int,
node_one_id: int, node1_id: int,
node_two_id: int, node2_id: int,
options: core_pb2.LinkOptions, options: core_pb2.LinkOptions,
interface_one_id: int = None, iface1_id: int = None,
interface_two_id: int = None, iface2_id: int = None,
source: str = None,
) -> core_pb2.EditLinkResponse: ) -> core_pb2.EditLinkResponse:
""" """
Edit a link between nodes. Edit a link between nodes.
:param session_id: session id :param session_id: session id
:param node_one_id: node one id :param node1_id: node one id
:param node_two_id: node two id :param node2_id: node two id
:param options: options for link (jitter, bandwidth, etc) :param options: options for link (jitter, bandwidth, etc)
:param interface_one_id: node one interface id :param iface1_id: node one interface id
:param interface_two_id: node two interface id :param iface2_id: node two interface id
:param source: application source
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session or one of the nodes don't exist :raises grpc.RpcError: when session or one of the nodes don't exist
""" """
request = core_pb2.EditLinkRequest( request = core_pb2.EditLinkRequest(
session_id=session_id, session_id=session_id,
node_one_id=node_one_id, node1_id=node1_id,
node_two_id=node_two_id, node2_id=node2_id,
options=options, options=options,
interface_one_id=interface_one_id, iface1_id=iface1_id,
interface_two_id=interface_two_id, iface2_id=iface2_id,
source=source,
) )
return self.stub.EditLink(request) return self.stub.EditLink(request)
def delete_link( def delete_link(
self, self,
session_id: int, session_id: int,
node_one_id: int, node1_id: int,
node_two_id: int, node2_id: int,
interface_one_id: int = None, iface1_id: int = None,
interface_two_id: int = None, iface2_id: int = None,
source: str = None,
) -> core_pb2.DeleteLinkResponse: ) -> core_pb2.DeleteLinkResponse:
""" """
Delete a link between nodes. Delete a link between nodes.
:param session_id: session id :param session_id: session id
:param node_one_id: node one id :param node1_id: node one id
:param node_two_id: node two id :param node2_id: node two id
:param interface_one_id: node one interface id :param iface1_id: node one interface id
:param interface_two_id: node two interface id :param iface2_id: node two interface id
:param source: application source
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
request = core_pb2.DeleteLinkRequest( request = core_pb2.DeleteLinkRequest(
session_id=session_id, session_id=session_id,
node_one_id=node_one_id, node1_id=node1_id,
node_two_id=node_two_id, node2_id=node2_id,
interface_one_id=interface_one_id, iface1_id=iface1_id,
interface_two_id=interface_two_id, iface2_id=iface2_id,
source=source,
) )
return self.stub.DeleteLink(request) return self.stub.DeleteLink(request)
@ -1028,7 +1092,7 @@ class CoreGrpcClient:
return self.stub.GetEmaneModels(request) return self.stub.GetEmaneModels(request)
def get_emane_model_config( def get_emane_model_config(
self, session_id: int, node_id: int, model: str, interface_id: int = -1 self, session_id: int, node_id: int, model: str, iface_id: int = -1
) -> GetEmaneModelConfigResponse: ) -> GetEmaneModelConfigResponse:
""" """
Get emane model configuration for a node or a node's interface. Get emane model configuration for a node or a node's interface.
@ -1036,12 +1100,12 @@ class CoreGrpcClient:
:param session_id: session id :param session_id: session id
:param node_id: node id :param node_id: node id
:param model: emane model name :param model: emane model name
:param interface_id: node interface id :param iface_id: node interface id
:return: response with a list of configuration groups :return: response with a list of configuration groups
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
request = GetEmaneModelConfigRequest( request = GetEmaneModelConfigRequest(
session_id=session_id, node_id=node_id, model=model, interface=interface_id session_id=session_id, node_id=node_id, model=model, iface_id=iface_id
) )
return self.stub.GetEmaneModelConfig(request) return self.stub.GetEmaneModelConfig(request)
@ -1051,7 +1115,7 @@ class CoreGrpcClient:
node_id: int, node_id: int,
model: str, model: str,
config: Dict[str, str] = None, config: Dict[str, str] = None,
interface_id: int = -1, iface_id: int = -1,
) -> SetEmaneModelConfigResponse: ) -> SetEmaneModelConfigResponse:
""" """
Set emane model configuration for a node or a node's interface. Set emane model configuration for a node or a node's interface.
@ -1060,12 +1124,12 @@ class CoreGrpcClient:
:param node_id: node id :param node_id: node id
:param model: emane model name :param model: emane model name
:param config: emane model configuration :param config: emane model configuration
:param interface_id: node interface id :param iface_id: node interface id
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
model_config = EmaneModelConfig( model_config = EmaneModelConfig(
node_id=node_id, model=model, config=config, interface_id=interface_id node_id=node_id, model=model, config=config, iface_id=iface_id
) )
request = SetEmaneModelConfigRequest( request = SetEmaneModelConfigRequest(
session_id=session_id, emane_model_config=model_config session_id=session_id, emane_model_config=model_config
@ -1111,24 +1175,24 @@ class CoreGrpcClient:
return self.stub.OpenXml(request) return self.stub.OpenXml(request)
def emane_link( def emane_link(
self, session_id: int, nem_one: int, nem_two: int, linked: bool self, session_id: int, nem1: int, nem2: int, linked: bool
) -> EmaneLinkResponse: ) -> EmaneLinkResponse:
""" """
Helps broadcast wireless link/unlink between EMANE nodes. Helps broadcast wireless link/unlink between EMANE nodes.
:param session_id: session to emane link :param session_id: session to emane link
:param nem_one: first nem for emane link :param nem1: first nem for emane link
:param nem_two: second nem for emane link :param nem2: second nem for emane link
:param linked: True to link, False to unlink :param linked: True to link, False to unlink
:return: get emane link response :return: get emane link response
:raises grpc.RpcError: when session or nodes related to nems do not exist :raises grpc.RpcError: when session or nodes related to nems do not exist
""" """
request = EmaneLinkRequest( request = EmaneLinkRequest(
session_id=session_id, nem_one=nem_one, nem_two=nem_two, linked=linked session_id=session_id, nem1=nem1, nem2=nem2, linked=linked
) )
return self.stub.EmaneLink(request) return self.stub.EmaneLink(request)
def get_interfaces(self) -> core_pb2.GetInterfacesResponse: def get_ifaces(self) -> core_pb2.GetInterfacesResponse:
""" """
Retrieves a list of interfaces available on the host machine that are not Retrieves a list of interfaces available on the host machine that are not
a part of a CORE session. a part of a CORE session.
@ -1243,24 +1307,24 @@ class CoreGrpcClient:
return self.stub.ExecuteScript(request) return self.stub.ExecuteScript(request)
def wlan_link( def wlan_link(
self, session_id: int, wlan: int, node_one: int, node_two: int, linked: bool self, session_id: int, wlan_id: int, node1_id: int, node2_id: int, linked: bool
) -> WlanLinkResponse: ) -> WlanLinkResponse:
""" """
Links/unlinks nodes on the same WLAN. Links/unlinks nodes on the same WLAN.
:param session_id: session id containing wlan and nodes :param session_id: session id containing wlan and nodes
:param wlan: wlan nodes must belong to :param wlan_id: wlan nodes must belong to
:param node_one: first node of pair to link/unlink :param node1_id: first node of pair to link/unlink
:param node_two: second node of pair to link/unlin :param node2_id: second node of pair to link/unlin
:param linked: True to link, False to unlink :param linked: True to link, False to unlink
:return: wlan link response :return: wlan link response
:raises grpc.RpcError: when session or one of the nodes do not exist :raises grpc.RpcError: when session or one of the nodes do not exist
""" """
request = WlanLinkRequest( request = WlanLinkRequest(
session_id=session_id, session_id=session_id,
wlan=wlan, wlan=wlan_id,
node_one=node_one, node1_id=node1_id,
node_two=node_two, node2_id=node2_id,
linked=linked, linked=linked,
) )
return self.stub.WlanLink(request) return self.stub.WlanLink(request)

View file

@ -1,6 +1,6 @@
import logging import logging
from queue import Empty, Queue from queue import Empty, Queue
from typing import Iterable 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
@ -15,115 +15,127 @@ from core.emulator.data import (
from core.emulator.session import Session from core.emulator.session import Session
def handle_node_event(event: NodeData) -> core_pb2.NodeEvent: def handle_node_event(node_data: NodeData) -> core_pb2.Event:
""" """
Handle node event when there is a node event Handle node event when there is a node event
:param event: node data :param node_data: node data
:return: node event that contains node id, name, model, position, and services :return: node event that contains node id, name, model, position, and services
""" """
position = core_pb2.Position(x=event.x_position, y=event.y_position) node = node_data.node
geo = core_pb2.Geo(lat=event.latitude, lon=event.longitude, alt=event.altitude) x, y, _ = node.position.get()
position = core_pb2.Position(x=x, y=y)
lon, lat, alt = node.position.get_geo()
geo = core_pb2.Geo(lon=lon, lat=lat, alt=alt)
services = [x.name for x in node.services]
node_proto = core_pb2.Node( node_proto = core_pb2.Node(
id=event.id, id=node.id,
name=event.name, name=node.name,
model=event.model, model=node.type,
position=position, position=position,
geo=geo, geo=geo,
services=event.services, services=services,
) )
return core_pb2.NodeEvent(node=node_proto, source=event.source) message_type = node_data.message_type.value
node_event = core_pb2.NodeEvent(message_type=message_type, node=node_proto)
return core_pb2.Event(node_event=node_event, source=node_data.source)
def handle_link_event(event: LinkData) -> core_pb2.LinkEvent: def handle_link_event(link_data: LinkData) -> core_pb2.Event:
""" """
Handle link event when there is a link event Handle link event when there is a link event
:param event: 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(event) link = convert_link(link_data)
return core_pb2.LinkEvent(message_type=event.message_type.value, link=link) message_type = link_data.message_type.value
link_event = core_pb2.LinkEvent(message_type=message_type, link=link)
return core_pb2.Event(link_event=link_event, source=link_data.source)
def handle_session_event(event: EventData) -> core_pb2.SessionEvent: def handle_session_event(event_data: EventData) -> core_pb2.Event:
""" """
Handle session event when there is a session event Handle session event when there is a session event
:param event: event data :param event_data: event data
:return: session event :return: session event
""" """
event_time = event.time event_time = event_data.time
if event_time is not None: if event_time is not None:
event_time = float(event_time) event_time = float(event_time)
return core_pb2.SessionEvent( session_event = core_pb2.SessionEvent(
node_id=event.node, node_id=event_data.node,
event=event.event_type.value, event=event_data.event_type.value,
name=event.name, name=event_data.name,
data=event.data, data=event_data.data,
time=event_time, time=event_time,
) )
return core_pb2.Event(session_event=session_event)
def handle_config_event(event: ConfigData) -> core_pb2.ConfigEvent: def handle_config_event(config_data: ConfigData) -> core_pb2.Event:
""" """
Handle configuration event when there is configuration event Handle configuration event when there is configuration event
:param event: configuration data :param config_data: configuration data
:return: configuration event :return: configuration event
""" """
return core_pb2.ConfigEvent( config_event = core_pb2.ConfigEvent(
message_type=event.message_type, message_type=config_data.message_type,
node_id=event.node, node_id=config_data.node,
object=event.object, object=config_data.object,
type=event.type, type=config_data.type,
captions=event.captions, captions=config_data.captions,
bitmap=event.bitmap, bitmap=config_data.bitmap,
data_values=event.data_values, data_values=config_data.data_values,
possible_values=event.possible_values, possible_values=config_data.possible_values,
groups=event.groups, groups=config_data.groups,
interface=event.interface_number, iface_id=config_data.iface_id,
network_id=event.network_id, network_id=config_data.network_id,
opaque=event.opaque, opaque=config_data.opaque,
data_types=event.data_types, data_types=config_data.data_types,
) )
return core_pb2.Event(config_event=config_event)
def handle_exception_event(event: ExceptionData) -> core_pb2.ExceptionEvent: def handle_exception_event(exception_data: ExceptionData) -> core_pb2.Event:
""" """
Handle exception event when there is exception event Handle exception event when there is exception event
:param event: exception data :param exception_data: exception data
:return: exception event :return: exception event
""" """
return core_pb2.ExceptionEvent( exception_event = core_pb2.ExceptionEvent(
node_id=event.node, node_id=exception_data.node,
level=event.level.value, level=exception_data.level.value,
source=event.source, source=exception_data.source,
date=event.date, date=exception_data.date,
text=event.text, text=exception_data.text,
opaque=event.opaque, opaque=exception_data.opaque,
) )
return core_pb2.Event(exception_event=exception_event)
def handle_file_event(event: FileData) -> core_pb2.FileEvent: def handle_file_event(file_data: FileData) -> core_pb2.Event:
""" """
Handle file event Handle file event
:param event: file data :param file_data: file data
:return: file event :return: file event
""" """
return core_pb2.FileEvent( file_event = core_pb2.FileEvent(
message_type=event.message_type.value, message_type=file_data.message_type.value,
node_id=event.node, node_id=file_data.node,
name=event.name, name=file_data.name,
mode=event.mode, mode=file_data.mode,
number=event.number, number=file_data.number,
type=event.type, type=file_data.type,
source=event.source, source=file_data.source,
data=event.data, data=file_data.data,
compressed_data=event.compressed_data, compressed_data=file_data.compressed_data,
) )
return core_pb2.Event(file_event=file_event)
class EventStreamer: class EventStreamer:
@ -140,9 +152,9 @@ class EventStreamer:
:param session: session to process events for :param session: session to process events for
:param event_types: types of events to process :param event_types: types of events to process
""" """
self.session = session self.session: Session = session
self.event_types = event_types self.event_types: Iterable[core_pb2.EventType] = event_types
self.queue = Queue() self.queue: Queue = Queue()
self.add_handlers() self.add_handlers()
def add_handlers(self) -> None: def add_handlers(self) -> None:
@ -164,32 +176,33 @@ class EventStreamer:
if core_pb2.EventType.SESSION in self.event_types: if core_pb2.EventType.SESSION in self.event_types:
self.session.event_handlers.append(self.queue.put) self.session.event_handlers.append(self.queue.put)
def process(self) -> core_pb2.Event: def process(self) -> Optional[core_pb2.Event]:
""" """
Process the next event in the queue. Process the next event in the queue.
:return: grpc event, or None when invalid event or queue timeout :return: grpc event, or None when invalid event or queue timeout
""" """
event = core_pb2.Event(session_id=self.session.id) event = None
try: try:
data = self.queue.get(timeout=1) data = self.queue.get(timeout=1)
if isinstance(data, NodeData): if isinstance(data, NodeData):
event.node_event.CopyFrom(handle_node_event(data)) event = handle_node_event(data)
elif isinstance(data, LinkData): elif isinstance(data, LinkData):
event.link_event.CopyFrom(handle_link_event(data)) event = handle_link_event(data)
elif isinstance(data, EventData): elif isinstance(data, EventData):
event.session_event.CopyFrom(handle_session_event(data)) event = handle_session_event(data)
elif isinstance(data, ConfigData): elif isinstance(data, ConfigData):
event.config_event.CopyFrom(handle_config_event(data)) event = handle_config_event(data)
elif isinstance(data, ExceptionData): elif isinstance(data, ExceptionData):
event.exception_event.CopyFrom(handle_exception_event(data)) event = handle_exception_event(data)
elif isinstance(data, FileData): elif isinstance(data, FileData):
event.file_event.CopyFrom(handle_file_event(data)) event = handle_file_event(data)
else: else:
logging.error("unknown event: %s", data) logging.error("unknown event: %s", data)
event = None
except Empty: except Empty:
event = None pass
if event:
event.session_id = self.session.id
return event return event
def remove_handlers(self) -> None: def remove_handlers(self) -> None:

View file

@ -1,9 +1,9 @@
import logging import logging
import time import time
from typing import Any, Dict, List, Tuple, Type from pathlib import Path
from typing import Any, Dict, List, Tuple, Type, Union
import grpc import grpc
import netaddr
from grpc import ServicerContext from grpc import ServicerContext
from core import utils from core import utils
@ -11,8 +11,7 @@ from core.api.grpc import common_pb2, core_pb2
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig
from core.config import ConfigurableOptions from core.config import ConfigurableOptions
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.enumerations import LinkTypes, NodeTypes
from core.emulator.session import Session from core.emulator.session import Session
from core.nodes.base import CoreNode, NodeBase from core.nodes.base import CoreNode, NodeBase
@ -22,6 +21,25 @@ from core.services.coreservices import CoreService
WORKERS = 10 WORKERS = 10
class CpuUsage:
def __init__(self) -> None:
self.stat_file: Path = Path("/proc/stat")
self.prev_idle: int = 0
self.prev_total: int = 0
def run(self) -> float:
lines = self.stat_file.read_text().splitlines()[0]
values = [int(x) for x in lines.split()[1:]]
idle = sum(values[3:5])
non_idle = sum(values[:3] + values[5:8])
total = idle + non_idle
total_diff = total - self.prev_total
idle_diff = idle - self.prev_idle
self.prev_idle = idle
self.prev_total = total
return (total_diff - idle_diff) / total_diff
def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOptions]: def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOptions]:
""" """
Convert node protobuf message to data for creating a node. Convert node protobuf message to data for creating a node.
@ -35,7 +53,6 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
name=node_proto.name, name=node_proto.name,
model=node_proto.model, model=node_proto.model,
icon=node_proto.icon, icon=node_proto.icon,
opaque=node_proto.opaque,
image=node_proto.image, image=node_proto.image,
services=node_proto.services, services=node_proto.services,
config_services=node_proto.config_services, config_services=node_proto.config_services,
@ -52,58 +69,57 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
return _type, _id, options return _type, _id, options
def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData: def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
""" """
Create interface data from interface proto. Create interface data from interface proto.
:param interface_proto: interface proto :param iface_proto: interface proto
:return: interface data :return: interface data
""" """
interface = None iface_data = None
if interface_proto: if iface_proto:
name = interface_proto.name if interface_proto.name else None name = iface_proto.name if iface_proto.name else None
mac = interface_proto.mac if interface_proto.mac else None mac = iface_proto.mac if iface_proto.mac else None
ip4 = interface_proto.ip4 if interface_proto.ip4 else None ip4 = iface_proto.ip4 if iface_proto.ip4 else None
ip6 = interface_proto.ip6 if interface_proto.ip6 else None ip6 = iface_proto.ip6 if iface_proto.ip6 else None
interface = InterfaceData( iface_data = InterfaceData(
id=interface_proto.id, id=iface_proto.id,
name=name, name=name,
mac=mac, mac=mac,
ip4=ip4, ip4=ip4,
ip4_mask=interface_proto.ip4mask, ip4_mask=iface_proto.ip4_mask,
ip6=ip6, ip6=ip6,
ip6_mask=interface_proto.ip6mask, ip6_mask=iface_proto.ip6_mask,
) )
return interface return iface_data
def add_link_data( def add_link_data(
link_proto: core_pb2.Link link_proto: core_pb2.Link
) -> Tuple[InterfaceData, InterfaceData, LinkOptions]: ) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]:
""" """
Convert link proto to link interfaces and options data. Convert link proto to link interfaces and options data.
:param link_proto: link proto :param link_proto: link proto
:return: link interfaces and options :return: link interfaces and options
""" """
interface_one = link_interface(link_proto.interface_one) iface1_data = link_iface(link_proto.iface1)
interface_two = link_interface(link_proto.interface_two) iface2_data = link_iface(link_proto.iface2)
link_type = LinkTypes(link_proto.type) link_type = LinkTypes(link_proto.type)
options = LinkOptions(type=link_type) options = LinkOptions()
options_data = link_proto.options options_proto = link_proto.options
if options_data: if options_proto:
options.delay = options_data.delay options.delay = options_proto.delay
options.bandwidth = options_data.bandwidth options.bandwidth = options_proto.bandwidth
options.per = options_data.per options.loss = options_proto.loss
options.dup = options_data.dup options.dup = options_proto.dup
options.jitter = options_data.jitter options.jitter = options_proto.jitter
options.mer = options_data.mer options.mer = options_proto.mer
options.burst = options_data.burst options.burst = options_proto.burst
options.mburst = options_data.mburst options.mburst = options_proto.mburst
options.unidirectional = options_data.unidirectional options.unidirectional = options_proto.unidirectional
options.key = options_data.key options.key = options_proto.key
options.opaque = options_data.opaque return iface1_data, iface2_data, options, link_type
return interface_one, interface_two, options
def create_nodes( def create_nodes(
@ -141,10 +157,10 @@ def create_links(
""" """
funcs = [] funcs = []
for link_proto in link_protos: for link_proto in link_protos:
node_one_id = link_proto.node_one_id node1_id = link_proto.node1_id
node_two_id = link_proto.node_two_id node2_id = link_proto.node2_id
interface_one, interface_two, options = add_link_data(link_proto) iface1, iface2, options, link_type = add_link_data(link_proto)
args = (node_one_id, node_two_id, interface_one, interface_two, options) args = (node1_id, node2_id, iface1, iface2, options, link_type)
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)
@ -165,10 +181,10 @@ def edit_links(
""" """
funcs = [] funcs = []
for link_proto in link_protos: for link_proto in link_protos:
node_one_id = link_proto.node_one_id node1_id = link_proto.node1_id
node_two_id = link_proto.node_two_id node2_id = link_proto.node2_id
interface_one, interface_two, options = add_link_data(link_proto) iface1, iface2, options, link_type = add_link_data(link_proto)
args = (node_one_id, node_two_id, interface_one.id, interface_two.id, options) args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type)
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)
@ -190,7 +206,8 @@ def convert_value(value: Any) -> str:
def get_config_options( def get_config_options(
config: Dict[str, str], configurable_options: Type[ConfigurableOptions] config: Dict[str, str],
configurable_options: Union[ConfigurableOptions, Type[ConfigurableOptions]],
) -> Dict[str, common_pb2.ConfigOption]: ) -> Dict[str, common_pb2.ConfigOption]:
""" """
Retrieve configuration options in a form that is used by the grpc server. Retrieve configuration options in a form that is used by the grpc server.
@ -272,22 +289,22 @@ def get_links(node: NodeBase):
:return: protobuf links :return: protobuf links
""" """
links = [] links = []
for link_data in node.all_link_data(): for link in node.links():
link = convert_link(link_data) link_proto = convert_link(link)
links.append(link) links.append(link_proto)
return links return links
def get_emane_model_id(node_id: int, interface_id: int) -> int: def get_emane_model_id(node_id: int, iface_id: int) -> int:
""" """
Get EMANE model id Get EMANE model id
:param node_id: node id :param node_id: node id
:param interface_id: interface id :param iface_id: interface id
:return: EMANE model id :return: EMANE model id
""" """
if interface_id >= 0: if iface_id >= 0:
return node_id * 1000 + interface_id return node_id * 1000 + iface_id
else: else:
return node_id return node_id
@ -299,12 +316,39 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]:
:param _id: id to parse :param _id: id to parse
:return: node id and interface id :return: node id and interface id
""" """
interface = -1 iface_id = -1
node_id = _id node_id = _id
if _id >= 1000: if _id >= 1000:
interface = _id % 1000 iface_id = _id % 1000
node_id = int(_id / 1000) node_id = int(_id / 1000)
return node_id, interface return node_id, iface_id
def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface:
return core_pb2.Interface(
id=iface_data.id,
name=iface_data.name,
mac=iface_data.mac,
ip4=iface_data.ip4,
ip4_mask=iface_data.ip4_mask,
ip6=iface_data.ip6,
ip6_mask=iface_data.ip6_mask,
)
def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions:
return core_pb2.LinkOptions(
jitter=options_data.jitter,
key=options_data.key,
mburst=options_data.mburst,
mer=options_data.mer,
loss=options_data.loss,
bandwidth=options_data.bandwidth,
burst=options_data.burst,
delay=options_data.delay,
dup=options_data.dup,
unidirectional=options_data.unidirectional,
)
def convert_link(link_data: LinkData) -> core_pb2.Link: def convert_link(link_data: LinkData) -> core_pb2.Link:
@ -314,47 +358,19 @@ def convert_link(link_data: LinkData) -> core_pb2.Link:
:param link_data: link to convert :param link_data: link to convert
:return: core protobuf Link :return: core protobuf Link
""" """
interface_one = None iface1 = None
if link_data.interface1_id is not None: if link_data.iface1 is not None:
interface_one = core_pb2.Interface( iface1 = convert_iface(link_data.iface1)
id=link_data.interface1_id, iface2 = None
name=link_data.interface1_name, if link_data.iface2 is not None:
mac=convert_value(link_data.interface1_mac), iface2 = convert_iface(link_data.iface2)
ip4=convert_value(link_data.interface1_ip4), options = convert_link_options(link_data.options)
ip4mask=link_data.interface1_ip4_mask,
ip6=convert_value(link_data.interface1_ip6),
ip6mask=link_data.interface1_ip6_mask,
)
interface_two = None
if link_data.interface2_id is not None:
interface_two = core_pb2.Interface(
id=link_data.interface2_id,
name=link_data.interface2_name,
mac=convert_value(link_data.interface2_mac),
ip4=convert_value(link_data.interface2_ip4),
ip4mask=link_data.interface2_ip4_mask,
ip6=convert_value(link_data.interface2_ip6),
ip6mask=link_data.interface2_ip6_mask,
)
options = core_pb2.LinkOptions(
opaque=link_data.opaque,
jitter=link_data.jitter,
key=link_data.key,
mburst=link_data.mburst,
mer=link_data.mer,
per=link_data.per,
bandwidth=link_data.bandwidth,
burst=link_data.burst,
delay=link_data.delay,
dup=link_data.dup,
unidirectional=link_data.unidirectional,
)
return core_pb2.Link( return core_pb2.Link(
type=link_data.link_type.value, type=link_data.type.value,
node_one_id=link_data.node1_id, node1_id=link_data.node1_id,
node_two_id=link_data.node2_id, node2_id=link_data.node2_id,
interface_one=interface_one, iface1=iface1,
interface_two=interface_two, iface2=iface2,
options=options, options=options,
network_id=link_data.network_id, network_id=link_data.network_id,
label=link_data.label, label=link_data.label,
@ -418,7 +434,7 @@ def service_configuration(session: Session, config: ServiceConfig) -> None:
service.shutdown = tuple(config.shutdown) service.shutdown = tuple(config.shutdown)
def get_service_configuration(service: Type[CoreService]) -> NodeServiceData: def get_service_configuration(service: CoreService) -> NodeServiceData:
""" """
Convenience for converting a service to service data proto. Convenience for converting a service to service data proto.
@ -439,58 +455,84 @@ def get_service_configuration(service: Type[CoreService]) -> NodeServiceData:
) )
def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface: def iface_to_data(iface: CoreInterface) -> InterfaceData:
""" ip4 = iface.get_ip4()
Convenience for converting a core interface to the protobuf representation. ip4_addr = str(ip4.ip) if ip4 else None
:param interface: interface to convert ip4_mask = ip4.prefixlen if ip4 else None
:return: interface proto ip6 = iface.get_ip6()
""" ip6_addr = str(ip6.ip) if ip6 else None
net_id = None ip6_mask = ip6.prefixlen if ip6 else None
if interface.net: return InterfaceData(
net_id = interface.net.id id=iface.node_id,
ip4 = None name=iface.name,
ip4mask = None mac=str(iface.mac),
ip6 = None ip4=ip4_addr,
ip6mask = None ip4_mask=ip4_mask,
for addr in interface.addrlist: ip6=ip6_addr,
network = netaddr.IPNetwork(addr) ip6_mask=ip6_mask,
mask = network.prefixlen
ip = str(network.ip)
if netaddr.valid_ipv4(ip) and not ip4:
ip4 = ip
ip4mask = mask
elif netaddr.valid_ipv6(ip) and not ip6:
ip6 = ip
ip6mask = mask
return core_pb2.Interface(
id=interface.netindex,
netid=net_id,
name=interface.name,
mac=str(interface.hwaddr),
mtu=interface.mtu,
flowid=interface.flow_id,
ip4=ip4,
ip4mask=ip4mask,
ip6=ip6,
ip6mask=ip6mask,
) )
def get_nem_id(node: CoreNode, netif_id: int, context: ServicerContext) -> int: def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
"""
Convenience for converting a core interface to the protobuf representation.
:param node_id: id of node to convert interface for
:param iface: interface to convert
:return: interface proto
"""
if iface.node and iface.node.id == node_id:
_id = iface.node_id
else:
_id = iface.net_id
net_id = iface.net.id if iface.net else None
node_id = iface.node.id if iface.node else None
net2_id = iface.othernet.id if iface.othernet else None
ip4_net = iface.get_ip4()
ip4 = str(ip4_net.ip) if ip4_net else None
ip4_mask = ip4_net.prefixlen if ip4_net else None
ip6_net = iface.get_ip6()
ip6 = str(ip6_net.ip) if ip6_net else None
ip6_mask = ip6_net.prefixlen if ip6_net else None
mac = str(iface.mac) if iface.mac else None
return core_pb2.Interface(
id=_id,
net_id=net_id,
net2_id=net2_id,
node_id=node_id,
name=iface.name,
mac=mac,
mtu=iface.mtu,
flow_id=iface.flow_id,
ip4=ip4,
ip4_mask=ip4_mask,
ip6=ip6,
ip6_mask=ip6_mask,
)
def get_nem_id(
session: Session, node: CoreNode, iface_id: int, context: ServicerContext
) -> int:
""" """
Get nem id for a given node and interface id. Get nem id for a given node and interface id.
:param session: session node belongs to
:param node: node to get nem id for :param node: node to get nem id for
:param netif_id: id of interface on node to get nem id for :param iface_id: id of interface on node to get nem id for
:param context: request context :param context: request context
:return: nem id :return: nem id
""" """
netif = node.netif(netif_id) iface = node.ifaces.get(iface_id)
if not netif: if not iface:
message = f"{node.name} missing interface {netif_id}" message = f"{node.name} missing interface {iface_id}"
context.abort(grpc.StatusCode.NOT_FOUND, message) context.abort(grpc.StatusCode.NOT_FOUND, message)
net = netif.net net = iface.net
if not isinstance(net, EmaneNet): if not isinstance(net, EmaneNet):
message = f"{node.name} interface {netif_id} is not an EMANE network" message = f"{node.name} interface {iface_id} is not an EMANE network"
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message) context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
return net.getnemid(netif) nem_id = session.emane.get_nem_id(iface)
if nem_id is None:
message = f"{node.name} interface {iface_id} nem id does not exist"
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
return nem_id

View file

@ -6,7 +6,7 @@ import tempfile
import threading import threading
import time import time
from concurrent import futures from concurrent import futures
from typing import Iterable, Type from typing import Iterable, Optional, Pattern, Type
import grpc import grpc
from grpc import ServicerContext from grpc import ServicerContext
@ -108,18 +108,22 @@ from core.api.grpc.wlan_pb2 import (
WlanLinkResponse, WlanLinkResponse,
) )
from core.emulator.coreemu import CoreEmu from core.emulator.coreemu import CoreEmu
from core.emulator.data import LinkData from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
from core.emulator.emudata import LinkOptions, NodeOptions from core.emulator.enumerations import (
from core.emulator.enumerations import EventTypes, LinkTypes, 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, CoreNodeBase, NodeBase from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
from core.nodes.network import WlanNode from core.nodes.network import PtpNet, WlanNode
from core.services.coreservices import ServiceManager from core.services.coreservices import ServiceManager
_ONE_DAY_IN_SECONDS = 60 * 60 * 24 _ONE_DAY_IN_SECONDS: int = 60 * 60 * 24
_INTERFACE_REGEX = re.compile(r"veth(?P<node>[0-9a-fA-F]+)") _INTERFACE_REGEX: Pattern = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
@ -131,9 +135,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
def __init__(self, coreemu: CoreEmu) -> None: def __init__(self, coreemu: CoreEmu) -> None:
super().__init__() super().__init__()
self.coreemu = coreemu self.coreemu: CoreEmu = coreemu
self.running = True self.running: bool = True
self.server = None self.server: Optional[grpc.Server] = None
atexit.register(self._exit_handler) atexit.register(self._exit_handler)
def _exit_handler(self) -> None: def _exit_handler(self) -> None:
@ -246,7 +250,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
config = session.emane.get_configs() config = session.emane.get_configs()
config.update(request.emane_config) config.update(request.emane_config)
for config in request.emane_model_configs: for config in request.emane_model_configs:
_id = get_emane_model_id(config.node_id, config.interface_id) _id = get_emane_model_id(config.node_id, config.iface_id)
session.emane.set_model_config(_id, config.model, config.config) session.emane.set_model_config(_id, config.model, config.config)
# wlan configs # wlan configs
@ -449,6 +453,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
return core_pb2.SetSessionStateResponse(result=result) return core_pb2.SetSessionStateResponse(result=result)
def SetSessionUser(
self, request: core_pb2.SetSessionUserRequest, context: ServicerContext
) -> core_pb2.SetSessionUserResponse:
"""
Sets the user for a session.
:param request: set session user request
:param context: context object
:return: set session user response
"""
logging.debug("set session user: %s", request)
session = self.get_session(request.session_id, context)
session.user = request.user
return core_pb2.SetSessionUserResponse(result=True)
def GetSessionOptions( def GetSessionOptions(
self, request: core_pb2.GetSessionOptionsRequest, context: ServicerContext self, request: core_pb2.GetSessionOptionsRequest, context: ServicerContext
) -> core_pb2.GetSessionOptionsResponse: ) -> core_pb2.GetSessionOptionsResponse:
@ -544,10 +563,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
nodes = [] nodes = []
for _id in session.nodes: for _id in session.nodes:
node = session.nodes[_id] node = session.nodes[_id]
if not isinstance(node.id, int): if not isinstance(node, PtpNet):
continue node_proto = grpcutils.get_node_proto(session, node)
node_proto = grpcutils.get_node_proto(session, node) nodes.append(node_proto)
nodes.append(node_proto)
node_links = get_links(node) node_links = get_links(node)
links.extend(node_links) links.extend(node_links)
@ -571,6 +589,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
session.distributed.add_server(request.name, request.host) session.distributed.add_server(request.name, request.host)
return core_pb2.AddSessionServerResponse(result=True) return core_pb2.AddSessionServerResponse(result=True)
def SessionAlert(
self, request: core_pb2.SessionAlertRequest, context: ServicerContext
) -> core_pb2.SessionAlertResponse:
session = self.get_session(request.session_id, context)
level = ExceptionLevels(request.level)
node_id = request.node_id if request.node_id else None
session.exception(level, request.source, request.text, node_id)
return core_pb2.SessionAlertResponse(result=True)
def Events(self, request: core_pb2.EventsRequest, context: ServicerContext) -> None: def Events(self, request: core_pb2.EventsRequest, context: ServicerContext) -> None:
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
event_types = set(request.events) event_types = set(request.events)
@ -625,16 +652,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
key = key.split(".") key = key.split(".")
node_id = _INTERFACE_REGEX.search(key[0]).group("node") node_id = _INTERFACE_REGEX.search(key[0]).group("node")
node_id = int(node_id, base=16) node_id = int(node_id, base=16)
interface_id = int(key[1], base=16) iface_id = int(key[1], base=16)
session_id = int(key[2], base=16) session_id = int(key[2], base=16)
if session.id != session_id: if session.id != session_id:
continue continue
interface_throughput = ( iface_throughput = throughputs_event.iface_throughputs.add()
throughputs_event.interface_throughputs.add() iface_throughput.node_id = node_id
) iface_throughput.iface_id = iface_id
interface_throughput.node_id = node_id iface_throughput.throughput = throughput
interface_throughput.interface_id = interface_id
interface_throughput.throughput = throughput
elif key.startswith("b."): elif key.startswith("b."):
try: try:
key = key.split(".") key = key.split(".")
@ -656,6 +681,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
last_stats = stats last_stats = stats
time.sleep(delay) time.sleep(delay)
def CpuUsage(
self, request: core_pb2.CpuUsageRequest, context: ServicerContext
) -> None:
cpu_usage = grpcutils.CpuUsage()
while self._is_running(context):
usage = cpu_usage.run()
yield core_pb2.CpuUsageEvent(usage=usage)
time.sleep(request.delay)
def AddNode( def AddNode(
self, request: core_pb2.AddNodeRequest, context: ServicerContext self, request: core_pb2.AddNodeRequest, context: ServicerContext
) -> core_pb2.AddNodeResponse: ) -> core_pb2.AddNodeResponse:
@ -671,6 +705,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
_type, _id, options = grpcutils.add_node_data(request.node) _type, _id, options = grpcutils.add_node_data(request.node)
_class = session.get_node_class(_type) _class = session.get_node_class(_type)
node = session.add_node(_class, _id, options) node = session.add_node(_class, _id, options)
source = request.source if request.source else None
session.broadcast_node(node, MessageFlags.ADD, source)
return core_pb2.AddNodeResponse(node_id=node.id) return core_pb2.AddNodeResponse(node_id=node.id)
def GetNode( def GetNode(
@ -686,13 +722,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get node: %s", request) logging.debug("get node: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context, NodeBase) node = self.get_node(session, request.node_id, context, NodeBase)
interfaces = [] ifaces = []
for interface_id in node._netif: for iface_id in node.ifaces:
interface = node._netif[interface_id] iface = node.ifaces[iface_id]
interface_proto = grpcutils.interface_to_proto(interface) iface_proto = grpcutils.iface_to_proto(request.node_id, iface)
interfaces.append(interface_proto) ifaces.append(iface_proto)
node_proto = grpcutils.get_node_proto(session, node) node_proto = grpcutils.get_node_proto(session, node)
return core_pb2.GetNodeResponse(node=node_proto, interfaces=interfaces) return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces)
def MoveNodes( def MoveNodes(
self, self,
@ -778,7 +814,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("delete node: %s", request) logging.debug("delete node: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
result = session.delete_node(request.node_id) result = False
if request.node_id in session.nodes:
node = self.get_node(session, request.node_id, context, NodeBase)
result = session.delete_node(node.id)
source = request.source if request.source else None
session.broadcast_node(node, MessageFlags.DELETE, source)
return core_pb2.DeleteNodeResponse(result=result) return core_pb2.DeleteNodeResponse(result=result)
def NodeCommand( def NodeCommand(
@ -845,27 +886,42 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:return: add-link response :return: add-link response
""" """
logging.debug("add link: %s", request) logging.debug("add link: %s", request)
# validate session and nodes
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
self.get_node(session, request.link.node_one_id, context, NodeBase) node1_id = request.link.node1_id
self.get_node(session, request.link.node_two_id, context, NodeBase) node2_id = request.link.node2_id
self.get_node(session, node1_id, context, NodeBase)
node_one_id = request.link.node_one_id self.get_node(session, node2_id, context, NodeBase)
node_two_id = request.link.node_two_id iface1_data, iface2_data, options, link_type = grpcutils.add_link_data(
interface_one, interface_two, options = grpcutils.add_link_data(request.link) request.link
node_one_interface, node_two_interface = session.add_link(
node_one_id, node_two_id, interface_one, interface_two, options=options
) )
interface_one_proto = None node1_iface, node2_iface = session.add_link(
interface_two_proto = None node1_id, node2_id, iface1_data, iface2_data, options, link_type
if node_one_interface: )
interface_one_proto = grpcutils.interface_to_proto(node_one_interface) iface1_data = None
if node_two_interface: if node1_iface:
interface_two_proto = grpcutils.interface_to_proto(node_two_interface) iface1_data = grpcutils.iface_to_data(node1_iface)
iface2_data = None
if node2_iface:
iface2_data = grpcutils.iface_to_data(node2_iface)
source = request.source if request.source else None
link_data = LinkData(
message_type=MessageFlags.ADD,
node1_id=node1_id,
node2_id=node2_id,
iface1=iface1_data,
iface2=iface2_data,
options=options,
source=source,
)
session.broadcast_link(link_data)
iface1_proto = None
iface2_proto = None
if node1_iface:
iface1_proto = grpcutils.iface_to_proto(node1_id, node1_iface)
if node2_iface:
iface2_proto = grpcutils.iface_to_proto(node2_id, node2_iface)
return core_pb2.AddLinkResponse( return core_pb2.AddLinkResponse(
result=True, result=True, iface1=iface1_proto, iface2=iface2_proto
interface_one=interface_one_proto,
interface_two=interface_two_proto,
) )
def EditLink( def EditLink(
@ -880,26 +936,37 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("edit link: %s", request) logging.debug("edit link: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
node_one_id = request.node_one_id node1_id = request.node1_id
node_two_id = request.node_two_id node2_id = request.node2_id
interface_one_id = request.interface_one_id iface1_id = request.iface1_id
interface_two_id = request.interface_two_id iface2_id = request.iface2_id
options_data = request.options options_proto = request.options
link_options = LinkOptions() options = LinkOptions(
link_options.delay = options_data.delay delay=options_proto.delay,
link_options.bandwidth = options_data.bandwidth bandwidth=options_proto.bandwidth,
link_options.per = options_data.per loss=options_proto.loss,
link_options.dup = options_data.dup dup=options_proto.dup,
link_options.jitter = options_data.jitter jitter=options_proto.jitter,
link_options.mer = options_data.mer mer=options_proto.mer,
link_options.burst = options_data.burst burst=options_proto.burst,
link_options.mburst = options_data.mburst mburst=options_proto.mburst,
link_options.unidirectional = options_data.unidirectional unidirectional=options_proto.unidirectional,
link_options.key = options_data.key key=options_proto.key,
link_options.opaque = options_data.opaque
session.update_link(
node_one_id, node_two_id, interface_one_id, interface_two_id, link_options
) )
session.update_link(node1_id, node2_id, iface1_id, iface2_id, options)
iface1 = InterfaceData(id=iface1_id)
iface2 = InterfaceData(id=iface2_id)
source = request.source if request.source else None
link_data = LinkData(
message_type=MessageFlags.NONE,
node1_id=node1_id,
node2_id=node2_id,
iface1=iface1,
iface2=iface2,
options=options,
source=source,
)
session.broadcast_link(link_data)
return core_pb2.EditLinkResponse(result=True) return core_pb2.EditLinkResponse(result=True)
def DeleteLink( def DeleteLink(
@ -914,13 +981,23 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("delete link: %s", request) logging.debug("delete link: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
node_one_id = request.node_one_id node1_id = request.node1_id
node_two_id = request.node_two_id node2_id = request.node2_id
interface_one_id = request.interface_one_id iface1_id = request.iface1_id
interface_two_id = request.interface_two_id iface2_id = request.iface2_id
session.delete_link( session.delete_link(node1_id, node2_id, iface1_id, iface2_id)
node_one_id, node_two_id, interface_one_id, interface_two_id iface1 = InterfaceData(id=iface1_id)
iface2 = InterfaceData(id=iface2_id)
source = request.source if request.source else None
link_data = LinkData(
message_type=MessageFlags.DELETE,
node1_id=node1_id,
node2_id=node2_id,
iface1=iface1,
iface2=iface2,
source=source,
) )
session.broadcast_link(link_data)
return core_pb2.DeleteLinkResponse(result=True) return core_pb2.DeleteLinkResponse(result=True)
def GetHooks( def GetHooks(
@ -936,8 +1013,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get hooks: %s", request) logging.debug("get hooks: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
hooks = [] hooks = []
for state in session._hooks: for state in session.hooks:
state_hooks = session._hooks[state] state_hooks = session.hooks[state]
for file_name, file_data in state_hooks: for file_name, file_data in state_hooks:
hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data) hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data)
hooks.append(hook) hooks.append(hook)
@ -1304,13 +1381,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("set wlan config: %s", request) logging.debug("set wlan config: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
wlan_config = request.wlan_config node_id = request.wlan_config.node_id
session.mobility.set_model_config( config = request.wlan_config.config
wlan_config.node_id, BasicRangeModel.name, wlan_config.config session.mobility.set_model_config(node_id, BasicRangeModel.name, config)
)
if session.state == EventTypes.RUNTIME_STATE: if session.state == EventTypes.RUNTIME_STATE:
node = self.get_node(session, wlan_config.node_id, context, WlanNode) node = self.get_node(session, node_id, context, WlanNode)
node.updatemodel(wlan_config.config) node.updatemodel(config)
return SetWlanConfigResponse(result=True) return SetWlanConfigResponse(result=True)
def GetEmaneConfig( def GetEmaneConfig(
@ -1378,7 +1454,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get emane model config: %s", request) logging.debug("get emane model config: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
model = session.emane.models[request.model] model = session.emane.models[request.model]
_id = get_emane_model_id(request.node_id, request.interface) _id = get_emane_model_id(request.node_id, request.iface_id)
current_config = session.emane.get_model_config(_id, request.model) current_config = session.emane.get_model_config(_id, request.model)
config = get_config_options(current_config, model) config = get_config_options(current_config, model)
return GetEmaneModelConfigResponse(config=config) return GetEmaneModelConfigResponse(config=config)
@ -1397,7 +1473,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("set emane model config: %s", request) logging.debug("set emane model config: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
model_config = request.emane_model_config model_config = request.emane_model_config
_id = get_emane_model_id(model_config.node_id, model_config.interface_id) _id = get_emane_model_id(model_config.node_id, model_config.iface_id)
session.emane.set_model_config(_id, model_config.model, model_config.config) session.emane.set_model_config(_id, model_config.model, model_config.config)
return SetEmaneModelConfigResponse(result=True) return SetEmaneModelConfigResponse(result=True)
@ -1426,12 +1502,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
model = session.emane.models[model_name] model = session.emane.models[model_name]
current_config = session.emane.get_model_config(_id, model_name) current_config = session.emane.get_model_config(_id, model_name)
config = get_config_options(current_config, model) config = get_config_options(current_config, model)
node_id, interface = grpcutils.parse_emane_model_id(_id) node_id, iface_id = grpcutils.parse_emane_model_id(_id)
model_config = GetEmaneModelConfigsResponse.ModelConfig( model_config = GetEmaneModelConfigsResponse.ModelConfig(
node_id=node_id, node_id=node_id, model=model_name, iface_id=iface_id, config=config
model=model_name,
interface=interface,
config=config,
) )
configs.append(model_config) configs.append(model_config)
return GetEmaneModelConfigsResponse(configs=configs) return GetEmaneModelConfigsResponse(configs=configs)
@ -1496,16 +1569,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:param context: context object :param context: context object
:return: get-interfaces response that has all the system's interfaces :return: get-interfaces response that has all the system's interfaces
""" """
interfaces = [] ifaces = []
for interface in os.listdir("/sys/class/net"): for iface in os.listdir("/sys/class/net"):
if ( if iface.startswith("b.") or iface.startswith("veth") or iface == "lo":
interface.startswith("b.")
or interface.startswith("veth")
or interface == "lo"
):
continue continue
interfaces.append(interface) ifaces.append(iface)
return core_pb2.GetInterfacesResponse(interfaces=interfaces) return core_pb2.GetInterfacesResponse(ifaces=ifaces)
def EmaneLink( def EmaneLink(
self, request: EmaneLinkRequest, context: ServicerContext self, request: EmaneLinkRequest, context: ServicerContext
@ -1519,30 +1588,30 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("emane link: %s", request) logging.debug("emane link: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
nem_one = request.nem_one nem1 = request.nem1
emane_one, netif = session.emane.nemlookup(nem_one) iface1 = session.emane.get_iface(nem1)
if not emane_one or not netif: if not iface1:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem_one} not found") context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found")
node_one = netif.node node1 = iface1.node
nem_two = request.nem_two nem2 = request.nem2
emane_two, netif = session.emane.nemlookup(nem_two) iface2 = session.emane.get_iface(nem2)
if not emane_two or not netif: if not iface2:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem_two} not found") context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found")
node_two = netif.node node2 = iface2.node
if emane_one.id == emane_two.id: if iface1.net == iface2.net:
if request.linked: if request.linked:
flag = MessageFlags.ADD flag = MessageFlags.ADD
else: else:
flag = MessageFlags.DELETE flag = MessageFlags.DELETE
color = session.get_link_color(emane_one.id) color = session.get_link_color(iface1.net.id)
link = LinkData( link = LinkData(
message_type=flag, message_type=flag,
link_type=LinkTypes.WIRELESS, type=LinkTypes.WIRELESS,
node1_id=node_one.id, node1_id=node1.id,
node2_id=node_two.id, node2_id=node2.id,
network_id=emane_one.id, network_id=iface1.net.id,
color=color, color=color,
) )
session.broadcast_link(link) session.broadcast_link(link)
@ -1739,21 +1808,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
grpc.StatusCode.NOT_FOUND, grpc.StatusCode.NOT_FOUND,
f"wlan node {request.wlan} does not using BasicRangeModel", f"wlan node {request.wlan} does not using BasicRangeModel",
) )
n1 = self.get_node(session, request.node_one, context, CoreNode) node1 = self.get_node(session, request.node1_id, context, CoreNode)
n2 = self.get_node(session, request.node_two, context, CoreNode) node2 = self.get_node(session, request.node2_id, context, CoreNode)
n1_netif, n2_netif = None, None node1_iface, node2_iface = None, None
for net, netif1, netif2 in n1.commonnets(n2): for net, iface1, iface2 in node1.commonnets(node2):
if net == wlan: if net == wlan:
n1_netif = netif1 node1_iface = iface1
n2_netif = netif2 node2_iface = iface2
break break
result = False result = False
if n1_netif and n2_netif: if node1_iface and node2_iface:
if request.linked: if request.linked:
wlan.link(n1_netif, n2_netif) wlan.link(node1_iface, node2_iface)
else: else:
wlan.unlink(n1_netif, n2_netif) wlan.unlink(node1_iface, node2_iface)
wlan.model.sendlinkmsg(n1_netif, n2_netif, unlink=not request.linked) wlan.model.sendlinkmsg(node1_iface, node2_iface, unlink=not request.linked)
result = True result = True
return WlanLinkResponse(result=result) return WlanLinkResponse(result=result)
@ -1764,9 +1833,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
) -> EmanePathlossesResponse: ) -> EmanePathlossesResponse:
for request in request_iterator: for request in request_iterator:
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
n1 = self.get_node(session, request.node_one, context, CoreNode) node1 = self.get_node(session, request.node1_id, context, CoreNode)
nem1 = grpcutils.get_nem_id(n1, request.interface_one_id, context) nem1 = grpcutils.get_nem_id(session, node1, request.iface1_id, context)
n2 = self.get_node(session, request.node_two, context, CoreNode) node2 = self.get_node(session, request.node2_id, context, CoreNode)
nem2 = grpcutils.get_nem_id(n2, request.interface_two_id, context) nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context)
session.emane.publish_pathloss(nem1, nem2, request.rx_one, request.rx_two) session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
return EmanePathlossesResponse() return EmanePathlossesResponse()

View file

@ -495,7 +495,7 @@ class CoreLinkTlv(CoreTlv):
LinkTlvs.N2_NUMBER.value: CoreTlvDataUint32, LinkTlvs.N2_NUMBER.value: CoreTlvDataUint32,
LinkTlvs.DELAY.value: CoreTlvDataUint64, LinkTlvs.DELAY.value: CoreTlvDataUint64,
LinkTlvs.BANDWIDTH.value: CoreTlvDataUint64, LinkTlvs.BANDWIDTH.value: CoreTlvDataUint64,
LinkTlvs.PER.value: CoreTlvDataString, LinkTlvs.LOSS.value: CoreTlvDataString,
LinkTlvs.DUP.value: CoreTlvDataString, LinkTlvs.DUP.value: CoreTlvDataString,
LinkTlvs.JITTER.value: CoreTlvDataUint64, LinkTlvs.JITTER.value: CoreTlvDataUint64,
LinkTlvs.MER.value: CoreTlvDataUint16, LinkTlvs.MER.value: CoreTlvDataUint16,
@ -508,18 +508,18 @@ class CoreLinkTlv(CoreTlv):
LinkTlvs.EMULATION_ID.value: CoreTlvDataUint32, LinkTlvs.EMULATION_ID.value: CoreTlvDataUint32,
LinkTlvs.NETWORK_ID.value: CoreTlvDataUint32, LinkTlvs.NETWORK_ID.value: CoreTlvDataUint32,
LinkTlvs.KEY.value: CoreTlvDataUint32, LinkTlvs.KEY.value: CoreTlvDataUint32,
LinkTlvs.INTERFACE1_NUMBER.value: CoreTlvDataUint16, LinkTlvs.IFACE1_NUMBER.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE1_IP4.value: CoreTlvDataIpv4Addr, LinkTlvs.IFACE1_IP4.value: CoreTlvDataIpv4Addr,
LinkTlvs.INTERFACE1_IP4_MASK.value: CoreTlvDataUint16, LinkTlvs.IFACE1_IP4_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE1_MAC.value: CoreTlvDataMacAddr, LinkTlvs.IFACE1_MAC.value: CoreTlvDataMacAddr,
LinkTlvs.INTERFACE1_IP6.value: CoreTlvDataIPv6Addr, LinkTlvs.IFACE1_IP6.value: CoreTlvDataIPv6Addr,
LinkTlvs.INTERFACE1_IP6_MASK.value: CoreTlvDataUint16, LinkTlvs.IFACE1_IP6_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE2_NUMBER.value: CoreTlvDataUint16, LinkTlvs.IFACE2_NUMBER.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE2_IP4.value: CoreTlvDataIpv4Addr, LinkTlvs.IFACE2_IP4.value: CoreTlvDataIpv4Addr,
LinkTlvs.INTERFACE2_IP4_MASK.value: CoreTlvDataUint16, LinkTlvs.IFACE2_IP4_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE2_MAC.value: CoreTlvDataMacAddr, LinkTlvs.IFACE2_MAC.value: CoreTlvDataMacAddr,
LinkTlvs.INTERFACE2_IP6.value: CoreTlvDataIPv6Addr, LinkTlvs.IFACE2_IP6.value: CoreTlvDataIPv6Addr,
LinkTlvs.INTERFACE2_IP6_MASK.value: CoreTlvDataUint16, LinkTlvs.IFACE2_IP6_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString, LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString,
LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString, LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString,
LinkTlvs.OPAQUE.value: CoreTlvDataString, LinkTlvs.OPAQUE.value: CoreTlvDataString,
@ -577,7 +577,7 @@ class CoreConfigTlv(CoreTlv):
ConfigTlvs.POSSIBLE_VALUES.value: CoreTlvDataString, ConfigTlvs.POSSIBLE_VALUES.value: CoreTlvDataString,
ConfigTlvs.GROUPS.value: CoreTlvDataString, ConfigTlvs.GROUPS.value: CoreTlvDataString,
ConfigTlvs.SESSION.value: CoreTlvDataString, ConfigTlvs.SESSION.value: CoreTlvDataString,
ConfigTlvs.INTERFACE_NUMBER.value: CoreTlvDataUint16, ConfigTlvs.IFACE_ID.value: CoreTlvDataUint16,
ConfigTlvs.NETWORK_ID.value: CoreTlvDataUint32, ConfigTlvs.NETWORK_ID.value: CoreTlvDataUint32,
ConfigTlvs.OPAQUE.value: CoreTlvDataString, ConfigTlvs.OPAQUE.value: CoreTlvDataString,
} }

View file

@ -12,6 +12,7 @@ import threading
import time import time
from itertools import repeat from itertools import repeat
from queue import Empty, Queue from queue import Empty, Queue
from typing import Optional
from core import utils from core import utils
from core.api.tlv import coreapi, dataconversion, structutils from core.api.tlv import coreapi, dataconversion, structutils
@ -28,8 +29,15 @@ from core.api.tlv.enumerations import (
NodeTlvs, NodeTlvs,
SessionTlvs, SessionTlvs,
) )
from core.emulator.data import ConfigData, EventData, ExceptionData, FileData from core.emulator.data import (
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions ConfigData,
EventData,
ExceptionData,
FileData,
InterfaceData,
LinkOptions,
NodeOptions,
)
from core.emulator.enumerations import ( from core.emulator.enumerations import (
ConfigDataTypes, ConfigDataTypes,
EventTypes, EventTypes,
@ -39,6 +47,7 @@ from core.emulator.enumerations import (
NodeTypes, NodeTypes,
RegisterTlvs, RegisterTlvs,
) )
from core.emulator.session import Session
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.location.mobility import BasicRangeModel from core.location.mobility import BasicRangeModel
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
@ -69,7 +78,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
MessageTypes.REGISTER.value: self.handle_register_message, MessageTypes.REGISTER.value: self.handle_register_message,
MessageTypes.CONFIG.value: self.handle_config_message, MessageTypes.CONFIG.value: self.handle_config_message,
MessageTypes.FILE.value: self.handle_file_message, MessageTypes.FILE.value: self.handle_file_message,
MessageTypes.INTERFACE.value: self.handle_interface_message, MessageTypes.INTERFACE.value: self.handle_iface_message,
MessageTypes.EVENT.value: self.handle_event_message, MessageTypes.EVENT.value: self.handle_event_message,
MessageTypes.SESSION.value: self.handle_session_message, MessageTypes.SESSION.value: self.handle_session_message,
} }
@ -83,7 +92,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
thread.start() thread.start()
self.handler_threads.append(thread) self.handler_threads.append(thread)
self.session = None self.session: Optional[Session] = None
self.coreemu = server.coreemu self.coreemu = server.coreemu
utils.close_onexec(request.fileno()) utils.close_onexec(request.fileno())
socketserver.BaseRequestHandler.__init__(self, request, client_address, server) socketserver.BaseRequestHandler.__init__(self, request, client_address, server)
@ -176,7 +185,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
node_count_list.append(str(session.get_node_count())) node_count_list.append(str(session.get_node_count()))
date_list.append(time.ctime(session._state_time)) date_list.append(time.ctime(session.state_time))
thumb = session.thumbnail thumb = session.thumbnail
if not thumb: if not thumb:
@ -320,7 +329,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
""" """
logging.debug("handling broadcast node: %s", node_data) logging.debug("handling broadcast node: %s", node_data)
message = dataconversion.convert_node(node_data) message = dataconversion.convert_node(node_data)
try: try:
self.sendall(message) self.sendall(message)
except IOError: except IOError:
@ -334,46 +342,49 @@ class CoreHandler(socketserver.BaseRequestHandler):
:return: nothing :return: nothing
""" """
logging.debug("handling broadcast link: %s", link_data) logging.debug("handling broadcast link: %s", link_data)
per = "" options_data = link_data.options
if link_data.per is not None: loss = ""
per = str(link_data.per) if options_data.loss is not None:
loss = str(options_data.loss)
dup = "" dup = ""
if link_data.dup is not None: if options_data.dup is not None:
dup = str(link_data.dup) dup = str(options_data.dup)
iface1 = link_data.iface1
if iface1 is None:
iface1 = InterfaceData()
iface2 = link_data.iface2
if iface2 is None:
iface2 = InterfaceData()
tlv_data = structutils.pack_values( tlv_data = structutils.pack_values(
coreapi.CoreLinkTlv, coreapi.CoreLinkTlv,
[ [
(LinkTlvs.N1_NUMBER, link_data.node1_id), (LinkTlvs.N1_NUMBER, link_data.node1_id),
(LinkTlvs.N2_NUMBER, link_data.node2_id), (LinkTlvs.N2_NUMBER, link_data.node2_id),
(LinkTlvs.DELAY, link_data.delay), (LinkTlvs.DELAY, options_data.delay),
(LinkTlvs.BANDWIDTH, link_data.bandwidth), (LinkTlvs.BANDWIDTH, options_data.bandwidth),
(LinkTlvs.PER, per), (LinkTlvs.LOSS, loss),
(LinkTlvs.DUP, dup), (LinkTlvs.DUP, dup),
(LinkTlvs.JITTER, link_data.jitter), (LinkTlvs.JITTER, options_data.jitter),
(LinkTlvs.MER, link_data.mer), (LinkTlvs.MER, options_data.mer),
(LinkTlvs.BURST, link_data.burst), (LinkTlvs.BURST, options_data.burst),
(LinkTlvs.SESSION, link_data.session), (LinkTlvs.MBURST, options_data.mburst),
(LinkTlvs.MBURST, link_data.mburst), (LinkTlvs.TYPE, link_data.type.value),
(LinkTlvs.TYPE, link_data.link_type.value), (LinkTlvs.UNIDIRECTIONAL, options_data.unidirectional),
(LinkTlvs.GUI_ATTRIBUTES, link_data.gui_attributes),
(LinkTlvs.UNIDIRECTIONAL, link_data.unidirectional),
(LinkTlvs.EMULATION_ID, link_data.emulation_id),
(LinkTlvs.NETWORK_ID, link_data.network_id), (LinkTlvs.NETWORK_ID, link_data.network_id),
(LinkTlvs.KEY, link_data.key), (LinkTlvs.KEY, options_data.key),
(LinkTlvs.INTERFACE1_NUMBER, link_data.interface1_id), (LinkTlvs.IFACE1_NUMBER, iface1.id),
(LinkTlvs.INTERFACE1_IP4, link_data.interface1_ip4), (LinkTlvs.IFACE1_IP4, iface1.ip4),
(LinkTlvs.INTERFACE1_IP4_MASK, link_data.interface1_ip4_mask), (LinkTlvs.IFACE1_IP4_MASK, iface1.ip4_mask),
(LinkTlvs.INTERFACE1_MAC, link_data.interface1_mac), (LinkTlvs.IFACE1_MAC, iface1.mac),
(LinkTlvs.INTERFACE1_IP6, link_data.interface1_ip6), (LinkTlvs.IFACE1_IP6, iface1.ip6),
(LinkTlvs.INTERFACE1_IP6_MASK, link_data.interface1_ip6_mask), (LinkTlvs.IFACE1_IP6_MASK, iface1.ip6_mask),
(LinkTlvs.INTERFACE2_NUMBER, link_data.interface2_id), (LinkTlvs.IFACE2_NUMBER, iface2.id),
(LinkTlvs.INTERFACE2_IP4, link_data.interface2_ip4), (LinkTlvs.IFACE2_IP4, iface2.ip4),
(LinkTlvs.INTERFACE2_IP4_MASK, link_data.interface2_ip4_mask), (LinkTlvs.IFACE2_IP4_MASK, iface2.ip4_mask),
(LinkTlvs.INTERFACE2_MAC, link_data.interface2_mac), (LinkTlvs.IFACE2_MAC, iface2.mac),
(LinkTlvs.INTERFACE2_IP6, link_data.interface2_ip6), (LinkTlvs.IFACE2_IP6, iface2.ip6),
(LinkTlvs.INTERFACE2_IP6_MASK, link_data.interface2_ip6_mask), (LinkTlvs.IFACE2_IP6_MASK, iface2.ip6_mask),
(LinkTlvs.OPAQUE, link_data.opaque),
], ],
) )
@ -707,7 +718,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
options.icon = message.get_tlv(NodeTlvs.ICON.value) options.icon = message.get_tlv(NodeTlvs.ICON.value)
options.canvas = message.get_tlv(NodeTlvs.CANVAS.value) options.canvas = message.get_tlv(NodeTlvs.CANVAS.value)
options.opaque = message.get_tlv(NodeTlvs.OPAQUE.value)
options.server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value) options.server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value)
services = message.get_tlv(NodeTlvs.SERVICES.value) services = message.get_tlv(NodeTlvs.SERVICES.value)
@ -745,67 +755,54 @@ class CoreHandler(socketserver.BaseRequestHandler):
:param core.api.tlv.coreapi.CoreLinkMessage message: link message to handle :param core.api.tlv.coreapi.CoreLinkMessage message: link message to handle
:return: link message replies :return: link message replies
""" """
node_one_id = message.get_tlv(LinkTlvs.N1_NUMBER.value) node1_id = message.get_tlv(LinkTlvs.N1_NUMBER.value)
node_two_id = message.get_tlv(LinkTlvs.N2_NUMBER.value) node2_id = message.get_tlv(LinkTlvs.N2_NUMBER.value)
iface1_data = InterfaceData(
interface_one = InterfaceData( id=message.get_tlv(LinkTlvs.IFACE1_NUMBER.value),
id=message.get_tlv(LinkTlvs.INTERFACE1_NUMBER.value),
name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value), name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value),
mac=message.get_tlv(LinkTlvs.INTERFACE1_MAC.value), mac=message.get_tlv(LinkTlvs.IFACE1_MAC.value),
ip4=message.get_tlv(LinkTlvs.INTERFACE1_IP4.value), ip4=message.get_tlv(LinkTlvs.IFACE1_IP4.value),
ip4_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP4_MASK.value), ip4_mask=message.get_tlv(LinkTlvs.IFACE1_IP4_MASK.value),
ip6=message.get_tlv(LinkTlvs.INTERFACE1_IP6.value), ip6=message.get_tlv(LinkTlvs.IFACE1_IP6.value),
ip6_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP6_MASK.value), ip6_mask=message.get_tlv(LinkTlvs.IFACE1_IP6_MASK.value),
) )
interface_two = InterfaceData( iface2_data = InterfaceData(
id=message.get_tlv(LinkTlvs.INTERFACE2_NUMBER.value), id=message.get_tlv(LinkTlvs.IFACE2_NUMBER.value),
name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value), name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value),
mac=message.get_tlv(LinkTlvs.INTERFACE2_MAC.value), mac=message.get_tlv(LinkTlvs.IFACE2_MAC.value),
ip4=message.get_tlv(LinkTlvs.INTERFACE2_IP4.value), ip4=message.get_tlv(LinkTlvs.IFACE2_IP4.value),
ip4_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP4_MASK.value), ip4_mask=message.get_tlv(LinkTlvs.IFACE2_IP4_MASK.value),
ip6=message.get_tlv(LinkTlvs.INTERFACE2_IP6.value), ip6=message.get_tlv(LinkTlvs.IFACE2_IP6.value),
ip6_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP6_MASK.value), ip6_mask=message.get_tlv(LinkTlvs.IFACE2_IP6_MASK.value),
) )
link_type = LinkTypes.WIRED
link_type = None
link_type_value = message.get_tlv(LinkTlvs.TYPE.value) link_type_value = message.get_tlv(LinkTlvs.TYPE.value)
if link_type_value is not None: if link_type_value is not None:
link_type = LinkTypes(link_type_value) link_type = LinkTypes(link_type_value)
options = LinkOptions()
link_options = LinkOptions(type=link_type) options.delay = message.get_tlv(LinkTlvs.DELAY.value)
link_options.delay = message.get_tlv(LinkTlvs.DELAY.value) options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value)
link_options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value) options.loss = message.get_tlv(LinkTlvs.LOSS.value)
link_options.session = message.get_tlv(LinkTlvs.SESSION.value) options.dup = message.get_tlv(LinkTlvs.DUP.value)
link_options.per = message.get_tlv(LinkTlvs.PER.value) options.jitter = message.get_tlv(LinkTlvs.JITTER.value)
link_options.dup = message.get_tlv(LinkTlvs.DUP.value) options.mer = message.get_tlv(LinkTlvs.MER.value)
link_options.jitter = message.get_tlv(LinkTlvs.JITTER.value) options.burst = message.get_tlv(LinkTlvs.BURST.value)
link_options.mer = message.get_tlv(LinkTlvs.MER.value) options.mburst = message.get_tlv(LinkTlvs.MBURST.value)
link_options.burst = message.get_tlv(LinkTlvs.BURST.value) options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value)
link_options.mburst = message.get_tlv(LinkTlvs.MBURST.value) options.key = message.get_tlv(LinkTlvs.KEY.value)
link_options.gui_attributes = message.get_tlv(LinkTlvs.GUI_ATTRIBUTES.value)
link_options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value)
link_options.emulation_id = message.get_tlv(LinkTlvs.EMULATION_ID.value)
link_options.network_id = message.get_tlv(LinkTlvs.NETWORK_ID.value)
link_options.key = message.get_tlv(LinkTlvs.KEY.value)
link_options.opaque = message.get_tlv(LinkTlvs.OPAQUE.value)
if message.flags & MessageFlags.ADD.value: if message.flags & MessageFlags.ADD.value:
self.session.add_link( self.session.add_link(
node_one_id, node_two_id, interface_one, interface_two, link_options node1_id, node2_id, iface1_data, iface2_data, options, link_type
) )
elif message.flags & MessageFlags.DELETE.value: elif message.flags & MessageFlags.DELETE.value:
self.session.delete_link( self.session.delete_link(
node_one_id, node_two_id, interface_one.id, interface_two.id node1_id, node2_id, iface1_data.id, iface2_data.id, link_type
) )
else: else:
self.session.update_link( self.session.update_link(
node_one_id, node1_id, node2_id, iface1_data.id, iface2_data.id, options, link_type
node_two_id,
interface_one.id,
interface_two.id,
link_options,
) )
return () return ()
def handle_execute_message(self, message): def handle_execute_message(self, message):
@ -815,38 +812,38 @@ class CoreHandler(socketserver.BaseRequestHandler):
:param core.api.tlv.coreapi.CoreExecMessage message: execute message to handle :param core.api.tlv.coreapi.CoreExecMessage message: execute message to handle
:return: reply messages :return: reply messages
""" """
node_num = message.get_tlv(ExecuteTlvs.NODE.value) node_id = message.get_tlv(ExecuteTlvs.NODE.value)
execute_num = message.get_tlv(ExecuteTlvs.NUMBER.value) execute_num = message.get_tlv(ExecuteTlvs.NUMBER.value)
execute_time = message.get_tlv(ExecuteTlvs.TIME.value) execute_time = message.get_tlv(ExecuteTlvs.TIME.value)
command = message.get_tlv(ExecuteTlvs.COMMAND.value) command = message.get_tlv(ExecuteTlvs.COMMAND.value)
# local flag indicates command executed locally, not on a node # local flag indicates command executed locally, not on a node
if node_num is None and not message.flags & MessageFlags.LOCAL.value: if node_id is None and not message.flags & MessageFlags.LOCAL.value:
raise ValueError("Execute Message is missing node number.") raise ValueError("Execute Message is missing node number.")
if execute_num is None: if execute_num is None:
raise ValueError("Execute Message is missing execution number.") raise ValueError("Execute Message is missing execution number.")
if execute_time is not None: if execute_time is not None:
self.session.add_event(execute_time, node=node_num, name=None, data=command) self.session.add_event(
float(execute_time), node_id=node_id, name=None, data=command
)
return () return ()
try: try:
node = self.session.get_node(node_num, CoreNodeBase) node = self.session.get_node(node_id, CoreNodeBase)
# build common TLV items for reply # build common TLV items for reply
tlv_data = b"" tlv_data = b""
if node_num is not None: if node_id is not None:
tlv_data += coreapi.CoreExecuteTlv.pack( tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.NODE.value, node_id)
ExecuteTlvs.NODE.value, node_num
)
tlv_data += coreapi.CoreExecuteTlv.pack( tlv_data += coreapi.CoreExecuteTlv.pack(
ExecuteTlvs.NUMBER.value, execute_num ExecuteTlvs.NUMBER.value, execute_num
) )
tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.COMMAND.value, command) tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.COMMAND.value, command)
if message.flags & MessageFlags.TTY.value: if message.flags & MessageFlags.TTY.value:
if node_num is None: if node_id is None:
raise NotImplementedError raise NotImplementedError
# echo back exec message with cmd for spawning interactive terminal # echo back exec message with cmd for spawning interactive terminal
if command == "bash": if command == "bash":
@ -856,7 +853,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
reply = coreapi.CoreExecMessage.pack(MessageFlags.TTY.value, tlv_data) reply = coreapi.CoreExecMessage.pack(MessageFlags.TTY.value, tlv_data)
return (reply,) return (reply,)
else: else:
logging.info("execute message with cmd=%s", command)
# execute command and send a response # execute command and send a response
if ( if (
message.flags & MessageFlags.STRING.value message.flags & MessageFlags.STRING.value
@ -876,7 +872,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
except CoreCommandError as e: except CoreCommandError as e:
res = e.stderr res = e.stderr
status = e.returncode status = e.returncode
logging.info("done exec cmd=%s with status=%d", command, status)
if message.flags & MessageFlags.TEXT.value: if message.flags & MessageFlags.TEXT.value:
tlv_data += coreapi.CoreExecuteTlv.pack( tlv_data += coreapi.CoreExecuteTlv.pack(
ExecuteTlvs.RESULT.value, res ExecuteTlvs.RESULT.value, res
@ -894,7 +889,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
else: else:
node.cmd(command, wait=False) node.cmd(command, wait=False)
except CoreError: except CoreError:
logging.exception("error getting object: %s", node_num) logging.exception("error getting object: %s", node_id)
# XXX wait and queue this message to try again later # XXX wait and queue this message to try again later
# XXX maybe this should be done differently # XXX maybe this should be done differently
if not message.flags & MessageFlags.LOCAL.value: if not message.flags & MessageFlags.LOCAL.value:
@ -1016,7 +1011,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
possible_values=message.get_tlv(ConfigTlvs.POSSIBLE_VALUES.value), possible_values=message.get_tlv(ConfigTlvs.POSSIBLE_VALUES.value),
groups=message.get_tlv(ConfigTlvs.GROUPS.value), groups=message.get_tlv(ConfigTlvs.GROUPS.value),
session=message.get_tlv(ConfigTlvs.SESSION.value), session=message.get_tlv(ConfigTlvs.SESSION.value),
interface_number=message.get_tlv(ConfigTlvs.INTERFACE_NUMBER.value), iface_id=message.get_tlv(ConfigTlvs.IFACE_ID.value),
network_id=message.get_tlv(ConfigTlvs.NETWORK_ID.value), network_id=message.get_tlv(ConfigTlvs.NETWORK_ID.value),
opaque=message.get_tlv(ConfigTlvs.OPAQUE.value), opaque=message.get_tlv(ConfigTlvs.OPAQUE.value),
) )
@ -1333,11 +1328,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
replies = [] replies = []
node_id = config_data.node node_id = config_data.node
object_name = config_data.object object_name = config_data.object
interface_id = config_data.interface_number iface_id = config_data.iface_id
values_str = config_data.data_values values_str = config_data.data_values
if interface_id is not None: if iface_id is not None:
node_id = node_id * 1000 + interface_id node_id = node_id * 1000 + iface_id
logging.debug( logging.debug(
"received configure message for %s nodenum: %s", object_name, node_id "received configure message for %s nodenum: %s", object_name, node_id
@ -1383,11 +1378,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
replies = [] replies = []
node_id = config_data.node node_id = config_data.node
object_name = config_data.object object_name = config_data.object
interface_id = config_data.interface_number iface_id = config_data.iface_id
values_str = config_data.data_values values_str = config_data.data_values
if interface_id is not None: if iface_id is not None:
node_id = node_id * 1000 + interface_id node_id = node_id * 1000 + iface_id
logging.debug( logging.debug(
"received configure message for %s nodenum: %s", object_name, node_id "received configure message for %s nodenum: %s", object_name, node_id
@ -1415,11 +1410,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
replies = [] replies = []
node_id = config_data.node node_id = config_data.node
object_name = config_data.object object_name = config_data.object
interface_id = config_data.interface_number iface_id = config_data.iface_id
values_str = config_data.data_values values_str = config_data.data_values
if interface_id is not None: if iface_id is not None:
node_id = node_id * 1000 + interface_id node_id = node_id * 1000 + iface_id
logging.debug( logging.debug(
"received configure message for %s nodenum: %s", object_name, node_id "received configure message for %s nodenum: %s", object_name, node_id
@ -1513,7 +1508,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
return () return ()
def handle_interface_message(self, message): def handle_iface_message(self, message):
""" """
Interface Message handler. Interface Message handler.
@ -1555,11 +1550,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
if event_type == EventTypes.INSTANTIATION_STATE and isinstance( if event_type == EventTypes.INSTANTIATION_STATE and isinstance(
node, WlanNode node, WlanNode
): ):
self.session.start_mobility(node_ids=(node.id,)) self.session.start_mobility(node_ids=[node.id])
return () return ()
logging.warning( logging.warning(
"dropping unhandled event message for node: %s", node_id "dropping unhandled event message for node: %s", node.name
) )
return () return ()
self.session.set_state(event_type) self.session.set_state(event_type)
@ -1617,14 +1612,16 @@ class CoreHandler(socketserver.BaseRequestHandler):
self.session.save_xml(filename) self.session.save_xml(filename)
elif event_type == EventTypes.SCHEDULED: elif event_type == EventTypes.SCHEDULED:
etime = event_data.time etime = event_data.time
node = event_data.node node_id = event_data.node
name = event_data.name name = event_data.name
data = event_data.data data = event_data.data
if etime is None: if etime is None:
logging.warning("Event message scheduled event missing start time") logging.warning("Event message scheduled event missing start time")
return () return ()
if message.flags & MessageFlags.ADD.value: if message.flags & MessageFlags.ADD.value:
self.session.add_event(float(etime), node=node, name=name, data=data) self.session.add_event(
float(etime), node_id=node_id, name=name, data=data
)
else: else:
raise NotImplementedError raise NotImplementedError
@ -1827,16 +1824,16 @@ class CoreHandler(socketserver.BaseRequestHandler):
Return API messages that describe the current session. Return API messages that describe the current session.
""" """
# find all nodes and links # find all nodes and links
links_data = [] all_links = []
with self.session._nodes_lock: with self.session.nodes_lock:
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]
self.session.broadcast_node(node, MessageFlags.ADD) self.session.broadcast_node(node, MessageFlags.ADD)
node_links = node.all_link_data(flags=MessageFlags.ADD) links = node.links(flags=MessageFlags.ADD)
links_data.extend(node_links) all_links.extend(links)
for link_data in links_data: for link in all_links:
self.session.broadcast_link(link_data) self.session.broadcast_link(link)
# send mobility model info # send mobility model info
for node_id in self.session.mobility.nodes(): for node_id in self.session.mobility.nodes():
@ -1906,8 +1903,8 @@ class CoreHandler(socketserver.BaseRequestHandler):
# TODO: send location info # TODO: send location info
# send hook scripts # send hook scripts
for state in sorted(self.session._hooks.keys()): for state in sorted(self.session.hooks.keys()):
for file_name, config_data in self.session._hooks[state]: for file_name, config_data in self.session.hooks[state]:
file_data = FileData( file_data = FileData(
message_type=MessageFlags.ADD, message_type=MessageFlags.ADD,
name=str(file_name), name=str(file_name),
@ -1943,7 +1940,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
node_count = self.session.get_node_count() node_count = self.session.get_node_count()
logging.info( logging.info(
"informed GUI about %d nodes and %d links", node_count, len(links_data) "informed GUI about %d nodes and %d links", node_count, len(all_links)
) )
@ -1956,7 +1953,7 @@ class CoreUdpHandler(CoreHandler):
MessageTypes.REGISTER.value: self.handle_register_message, MessageTypes.REGISTER.value: self.handle_register_message,
MessageTypes.CONFIG.value: self.handle_config_message, MessageTypes.CONFIG.value: self.handle_config_message,
MessageTypes.FILE.value: self.handle_file_message, MessageTypes.FILE.value: self.handle_file_message,
MessageTypes.INTERFACE.value: self.handle_interface_message, MessageTypes.INTERFACE.value: self.handle_iface_message,
MessageTypes.EVENT.value: self.handle_event_message, MessageTypes.EVENT.value: self.handle_event_message,
MessageTypes.SESSION.value: self.handle_session_message, MessageTypes.SESSION.value: self.handle_session_message,
} }

View file

@ -8,45 +8,39 @@ from typing import Dict, List
from core.api.tlv import coreapi, structutils from core.api.tlv import coreapi, structutils
from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs
from core.config import ConfigGroup, ConfigurableOptions from core.config import ConfigGroup, ConfigurableOptions
from core.emulator.data import ConfigData from core.emulator.data import ConfigData, NodeData
def convert_node(node_data): def convert_node(node_data: NodeData):
""" """
Convenience method for converting NodeData to a packed TLV message. Convenience method for converting NodeData to a packed TLV message.
:param core.emulator.data.NodeData node_data: node data to convert :param core.emulator.data.NodeData node_data: node data to convert
:return: packed node message :return: packed node message
""" """
session = None node = node_data.node
if node_data.session is not None:
session = str(node_data.session)
services = None services = None
if node_data.services is not None: if node.services is not None:
services = "|".join([x for x in node_data.services]) 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( tlv_data = structutils.pack_values(
coreapi.CoreNodeTlv, coreapi.CoreNodeTlv,
[ [
(NodeTlvs.NUMBER, node_data.id), (NodeTlvs.NUMBER, node.id),
(NodeTlvs.TYPE, node_data.node_type.value), (NodeTlvs.TYPE, node.apitype.value),
(NodeTlvs.NAME, node_data.name), (NodeTlvs.NAME, node.name),
(NodeTlvs.IP_ADDRESS, node_data.ip_address), (NodeTlvs.MODEL, node.type),
(NodeTlvs.MAC_ADDRESS, node_data.mac_address), (NodeTlvs.EMULATION_SERVER, server),
(NodeTlvs.IP6_ADDRESS, node_data.ip6_address), (NodeTlvs.X_POSITION, int(node.position.x)),
(NodeTlvs.MODEL, node_data.model), (NodeTlvs.Y_POSITION, int(node.position.y)),
(NodeTlvs.EMULATION_ID, node_data.emulation_id), (NodeTlvs.CANVAS, node.canvas),
(NodeTlvs.EMULATION_SERVER, node_data.server),
(NodeTlvs.SESSION, session),
(NodeTlvs.X_POSITION, int(node_data.x_position)),
(NodeTlvs.Y_POSITION, int(node_data.y_position)),
(NodeTlvs.CANVAS, node_data.canvas),
(NodeTlvs.NETWORK_ID, node_data.network_id),
(NodeTlvs.SERVICES, services), (NodeTlvs.SERVICES, services),
(NodeTlvs.LATITUDE, str(node_data.latitude)), (NodeTlvs.LATITUDE, str(node.position.lat)),
(NodeTlvs.LONGITUDE, str(node_data.longitude)), (NodeTlvs.LONGITUDE, str(node.position.lon)),
(NodeTlvs.ALTITUDE, str(node_data.altitude)), (NodeTlvs.ALTITUDE, str(node.position.alt)),
(NodeTlvs.ICON, node_data.icon), (NodeTlvs.ICON, node.icon),
(NodeTlvs.OPAQUE, node_data.opaque),
], ],
) )
return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data) return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data)
@ -75,7 +69,7 @@ def convert_config(config_data):
(ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values), (ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values),
(ConfigTlvs.GROUPS, config_data.groups), (ConfigTlvs.GROUPS, config_data.groups),
(ConfigTlvs.SESSION, session), (ConfigTlvs.SESSION, session),
(ConfigTlvs.INTERFACE_NUMBER, config_data.interface_number), (ConfigTlvs.IFACE_ID, config_data.iface_id),
(ConfigTlvs.NETWORK_ID, config_data.network_id), (ConfigTlvs.NETWORK_ID, config_data.network_id),
(ConfigTlvs.OPAQUE, config_data.opaque), (ConfigTlvs.OPAQUE, config_data.opaque),
], ],

View file

@ -59,7 +59,7 @@ class LinkTlvs(Enum):
N2_NUMBER = 0x02 N2_NUMBER = 0x02
DELAY = 0x03 DELAY = 0x03
BANDWIDTH = 0x04 BANDWIDTH = 0x04
PER = 0x05 LOSS = 0x05
DUP = 0x06 DUP = 0x06
JITTER = 0x07 JITTER = 0x07
MER = 0x08 MER = 0x08
@ -72,18 +72,18 @@ class LinkTlvs(Enum):
EMULATION_ID = 0x23 EMULATION_ID = 0x23
NETWORK_ID = 0x24 NETWORK_ID = 0x24
KEY = 0x25 KEY = 0x25
INTERFACE1_NUMBER = 0x30 IFACE1_NUMBER = 0x30
INTERFACE1_IP4 = 0x31 IFACE1_IP4 = 0x31
INTERFACE1_IP4_MASK = 0x32 IFACE1_IP4_MASK = 0x32
INTERFACE1_MAC = 0x33 IFACE1_MAC = 0x33
INTERFACE1_IP6 = 0x34 IFACE1_IP6 = 0x34
INTERFACE1_IP6_MASK = 0x35 IFACE1_IP6_MASK = 0x35
INTERFACE2_NUMBER = 0x36 IFACE2_NUMBER = 0x36
INTERFACE2_IP4 = 0x37 IFACE2_IP4 = 0x37
INTERFACE2_IP4_MASK = 0x38 IFACE2_IP4_MASK = 0x38
INTERFACE2_MAC = 0x39 IFACE2_MAC = 0x39
INTERFACE2_IP6 = 0x40 IFACE2_IP6 = 0x40
INTERFACE2_IP6_MASK = 0x41 IFACE2_IP6_MASK = 0x41
INTERFACE1_NAME = 0x42 INTERFACE1_NAME = 0x42
INTERFACE2_NAME = 0x43 INTERFACE2_NAME = 0x43
OPAQUE = 0x50 OPAQUE = 0x50
@ -118,7 +118,7 @@ class ConfigTlvs(Enum):
POSSIBLE_VALUES = 0x08 POSSIBLE_VALUES = 0x08
GROUPS = 0x09 GROUPS = 0x09
SESSION = 0x0A SESSION = 0x0A
INTERFACE_NUMBER = 0x0B IFACE_ID = 0x0B
NETWORK_ID = 0x24 NETWORK_ID = 0x24
OPAQUE = 0x50 OPAQUE = 0x50

View file

@ -4,7 +4,7 @@ Common support for configurable CORE objects.
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Union from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.enumerations import ConfigDataTypes from core.emulator.enumerations import ConfigDataTypes
@ -29,9 +29,9 @@ class ConfigGroup:
:param start: configurations start index for this group :param start: configurations start index for this group
:param stop: configurations stop index for this group :param stop: configurations stop index for this group
""" """
self.name = name self.name: str = name
self.start = start self.start: int = start
self.stop = stop self.stop: int = stop
class Configuration: class Configuration:
@ -56,18 +56,21 @@ class Configuration:
:param default: default value for configuration :param default: default value for configuration
:param options: list options if this is a configuration with a combobox :param options: list options if this is a configuration with a combobox
""" """
self.id = _id self.id: str = _id
self.type = _type self.type: ConfigDataTypes = _type
self.default = default self.default: str = default
if not options: if not options:
options = [] options = []
self.options = options self.options: List[str] = options
if not label: if not label:
label = _id label = _id
self.label = label self.label: str = label
def __str__(self): def __str__(self):
return f"{self.__class__.__name__}(id={self.id}, type={self.type}, default={self.default}, options={self.options})" return (
f"{self.__class__.__name__}(id={self.id}, type={self.type}, "
f"default={self.default}, options={self.options})"
)
class ConfigurableOptions: class ConfigurableOptions:
@ -75,9 +78,9 @@ class ConfigurableOptions:
Provides a base for defining configuration options within CORE. Provides a base for defining configuration options within CORE.
""" """
name = None name: Optional[str] = None
bitmap = None bitmap: Optional[str] = None
options = [] options: List[Configuration] = []
@classmethod @classmethod
def configurations(cls) -> List[Configuration]: def configurations(cls) -> List[Configuration]:
@ -115,8 +118,8 @@ class ConfigurableManager:
nodes. nodes.
""" """
_default_node = -1 _default_node: int = -1
_default_type = _default_node _default_type: int = _default_node
def __init__(self) -> None: def __init__(self) -> None:
""" """
@ -136,7 +139,8 @@ class ConfigurableManager:
""" """
Clears all configurations or configuration for a specific node. Clears all configurations or configuration for a specific node.
:param node_id: node id to clear configurations for, default is None and clears all configurations :param node_id: node id to clear configurations for, default is None and clears
all configurations
:return: nothing :return: nothing
""" """
if not node_id: if not node_id:
@ -222,7 +226,7 @@ class ConfigurableManager:
result = node_configs.get(config_type) result = node_configs.get(config_type)
return result return result
def get_all_configs(self, node_id: int = _default_node) -> List[Dict[str, str]]: def get_all_configs(self, node_id: int = _default_node) -> Dict[str, Any]:
""" """
Retrieve all current configuration types for a node. Retrieve all current configuration types for a node.
@ -242,8 +246,8 @@ class ModelManager(ConfigurableManager):
Creates a ModelManager object. Creates a ModelManager object.
""" """
super().__init__() super().__init__()
self.models = {} self.models: Dict[str, Any] = {}
self.node_models = {} self.node_models: Dict[int, str] = {}
def set_model_config( def set_model_config(
self, node_id: int, model_name: str, config: Dict[str, str] = None self, node_id: int, model_name: str, config: Dict[str, str] = None

View file

@ -14,7 +14,7 @@ from core.config import Configuration
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
TEMPLATES_DIR = "templates" TEMPLATES_DIR: str = "templates"
class ConfigServiceMode(enum.Enum): class ConfigServiceMode(enum.Enum):
@ -33,10 +33,10 @@ class ConfigService(abc.ABC):
""" """
# validation period in seconds, how frequent validation is attempted # validation period in seconds, how frequent validation is attempted
validation_period = 0.5 validation_period: float = 0.5
# time to wait in seconds for determining if service started successfully # time to wait in seconds for determining if service started successfully
validation_timer = 5 validation_timer: int = 5
def __init__(self, node: CoreNode) -> None: def __init__(self, node: CoreNode) -> None:
""" """
@ -44,13 +44,13 @@ class ConfigService(abc.ABC):
:param node: node this service is assigned to :param node: node this service is assigned to
""" """
self.node = node self.node: CoreNode = node
class_file = inspect.getfile(self.__class__) class_file = inspect.getfile(self.__class__)
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR) templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
self.templates = TemplateLookup(directories=templates_path) self.templates: TemplateLookup = TemplateLookup(directories=templates_path)
self.config = {} self.config: Dict[str, Configuration] = {}
self.custom_templates = {} self.custom_templates: Dict[str, str] = {}
self.custom_config = {} self.custom_config: Dict[str, str] = {}
configs = self.default_configs[:] configs = self.default_configs[:]
self._define_config(configs) self._define_config(configs)

View file

@ -1,5 +1,5 @@
import logging import logging
from typing import TYPE_CHECKING, Dict, List from typing import TYPE_CHECKING, Dict, List, Set
if TYPE_CHECKING: if TYPE_CHECKING:
from core.configservice.base import ConfigService from core.configservice.base import ConfigService
@ -17,9 +17,9 @@ class ConfigServiceDependencies:
:param services: services for determining dependency sets :param services: services for determining dependency sets
""" """
# helpers to check validity # helpers to check validity
self.dependents = {} self.dependents: Dict[str, Set[str]] = {}
self.started = set() self.started: Set[str] = set()
self.node_services = {} self.node_services: Dict[str, "ConfigService"] = {}
for service in services.values(): for service in services.values():
self.node_services[service.name] = service self.node_services[service.name] = service
for dependency in service.dependencies: for dependency in service.dependencies:
@ -27,9 +27,9 @@ class ConfigServiceDependencies:
dependents.add(service.name) dependents.add(service.name)
# used to find paths # used to find paths
self.path = [] self.path: List["ConfigService"] = []
self.visited = set() self.visited: Set[str] = set()
self.visiting = set() self.visiting: Set[str] = set()
def startup_paths(self) -> List[List["ConfigService"]]: def startup_paths(self) -> List[List["ConfigService"]]:
""" """

View file

@ -1,6 +1,6 @@
import logging import logging
import pathlib import pathlib
from typing import List, Type from typing import Dict, List, Type
from core import utils from core import utils
from core.configservice.base import ConfigService from core.configservice.base import ConfigService
@ -16,7 +16,7 @@ class ConfigServiceManager:
""" """
Create a ConfigServiceManager instance. Create a ConfigServiceManager instance.
""" """
self.services = {} self.services: Dict[str, Type[ConfigService]] = {}
def get_service(self, name: str) -> Type[ConfigService]: def get_service(self, name: str) -> Type[ConfigService]:
""" """
@ -31,7 +31,7 @@ class ConfigServiceManager:
raise CoreError(f"service does not exit {name}") raise CoreError(f"service does not exit {name}")
return service_class return service_class
def add(self, service: ConfigService) -> None: def add(self, service: Type[ConfigService]) -> None:
""" """
Add service to manager, checking service requirements have been met. Add service to manager, checking service requirements have been met.
@ -40,7 +40,9 @@ class ConfigServiceManager:
:raises CoreError: when service is a duplicate or has unmet executables :raises CoreError: when service is a duplicate or has unmet executables
""" """
name = service.name name = service.name
logging.debug("loading service: class(%s) name(%s)", service.__class__, name) logging.debug(
"loading service: class(%s) name(%s)", service.__class__.__name__, name
)
# avoid duplicate services # avoid duplicate services
if name in self.services: if name in self.services:
@ -50,10 +52,8 @@ class ConfigServiceManager:
for executable in service.executables: for executable in service.executables:
try: try:
utils.which(executable, required=True) utils.which(executable, required=True)
except ValueError: except CoreError as e:
raise CoreError( raise CoreError(f"config service({service.name}): {e}")
f"service({service.name}) missing executable {executable}"
)
# make service available # make service available
self.services[name] = service self.services[name] = service
@ -73,7 +73,6 @@ class ConfigServiceManager:
logging.debug("loading config services from: %s", subdir) logging.debug("loading config services from: %s", subdir)
services = utils.load_classes(str(subdir), ConfigService) services = utils.load_classes(str(subdir), ConfigService)
for service in services: for service in services:
logging.debug("found service: %s", service)
try: try:
self.add(service) self.add(service)
except CoreError as e: except CoreError as e:

View file

@ -1,45 +1,44 @@
import abc import abc
from typing import Any, Dict from typing import Any, Dict, List
import netaddr from core.config import Configuration
from core import constants
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
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
GROUP = "FRR" GROUP: str = "FRR"
FRR_STATE_DIR: str = "/var/run/frr"
def has_mtu_mismatch(ifc: 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
mtu-ignore command. This is needed when e.g. a node is linked via a mtu-ignore command. This is needed when e.g. a node is linked via a
GreTap device. GreTap device.
""" """
if ifc.mtu != 1500: if iface.mtu != 1500:
return True return True
if not ifc.net: if not iface.net:
return False return False
for i in ifc.net.netifs(): for iface in iface.net.get_ifaces():
if i.mtu != ifc.mtu: if iface.mtu != iface.mtu:
return True return True
return False return False
def get_min_mtu(ifc): def get_min_mtu(iface: CoreInterface) -> int:
""" """
Helper to discover the minimum MTU of interfaces linked with the Helper to discover the minimum MTU of interfaces linked with the
given interface. given interface.
""" """
mtu = ifc.mtu mtu = iface.mtu
if not ifc.net: if not iface.net:
return mtu return mtu
for i in ifc.net.netifs(): for iface in iface.net.get_ifaces():
if i.mtu < mtu: if iface.mtu < mtu:
mtu = i.mtu mtu = iface.mtu
return mtu return mtu
@ -47,34 +46,31 @@ def get_router_id(node: CoreNodeBase) -> str:
""" """
Helper to return the first IPv4 address of a node as its router ID. Helper to return the first IPv4 address of a node as its router ID.
""" """
for ifc in node.netifs(): for iface in node.get_ifaces(control=False):
if getattr(ifc, "control", False): ip4 = iface.get_ip4()
continue if ip4:
for a in ifc.addrlist: return str(ip4.ip)
a = a.split("/")[0]
if netaddr.valid_ipv4(a):
return a
return "0.0.0.0" return "0.0.0.0"
class FRRZebra(ConfigService): class FRRZebra(ConfigService):
name = "FRRzebra" name: str = "FRRzebra"
group = GROUP group: str = GROUP
directories = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"] directories: List[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
files = [ files: List[str] = [
"/usr/local/etc/frr/frr.conf", "/usr/local/etc/frr/frr.conf",
"frrboot.sh", "frrboot.sh",
"/usr/local/etc/frr/vtysh.conf", "/usr/local/etc/frr/vtysh.conf",
"/usr/local/etc/frr/daemons", "/usr/local/etc/frr/daemons",
] ]
executables = ["zebra"] executables: List[str] = ["zebra"]
dependencies = [] dependencies: List[str] = []
startup = ["sh frrboot.sh zebra"] startup: List[str] = ["sh frrboot.sh zebra"]
validate = ["pidof zebra"] validate: List[str] = ["pidof zebra"]
shutdown = ["killall zebra"] shutdown: List[str] = ["killall zebra"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
frr_conf = self.files[0] frr_conf = self.files[0]
@ -91,31 +87,31 @@ class FRRZebra(ConfigService):
for service in self.node.config_services.values(): for service in self.node.config_services.values():
if self.name not in service.dependencies: if self.name not in service.dependencies:
continue continue
if not isinstance(service, FrrService):
continue
if service.ipv4_routing: if service.ipv4_routing:
want_ip4 = True want_ip4 = True
if service.ipv6_routing: if service.ipv6_routing:
want_ip6 = True want_ip6 = True
services.append(service) services.append(service)
interfaces = [] ifaces = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
ip4s = [] ip4s = []
ip6s = [] ip6s = []
for x in ifc.addrlist: for ip4 in iface.ip4s:
addr = x.split("/")[0] ip4s.append(str(ip4.ip))
if netaddr.valid_ipv4(addr): for ip6 in iface.ip6s:
ip4s.append(x) ip6s.append(str(ip6.ip))
else: is_control = getattr(iface, "control", False)
ip6s.append(x) ifaces.append((iface, ip4s, ip6s, is_control))
is_control = getattr(ifc, "control", False)
interfaces.append((ifc, ip4s, ip6s, is_control))
return dict( return dict(
frr_conf=frr_conf, frr_conf=frr_conf,
frr_sbin_search=frr_sbin_search, frr_sbin_search=frr_sbin_search,
frr_bin_search=frr_bin_search, frr_bin_search=frr_bin_search,
frr_state_dir=constants.FRR_STATE_DIR, frr_state_dir=FRR_STATE_DIR,
interfaces=interfaces, ifaces=ifaces,
want_ip4=want_ip4, want_ip4=want_ip4,
want_ip6=want_ip6, want_ip6=want_ip6,
services=services, services=services,
@ -123,22 +119,22 @@ class FRRZebra(ConfigService):
class FrrService(abc.ABC): class FrrService(abc.ABC):
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = [] files: List[str] = []
executables = [] executables: List[str] = []
dependencies = ["FRRzebra"] dependencies: List[str] = ["FRRzebra"]
startup = [] startup: List[str] = []
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
ipv4_routing = False ipv4_routing: bool = False
ipv6_routing = False ipv6_routing: bool = False
@abc.abstractmethod @abc.abstractmethod
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
@ -153,22 +149,17 @@ class FRROspfv2(FrrService, ConfigService):
unified frr.conf file. unified frr.conf file.
""" """
name = "FRROSPFv2" name: str = "FRROSPFv2"
startup = () shutdown: List[str] = ["killall ospfd"]
shutdown = ["killall ospfd"] validate: List[str] = ["pidof ospfd"]
validate = ["pidof ospfd"] ipv4_routing: bool = True
ipv4_routing = True
def frr_config(self) -> str: def frr_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
addresses = [] addresses = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): for ip4 in iface.ip4s:
continue addresses.append(str(ip4.ip))
for a in ifc.addrlist:
addr = a.split("/")[0]
if netaddr.valid_ipv4(addr):
addresses.append(a)
data = dict(router_id=router_id, addresses=addresses) data = dict(router_id=router_id, addresses=addresses)
text = """ text = """
router ospf router ospf
@ -180,8 +171,8 @@ class FRROspfv2(FrrService, ConfigService):
""" """
return self.render_text(text, data) return self.render_text(text, data)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
if has_mtu_mismatch(ifc): if has_mtu_mismatch(iface):
return "ip ospf mtu-ignore" return "ip ospf mtu-ignore"
else: else:
return "" return ""
@ -194,19 +185,17 @@ class FRROspfv3(FrrService, ConfigService):
unified frr.conf file. unified frr.conf file.
""" """
name = "FRROSPFv3" name: str = "FRROSPFv3"
shutdown = ["killall ospf6d"] shutdown: List[str] = ["killall ospf6d"]
validate = ["pidof ospf6d"] validate: List[str] = ["pidof ospf6d"]
ipv4_routing = True ipv4_routing: bool = True
ipv6_routing = True ipv6_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
data = dict(router_id=router_id, ifnames=ifnames) data = dict(router_id=router_id, ifnames=ifnames)
text = """ text = """
router ospf6 router ospf6
@ -218,9 +207,9 @@ class FRROspfv3(FrrService, ConfigService):
""" """
return self.render_text(text, data) return self.render_text(text, data)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
mtu = get_min_mtu(ifc) mtu = get_min_mtu(iface)
if mtu < ifc.mtu: if mtu < iface.mtu:
return f"ipv6 ospf6 ifmtu {mtu}" return f"ipv6 ospf6 ifmtu {mtu}"
else: else:
return "" return ""
@ -233,12 +222,12 @@ class FRRBgp(FrrService, ConfigService):
having the same AS number. having the same AS number.
""" """
name = "FRRBGP" name: str = "FRRBGP"
shutdown = ["killall bgpd"] shutdown: List[str] = ["killall bgpd"]
validate = ["pidof bgpd"] validate: List[str] = ["pidof bgpd"]
custom_needed = True custom_needed: bool = True
ipv4_routing = True ipv4_routing: bool = True
ipv6_routing = True ipv6_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
@ -254,7 +243,7 @@ class FRRBgp(FrrService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
return "" return ""
@ -263,10 +252,10 @@ class FRRRip(FrrService, ConfigService):
The RIP service provides IPv4 routing for wired networks. The RIP service provides IPv4 routing for wired networks.
""" """
name = "FRRRIP" name: str = "FRRRIP"
shutdown = ["killall ripd"] shutdown: List[str] = ["killall ripd"]
validate = ["pidof ripd"] validate: List[str] = ["pidof ripd"]
ipv4_routing = True ipv4_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
text = """ text = """
@ -279,7 +268,7 @@ class FRRRip(FrrService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
return "" return ""
@ -288,10 +277,10 @@ class FRRRipng(FrrService, ConfigService):
The RIP NG service provides IPv6 routing for wired networks. The RIP NG service provides IPv6 routing for wired networks.
""" """
name = "FRRRIPNG" name: str = "FRRRIPNG"
shutdown = ["killall ripngd"] shutdown: List[str] = ["killall ripngd"]
validate = ["pidof ripngd"] validate: List[str] = ["pidof ripngd"]
ipv6_routing = True ipv6_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
text = """ text = """
@ -304,7 +293,7 @@ class FRRRipng(FrrService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
return "" return ""
@ -314,17 +303,15 @@ class FRRBabel(FrrService, ConfigService):
protocol for IPv6 and IPv4 with fast convergence properties. protocol for IPv6 and IPv4 with fast convergence properties.
""" """
name = "FRRBabel" name: str = "FRRBabel"
shutdown = ["killall babeld"] shutdown: List[str] = ["killall babeld"]
validate = ["pidof babeld"] validate: List[str] = ["pidof babeld"]
ipv6_routing = True ipv6_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
text = """ text = """
router babel router babel
% for ifname in ifnames: % for ifname in ifnames:
@ -337,8 +324,8 @@ class FRRBabel(FrrService, ConfigService):
data = dict(ifnames=ifnames) data = dict(ifnames=ifnames)
return self.render_text(text, data) return self.render_text(text, data)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
if isinstance(ifc.net, (WlanNode, EmaneNet)): if isinstance(iface.net, (WlanNode, EmaneNet)):
text = """ text = """
babel wireless babel wireless
no babel split-horizon no babel split-horizon
@ -356,16 +343,16 @@ class FRRpimd(FrrService, ConfigService):
PIM multicast routing based on XORP. PIM multicast routing based on XORP.
""" """
name = "FRRpimd" name: str = "FRRpimd"
shutdown = ["killall pimd"] shutdown: List[str] = ["killall pimd"]
validate = ["pidof pimd"] validate: List[str] = ["pidof pimd"]
ipv4_routing = True ipv4_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
ifname = "eth0" ifname = "eth0"
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
if ifc.name != "lo": if iface.name != "lo":
ifname = ifc.name ifname = iface.name
break break
text = f""" text = f"""
@ -382,7 +369,7 @@ class FRRpimd(FrrService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
text = """ text = """
ip mfea ip mfea
ip igmp ip igmp

View file

@ -20,6 +20,7 @@ nhrpd=yes
eigrpd=yes eigrpd=yes
babeld=yes babeld=yes
sharpd=yes sharpd=yes
staticd=yes
pbrd=yes pbrd=yes
bfdd=yes bfdd=yes
fabricd=yes fabricd=yes

View file

@ -1,5 +1,5 @@
% for ifc, ip4s, ip6s, is_control in interfaces: % for iface, ip4s, ip6s, is_control in ifaces:
interface ${ifc.name} interface ${iface.name}
% if want_ip4: % if want_ip4:
% for addr in ip4s: % for addr in ip4s:
ip address ${addr} ip address ${addr}
@ -12,7 +12,7 @@ interface ${ifc.name}
% endif % endif
% if not is_control: % if not is_control:
% for service in services: % for service in services:
% for line in service.frr_interface_config(ifc).split("\n"): % for line in service.frr_iface_config(iface).split("\n"):
${line} ${line}
% endfor % endfor
% endfor % endfor

View file

@ -98,8 +98,8 @@ confcheck
bootfrr bootfrr
# reset interfaces # reset interfaces
% for ifc, _, _ , _ in interfaces: % for iface, _, _ , _ in ifaces:
ip link set dev ${ifc.name} down ip link set dev ${iface.name} down
sleep 1 sleep 1
ip link set dev ${ifc.name} up ip link set dev ${iface.name} up
% endfor % endfor

View file

@ -1,72 +1,69 @@
from typing import Any, Dict from typing import Any, Dict, List
import netaddr
from core import utils from core import utils
from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
GROUP = "ProtoSvc" GROUP: str = "ProtoSvc"
class MgenSinkService(ConfigService): class MgenSinkService(ConfigService):
name = "MGEN_Sink" name: str = "MGEN_Sink"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["mgensink.sh", "sink.mgen"] files: List[str] = ["mgensink.sh", "sink.mgen"]
executables = ["mgen"] executables: List[str] = ["mgen"]
dependencies = [] dependencies: List[str] = []
startup = ["sh mgensink.sh"] startup: List[str] = ["sh mgensink.sh"]
validate = ["pidof mgen"] validate: List[str] = ["pidof mgen"]
shutdown = ["killall mgen"] shutdown: List[str] = ["killall mgen"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
name = utils.sysctl_devname(ifc.name) name = utils.sysctl_devname(iface.name)
ifnames.append(name) ifnames.append(name)
return dict(ifnames=ifnames) return dict(ifnames=ifnames)
class NrlNhdp(ConfigService): class NrlNhdp(ConfigService):
name = "NHDP" name: str = "NHDP"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["nrlnhdp.sh"] files: List[str] = ["nrlnhdp.sh"]
executables = ["nrlnhdp"] executables: List[str] = ["nrlnhdp"]
dependencies = [] dependencies: List[str] = []
startup = ["sh nrlnhdp.sh"] startup: List[str] = ["sh nrlnhdp.sh"]
validate = ["pidof nrlnhdp"] validate: List[str] = ["pidof nrlnhdp"]
shutdown = ["killall nrlnhdp"] shutdown: List[str] = ["killall nrlnhdp"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
return dict(has_smf=has_smf, ifnames=ifnames) return dict(has_smf=has_smf, ifnames=ifnames)
class NrlSmf(ConfigService): class NrlSmf(ConfigService):
name = "SMF" name: str = "SMF"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["startsmf.sh"] files: List[str] = ["startsmf.sh"]
executables = ["nrlsmf", "killall"] executables: List[str] = ["nrlsmf", "killall"]
dependencies = [] dependencies: List[str] = []
startup = ["sh startsmf.sh"] startup: List[str] = ["sh startsmf.sh"]
validate = ["pidof nrlsmf"] validate: List[str] = ["pidof nrlsmf"]
shutdown = ["killall nrlsmf"] shutdown: List[str] = ["killall nrlsmf"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_arouted = "arouted" in self.node.config_services has_arouted = "arouted" in self.node.config_services
@ -74,17 +71,12 @@ class NrlSmf(ConfigService):
has_olsr = "OLSR" in self.node.config_services has_olsr = "OLSR" in self.node.config_services
ifnames = [] ifnames = []
ip4_prefix = None ip4_prefix = None
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue ip4 = iface.get_ip4()
ifnames.append(ifc.name) if ip4:
if ip4_prefix: ip4_prefix = f"{ip4.ip}/{24}"
continue break
for a in ifc.addrlist:
a = a.split("/")[0]
if netaddr.valid_ipv4(a):
ip4_prefix = f"{a}/{24}"
break
return dict( return dict(
has_arouted=has_arouted, has_arouted=has_arouted,
has_nhdp=has_nhdp, has_nhdp=has_nhdp,
@ -95,118 +87,107 @@ class NrlSmf(ConfigService):
class NrlOlsr(ConfigService): class NrlOlsr(ConfigService):
name = "OLSR" name: str = "OLSR"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["nrlolsrd.sh"] files: List[str] = ["nrlolsrd.sh"]
executables = ["nrlolsrd"] executables: List[str] = ["nrlolsrd"]
dependencies = [] dependencies: List[str] = []
startup = ["sh nrlolsrd.sh"] startup: List[str] = ["sh nrlolsrd.sh"]
validate = ["pidof nrlolsrd"] validate: List[str] = ["pidof nrlolsrd"]
shutdown = ["killall nrlolsrd"] shutdown: List[str] = ["killall nrlolsrd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
has_zebra = "zebra" in self.node.config_services has_zebra = "zebra" in self.node.config_services
ifname = None ifname = None
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifname = iface.name
continue
ifname = ifc.name
break break
return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname) return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname)
class NrlOlsrv2(ConfigService): class NrlOlsrv2(ConfigService):
name = "OLSRv2" name: str = "OLSRv2"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["nrlolsrv2.sh"] files: List[str] = ["nrlolsrv2.sh"]
executables = ["nrlolsrv2"] executables: List[str] = ["nrlolsrv2"]
dependencies = [] dependencies: List[str] = []
startup = ["sh nrlolsrv2.sh"] startup: List[str] = ["sh nrlolsrv2.sh"]
validate = ["pidof nrlolsrv2"] validate: List[str] = ["pidof nrlolsrv2"]
shutdown = ["killall nrlolsrv2"] shutdown: List[str] = ["killall nrlolsrv2"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
return dict(has_smf=has_smf, ifnames=ifnames) return dict(has_smf=has_smf, ifnames=ifnames)
class OlsrOrg(ConfigService): class OlsrOrg(ConfigService):
name = "OLSRORG" name: str = "OLSRORG"
group = GROUP group: str = GROUP
directories = ["/etc/olsrd"] directories: List[str] = ["/etc/olsrd"]
files = ["olsrd.sh", "/etc/olsrd/olsrd.conf"] files: List[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
executables = ["olsrd"] executables: List[str] = ["olsrd"]
dependencies = [] dependencies: List[str] = []
startup = ["sh olsrd.sh"] startup: List[str] = ["sh olsrd.sh"]
validate = ["pidof olsrd"] validate: List[str] = ["pidof olsrd"]
shutdown = ["killall olsrd"] shutdown: List[str] = ["killall olsrd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
return dict(has_smf=has_smf, ifnames=ifnames) return dict(has_smf=has_smf, ifnames=ifnames)
class MgenActor(ConfigService): class MgenActor(ConfigService):
name = "MgenActor" name: str = "MgenActor"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["start_mgen_actor.sh"] files: List[str] = ["start_mgen_actor.sh"]
executables = ["mgen"] executables: List[str] = ["mgen"]
dependencies = [] dependencies: List[str] = []
startup = ["sh start_mgen_actor.sh"] startup: List[str] = ["sh start_mgen_actor.sh"]
validate = ["pidof mgen"] validate: List[str] = ["pidof mgen"]
shutdown = ["killall mgen"] shutdown: List[str] = ["killall mgen"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
class Arouted(ConfigService): class Arouted(ConfigService):
name = "arouted" name: str = "arouted"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["startarouted.sh"] files: List[str] = ["startarouted.sh"]
executables = ["arouted"] executables: List[str] = ["arouted"]
dependencies = [] dependencies: List[str] = []
startup = ["sh startarouted.sh"] startup: List[str] = ["sh startarouted.sh"]
validate = ["pidof arouted"] validate: List[str] = ["pidof arouted"]
shutdown = ["pkill arouted"] shutdown: List[str] = ["pkill arouted"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
ip4_prefix = None ip4_prefix = None
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ip4 = iface.get_ip4()
continue if ip4:
if ip4_prefix: ip4_prefix = f"{ip4.ip}/{24}"
continue break
for a in ifc.addrlist:
a = a.split("/")[0]
if netaddr.valid_ipv4(a):
ip4_prefix = f"{a}/{24}"
break
return dict(ip4_prefix=ip4_prefix) return dict(ip4_prefix=ip4_prefix)

View file

@ -1,7 +1,7 @@
<% <%
interfaces = "-i " + " -i ".join(ifnames) ifaces = "-i " + " -i ".join(ifnames)
smf = "" smf = ""
if has_smf: if has_smf:
smf = "-flooding ecds -smfClient %s_smf" % node.name smf = "-flooding ecds -smfClient %s_smf" % node.name
%> %>
nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${interfaces} nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${ifaces}

View file

@ -1,7 +1,7 @@
<% <%
interfaces = "-i " + " -i ".join(ifnames) ifaces = "-i " + " -i ".join(ifnames)
smf = "" smf = ""
if has_smf: if has_smf:
smf = "-flooding ecds -smfClient %s_smf" % node.name smf = "-flooding ecds -smfClient %s_smf" % node.name
%> %>
nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${interfaces} nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${ifaces}

View file

@ -1,4 +1,4 @@
<% <%
interfaces = "-i " + " -i ".join(ifnames) ifaces = "-i " + " -i ".join(ifnames)
%> %>
olsrd ${interfaces} olsrd ${ifaces}

View file

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

View file

@ -1,46 +1,45 @@
import abc import abc
import logging import logging
from typing import Any, Dict from typing import Any, Dict, List
import netaddr from core.config import Configuration
from core import constants
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
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
GROUP = "Quagga" GROUP: str = "Quagga"
QUAGGA_STATE_DIR: str = "/var/run/quagga"
def has_mtu_mismatch(ifc: 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
mtu-ignore command. This is needed when e.g. a node is linked via a mtu-ignore command. This is needed when e.g. a node is linked via a
GreTap device. GreTap device.
""" """
if ifc.mtu != 1500: if iface.mtu != 1500:
return True return True
if not ifc.net: if not iface.net:
return False return False
for i in ifc.net.netifs(): for iface in iface.net.get_ifaces():
if i.mtu != ifc.mtu: if iface.mtu != iface.mtu:
return True return True
return False return False
def get_min_mtu(ifc): def get_min_mtu(iface: CoreInterface):
""" """
Helper to discover the minimum MTU of interfaces linked with the Helper to discover the minimum MTU of interfaces linked with the
given interface. given interface.
""" """
mtu = ifc.mtu mtu = iface.mtu
if not ifc.net: if not iface.net:
return mtu return mtu
for i in ifc.net.netifs(): for iface in iface.net.get_ifaces():
if i.mtu < mtu: if iface.mtu < mtu:
mtu = i.mtu mtu = iface.mtu
return mtu return mtu
@ -48,33 +47,30 @@ def get_router_id(node: CoreNodeBase) -> str:
""" """
Helper to return the first IPv4 address of a node as its router ID. Helper to return the first IPv4 address of a node as its router ID.
""" """
for ifc in node.netifs(): for iface in node.get_ifaces(control=False):
if getattr(ifc, "control", False): ip4 = iface.get_ip4()
continue if ip4:
for a in ifc.addrlist: return str(ip4.ip)
a = a.split("/")[0]
if netaddr.valid_ipv4(a):
return a
return "0.0.0.0" return "0.0.0.0"
class Zebra(ConfigService): class Zebra(ConfigService):
name = "zebra" name: str = "zebra"
group = GROUP group: str = GROUP
directories = ["/usr/local/etc/quagga", "/var/run/quagga"] directories: List[str] = ["/usr/local/etc/quagga", "/var/run/quagga"]
files = [ files: List[str] = [
"/usr/local/etc/quagga/Quagga.conf", "/usr/local/etc/quagga/Quagga.conf",
"quaggaboot.sh", "quaggaboot.sh",
"/usr/local/etc/quagga/vtysh.conf", "/usr/local/etc/quagga/vtysh.conf",
] ]
executables = ["zebra"] executables: List[str] = ["zebra"]
dependencies = [] dependencies: List[str] = []
startup = ["sh quaggaboot.sh zebra"] startup: List[str] = ["sh quaggaboot.sh zebra"]
validate = ["pidof zebra"] validate: List[str] = ["pidof zebra"]
shutdown = ["killall zebra"] shutdown: List[str] = ["killall zebra"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
quagga_bin_search = self.node.session.options.get_config( quagga_bin_search = self.node.session.options.get_config(
@ -83,7 +79,7 @@ class Zebra(ConfigService):
quagga_sbin_search = self.node.session.options.get_config( quagga_sbin_search = self.node.session.options.get_config(
"quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga" "quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga"
).strip('"') ).strip('"')
quagga_state_dir = constants.QUAGGA_STATE_DIR quagga_state_dir = QUAGGA_STATE_DIR
quagga_conf = self.files[0] quagga_conf = self.files[0]
services = [] services = []
@ -92,31 +88,31 @@ class Zebra(ConfigService):
for service in self.node.config_services.values(): for service in self.node.config_services.values():
if self.name not in service.dependencies: if self.name not in service.dependencies:
continue continue
if not isinstance(service, QuaggaService):
continue
if service.ipv4_routing: if service.ipv4_routing:
want_ip4 = True want_ip4 = True
if service.ipv6_routing: if service.ipv6_routing:
want_ip6 = True want_ip6 = True
services.append(service) services.append(service)
interfaces = [] ifaces = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
ip4s = [] ip4s = []
ip6s = [] ip6s = []
for x in ifc.addrlist: for ip4 in iface.ip4s:
addr = x.split("/")[0] ip4s.append(str(ip4.ip))
if netaddr.valid_ipv4(addr): for ip6 in iface.ip6s:
ip4s.append(x) ip6s.append(str(ip6.ip))
else: is_control = getattr(iface, "control", False)
ip6s.append(x) ifaces.append((iface, ip4s, ip6s, is_control))
is_control = getattr(ifc, "control", False)
interfaces.append((ifc, ip4s, ip6s, is_control))
return dict( return dict(
quagga_bin_search=quagga_bin_search, quagga_bin_search=quagga_bin_search,
quagga_sbin_search=quagga_sbin_search, quagga_sbin_search=quagga_sbin_search,
quagga_state_dir=quagga_state_dir, quagga_state_dir=quagga_state_dir,
quagga_conf=quagga_conf, quagga_conf=quagga_conf,
interfaces=interfaces, ifaces=ifaces,
want_ip4=want_ip4, want_ip4=want_ip4,
want_ip6=want_ip6, want_ip6=want_ip6,
services=services, services=services,
@ -124,22 +120,22 @@ class Zebra(ConfigService):
class QuaggaService(abc.ABC): class QuaggaService(abc.ABC):
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = [] files: List[str] = []
executables = [] executables: List[str] = []
dependencies = ["zebra"] dependencies: List[str] = ["zebra"]
startup = [] startup: List[str] = []
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
ipv4_routing = False ipv4_routing: bool = False
ipv6_routing = False ipv6_routing: bool = False
@abc.abstractmethod @abc.abstractmethod
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
@ -154,13 +150,13 @@ class Ospfv2(QuaggaService, ConfigService):
unified Quagga.conf file. unified Quagga.conf file.
""" """
name = "OSPFv2" name: str = "OSPFv2"
validate = ["pidof ospfd"] validate: List[str] = ["pidof ospfd"]
shutdown = ["killall ospfd"] shutdown: List[str] = ["killall ospfd"]
ipv4_routing = True ipv4_routing: bool = True
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
if has_mtu_mismatch(ifc): if has_mtu_mismatch(iface):
return "ip ospf mtu-ignore" return "ip ospf mtu-ignore"
else: else:
return "" return ""
@ -168,13 +164,9 @@ class Ospfv2(QuaggaService, ConfigService):
def quagga_config(self) -> str: def quagga_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
addresses = [] addresses = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): for ip4 in iface.ip4s:
continue addresses.append(str(ip4.ip))
for a in ifc.addrlist:
addr = a.split("/")[0]
if netaddr.valid_ipv4(addr):
addresses.append(a)
data = dict(router_id=router_id, addresses=addresses) data = dict(router_id=router_id, addresses=addresses)
text = """ text = """
router ospf router ospf
@ -194,15 +186,15 @@ class Ospfv3(QuaggaService, ConfigService):
unified Quagga.conf file. unified Quagga.conf file.
""" """
name = "OSPFv3" name: str = "OSPFv3"
shutdown = ("killall ospf6d",) shutdown: List[str] = ["killall ospf6d"]
validate = ("pidof ospf6d",) validate: List[str] = ["pidof ospf6d"]
ipv4_routing = True ipv4_routing: bool = True
ipv6_routing = True ipv6_routing: bool = True
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
mtu = get_min_mtu(ifc) mtu = get_min_mtu(iface)
if mtu < ifc.mtu: if mtu < iface.mtu:
return f"ipv6 ospf6 ifmtu {mtu}" return f"ipv6 ospf6 ifmtu {mtu}"
else: else:
return "" return ""
@ -210,10 +202,8 @@ class Ospfv3(QuaggaService, ConfigService):
def quagga_config(self) -> str: def quagga_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
data = dict(router_id=router_id, ifnames=ifnames) data = dict(router_id=router_id, ifnames=ifnames)
text = """ text = """
router ospf6 router ospf6
@ -235,17 +225,17 @@ class Ospfv3mdr(Ospfv3):
unified Quagga.conf file. unified Quagga.conf file.
""" """
name = "OSPFv3MDR" name: str = "OSPFv3MDR"
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
is_wireless = isinstance(ifc.net, (WlanNode, EmaneNet)) is_wireless = isinstance(iface.net, (WlanNode, EmaneNet))
logging.info("MDR wireless: %s", is_wireless) logging.info("MDR wireless: %s", is_wireless)
return dict() return dict()
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
config = super().quagga_interface_config(ifc) config = super().quagga_iface_config(iface)
if isinstance(ifc.net, (WlanNode, EmaneNet)): if isinstance(iface.net, (WlanNode, EmaneNet)):
config = self.clean_text( config = self.clean_text(
f""" f"""
{config} {config}
@ -268,16 +258,16 @@ class Bgp(QuaggaService, ConfigService):
having the same AS number. having the same AS number.
""" """
name = "BGP" name: str = "BGP"
shutdown = ["killall bgpd"] shutdown: List[str] = ["killall bgpd"]
validate = ["pidof bgpd"] validate: List[str] = ["pidof bgpd"]
ipv4_routing = True ipv4_routing: bool = True
ipv6_routing = True ipv6_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
return "" return ""
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
text = f""" text = f"""
! BGP configuration ! BGP configuration
@ -297,10 +287,10 @@ class Rip(QuaggaService, ConfigService):
The RIP service provides IPv4 routing for wired networks. The RIP service provides IPv4 routing for wired networks.
""" """
name = "RIP" name: str = "RIP"
shutdown = ["killall ripd"] shutdown: List[str] = ["killall ripd"]
validate = ["pidof ripd"] validate: List[str] = ["pidof ripd"]
ipv4_routing = True ipv4_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
text = """ text = """
@ -313,7 +303,7 @@ class Rip(QuaggaService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
return "" return ""
@ -322,10 +312,10 @@ class Ripng(QuaggaService, ConfigService):
The RIP NG service provides IPv6 routing for wired networks. The RIP NG service provides IPv6 routing for wired networks.
""" """
name = "RIPNG" name: str = "RIPNG"
shutdown = ["killall ripngd"] shutdown: List[str] = ["killall ripngd"]
validate = ["pidof ripngd"] validate: List[str] = ["pidof ripngd"]
ipv6_routing = True ipv6_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
text = """ text = """
@ -338,7 +328,7 @@ class Ripng(QuaggaService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
return "" return ""
@ -348,17 +338,15 @@ class Babel(QuaggaService, ConfigService):
protocol for IPv6 and IPv4 with fast convergence properties. protocol for IPv6 and IPv4 with fast convergence properties.
""" """
name = "Babel" name: str = "Babel"
shutdown = ["killall babeld"] shutdown: List[str] = ["killall babeld"]
validate = ["pidof babeld"] validate: List[str] = ["pidof babeld"]
ipv6_routing = True ipv6_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
text = """ text = """
router babel router babel
% for ifname in ifnames: % for ifname in ifnames:
@ -371,8 +359,8 @@ class Babel(QuaggaService, ConfigService):
data = dict(ifnames=ifnames) data = dict(ifnames=ifnames)
return self.render_text(text, data) return self.render_text(text, data)
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
if isinstance(ifc.net, (WlanNode, EmaneNet)): if isinstance(iface.net, (WlanNode, EmaneNet)):
text = """ text = """
babel wireless babel wireless
no babel split-horizon no babel split-horizon
@ -390,16 +378,16 @@ class Xpimd(QuaggaService, ConfigService):
PIM multicast routing based on XORP. PIM multicast routing based on XORP.
""" """
name = "Xpimd" name: str = "Xpimd"
shutdown = ["killall xpimd"] shutdown: List[str] = ["killall xpimd"]
validate = ["pidof xpimd"] validate: List[str] = ["pidof xpimd"]
ipv4_routing = True ipv4_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
ifname = "eth0" ifname = "eth0"
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
if ifc.name != "lo": if iface.name != "lo":
ifname = ifc.name ifname = iface.name
break break
text = f""" text = f"""
@ -416,7 +404,7 @@ class Xpimd(QuaggaService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
text = """ text = """
ip mfea ip mfea
ip pim ip pim

View file

@ -1,5 +1,5 @@
% for ifc, ip4s, ip6s, is_control in interfaces: % for iface, ip4s, ip6s, is_control in ifaces:
interface ${ifc.name} interface ${iface.name}
% if want_ip4: % if want_ip4:
% for addr in ip4s: % for addr in ip4s:
ip address ${addr} ip address ${addr}
@ -12,7 +12,7 @@ interface ${ifc.name}
% endif % endif
% if not is_control: % if not is_control:
% for service in services: % for service in services:
% for line in service.quagga_interface_config(ifc).split("\n"): % for line in service.quagga_iface_config(iface).split("\n"):
${line} ${line}
% endfor % endfor
% endfor % endfor

View file

@ -0,0 +1,135 @@
from typing import Any, Dict, List
from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode
from core.emulator.enumerations import ConfigDataTypes
GROUP_NAME: str = "Security"
class VpnClient(ConfigService):
name: str = "VPNClient"
group: str = GROUP_NAME
directories: List[str] = []
files: List[str] = ["vpnclient.sh"]
executables: List[str] = ["openvpn", "ip", "killall"]
dependencies: List[str] = []
startup: List[str] = ["sh vpnclient.sh"]
validate: List[str] = ["pidof openvpn"]
shutdown: List[str] = ["killall openvpn"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [
Configuration(
_id="keydir",
_type=ConfigDataTypes.STRING,
label="Key Dir",
default="/etc/core/keys",
),
Configuration(
_id="keyname",
_type=ConfigDataTypes.STRING,
label="Key Name",
default="client1",
),
Configuration(
_id="server",
_type=ConfigDataTypes.STRING,
label="Server",
default="10.0.2.10",
),
]
modes: Dict[str, Dict[str, str]] = {}
class VpnServer(ConfigService):
name: str = "VPNServer"
group: str = GROUP_NAME
directories: List[str] = []
files: List[str] = ["vpnserver.sh"]
executables: List[str] = ["openvpn", "ip", "killall"]
dependencies: List[str] = []
startup: List[str] = ["sh vpnserver.sh"]
validate: List[str] = ["pidof openvpn"]
shutdown: List[str] = ["killall openvpn"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [
Configuration(
_id="keydir",
_type=ConfigDataTypes.STRING,
label="Key Dir",
default="/etc/core/keys",
),
Configuration(
_id="keyname",
_type=ConfigDataTypes.STRING,
label="Key Name",
default="server",
),
Configuration(
_id="subnet",
_type=ConfigDataTypes.STRING,
label="Subnet",
default="10.0.200.0",
),
]
modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]:
address = None
for iface in self.node.get_ifaces(control=False):
ip4 = iface.get_ip4()
if ip4:
address = str(ip4.ip)
break
return dict(address=address)
class IPsec(ConfigService):
name: str = "IPsec"
group: str = GROUP_NAME
directories: List[str] = []
files: List[str] = ["ipsec.sh"]
executables: List[str] = ["racoon", "ip", "setkey", "killall"]
dependencies: List[str] = []
startup: List[str] = ["sh ipsec.sh"]
validate: List[str] = ["pidof racoon"]
shutdown: List[str] = ["killall racoon"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
class Firewall(ConfigService):
name: str = "Firewall"
group: str = GROUP_NAME
directories: List[str] = []
files: List[str] = ["firewall.sh"]
executables: List[str] = ["iptables"]
dependencies: List[str] = []
startup: List[str] = ["sh firewall.sh"]
validate: List[str] = []
shutdown: List[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
class Nat(ConfigService):
name: str = "NAT"
group: str = GROUP_NAME
directories: List[str] = []
files: List[str] = ["nat.sh"]
executables: List[str] = ["iptables"]
dependencies: List[str] = []
startup: List[str] = ["sh nat.sh"]
validate: List[str] = []
shutdown: List[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]:
ifnames = []
for iface in self.node.get_ifaces(control=False):
ifnames.append(iface.name)
return dict(ifnames=ifnames)

View file

@ -1,141 +0,0 @@
from typing import Any, Dict
import netaddr
from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode
from core.emulator.enumerations import ConfigDataTypes
GROUP_NAME = "Security"
class VpnClient(ConfigService):
name = "VPNClient"
group = GROUP_NAME
directories = []
files = ["vpnclient.sh"]
executables = ["openvpn", "ip", "killall"]
dependencies = []
startup = ["sh vpnclient.sh"]
validate = ["pidof openvpn"]
shutdown = ["killall openvpn"]
validation_mode = ConfigServiceMode.BLOCKING
default_configs = [
Configuration(
_id="keydir",
_type=ConfigDataTypes.STRING,
label="Key Dir",
default="/etc/core/keys",
),
Configuration(
_id="keyname",
_type=ConfigDataTypes.STRING,
label="Key Name",
default="client1",
),
Configuration(
_id="server",
_type=ConfigDataTypes.STRING,
label="Server",
default="10.0.2.10",
),
]
modes = {}
class VpnServer(ConfigService):
name = "VPNServer"
group = GROUP_NAME
directories = []
files = ["vpnserver.sh"]
executables = ["openvpn", "ip", "killall"]
dependencies = []
startup = ["sh vpnserver.sh"]
validate = ["pidof openvpn"]
shutdown = ["killall openvpn"]
validation_mode = ConfigServiceMode.BLOCKING
default_configs = [
Configuration(
_id="keydir",
_type=ConfigDataTypes.STRING,
label="Key Dir",
default="/etc/core/keys",
),
Configuration(
_id="keyname",
_type=ConfigDataTypes.STRING,
label="Key Name",
default="server",
),
Configuration(
_id="subnet",
_type=ConfigDataTypes.STRING,
label="Subnet",
default="10.0.200.0",
),
]
modes = {}
def data(self) -> Dict[str, Any]:
address = None
for ifc in self.node.netifs():
if getattr(ifc, "control", False):
continue
for x in ifc.addrlist:
addr = x.split("/")[0]
if netaddr.valid_ipv4(addr):
address = addr
return dict(address=address)
class IPsec(ConfigService):
name = "IPsec"
group = GROUP_NAME
directories = []
files = ["ipsec.sh"]
executables = ["racoon", "ip", "setkey", "killall"]
dependencies = []
startup = ["sh ipsec.sh"]
validate = ["pidof racoon"]
shutdown = ["killall racoon"]
validation_mode = ConfigServiceMode.BLOCKING
default_configs = []
modes = {}
class Firewall(ConfigService):
name = "Firewall"
group = GROUP_NAME
directories = []
files = ["firewall.sh"]
executables = ["iptables"]
dependencies = []
startup = ["sh firewall.sh"]
validate = []
shutdown = []
validation_mode = ConfigServiceMode.BLOCKING
default_configs = []
modes = {}
class Nat(ConfigService):
name = "NAT"
group = GROUP_NAME
directories = []
files = ["nat.sh"]
executables = ["iptables"]
dependencies = []
startup = ["sh nat.sh"]
validate = []
shutdown = []
validation_mode = ConfigServiceMode.BLOCKING
default_configs = []
modes = {}
def data(self) -> Dict[str, Any]:
ifnames = []
for ifc in self.node.netifs():
if getattr(ifc, "control", False):
continue
ifnames.append(ifc.name)
return dict(ifnames=ifnames)

View file

@ -1,20 +1,22 @@
from typing import 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.emulator.enumerations import ConfigDataTypes from core.emulator.enumerations import ConfigDataTypes
class SimpleService(ConfigService): class SimpleService(ConfigService):
name = "Simple" name: str = "Simple"
group = "SimpleGroup" group: str = "SimpleGroup"
directories = ["/etc/quagga", "/usr/local/lib"] directories: List[str] = ["/etc/quagga", "/usr/local/lib"]
files = ["test1.sh", "test2.sh"] files: List[str] = ["test1.sh", "test2.sh"]
executables = [] executables: List[str] = []
dependencies = [] dependencies: List[str] = []
startup = [] startup: List[str] = []
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [ default_configs: List[Configuration] = [
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"), Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"), Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
Configuration( Configuration(
@ -24,7 +26,7 @@ class SimpleService(ConfigService):
options=["value1", "value2", "value3"], options=["value1", "value2", "value3"],
), ),
] ]
modes = { modes: Dict[str, Dict[str, str]] = {
"mode1": {"value1": "value1", "value2": "0", "value3": "value2"}, "mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
"mode2": {"value1": "value2", "value2": "1", "value3": "value3"}, "mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
"mode3": {"value1": "value3", "value2": "0", "value3": "value1"}, "mode3": {"value1": "value3", "value2": "0", "value3": "value1"},

View file

@ -1,35 +1,36 @@
from typing import Any, Dict from typing import Any, Dict, List
import netaddr import netaddr
from core import utils from core import utils
from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
GROUP_NAME = "Utility" GROUP_NAME = "Utility"
class DefaultRouteService(ConfigService): class DefaultRouteService(ConfigService):
name = "DefaultRoute" name: str = "DefaultRoute"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["defaultroute.sh"] files: List[str] = ["defaultroute.sh"]
executables = ["ip"] executables: List[str] = ["ip"]
dependencies = [] dependencies: List[str] = []
startup = ["sh defaultroute.sh"] startup: List[str] = ["sh defaultroute.sh"]
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
# only add default routes for linked routing nodes # only add default routes for linked routing nodes
routes = [] routes = []
netifs = self.node.netifs(sort=True) ifaces = self.node.get_ifaces()
if netifs: if ifaces:
netif = netifs[0] iface = ifaces[0]
for x in netif.addrlist: for ip in iface.ips():
net = netaddr.IPNetwork(x).cidr net = ip.cidr
if net.size > 1: if net.size > 1:
router = net[1] router = net[1]
routes.append(str(router)) routes.append(str(router))
@ -37,95 +38,90 @@ class DefaultRouteService(ConfigService):
class DefaultMulticastRouteService(ConfigService): class DefaultMulticastRouteService(ConfigService):
name = "DefaultMulticastRoute" name: str = "DefaultMulticastRoute"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["defaultmroute.sh"] files: List[str] = ["defaultmroute.sh"]
executables = [] executables: List[str] = []
dependencies = [] dependencies: List[str] = []
startup = ["sh defaultmroute.sh"] startup: List[str] = ["sh defaultmroute.sh"]
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
ifname = None ifname = None
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifname = iface.name
continue
ifname = ifc.name
break break
return dict(ifname=ifname) return dict(ifname=ifname)
class StaticRouteService(ConfigService): class StaticRouteService(ConfigService):
name = "StaticRoute" name: str = "StaticRoute"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["staticroute.sh"] files: List[str] = ["staticroute.sh"]
executables = [] executables: List[str] = []
dependencies = [] dependencies: List[str] = []
startup = ["sh staticroute.sh"] startup: List[str] = ["sh staticroute.sh"]
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
routes = [] routes = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): for ip in iface.ips():
continue address = str(ip.ip)
for x in ifc.addrlist: if netaddr.valid_ipv6(address):
addr = x.split("/")[0]
if netaddr.valid_ipv6(addr):
dst = "3ffe:4::/64" dst = "3ffe:4::/64"
else: else:
dst = "10.9.8.0/24" dst = "10.9.8.0/24"
net = netaddr.IPNetwork(x) if ip[-2] != ip[1]:
if net[-2] != net[1]: routes.append((dst, ip[1]))
routes.append((dst, net[1]))
return dict(routes=routes) return dict(routes=routes)
class IpForwardService(ConfigService): class IpForwardService(ConfigService):
name = "IPForward" name: str = "IPForward"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["ipforward.sh"] files: List[str] = ["ipforward.sh"]
executables = ["sysctl"] executables: List[str] = ["sysctl"]
dependencies = [] dependencies: List[str] = []
startup = ["sh ipforward.sh"] startup: List[str] = ["sh ipforward.sh"]
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
devnames = [] devnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
devname = utils.sysctl_devname(ifc.name) devname = utils.sysctl_devname(iface.name)
devnames.append(devname) devnames.append(devname)
return dict(devnames=devnames) return dict(devnames=devnames)
class SshService(ConfigService): class SshService(ConfigService):
name = "SSH" name: str = "SSH"
group = GROUP_NAME group: str = GROUP_NAME
directories = ["/etc/ssh", "/var/run/sshd"] directories: List[str] = ["/etc/ssh", "/var/run/sshd"]
files = ["startsshd.sh", "/etc/ssh/sshd_config"] files: List[str] = ["startsshd.sh", "/etc/ssh/sshd_config"]
executables = ["sshd"] executables: List[str] = ["sshd"]
dependencies = [] dependencies: List[str] = []
startup = ["sh startsshd.sh"] startup: List[str] = ["sh startsshd.sh"]
validate = [] validate: List[str] = []
shutdown = ["killall sshd"] shutdown: List[str] = ["killall sshd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
return dict( return dict(
@ -136,146 +132,135 @@ class SshService(ConfigService):
class DhcpService(ConfigService): class DhcpService(ConfigService):
name = "DHCP" name: str = "DHCP"
group = GROUP_NAME group: str = GROUP_NAME
directories = ["/etc/dhcp", "/var/lib/dhcp"] directories: List[str] = ["/etc/dhcp", "/var/lib/dhcp"]
files = ["/etc/dhcp/dhcpd.conf"] files: List[str] = ["/etc/dhcp/dhcpd.conf"]
executables = ["dhcpd"] executables: List[str] = ["dhcpd"]
dependencies = [] dependencies: List[str] = []
startup = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"] startup: List[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
validate = ["pidof dhcpd"] validate: List[str] = ["pidof dhcpd"]
shutdown = ["killall dhcpd"] shutdown: List[str] = ["killall dhcpd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
subnets = [] subnets = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): for ip4 in iface.ip4s:
continue # divide the address space in half
for x in ifc.addrlist: index = (ip4.size - 2) / 2
addr = x.split("/")[0] rangelow = ip4[index]
if netaddr.valid_ipv4(addr): rangehigh = ip4[-2]
net = netaddr.IPNetwork(x) subnets.append((ip4.ip, ip4.netmask, rangelow, rangehigh, str(ip4.ip)))
# divide the address space in half
index = (net.size - 2) / 2
rangelow = net[index]
rangehigh = net[-2]
subnets.append((net.ip, net.netmask, rangelow, rangehigh, addr))
return dict(subnets=subnets) return dict(subnets=subnets)
class DhcpClientService(ConfigService): class DhcpClientService(ConfigService):
name = "DHCPClient" name: str = "DHCPClient"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["startdhcpclient.sh"] files: List[str] = ["startdhcpclient.sh"]
executables = ["dhclient"] executables: List[str] = ["dhclient"]
dependencies = [] dependencies: List[str] = []
startup = ["sh startdhcpclient.sh"] startup: List[str] = ["sh startdhcpclient.sh"]
validate = ["pidof dhclient"] validate: List[str] = ["pidof dhclient"]
shutdown = ["killall dhclient"] shutdown: List[str] = ["killall dhclient"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
return dict(ifnames=ifnames) return dict(ifnames=ifnames)
class FtpService(ConfigService): class FtpService(ConfigService):
name = "FTP" name: str = "FTP"
group = GROUP_NAME group: str = GROUP_NAME
directories = ["/var/run/vsftpd/empty", "/var/ftp"] directories: List[str] = ["/var/run/vsftpd/empty", "/var/ftp"]
files = ["vsftpd.conf"] files: List[str] = ["vsftpd.conf"]
executables = ["vsftpd"] executables: List[str] = ["vsftpd"]
dependencies = [] dependencies: List[str] = []
startup = ["vsftpd ./vsftpd.conf"] startup: List[str] = ["vsftpd ./vsftpd.conf"]
validate = ["pidof vsftpd"] validate: List[str] = ["pidof vsftpd"]
shutdown = ["killall vsftpd"] shutdown: List[str] = ["killall vsftpd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
class PcapService(ConfigService): class PcapService(ConfigService):
name = "pcap" name: str = "pcap"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["pcap.sh"] files: List[str] = ["pcap.sh"]
executables = ["tcpdump"] executables: List[str] = ["tcpdump"]
dependencies = [] dependencies: List[str] = []
startup = ["sh pcap.sh start"] startup: List[str] = ["sh pcap.sh start"]
validate = ["pidof tcpdump"] validate: List[str] = ["pidof tcpdump"]
shutdown = ["sh pcap.sh stop"] shutdown: List[str] = ["sh pcap.sh stop"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
return dict() return dict()
class RadvdService(ConfigService): class RadvdService(ConfigService):
name = "radvd" name: str = "radvd"
group = GROUP_NAME group: str = GROUP_NAME
directories = ["/etc/radvd"] directories: List[str] = ["/etc/radvd"]
files = ["/etc/radvd/radvd.conf"] files: List[str] = ["/etc/radvd/radvd.conf"]
executables = ["radvd"] executables: List[str] = ["radvd"]
dependencies = [] dependencies: List[str] = []
startup = ["radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"] startup: List[str] = [
validate = ["pidof radvd"] "radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"
shutdown = ["pkill radvd"] ]
validation_mode = ConfigServiceMode.BLOCKING validate: List[str] = ["pidof radvd"]
default_configs = [] shutdown: List[str] = ["pkill radvd"]
modes = {} validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
interfaces = [] ifaces = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False):
continue
prefixes = [] prefixes = []
for x in ifc.addrlist: for ip6 in iface.ip6s:
addr = x.split("/")[0] prefixes.append(str(ip6))
if netaddr.valid_ipv6(addr):
prefixes.append(x)
if not prefixes: if not prefixes:
continue continue
interfaces.append((ifc.name, prefixes)) ifaces.append((iface.name, prefixes))
return dict(interfaces=interfaces) return dict(ifaces=ifaces)
class AtdService(ConfigService): class AtdService(ConfigService):
name = "atd" name: str = "atd"
group = GROUP_NAME group: str = GROUP_NAME
directories = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"] directories: List[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
files = ["startatd.sh"] files: List[str] = ["startatd.sh"]
executables = ["atd"] executables: List[str] = ["atd"]
dependencies = [] dependencies: List[str] = []
startup = ["sh startatd.sh"] startup: List[str] = ["sh startatd.sh"]
validate = ["pidof atd"] validate: List[str] = ["pidof atd"]
shutdown = ["pkill atd"] shutdown: List[str] = ["pkill atd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
class HttpService(ConfigService): class HttpService(ConfigService):
name = "HTTP" name: str = "HTTP"
group = GROUP_NAME group: str = GROUP_NAME
directories = [ directories: List[str] = [
"/etc/apache2", "/etc/apache2",
"/var/run/apache2", "/var/run/apache2",
"/var/log/apache2", "/var/log/apache2",
@ -283,20 +268,22 @@ class HttpService(ConfigService):
"/var/lock/apache2", "/var/lock/apache2",
"/var/www", "/var/www",
] ]
files = ["/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html"] files: List[str] = [
executables = ["apache2ctl"] "/etc/apache2/apache2.conf",
dependencies = [] "/etc/apache2/envvars",
startup = ["chown www-data /var/lock/apache2", "apache2ctl start"] "/var/www/index.html",
validate = ["pidof apache2"] ]
shutdown = ["apache2ctl stop"] executables: List[str] = ["apache2ctl"]
validation_mode = ConfigServiceMode.BLOCKING dependencies: List[str] = []
default_configs = [] startup: List[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"]
modes = {} validate: List[str] = ["pidof apache2"]
shutdown: List[str] = ["apache2ctl stop"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
interfaces = [] ifaces = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifaces.append(iface)
continue return dict(ifaces=ifaces)
interfaces.append(ifc)
return dict(interfaces=interfaces)

View file

@ -5,8 +5,8 @@
<p>This is the default web page for this server.</p> <p>This is the default web page for this server.</p>
<p>The web server software is running but no content has been added, yet.</p> <p>The web server software is running but no content has been added, yet.</p>
<ul> <ul>
% for ifc in interfaces: % for iface in ifaces:
<li>${ifc.name} - ${ifc.addrlist}</li> <li>${iface.name} - ${iface.addrlist}</li>
% endfor % endfor
</ul> </ul>
</body> </body>

View file

@ -1,19 +1,3 @@
from core.utils import which
COREDPY_VERSION = "@PACKAGE_VERSION@" COREDPY_VERSION = "@PACKAGE_VERSION@"
CORE_CONF_DIR = "@CORE_CONF_DIR@" CORE_CONF_DIR = "@CORE_CONF_DIR@"
CORE_DATA_DIR = "@CORE_DATA_DIR@" CORE_DATA_DIR = "@CORE_DATA_DIR@"
QUAGGA_STATE_DIR = "@CORE_STATE_DIR@/run/quagga"
FRR_STATE_DIR = "@CORE_STATE_DIR@/run/frr"
VNODED_BIN = which("vnoded", required=True)
VCMD_BIN = which("vcmd", required=True)
SYSCTL_BIN = which("sysctl", required=True)
IP_BIN = which("ip", required=True)
ETHTOOL_BIN = which("ethtool", required=True)
TC_BIN = which("tc", required=True)
EBTABLES_BIN = which("ebtables", required=True)
MOUNT_BIN = which("mount", required=True)
UMOUNT_BIN = which("umount", required=True)
OVS_BIN = which("ovs-vsctl", required=False)
OVS_FLOW_BIN = which("ovs-ofctl", required=False)

View file

@ -1,6 +1,7 @@
""" """
EMANE Bypass model for CORE EMANE Bypass model for CORE
""" """
from typing import List, Set
from core.config import Configuration from core.config import Configuration
from core.emane import emanemodel from core.emane import emanemodel
@ -8,14 +9,14 @@ from core.emulator.enumerations import ConfigDataTypes
class EmaneBypassModel(emanemodel.EmaneModel): class EmaneBypassModel(emanemodel.EmaneModel):
name = "emane_bypass" name: str = "emane_bypass"
# values to ignore, when writing xml files # values to ignore, when writing xml files
config_ignore = {"none"} config_ignore: Set[str] = {"none"}
# mac definitions # mac definitions
mac_library = "bypassmaclayer" mac_library: str = "bypassmaclayer"
mac_config = [ mac_config: List[Configuration] = [
Configuration( Configuration(
_id="none", _id="none",
_type=ConfigDataTypes.BOOL, _type=ConfigDataTypes.BOOL,
@ -25,8 +26,8 @@ class EmaneBypassModel(emanemodel.EmaneModel):
] ]
# phy definitions # phy definitions
phy_library = "bypassphylayer" phy_library: str = "bypassphylayer"
phy_config = [] phy_config: List[Configuration] = []
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:

View file

@ -10,9 +10,7 @@ from lxml import etree
from core.config import ConfigGroup, Configuration from core.config import ConfigGroup, Configuration
from core.emane import emanemanifest, emanemodel from core.emane import emanemanifest, emanemodel
from core.emane.nodes import EmaneNet from core.emulator.data import LinkOptions
from core.emulator.emudata import LinkOptions
from core.emulator.enumerations import TransportType
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.xml import emanexml from core.xml import emanexml
@ -22,6 +20,7 @@ except ImportError:
try: try:
from emanesh.events.commeffectevent import CommEffectEvent from emanesh.events.commeffectevent import CommEffectEvent
except ImportError: except ImportError:
CommEffectEvent = None
logging.debug("compatible emane python bindings not installed") logging.debug("compatible emane python bindings not installed")
@ -38,16 +37,15 @@ def convert_none(x: float) -> int:
class EmaneCommEffectModel(emanemodel.EmaneModel): class EmaneCommEffectModel(emanemodel.EmaneModel):
name = "emane_commeffect" name: str = "emane_commeffect"
shim_library: str = "commeffectshim"
shim_library = "commeffectshim" shim_xml: str = "commeffectshim.xml"
shim_xml = "commeffectshim.xml" shim_defaults: Dict[str, str] = {}
shim_defaults = {} config_shim: List[Configuration] = []
config_shim = []
# comm effect does not need the default phy and external configurations # comm effect does not need the default phy and external configurations
phy_config = [] phy_config: List[Configuration] = []
external_config = [] external_config: List[Configuration] = []
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:
@ -62,9 +60,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
def config_groups(cls) -> List[ConfigGroup]: def config_groups(cls) -> List[ConfigGroup]:
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))] return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
def build_xml_files( def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
self, config: Dict[str, str], interface: CoreInterface = None
) -> None:
""" """
Build the necessary nem and commeffect XMLs in the given path. Build the necessary nem and commeffect XMLs in the given path.
If an individual NEM has a nonstandard config, we need to build If an individual NEM has a nonstandard config, we need to build
@ -72,26 +68,19 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used. nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used.
:param config: emane model configuration for the node and interface :param config: emane model configuration for the node and interface
:param interface: interface for the emane node :param iface: interface for the emane node
:return: nothing :return: nothing
""" """
# retrieve xml names
nem_name = emanexml.nem_file_name(self, interface)
shim_name = emanexml.shim_file_name(self, interface)
# create and write nem document # create and write nem document
nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured") nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured")
transport_type = TransportType.VIRTUAL transport_name = emanexml.transport_file_name(iface)
if interface and interface.transport_type == TransportType.RAW: etree.SubElement(nem_element, "transport", definition=transport_name)
transport_type = TransportType.RAW
transport_file = emanexml.transport_file_name(self.id, transport_type)
etree.SubElement(nem_element, "transport", definition=transport_file)
# set shim configuration # set shim configuration
nem_name = emanexml.nem_file_name(iface)
shim_name = emanexml.shim_file_name(iface)
etree.SubElement(nem_element, "shim", definition=shim_name) etree.SubElement(nem_element, "shim", definition=shim_name)
emanexml.create_iface_file(iface, nem_element, "nem", nem_name)
nem_file = os.path.join(self.session.session_dir, nem_name)
emanexml.create_file(nem_element, "nem", nem_file)
# create and write shim document # create and write shim document
shim_element = etree.Element( shim_element = etree.Element(
@ -110,12 +99,13 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
ff = config["filterfile"] ff = config["filterfile"]
if ff.strip() != "": if ff.strip() != "":
emanexml.add_param(shim_element, "filterfile", ff) emanexml.add_param(shim_element, "filterfile", ff)
emanexml.create_iface_file(iface, shim_element, "shim", shim_name)
shim_file = os.path.join(self.session.session_dir, shim_name) # create transport xml
emanexml.create_file(shim_element, "shim", shim_file) emanexml.create_transport_xml(iface, config)
def linkconfig( def linkconfig(
self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
) -> None: ) -> None:
""" """
Generate CommEffect events when a Link Message is received having Generate CommEffect events when a Link Message is received having
@ -126,24 +116,23 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
logging.warning("%s: EMANE event service unavailable", self.name) logging.warning("%s: EMANE event service unavailable", self.name)
return return
if netif is None or netif2 is None: if iface is None or iface2 is None:
logging.warning("%s: missing NEM information", self.name) logging.warning("%s: missing NEM information", self.name)
return return
# TODO: batch these into multiple events per transmission # TODO: batch these into multiple events per transmission
# TODO: may want to split out seconds portion of delay and jitter # TODO: may want to split out seconds portion of delay and jitter
event = CommEffectEvent() event = CommEffectEvent()
emane_node = self.session.get_node(self.id, EmaneNet) nem1 = self.session.emane.get_nem_id(iface)
nemid = emane_node.getnemid(netif) nem2 = self.session.emane.get_nem_id(iface2)
nemid2 = emane_node.getnemid(netif2)
logging.info("sending comm effect event") logging.info("sending comm effect event")
event.append( event.append(
nemid, nem1,
latency=convert_none(options.delay), latency=convert_none(options.delay),
jitter=convert_none(options.jitter), jitter=convert_none(options.jitter),
loss=convert_none(options.per), loss=convert_none(options.loss),
duplicate=convert_none(options.dup), duplicate=convert_none(options.dup),
unicast=int(convert_none(options.bandwidth)), unicast=int(convert_none(options.bandwidth)),
broadcast=int(convert_none(options.bandwidth)), broadcast=int(convert_none(options.bandwidth)),
) )
service.publish(nemid2, event) service.publish(nem2, event)

View file

@ -6,6 +6,7 @@ import logging
import os import os
import threading import threading
from collections import OrderedDict from collections import OrderedDict
from enum import Enum
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
from core import utils from core import utils
@ -28,9 +29,7 @@ from core.emulator.enumerations import (
) )
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNode, NodeBase from core.nodes.base import CoreNode, NodeBase
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface, TunTap
from core.nodes.network import CtrlNet
from core.nodes.physical import Rj45Node
from core.xml import emanexml from core.xml import emanexml
if TYPE_CHECKING: if TYPE_CHECKING:
@ -63,6 +62,12 @@ DEFAULT_EMANE_PREFIX = "/usr"
DEFAULT_DEV = "ctrl0" DEFAULT_DEV = "ctrl0"
class EmaneState(Enum):
SUCCESS = 0
NOT_NEEDED = 1
NOT_READY = 2
class EmaneManager(ModelManager): class EmaneManager(ModelManager):
""" """
EMANE controller object. Lives in a Session instance and is used for EMANE controller object. Lives in a Session instance and is used for
@ -70,11 +75,11 @@ class EmaneManager(ModelManager):
controlling the EMANE daemons. controlling the EMANE daemons.
""" """
name = "emane" name: str = "emane"
config_type = RegisterTlvs.EMULATION_SERVER config_type: RegisterTlvs = RegisterTlvs.EMULATION_SERVER
SUCCESS, NOT_NEEDED, NOT_READY = (0, 1, 2) NOT_READY: int = 2
EVENTCFGVAR = "LIBEMANEEVENTSERVICECONFIG" EVENTCFGVAR: str = "LIBEMANEEVENTSERVICECONFIG"
DEFAULT_LOG_LEVEL = 3 DEFAULT_LOG_LEVEL: int = 3
def __init__(self, session: "Session") -> None: def __init__(self, session: "Session") -> None:
""" """
@ -84,74 +89,71 @@ class EmaneManager(ModelManager):
:return: nothing :return: nothing
""" """
super().__init__() super().__init__()
self.session = session self.session: "Session" = session
self._emane_nets = {} self.nems_to_ifaces: Dict[int, CoreInterface] = {}
self._emane_node_lock = threading.Lock() self.ifaces_to_nems: Dict[CoreInterface, int] = {}
self._emane_nets: Dict[int, EmaneNet] = {}
self._emane_node_lock: threading.Lock = threading.Lock()
# port numbers are allocated from these counters # port numbers are allocated from these counters
self.platformport = self.session.options.get_config_int( self.platformport: int = self.session.options.get_config_int(
"emane_platform_port", 8100 "emane_platform_port", 8100
) )
self.transformport = self.session.options.get_config_int( self.transformport: int = self.session.options.get_config_int(
"emane_transform_port", 8200 "emane_transform_port", 8200
) )
self.doeventloop = False self.doeventloop: bool = False
self.eventmonthread = None self.eventmonthread: Optional[threading.Thread] = None
# model for global EMANE configuration options # model for global EMANE configuration options
self.emane_config = EmaneGlobalModel(session) self.emane_config: EmaneGlobalModel = EmaneGlobalModel(session)
self.set_configs(self.emane_config.default_values()) self.set_configs(self.emane_config.default_values())
# link monitor # link monitor
self.link_monitor = EmaneLinkMonitor(self) self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self)
self.service = None self.service: Optional[EventService] = None
self.eventchannel = None self.eventchannel: Optional[Tuple[str, int, str]] = None
self.event_device = None self.event_device: Optional[str] = None
self.emane_check() self.emane_check()
def getifcconfig( def next_nem_id(self) -> int:
self, node_id: int, interface: CoreInterface, model_name: str nem_id = int(self.get_config("nem_id_start"))
while nem_id in self.nems_to_ifaces:
nem_id += 1
return nem_id
def get_iface_config(
self, emane_net: EmaneNet, iface: CoreInterface
) -> Dict[str, str]: ) -> Dict[str, str]:
""" """
Retrieve interface configuration or node configuration if not provided. Retrieve configuration for a given interface.
:param node_id: node id :param emane_net: emane network the interface is connected to
:param interface: node interface :param iface: interface running emane
:param model_name: model to get configuration for :return: net, node, or interface model configuration
:return: node/interface model configuration
""" """
# use the network-wide config values or interface(NEM)-specific values? model_name = emane_net.model.name
if interface is None: # don"t use default values when interface config is the same as net
return self.get_configs(node_id=node_id, config_type=model_name) # note here that using iface.node.id as key allows for only one type
else: # of each model per node;
# don"t use default values when interface config is the same as net # TODO: use both node and interface as key
# note here that using ifc.node.id as key allows for only one type # Adamson change: first check for iface config keyed by "node:iface.name"
# of each model per node; # (so that nodes w/ multiple interfaces of same conftype can have
# TODO: use both node and interface as key # different configs for each separate interface)
key = 1000 * iface.node.id
# Adamson change: first check for iface config keyed by "node:ifc.name" if iface.node_id is not None:
# (so that nodes w/ multiple interfaces of same conftype can have key += iface.node_id
# different configs for each separate interface) # try retrieve interface specific configuration, avoid getting defaults
key = 1000 * interface.node.id config = self.get_configs(node_id=key, config_type=model_name)
if interface.netindex is not None: # otherwise retrieve the interfaces node configuration, avoid using defaults
key += interface.netindex if not config:
config = self.get_configs(node_id=iface.node.id, config_type=model_name)
# try retrieve interface specific configuration, avoid getting defaults # get non interface config, when none found
config = self.get_configs(node_id=key, config_type=model_name) if not config:
# with EMANE 0.9.2+, we need an extra NEM XML from
# otherwise retrieve the interfaces node configuration, avoid using defaults # model.buildnemxmlfiles(), so defaults are returned here
if not config: config = self.get_configs(node_id=emane_net.id, config_type=model_name)
config = self.get_configs( return config
node_id=interface.node.id, config_type=model_name
)
# get non interface config, when none found
if not config:
# with EMANE 0.9.2+, we need an extra NEM XML from
# model.buildnemxmlfiles(), so defaults are returned here
config = self.get_configs(node_id=node_id, config_type=model_name)
return config
def config_reset(self, node_id: int = None) -> None: def config_reset(self, node_id: int = None) -> None:
super().config_reset(node_id) super().config_reset(node_id)
@ -163,23 +165,24 @@ class EmaneManager(ModelManager):
:return: nothing :return: nothing
""" """
try: # check for emane
# check for emane path = utils.which("emane", required=False)
args = "emane --version" if not path:
emane_version = utils.cmd(args)
logging.info("using EMANE: %s", emane_version)
self.session.distributed.execute(lambda x: x.remote_cmd(args))
# load default emane models
self.load_models(EMANE_MODELS)
# load custom models
custom_models_path = self.session.options.get_config("emane_models_dir")
if custom_models_path:
emane_models = utils.load_classes(custom_models_path, EmaneModel)
self.load_models(emane_models)
except CoreCommandError:
logging.info("emane is not installed") logging.info("emane is not installed")
return
# get version
emane_version = utils.cmd("emane --version")
logging.info("using emane: %s", emane_version)
# load default emane models
self.load_models(EMANE_MODELS)
# load custom models
custom_models_path = self.session.options.get_config("emane_models_dir")
if custom_models_path:
emane_models = utils.load_classes(custom_models_path, EmaneModel)
self.load_models(emane_models)
def deleteeventservice(self) -> None: def deleteeventservice(self) -> None:
if self.service: if self.service:
@ -250,8 +253,8 @@ class EmaneManager(ModelManager):
""" """
with self._emane_node_lock: with self._emane_node_lock:
if emane_net.id in self._emane_nets: if emane_net.id in self._emane_nets:
raise KeyError( raise CoreError(
f"non-unique EMANE object id {emane_net.id} for {emane_net}" f"duplicate emane network({emane_net.id}): {emane_net.name}"
) )
self._emane_nets[emane_net.id] = emane_net self._emane_nets[emane_net.id] = emane_net
@ -260,14 +263,13 @@ class EmaneManager(ModelManager):
Return a set of CoreNodes that are linked to an EMANE network, Return a set of CoreNodes that are linked to an EMANE network,
e.g. containers having one or more radio interfaces. e.g. containers having one or more radio interfaces.
""" """
# assumes self._objslock already held
nodes = set() nodes = set()
for emane_net in self._emane_nets.values(): for emane_net in self._emane_nets.values():
for netif in emane_net.netifs(): for iface in emane_net.get_ifaces():
nodes.add(netif.node) nodes.add(iface.node)
return nodes return nodes
def setup(self) -> int: def setup(self) -> EmaneState:
""" """
Setup duties for EMANE manager. Setup duties for EMANE manager.
@ -275,9 +277,7 @@ class EmaneManager(ModelManager):
instantiation instantiation
""" """
logging.debug("emane setup") logging.debug("emane setup")
with self.session.nodes_lock:
# TODO: drive this from the session object
with self.session._nodes_lock:
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 isinstance(node, EmaneNet): if isinstance(node, EmaneNet):
@ -285,10 +285,9 @@ class EmaneManager(ModelManager):
"adding emane node: id(%s) name(%s)", node.id, node.name "adding emane node: id(%s) name(%s)", node.id, node.name
) )
self.add_node(node) self.add_node(node)
if not self._emane_nets: if not self._emane_nets:
logging.debug("no emane nodes in session") logging.debug("no emane nodes in session")
return EmaneManager.NOT_NEEDED return EmaneState.NOT_NEEDED
# check if bindings were installed # check if bindings were installed
if EventService is None: if EventService is None:
@ -304,7 +303,7 @@ class EmaneManager(ModelManager):
"EMANE cannot start, check core config. invalid OTA device provided: %s", "EMANE cannot start, check core config. invalid OTA device provided: %s",
otadev, otadev,
) )
return EmaneManager.NOT_READY return EmaneState.NOT_READY
self.session.add_remove_control_net( self.session.add_remove_control_net(
net_index=netidx, remove=False, conf_required=False net_index=netidx, remove=False, conf_required=False
@ -316,19 +315,18 @@ class EmaneManager(ModelManager):
logging.debug("emane event service device index: %s", netidx) logging.debug("emane event service device index: %s", netidx)
if netidx < 0: if netidx < 0:
logging.error( logging.error(
"EMANE cannot start, check core config. invalid event service device: %s", "emane cannot start due to invalid event service device: %s",
eventdev, eventdev,
) )
return EmaneManager.NOT_READY return EmaneState.NOT_READY
self.session.add_remove_control_net( self.session.add_remove_control_net(
net_index=netidx, remove=False, conf_required=False net_index=netidx, remove=False, conf_required=False
) )
self.check_node_models() self.check_node_models()
return EmaneManager.SUCCESS return EmaneState.SUCCESS
def startup(self) -> int: def startup(self) -> EmaneState:
""" """
After all the EMANE networks have been added, build XML files After all the EMANE networks have been added, build XML files
and start the daemons. and start the daemons.
@ -337,39 +335,63 @@ class EmaneManager(ModelManager):
instantiation instantiation
""" """
self.reset() self.reset()
r = self.setup() status = self.setup()
if status != EmaneState.SUCCESS:
# NOT_NEEDED or NOT_READY return status
if r != EmaneManager.SUCCESS: self.starteventmonitor()
return r self.buildeventservicexml()
nems = []
with self._emane_node_lock: with self._emane_node_lock:
self.buildxml() logging.info("emane building xmls...")
self.starteventmonitor() for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
if self.numnems() > 0: if not emane_net.model:
self.startdaemons() logging.error("emane net(%s) has no model", emane_net.name)
self.installnetifs() continue
for iface in emane_net.get_ifaces():
for node_id in self._emane_nets: self.start_iface(emane_net, iface)
emane_node = self._emane_nets[node_id]
for netif in emane_node.netifs():
nems.append(
(netif.node.name, netif.name, emane_node.getnemid(netif))
)
if nems:
emane_nems_filename = os.path.join(self.session.session_dir, "emane_nems")
try:
with open(emane_nems_filename, "w") as f:
for nodename, ifname, nemid in nems:
f.write(f"{nodename} {ifname} {nemid}\n")
except IOError:
logging.exception("Error writing EMANE NEMs file: %s")
if self.links_enabled(): if self.links_enabled():
self.link_monitor.start() self.link_monitor.start()
return EmaneManager.SUCCESS return EmaneState.SUCCESS
def start_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
if not iface.node:
logging.error(
"emane net(%s) connected interface(%s) missing node",
emane_net.name,
iface.name,
)
return
control_net = self.session.add_remove_control_net(
0, remove=False, conf_required=False
)
nem_id = self.next_nem_id()
self.set_nem(nem_id, iface)
self.write_nem(iface, nem_id)
emanexml.build_platform_xml(self, control_net, emane_net, iface, nem_id)
config = self.get_iface_config(emane_net, iface)
emane_net.model.build_xml_files(config, iface)
self.start_daemon(iface)
self.install_iface(emane_net, iface)
def set_nem(self, nem_id: int, iface: CoreInterface) -> None:
if nem_id in self.nems_to_ifaces:
raise CoreError(f"adding duplicate nem: {nem_id}")
self.nems_to_ifaces[nem_id] = iface
self.ifaces_to_nems[iface] = nem_id
def get_iface(self, nem_id: int) -> Optional[CoreInterface]:
return self.nems_to_ifaces.get(nem_id)
def get_nem_id(self, iface: CoreInterface) -> Optional[int]:
return self.ifaces_to_nems.get(iface)
def write_nem(self, iface: CoreInterface, nem_id: int) -> None:
path = os.path.join(self.session.session_dir, "emane_nems")
try:
with open(path, "a") as f:
f.write(f"{iface.node.name} {iface.name} {nem_id}\n")
except IOError:
logging.exception("error writing to emane nem file")
def links_enabled(self) -> bool: def links_enabled(self) -> bool:
return self.get_config("link_enabled") == "1" return self.get_config("link_enabled") == "1"
@ -380,18 +402,15 @@ class EmaneManager(ModelManager):
""" """
if not self.genlocationevents(): if not self.genlocationevents():
return return
with self._emane_node_lock: with self._emane_node_lock:
for key in sorted(self._emane_nets.keys()): for node_id in sorted(self._emane_nets):
emane_node = self._emane_nets[key] emane_net = self._emane_nets[node_id]
logging.debug( logging.debug(
"post startup for emane node: %s - %s", "post startup for emane node: %s - %s", emane_net.id, emane_net.name
emane_node.id,
emane_node.name,
) )
emane_node.model.post_startup() emane_net.model.post_startup()
for netif in emane_node.netifs(): for iface in emane_net.get_ifaces():
netif.setposition() iface.setposition()
def reset(self) -> None: def reset(self) -> None:
""" """
@ -400,13 +419,8 @@ class EmaneManager(ModelManager):
""" """
with self._emane_node_lock: with self._emane_node_lock:
self._emane_nets.clear() self._emane_nets.clear()
self.nems_to_ifaces.clear()
self.platformport = self.session.options.get_config_int( self.ifaces_to_nems.clear()
"emane_platform_port", 8100
)
self.transformport = self.session.options.get_config_int(
"emane_transform_port", 8200
)
def shutdown(self) -> None: def shutdown(self) -> None:
""" """
@ -418,44 +432,27 @@ class EmaneManager(ModelManager):
logging.info("stopping EMANE daemons") logging.info("stopping EMANE daemons")
if self.links_enabled(): if self.links_enabled():
self.link_monitor.stop() self.link_monitor.stop()
self.deinstallnetifs() self.deinstall_ifaces()
self.stopdaemons() self.stopdaemons()
self.stopeventmonitor() self.stopeventmonitor()
def buildxml(self) -> None:
"""
Build XML files required to run EMANE on each node.
NEMs run inside containers using the control network for passing
events and data.
"""
# assume self._objslock is already held here
logging.info("emane building xml...")
# on master, control network bridge added earlier in startup()
ctrlnet = self.session.add_remove_control_net(
net_index=0, remove=False, conf_required=False
)
self.buildplatformxml(ctrlnet)
self.buildnemxml()
self.buildeventservicexml()
def check_node_models(self) -> None: def check_node_models(self) -> None:
""" """
Associate EMANE model classes with EMANE network nodes. Associate EMANE model classes with EMANE network nodes.
""" """
for node_id in self._emane_nets: for node_id in self._emane_nets:
emane_node = self._emane_nets[node_id] emane_net = self._emane_nets[node_id]
logging.debug("checking emane model for node: %s", node_id) logging.debug("checking emane model for node: %s", node_id)
# skip nodes that already have a model set # skip nodes that already have a model set
if emane_node.model: if emane_net.model:
logging.debug( logging.debug(
"node(%s) already has model(%s)", "node(%s) already has model(%s)", emane_net.id, emane_net.model.name
emane_node.id,
emane_node.model.name,
) )
continue continue
# set model configured for node, due to legacy messaging configuration before nodes exist # set model configured for node, due to legacy messaging configuration
# before nodes exist
model_name = self.node_models.get(node_id) model_name = self.node_models.get(node_id)
if not model_name: if not model_name:
logging.error("emane node(%s) has no node model", node_id) logging.error("emane node(%s) has no node model", node_id)
@ -464,81 +461,34 @@ class EmaneManager(ModelManager):
config = self.get_model_config(node_id=node_id, model_name=model_name) config = self.get_model_config(node_id=node_id, model_name=model_name)
logging.debug("setting emane model(%s) config(%s)", model_name, config) logging.debug("setting emane model(%s) config(%s)", model_name, config)
model_class = self.models[model_name] model_class = self.models[model_name]
emane_node.setmodel(model_class, config) emane_net.setmodel(model_class, config)
def nemlookup(self, nemid) -> Tuple[Optional[EmaneNet], Optional[CoreInterface]]:
"""
Look for the given numerical NEM ID and return the first matching
EMANE network and NEM interface.
"""
emane_node = None
netif = None
for node_id in self._emane_nets:
emane_node = self._emane_nets[node_id]
netif = emane_node.getnemnetif(nemid)
if netif is not None:
break
else:
emane_node = None
return emane_node, netif
def get_nem_link( def get_nem_link(
self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE
) -> Optional[LinkData]: ) -> Optional[LinkData]:
emane1, netif = self.nemlookup(nem1) iface1 = self.get_iface(nem1)
if not emane1 or not netif: if not iface1:
logging.error("invalid nem: %s", nem1) logging.error("invalid nem: %s", nem1)
return None return None
node1 = netif.node node1 = iface1.node
emane2, netif = self.nemlookup(nem2) iface2 = self.get_iface(nem2)
if not emane2 or not netif: if not iface2:
logging.error("invalid nem: %s", nem2) logging.error("invalid nem: %s", nem2)
return None return None
node2 = netif.node node2 = iface2.node
color = self.session.get_link_color(emane1.id) if iface1.net != iface2.net:
return None
emane_net = iface1.net
color = self.session.get_link_color(emane_net.id)
return LinkData( return LinkData(
message_type=flags, message_type=flags,
type=LinkTypes.WIRELESS,
node1_id=node1.id, node1_id=node1.id,
node2_id=node2.id, node2_id=node2.id,
network_id=emane1.id, network_id=emane_net.id,
link_type=LinkTypes.WIRELESS,
color=color, color=color,
) )
def numnems(self) -> int:
"""
Return the number of NEMs emulated locally.
"""
count = 0
for node_id in self._emane_nets:
emane_node = self._emane_nets[node_id]
count += len(emane_node.netifs())
return count
def buildplatformxml(self, ctrlnet: CtrlNet) -> None:
"""
Build a platform.xml file now that all nodes are configured.
"""
nemid = int(self.get_config("nem_id_start"))
platform_xmls = {}
# assume self._objslock is already held here
for key in sorted(self._emane_nets.keys()):
emane_node = self._emane_nets[key]
nemid = emanexml.build_node_platform_xml(
self, ctrlnet, emane_node, nemid, platform_xmls
)
def buildnemxml(self) -> None:
"""
Builds the nem, mac, and phy xml files for each EMANE network.
"""
for key in sorted(self._emane_nets):
emane_net = self._emane_nets[key]
emanexml.build_xml_files(self, emane_net)
def buildeventservicexml(self) -> None: def buildeventservicexml(self) -> None:
""" """
Build the libemaneeventservice.xml file if event service options Build the libemaneeventservice.xml file if event service options
@ -571,7 +521,7 @@ class EmaneManager(ModelManager):
) )
) )
def startdaemons(self) -> None: def start_daemon(self, iface: CoreInterface) -> None:
""" """
Start one EMANE daemon per node having a radio. Start one EMANE daemon per node having a radio.
Add a control network even if the user has not configured one. Add a control network even if the user has not configured one.
@ -581,116 +531,91 @@ class EmaneManager(ModelManager):
cfgloglevel = self.session.options.get_config_int("emane_log_level") cfgloglevel = self.session.options.get_config_int("emane_log_level")
realtime = self.session.options.get_config_bool("emane_realtime", default=True) realtime = self.session.options.get_config_bool("emane_realtime", default=True)
if cfgloglevel: if cfgloglevel:
logging.info("setting user-defined EMANE log level: %d", cfgloglevel) logging.info("setting user-defined emane log level: %d", cfgloglevel)
loglevel = str(cfgloglevel) loglevel = str(cfgloglevel)
emanecmd = f"emane -d -l {loglevel}" emanecmd = f"emane -d -l {loglevel}"
if realtime: if realtime:
emanecmd += " -r" emanecmd += " -r"
node = iface.node
otagroup, _otaport = self.get_config("otamanagergroup").split(":") if iface.is_virtual():
otadev = self.get_config("otamanagerdevice") otagroup, _otaport = self.get_config("otamanagergroup").split(":")
otanetidx = self.session.get_control_net_index(otadev) otadev = self.get_config("otamanagerdevice")
otanetidx = self.session.get_control_net_index(otadev)
eventgroup, _eventport = self.get_config("eventservicegroup").split(":") eventgroup, _eventport = self.get_config("eventservicegroup").split(":")
eventdev = self.get_config("eventservicedevice") eventdev = self.get_config("eventservicedevice")
eventservicenetidx = self.session.get_control_net_index(eventdev) eventservicenetidx = self.session.get_control_net_index(eventdev)
run_emane_on_host = False
for node in self.getnodes():
if isinstance(node, Rj45Node):
run_emane_on_host = True
continue
path = self.session.session_dir
n = node.id
# control network not yet started here # control network not yet started here
self.session.add_remove_control_interface( self.session.add_remove_control_iface(
node, 0, remove=False, conf_required=False node, 0, remove=False, conf_required=False
) )
if otanetidx > 0: if otanetidx > 0:
logging.info("adding ota device ctrl%d", otanetidx) logging.info("adding ota device ctrl%d", otanetidx)
self.session.add_remove_control_interface( self.session.add_remove_control_iface(
node, otanetidx, remove=False, conf_required=False node, otanetidx, remove=False, conf_required=False
) )
if eventservicenetidx >= 0: if eventservicenetidx >= 0:
logging.info("adding event service device ctrl%d", eventservicenetidx) logging.info("adding event service device ctrl%d", eventservicenetidx)
self.session.add_remove_control_interface( self.session.add_remove_control_iface(
node, eventservicenetidx, remove=False, conf_required=False node, eventservicenetidx, remove=False, conf_required=False
) )
# multicast route is needed for OTA data # multicast route is needed for OTA data
logging.info("OTA GROUP(%s) OTA DEV(%s)", otagroup, otadev)
node.node_net_client.create_route(otagroup, otadev) node.node_net_client.create_route(otagroup, otadev)
# multicast route is also needed for event data if on control network # multicast route is also needed for event data if on control network
if eventservicenetidx >= 0 and eventgroup != otagroup: if eventservicenetidx >= 0 and eventgroup != otagroup:
node.node_net_client.create_route(eventgroup, eventdev) node.node_net_client.create_route(eventgroup, eventdev)
# start emane # start emane
log_file = os.path.join(path, f"emane{n}.log") log_file = os.path.join(node.nodedir, f"{iface.name}-emane.log")
platform_xml = os.path.join(path, f"platform{n}.xml") platform_xml = os.path.join(node.nodedir, f"{iface.name}-platform.xml")
args = f"{emanecmd} -f {log_file} {platform_xml}" args = f"{emanecmd} -f {log_file} {platform_xml}"
output = node.cmd(args) node.cmd(args)
logging.info("node(%s) emane daemon running: %s", node.name, args) logging.info("node(%s) emane daemon running: %s", node.name, args)
logging.debug("node(%s) emane daemon output: %s", node.name, output) else:
path = self.session.session_dir
if not run_emane_on_host: log_file = os.path.join(path, f"{iface.name}-emane.log")
return platform_xml = os.path.join(path, f"{iface.name}-platform.xml")
emanecmd += f" -f {log_file} {platform_xml}"
path = self.session.session_dir node.host_cmd(emanecmd, cwd=path)
log_file = os.path.join(path, "emane.log") logging.info("node(%s) host emane daemon running: %s", node.name, emanecmd)
platform_xml = os.path.join(path, "platform.xml")
emanecmd += f" -f {log_file} {platform_xml}"
utils.cmd(emanecmd, cwd=path)
self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path))
logging.info("host emane daemon running: %s", emanecmd)
def stopdaemons(self) -> None: def stopdaemons(self) -> None:
""" """
Kill the appropriate EMANE daemons. Kill the appropriate EMANE daemons.
""" """
# TODO: we may want to improve this if we had the PIDs from the specific EMANE
# daemons that we"ve started
kill_emaned = "killall -q emane" kill_emaned = "killall -q emane"
kill_transortd = "killall -q emanetransportd" for node_id in sorted(self._emane_nets):
stop_emane_on_host = False emane_net = self._emane_nets[node_id]
for node in self.getnodes(): for iface in emane_net.get_ifaces():
if isinstance(node, Rj45Node): node = iface.node
stop_emane_on_host = True if not node.up:
continue continue
if iface.is_raw():
node.host_cmd(kill_emaned, wait=False)
else:
node.cmd(kill_emaned, wait=False)
if node.up: def install_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
node.cmd(kill_emaned, wait=False) config = self.get_iface_config(emane_net, iface)
# TODO: RJ45 node external = config.get("external", "0")
if isinstance(iface, TunTap) and external == "0":
iface.set_ips()
# at this point we register location handlers for generating
# EMANE location events
if self.genlocationevents():
iface.poshook = emane_net.setnemposition
iface.setposition()
if stop_emane_on_host: def deinstall_ifaces(self) -> None:
try:
utils.cmd(kill_emaned)
utils.cmd(kill_transortd)
self.session.distributed.execute(lambda x: x.remote_cmd(kill_emaned))
self.session.distributed.execute(lambda x: x.remote_cmd(kill_transortd))
except CoreCommandError:
logging.exception("error shutting down emane daemons")
def installnetifs(self) -> None:
"""
Install TUN/TAP virtual interfaces into their proper namespaces
now that the EMANE daemons are running.
"""
for key in sorted(self._emane_nets.keys()):
emane_node = self._emane_nets[key]
logging.info("emane install netifs for node: %d", key)
emane_node.installnetifs()
def deinstallnetifs(self) -> None:
""" """
Uninstall TUN/TAP virtual interfaces. Uninstall TUN/TAP virtual interfaces.
""" """
for key in sorted(self._emane_nets.keys()): for key in sorted(self._emane_nets):
emane_node = self._emane_nets[key] emane_net = self._emane_nets[key]
emane_node.deinstallnetifs() for iface in emane_net.get_ifaces():
if iface.is_virtual():
iface.shutdown()
iface.poshook = None
def doeventmonitor(self) -> bool: def doeventmonitor(self) -> bool:
""" """
@ -718,7 +643,6 @@ class EmaneManager(ModelManager):
logging.info("emane start event monitor") logging.info("emane start event monitor")
if not self.doeventmonitor(): if not self.doeventmonitor():
return return
if self.service is None: if self.service is None:
logging.error( logging.error(
"Warning: EMANE events will not be generated " "Warning: EMANE events will not be generated "
@ -806,12 +730,12 @@ class EmaneManager(ModelManager):
Returns True if successfully parsed and a Node Message was sent. Returns True if successfully parsed and a Node Message was sent.
""" """
# convert nemid to node number # convert nemid to node number
_emanenode, netif = self.nemlookup(nemid) iface = self.get_iface(nemid)
if netif is None: if iface is None:
logging.info("location event for unknown NEM %s", nemid) logging.info("location event for unknown NEM %s", nemid)
return False return False
n = netif.node.id n = iface.node.id
# convert from lat/long/alt to x,y,z coordinates # convert from lat/long/alt to x,y,z coordinates
x, y, z = self.session.location.getxyz(lat, lon, alt) x, y, z = self.session.location.getxyz(lat, lon, alt)
x = int(x) x = int(x)
@ -890,12 +814,12 @@ class EmaneGlobalModel:
Global EMANE configuration options. Global EMANE configuration options.
""" """
name = "emane" name: str = "emane"
bitmap = None bitmap: Optional[str] = None
def __init__(self, session: "Session") -> None: def __init__(self, session: "Session") -> None:
self.session = session self.session: "Session" = session
self.core_config = [ self.core_config: List[Configuration] = [
Configuration( Configuration(
_id="platform_id_start", _id="platform_id_start",
_type=ConfigDataTypes.INT32, _type=ConfigDataTypes.INT32,

View file

@ -11,6 +11,7 @@ except ImportError:
try: try:
from emanesh import manifest from emanesh import manifest
except ImportError: except ImportError:
manifest = None
logging.debug("compatible emane python bindings not installed") logging.debug("compatible emane python bindings not installed")

View file

@ -3,13 +3,13 @@ Defines Emane Models used within CORE.
""" """
import logging import logging
import os import os
from typing import Dict, List from typing import Dict, List, Optional, Set
from core.config import ConfigGroup, Configuration from core.config import ConfigGroup, Configuration
from core.emane import emanemanifest from core.emane import emanemanifest
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.emudata import LinkOptions from core.emulator.data import LinkOptions
from core.emulator.enumerations import ConfigDataTypes, TransportType from core.emulator.enumerations import ConfigDataTypes
from core.errors import CoreError from core.errors import CoreError
from core.location.mobility import WirelessModel from core.location.mobility import WirelessModel
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
@ -25,19 +25,23 @@ class EmaneModel(WirelessModel):
""" """
# default mac configuration settings # default mac configuration settings
mac_library = None mac_library: Optional[str] = None
mac_xml = None mac_xml: Optional[str] = None
mac_defaults = {} mac_defaults: Dict[str, str] = {}
mac_config = [] mac_config: List[Configuration] = []
# default phy configuration settings, using the universal model # default phy configuration settings, using the universal model
phy_library = None phy_library: Optional[str] = None
phy_xml = "emanephy.xml" phy_xml: str = "emanephy.xml"
phy_defaults = {"subid": "1", "propagationmodel": "2ray", "noisemode": "none"} phy_defaults: Dict[str, str] = {
phy_config = [] "subid": "1",
"propagationmodel": "2ray",
"noisemode": "none",
}
phy_config: List[Configuration] = []
# support for external configurations # support for external configurations
external_config = [ external_config: List[Configuration] = [
Configuration("external", ConfigDataTypes.BOOL, default="0"), Configuration("external", ConfigDataTypes.BOOL, default="0"),
Configuration( Configuration(
"platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001" "platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001"
@ -47,7 +51,7 @@ class EmaneModel(WirelessModel):
), ),
] ]
config_ignore = set() config_ignore: Set[str] = set()
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:
@ -92,45 +96,20 @@ class EmaneModel(WirelessModel):
ConfigGroup("External Parameters", phy_len + 1, config_len), ConfigGroup("External Parameters", phy_len + 1, config_len),
] ]
def build_xml_files( def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
self, config: Dict[str, str], interface: CoreInterface = None
) -> None:
""" """
Builds xml files for this emane model. Creates a nem.xml file that points to Builds xml files for this emane model. Creates a nem.xml file that points to
both mac.xml and phy.xml definitions. both mac.xml and phy.xml definitions.
:param config: emane model configuration for the node and interface :param config: emane model configuration for the node and interface
:param interface: interface for the emane node :param iface: interface to run emane for
:return: nothing :return: nothing
""" """
nem_name = emanexml.nem_file_name(self, interface) # create nem, mac, and phy xml files
mac_name = emanexml.mac_file_name(self, interface) emanexml.create_nem_xml(self, iface, config)
phy_name = emanexml.phy_file_name(self, interface) emanexml.create_mac_xml(self, iface, config)
emanexml.create_phy_xml(self, iface, config)
# remote server for file emanexml.create_transport_xml(iface, config)
server = None
if interface is not None:
server = interface.node.server
# check if this is external
transport_type = TransportType.VIRTUAL
if interface and interface.transport_type == TransportType.RAW:
transport_type = TransportType.RAW
transport_name = emanexml.transport_file_name(self.id, transport_type)
# create nem xml file
nem_file = os.path.join(self.session.session_dir, nem_name)
emanexml.create_nem_xml(
self, config, nem_file, transport_name, mac_name, phy_name, server
)
# create mac xml file
mac_file = os.path.join(self.session.session_dir, mac_name)
emanexml.create_mac_xml(self, config, mac_file, server)
# create phy xml file
phy_file = os.path.join(self.session.session_dir, phy_name)
emanexml.create_phy_xml(self, config, phy_file, server)
def post_startup(self) -> None: def post_startup(self) -> None:
""" """
@ -140,31 +119,31 @@ class EmaneModel(WirelessModel):
""" """
logging.debug("emane model(%s) has no post setup tasks", self.name) logging.debug("emane model(%s) has no post setup tasks", self.name)
def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None: def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None:
""" """
Invoked from MobilityModel when nodes are moved; this causes Invoked from MobilityModel when nodes are moved; this causes
emane location events to be generated for the nodes in the moved emane location events to be generated for the nodes in the moved
list, making EmaneModels compatible with Ns2ScriptedMobility. list, making EmaneModels compatible with Ns2ScriptedMobility.
:param moved: moved nodes :param moved: moved nodes
:param moved_netifs: interfaces that were moved :param moved_ifaces: interfaces that were moved
:return: nothing :return: nothing
""" """
try: try:
wlan = self.session.get_node(self.id, EmaneNet) wlan = self.session.get_node(self.id, EmaneNet)
wlan.setnempositions(moved_netifs) wlan.setnempositions(moved_ifaces)
except CoreError: except CoreError:
logging.exception("error during update") logging.exception("error during update")
def linkconfig( def linkconfig(
self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
) -> None: ) -> None:
""" """
Invoked when a Link Message is received. Default is unimplemented. Invoked when a Link Message is received. Default is unimplemented.
:param netif: interface one :param iface: interface one
:param options: options for configuring link :param options: options for configuring link
:param netif2: interface two :param iface2: interface two
:return: nothing :return: nothing
""" """
logging.warning("emane model(%s) does not support link config", self.name) logging.warning("emane model(%s) does not support link config", self.name)

View file

@ -8,11 +8,11 @@ from core.emane import emanemodel
class EmaneIeee80211abgModel(emanemodel.EmaneModel): class EmaneIeee80211abgModel(emanemodel.EmaneModel):
# model name # model name
name = "emane_ieee80211abg" name: str = "emane_ieee80211abg"
# mac configuration # mac configuration
mac_library = "ieee80211abgmaclayer" mac_library: str = "ieee80211abgmaclayer"
mac_xml = "ieee80211abgmaclayer.xml" mac_xml: str = "ieee80211abgmaclayer.xml"
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:

View file

@ -2,9 +2,8 @@ import logging
import sched import sched
import threading import threading
import time import time
from typing import TYPE_CHECKING, Dict, List, Tuple from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
import netaddr
from lxml import etree from lxml import etree
from core.emulator.data import LinkData from core.emulator.data import LinkData
@ -17,28 +16,29 @@ except ImportError:
try: try:
from emanesh import shell from emanesh import shell
except ImportError: except ImportError:
shell = None
logging.debug("compatible emane python bindings not installed") logging.debug("compatible emane python bindings not installed")
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emane.emanemanager import EmaneManager from core.emane.emanemanager import EmaneManager
DEFAULT_PORT = 47_000 DEFAULT_PORT: int = 47_000
MAC_COMPONENT_INDEX = 1 MAC_COMPONENT_INDEX: int = 1
EMANE_RFPIPE = "rfpipemaclayer" EMANE_RFPIPE: str = "rfpipemaclayer"
EMANE_80211 = "ieee80211abgmaclayer" EMANE_80211: str = "ieee80211abgmaclayer"
EMANE_TDMA = "tdmaeventschedulerradiomodel" EMANE_TDMA: str = "tdmaeventschedulerradiomodel"
SINR_TABLE = "NeighborStatusTable" SINR_TABLE: str = "NeighborStatusTable"
NEM_SELF = 65535 NEM_SELF: int = 65535
class LossTable: class LossTable:
def __init__(self, losses: Dict[float, float]) -> None: def __init__(self, losses: Dict[float, float]) -> None:
self.losses = losses self.losses: Dict[float, float] = losses
self.sinrs = sorted(self.losses.keys()) self.sinrs: List[float] = sorted(self.losses.keys())
self.loss_lookup = {} self.loss_lookup: Dict[int, float] = {}
for index, value in enumerate(self.sinrs): for index, value in enumerate(self.sinrs):
self.loss_lookup[index] = self.losses[value] self.loss_lookup[index] = self.losses[value]
self.mac_id = None self.mac_id: Optional[str] = None
def get_loss(self, sinr: float) -> float: def get_loss(self, sinr: float) -> float:
index = self._get_index(sinr) index = self._get_index(sinr)
@ -54,11 +54,11 @@ class LossTable:
class EmaneLink: class EmaneLink:
def __init__(self, from_nem: int, to_nem: int, sinr: float) -> None: def __init__(self, from_nem: int, to_nem: int, sinr: float) -> None:
self.from_nem = from_nem self.from_nem: int = from_nem
self.to_nem = to_nem self.to_nem: int = to_nem
self.sinr = sinr self.sinr: float = sinr
self.last_seen = None self.last_seen: Optional[float] = None
self.updated = False self.updated: bool = False
self.touch() self.touch()
def update(self, sinr: float) -> None: def update(self, sinr: float) -> None:
@ -78,9 +78,11 @@ class EmaneLink:
class EmaneClient: class EmaneClient:
def __init__(self, address: str) -> None: def __init__(self, address: str) -> None:
self.address = address self.address: str = address
self.client = shell.ControlPortClient(self.address, DEFAULT_PORT) self.client: shell.ControlPortClient = shell.ControlPortClient(
self.nems = {} self.address, DEFAULT_PORT
)
self.nems: Dict[int, LossTable] = {}
self.setup() self.setup()
def setup(self) -> None: def setup(self) -> None:
@ -174,15 +176,15 @@ class EmaneClient:
class EmaneLinkMonitor: class EmaneLinkMonitor:
def __init__(self, emane_manager: "EmaneManager") -> None: def __init__(self, emane_manager: "EmaneManager") -> None:
self.emane_manager = emane_manager self.emane_manager: "EmaneManager" = emane_manager
self.clients = [] self.clients: List[EmaneClient] = []
self.links = {} self.links: Dict[Tuple[int, int], EmaneLink] = {}
self.complete_links = set() self.complete_links: Set[Tuple[int, int]] = set()
self.loss_threshold = None self.loss_threshold: Optional[int] = None
self.link_interval = None self.link_interval: Optional[int] = None
self.link_timeout = None self.link_timeout: Optional[int] = None
self.scheduler = None self.scheduler: Optional[sched.scheduler] = None
self.running = False self.running: bool = False
def start(self) -> None: def start(self) -> None:
self.loss_threshold = int(self.emane_manager.get_config("loss_threshold")) self.loss_threshold = int(self.emane_manager.get_config("loss_threshold"))
@ -209,15 +211,12 @@ class EmaneLinkMonitor:
addresses = [] addresses = []
nodes = self.emane_manager.getnodes() nodes = self.emane_manager.getnodes()
for node in nodes: for node in nodes:
for netif in node.netifs(): for iface in node.get_ifaces():
if isinstance(netif.net, CtrlNet): if isinstance(iface.net, CtrlNet):
ip4 = None ip4 = iface.get_ip4()
for x in netif.addrlist:
address, prefix = x.split("/")
if netaddr.valid_ipv4(address):
ip4 = address
if ip4: if ip4:
addresses.append(ip4) address = str(ip4.ip)
addresses.append(address)
break break
return addresses return addresses
@ -266,11 +265,11 @@ class EmaneLinkMonitor:
self.scheduler.enter(self.link_interval, 0, self.check_links) self.scheduler.enter(self.link_interval, 0, self.check_links)
def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]: def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]:
value_one, value_two = link_id value1, value2 = link_id
if value_one < value_two: if value1 < value2:
return value_one, value_two return value1, value2
else: else:
return value_two, value_one return value2, value1
def is_complete_link(self, link_id: Tuple[int, int]) -> bool: def is_complete_link(self, link_id: Tuple[int, int]) -> bool:
reverse_id = link_id[1], link_id[0] reverse_id = link_id[1], link_id[0]
@ -284,8 +283,8 @@ class EmaneLinkMonitor:
return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}" return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}"
def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None: def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None:
nem_one, nem_two = link_id nem1, nem2 = link_id
link = self.emane_manager.get_nem_link(nem_one, nem_two, message_type) link = self.emane_manager.get_nem_link(nem1, nem2, message_type)
if link: if link:
label = self.get_link_label(link_id) label = self.get_link_label(link_id)
link.label = label link.label = label
@ -295,18 +294,18 @@ class EmaneLinkMonitor:
self, self,
message_type: MessageFlags, message_type: MessageFlags,
label: str, label: str,
node_one: int, node1: int,
node_two: int, node2: int,
emane_id: int, emane_id: int,
) -> None: ) -> None:
color = self.emane_manager.session.get_link_color(emane_id) color = self.emane_manager.session.get_link_color(emane_id)
link_data = LinkData( link_data = LinkData(
message_type=message_type, message_type=message_type,
type=LinkTypes.WIRELESS,
label=label, label=label,
node1_id=node_one, node1_id=node1,
node2_id=node_two, node2_id=node2,
network_id=emane_id, network_id=emane_id,
link_type=LinkTypes.WIRELESS,
color=color, color=color,
) )
self.emane_manager.session.broadcast_link(link_data) self.emane_manager.session.broadcast_link(link_data)

View file

@ -6,23 +6,25 @@ share the same MAC+PHY model.
import logging import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
from core.emulator.data import LinkData from core.emulator.data import InterfaceData, LinkData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.emudata import LinkOptions
from core.emulator.enumerations import ( from core.emulator.enumerations import (
EventTypes,
LinkTypes, LinkTypes,
MessageFlags, MessageFlags,
NodeTypes, NodeTypes,
RegisterTlvs, RegisterTlvs,
TransportType,
) )
from core.nodes.base import CoreNetworkBase from core.errors import CoreError
from core.nodes.base import CoreNetworkBase, CoreNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emane.emanemodel import EmaneModel
from core.emulator.session import Session from core.emulator.session import Session
from core.location.mobility import WirelessModel from core.location.mobility import WirelessModel, WayPointMobility
OptionalEmaneModel = Optional[EmaneModel]
WirelessModelType = Type[WirelessModel] WirelessModelType = Type[WirelessModel]
try: try:
@ -31,6 +33,7 @@ except ImportError:
try: try:
from emanesh.events import LocationEvent from emanesh.events import LocationEvent
except ImportError: except ImportError:
LocationEvent = None
logging.debug("compatible emane python bindings not installed") logging.debug("compatible emane python bindings not installed")
@ -41,60 +44,63 @@ class EmaneNet(CoreNetworkBase):
Emane controller object that exists in a session. Emane controller object that exists in a session.
""" """
apitype = NodeTypes.EMANE apitype: NodeTypes = NodeTypes.EMANE
linktype = LinkTypes.WIRED linktype: LinkTypes = LinkTypes.WIRED
type = "wlan" type: str = "wlan"
is_emane = True has_custom_iface: bool = True
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
start: bool = True,
server: DistributedServer = None, server: DistributedServer = None,
) -> None: ) -> None:
super().__init__(session, _id, name, start, server) super().__init__(session, _id, name, server)
self.conf = "" self.conf: str = ""
self.nemidmap = {} self.model: "OptionalEmaneModel" = None
self.model = None self.mobility: Optional[WayPointMobility] = None
self.mobility = None
def linkconfig( def linkconfig(
self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
) -> None: ) -> None:
""" """
The CommEffect model supports link configuration. The CommEffect model supports link configuration.
""" """
if not self.model: if not self.model:
return return
self.model.linkconfig(netif, options, netif2) self.model.linkconfig(iface, options, iface2)
def config(self, conf: str) -> None: def config(self, conf: str) -> None:
self.conf = conf self.conf = conf
def startup(self) -> None:
pass
def shutdown(self) -> None: def shutdown(self) -> None:
pass pass
def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None: def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass pass
def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None: def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass pass
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
raise CoreError("emane networks cannot be linked to other networks")
def updatemodel(self, config: Dict[str, str]) -> None: def updatemodel(self, config: Dict[str, str]) -> None:
if not self.model: if not self.model:
raise ValueError("no model set to update for node(%s)", self.id) raise CoreError(f"no model set to update for node({self.name})")
logging.info( logging.info(
"node(%s) updating model(%s): %s", self.id, self.model.name, config "node(%s) updating model(%s): %s", self.id, self.model.name, config
) )
self.model.set_configs(config, node_id=self.id) self.model.update_config(config)
def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None: def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None:
""" """
set the EmaneModel associated with this node set the EmaneModel associated with this node
""" """
logging.info("adding model: %s", model.name)
if model.config_type == RegisterTlvs.WIRELESS: if model.config_type == RegisterTlvs.WIRELESS:
# EmaneModel really uses values from ConfigurableManager # EmaneModel really uses values from ConfigurableManager
# when buildnemxml() is called, not during init() # when buildnemxml() is called, not during init()
@ -104,94 +110,21 @@ class EmaneNet(CoreNetworkBase):
self.mobility = model(session=self.session, _id=self.id) self.mobility = model(session=self.session, _id=self.id)
self.mobility.update_config(config) self.mobility.update_config(config)
def setnemid(self, netif: CoreInterface, nemid: int) -> None:
"""
Record an interface to numerical ID mapping. The Emane controller
object manages and assigns these IDs for all NEMs.
"""
self.nemidmap[netif] = nemid
def getnemid(self, netif: CoreInterface) -> Optional[int]:
"""
Given an interface, return its numerical ID.
"""
if netif not in self.nemidmap:
return None
else:
return self.nemidmap[netif]
def getnemnetif(self, nemid: int) -> Optional[CoreInterface]:
"""
Given a numerical NEM ID, return its interface. This returns the
first interface that matches the given NEM ID.
"""
for netif in self.nemidmap:
if self.nemidmap[netif] == nemid:
return netif
return None
def netifs(self, sort: bool = True) -> List[CoreInterface]:
"""
Retrieve list of linked interfaces sorted by node number.
"""
return sorted(self._netif.values(), key=lambda ifc: ifc.node.id)
def installnetifs(self) -> None:
"""
Install TAP devices into their namespaces. This is done after
EMANE daemons have been started, because that is their only chance
to bind to the TAPs.
"""
if (
self.session.emane.genlocationevents()
and self.session.emane.service is None
):
warntxt = "unable to publish EMANE events because the eventservice "
warntxt += "Python bindings failed to load"
logging.error(warntxt)
for netif in self.netifs():
external = self.session.emane.get_config(
"external", self.id, self.model.name
)
if external == "0":
netif.setaddrs()
if not self.session.emane.genlocationevents():
netif.poshook = None
continue
# at this point we register location handlers for generating
# EMANE location events
netif.poshook = self.setnemposition
netif.setposition()
def deinstallnetifs(self) -> None:
"""
Uninstall TAP devices. This invokes their shutdown method for
any required cleanup; the device may be actually removed when
emanetransportd terminates.
"""
for netif in self.netifs():
if netif.transport_type == TransportType.VIRTUAL:
netif.shutdown()
netif.poshook = None
def _nem_position( def _nem_position(
self, netif: CoreInterface self, iface: CoreInterface
) -> Optional[Tuple[int, float, float, float]]: ) -> Optional[Tuple[int, float, float, float]]:
""" """
Creates nem position for emane event for a given interface. Creates nem position for emane event for a given interface.
:param netif: interface to get nem emane position for :param iface: interface to get nem emane position for
:return: nem position tuple, None otherwise :return: nem position tuple, None otherwise
""" """
nemid = self.getnemid(netif) nem_id = self.session.emane.get_nem_id(iface)
ifname = netif.localname ifname = iface.localname
if nemid is None: if nem_id is None:
logging.info("nemid for %s is unknown", ifname) logging.info("nemid for %s is unknown", ifname)
return return
node = netif.node node = iface.node
x, y, z = node.getposition() x, y, z = node.getposition()
lat, lon, alt = self.session.location.getgeo(x, y, z) lat, lon, alt = self.session.location.getgeo(x, y, z)
if node.position.alt is not None: if node.position.alt is not None:
@ -199,32 +132,31 @@ class EmaneNet(CoreNetworkBase):
node.position.set_geo(lon, lat, alt) node.position.set_geo(lon, lat, alt)
# altitude must be an integer or warning is printed # altitude must be an integer or warning is printed
alt = int(round(alt)) alt = int(round(alt))
return nemid, lon, lat, alt return nem_id, lon, lat, alt
def setnemposition(self, netif: CoreInterface) -> None: def setnemposition(self, iface: CoreInterface) -> None:
""" """
Publish a NEM location change event using the EMANE event service. Publish a NEM location change event using the EMANE event service.
:param netif: interface to set nem position for :param iface: interface to set nem position for
""" """
if self.session.emane.service is None: if self.session.emane.service is None:
logging.info("position service not available") logging.info("position service not available")
return return
position = self._nem_position(iface)
position = self._nem_position(netif)
if position: if position:
nemid, lon, lat, alt = position nemid, lon, lat, alt = position
event = LocationEvent() event = LocationEvent()
event.append(nemid, latitude=lat, longitude=lon, altitude=alt) event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.session.emane.service.publish(0, event) self.session.emane.service.publish(0, event)
def setnempositions(self, moved_netifs: List[CoreInterface]) -> None: def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None:
""" """
Several NEMs have moved, from e.g. a WaypointMobilityModel Several NEMs have moved, from e.g. a WaypointMobilityModel
calculation. Generate an EMANE Location Event having several calculation. Generate an EMANE Location Event having several
entries for each netif that has moved. entries for each interface that has moved.
""" """
if len(moved_netifs) == 0: if len(moved_ifaces) == 0:
return return
if self.session.emane.service is None: if self.session.emane.service is None:
@ -232,18 +164,21 @@ class EmaneNet(CoreNetworkBase):
return return
event = LocationEvent() event = LocationEvent()
for netif in moved_netifs: for iface in moved_ifaces:
position = self._nem_position(netif) position = self._nem_position(iface)
if position: if position:
nemid, lon, lat, alt = position nemid, lon, lat, alt = position
event.append(nemid, latitude=lat, longitude=lon, altitude=alt) event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.session.emane.service.publish(0, event) self.session.emane.service.publish(0, event)
def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
links = super().all_link_data(flags) links = super().links(flags)
# gather current emane links
nem_ids = set(self.nemidmap.values())
emane_manager = self.session.emane emane_manager = self.session.emane
# gather current emane links
nem_ids = set()
for iface in self.get_ifaces():
nem_id = emane_manager.get_nem_id(iface)
nem_ids.add(nem_id)
emane_links = emane_manager.link_monitor.links emane_links = emane_manager.link_monitor.links
considered = set() considered = set()
for link_key in emane_links: for link_key in emane_links:
@ -262,3 +197,18 @@ class EmaneNet(CoreNetworkBase):
if link: if link:
links.append(link) links.append(link)
return links return links
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
# TUN/TAP is not ready for addressing yet; the device may
# take some time to appear, and installing it into a
# namespace after it has been bound removes addressing;
# save addresses with the interface now
iface_id = node.newtuntap(iface_data.id, iface_data.name)
node.attachnet(iface_id, self)
iface = node.get_iface(iface_id)
iface.set_mac(iface_data.mac)
for ip in iface_data.get_ips():
iface.add_ip(ip)
if self.session.state == EventTypes.RUNTIME_STATE:
self.session.emane.start_iface(self, iface)
return iface

View file

@ -8,11 +8,11 @@ from core.emane import emanemodel
class EmaneRfPipeModel(emanemodel.EmaneModel): class EmaneRfPipeModel(emanemodel.EmaneModel):
# model name # model name
name = "emane_rfpipe" name: str = "emane_rfpipe"
# mac configuration # mac configuration
mac_library = "rfpipemaclayer" mac_library: str = "rfpipemaclayer"
mac_xml = "rfpipemaclayer.xml" mac_xml: str = "rfpipemaclayer.xml"
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:

View file

@ -4,6 +4,7 @@ tdma.py: EMANE TDMA model bindings for CORE
import logging import logging
import os import os
from typing import Set
from core import constants, utils from core import constants, utils
from core.config import Configuration from core.config import Configuration
@ -13,18 +14,18 @@ from core.emulator.enumerations import ConfigDataTypes
class EmaneTdmaModel(emanemodel.EmaneModel): class EmaneTdmaModel(emanemodel.EmaneModel):
# model name # model name
name = "emane_tdma" name: str = "emane_tdma"
# mac configuration # mac configuration
mac_library = "tdmaeventschedulerradiomodel" mac_library: str = "tdmaeventschedulerradiomodel"
mac_xml = "tdmaeventschedulerradiomodel.xml" mac_xml: str = "tdmaeventschedulerradiomodel.xml"
# add custom schedule options and ignore it when writing emane xml # add custom schedule options and ignore it when writing emane xml
schedule_name = "schedule" schedule_name: str = "schedule"
default_schedule = os.path.join( default_schedule: str = os.path.join(
constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml" constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml"
) )
config_ignore = {schedule_name} config_ignore: Set[str] = {schedule_name}
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:

View file

@ -6,9 +6,10 @@ import sys
from typing import Dict, List, Type from typing import Dict, List, Type
import core.services import core.services
from core import configservices from core import configservices, utils
from core.configservice.manager import ConfigServiceManager from core.configservice.manager import ConfigServiceManager
from core.emulator.session import Session from core.emulator.session import Session
from core.executables import get_requirements
from core.services.coreservices import ServiceManager from core.services.coreservices import ServiceManager
@ -65,10 +66,29 @@ class CoreEmu:
if custom_dir: if custom_dir:
self.service_manager.load(custom_dir) self.service_manager.load(custom_dir)
# check executables exist on path
self._validate_env()
# catch exit event # catch exit event
atexit.register(self.shutdown) atexit.register(self.shutdown)
def _validate_env(self) -> None:
"""
Validates executables CORE depends on exist on path.
:return: nothing
:raises core.errors.CoreError: when an executable does not exist on path
"""
use_ovs = self.config.get("ovs") == "1"
for requirement in get_requirements(use_ovs):
utils.which(requirement, required=True)
def load_services(self) -> None: def load_services(self) -> None:
"""
Loads default and custom services for use within CORE.
:return: nothing
"""
# load default services # load default services
self.service_errors = core.services.load() self.service_errors = core.services.load()

View file

@ -1,18 +1,22 @@
""" """
CORE data objects. CORE data objects.
""" """
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional, Tuple
from dataclasses import dataclass import netaddr
from typing import List, Tuple
from core import utils
from core.emulator.enumerations import ( from core.emulator.enumerations import (
EventTypes, EventTypes,
ExceptionLevels, ExceptionLevels,
LinkTypes, LinkTypes,
MessageFlags, MessageFlags,
NodeTypes,
) )
if TYPE_CHECKING:
from core.nodes.base import CoreNode, NodeBase
@dataclass @dataclass
class ConfigData: class ConfigData:
@ -27,7 +31,7 @@ class ConfigData:
possible_values: str = None possible_values: str = None
groups: str = None groups: str = None
session: int = None session: int = None
interface_number: int = None iface_id: int = None
network_id: int = None network_id: int = None
opaque: str = None opaque: str = None
@ -68,65 +72,218 @@ class FileData:
@dataclass @dataclass
class NodeData: class NodeOptions:
message_type: MessageFlags = None """
id: int = None Options for creating and updating nodes within core.
node_type: NodeTypes = None """
name: str = None name: str = None
ip_address: str = None model: Optional[str] = "PC"
mac_address: str = None
ip6_address: str = None
model: str = None
emulation_id: int = None
server: str = None
session: int = None
x_position: float = None
y_position: float = None
canvas: int = None canvas: int = None
network_id: int = None
services: List[str] = None
latitude: float = None
longitude: float = None
altitude: float = None
icon: str = None icon: str = None
opaque: str = None services: List[str] = field(default_factory=list)
config_services: List[str] = field(default_factory=list)
x: float = None
y: float = None
lat: float = None
lon: float = None
alt: float = None
server: str = None
image: str = None
emane: str = None
def set_position(self, x: float, y: float) -> None:
"""
Convenience method for setting position.
:param x: x position
:param y: y position
:return: nothing
"""
self.x = x
self.y = y
def set_location(self, lat: float, lon: float, alt: float) -> None:
"""
Convenience method for setting location.
:param lat: latitude
:param lon: longitude
:param alt: altitude
:return: nothing
"""
self.lat = lat
self.lon = lon
self.alt = alt
@dataclass
class NodeData:
"""
Node to broadcast.
"""
node: "NodeBase"
message_type: MessageFlags = None
source: str = None source: str = None
@dataclass
class InterfaceData:
"""
Convenience class for storing interface data.
"""
id: int = None
name: str = None
mac: str = None
ip4: str = None
ip4_mask: int = None
ip6: str = None
ip6_mask: int = None
def get_ips(self) -> List[str]:
"""
Returns a list of ip4 and ip6 addresses when present.
:return: list of ip addresses
"""
ips = []
if self.ip4 and self.ip4_mask:
ips.append(f"{self.ip4}/{self.ip4_mask}")
if self.ip6 and self.ip6_mask:
ips.append(f"{self.ip6}/{self.ip6_mask}")
return ips
@dataclass
class LinkOptions:
"""
Options for creating and updating links within core.
"""
delay: int = None
bandwidth: int = None
loss: float = None
dup: int = None
jitter: int = None
mer: int = None
burst: int = None
mburst: int = None
unidirectional: int = None
key: int = None
@dataclass @dataclass
class LinkData: class LinkData:
"""
Represents all data associated with a link.
"""
message_type: MessageFlags = None message_type: MessageFlags = None
type: LinkTypes = LinkTypes.WIRED
label: str = None label: str = None
node1_id: int = None node1_id: int = None
node2_id: int = None node2_id: int = None
delay: float = None
bandwidth: float = None
per: float = None
dup: float = None
jitter: float = None
mer: float = None
burst: float = None
session: int = None
mburst: float = None
link_type: LinkTypes = None
gui_attributes: str = None
unidirectional: int = None
emulation_id: int = None
network_id: int = None network_id: int = None
key: int = None iface1: InterfaceData = None
interface1_id: int = None iface2: InterfaceData = None
interface1_name: str = None options: LinkOptions = LinkOptions()
interface1_ip4: str = None
interface1_ip4_mask: int = None
interface1_mac: str = None
interface1_ip6: str = None
interface1_ip6_mask: int = None
interface2_id: int = None
interface2_name: str = None
interface2_ip4: str = None
interface2_ip4_mask: int = None
interface2_mac: str = None
interface2_ip6: str = None
interface2_ip6_mask: int = None
opaque: str = None
color: str = None color: str = None
source: str = None
class IpPrefixes:
"""
Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
"""
def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
"""
Creates an IpPrefixes object.
:param ip4_prefix: ip4 prefix to use for generation
:param ip6_prefix: ip6 prefix to use for generation
:raises ValueError: when both ip4 and ip6 prefixes have not been provided
"""
if not ip4_prefix and not ip6_prefix:
raise ValueError("ip4 or ip6 must be provided")
self.ip4 = None
if ip4_prefix:
self.ip4 = netaddr.IPNetwork(ip4_prefix)
self.ip6 = None
if ip6_prefix:
self.ip6 = netaddr.IPNetwork(ip6_prefix)
def ip4_address(self, node_id: int) -> str:
"""
Convenience method to return the IP4 address for a node.
:param node_id: node id to get IP4 address for
:return: IP4 address or None
"""
if not self.ip4:
raise ValueError("ip4 prefixes have not been set")
return str(self.ip4[node_id])
def ip6_address(self, node_id: int) -> str:
"""
Convenience method to return the IP6 address for a node.
:param node_id: node id to get IP6 address for
:return: IP4 address or None
"""
if not self.ip6:
raise ValueError("ip6 prefixes have not been set")
return str(self.ip6[node_id])
def gen_iface(self, node_id: int, name: str = None, mac: str = None):
"""
Creates interface data for linking nodes, using the nodes unique id for
generation, along with a random mac address, unless provided.
:param node_id: node id to create an interface for
:param name: name to set for interface, default is eth{id}
:param mac: mac address to use for this interface, default is random
generation
:return: new interface data for the provided node
"""
# generate ip4 data
ip4 = None
ip4_mask = None
if self.ip4:
ip4 = self.ip4_address(node_id)
ip4_mask = self.ip4.prefixlen
# generate ip6 data
ip6 = None
ip6_mask = None
if self.ip6:
ip6 = self.ip6_address(node_id)
ip6_mask = self.ip6.prefixlen
# random mac
if not mac:
mac = utils.random_mac()
return InterfaceData(
name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac
)
def create_iface(
self, node: "CoreNode", name: str = None, mac: str = None
) -> InterfaceData:
"""
Creates interface data for linking nodes, using the nodes unique id for
generation, along with a random mac address, unless provided.
:param node: node to create interface for
:param name: name to set for interface, default is eth{id}
:param mac: mac address to use for this interface, default is random
generation
:return: new interface data for the provided node
"""
iface_data = self.gen_iface(node.id, name, mac)
iface_data.id = node.next_iface_id()
return iface_data

View file

@ -14,7 +14,8 @@ from fabric import Connection
from invoke import UnexpectedExit from invoke import UnexpectedExit
from core import utils from core import utils
from core.errors import CoreCommandError from core.errors import CoreCommandError, CoreError
from core.executables import get_requirements
from core.nodes.interface import GreTap from core.nodes.interface import GreTap
from core.nodes.network import CoreNetwork, CtrlNet from core.nodes.network import CoreNetwork, CtrlNet
@ -131,8 +132,17 @@ class DistributedController:
:param name: distributed server name :param name: distributed server name
:param host: distributed server host address :param host: distributed server host address
:return: nothing :return: nothing
:raises CoreError: when there is an error validating server
""" """
server = DistributedServer(name, host) server = DistributedServer(name, host)
for requirement in get_requirements(self.session.use_ovs()):
try:
server.remote_cmd(f"which {requirement}")
except CoreCommandError:
raise CoreError(
f"server({server.name}) failed validation for "
f"command({requirement})"
)
self.servers[name] = server self.servers[name] = server
cmd = f"mkdir -p {self.session.session_dir}" cmd = f"mkdir -p {self.session.session_dir}"
server.remote_cmd(cmd) server.remote_cmd(cmd)
@ -208,7 +218,7 @@ class DistributedController:
"local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key
) )
local_tap = GreTap(session=self.session, remoteip=host, key=key) local_tap = GreTap(session=self.session, remoteip=host, key=key)
local_tap.net_client.set_interface_master(node.brname, local_tap.localname) local_tap.net_client.set_iface_master(node.brname, local_tap.localname)
# server to local # server to local
logging.info( logging.info(
@ -217,25 +227,27 @@ class DistributedController:
remote_tap = GreTap( remote_tap = GreTap(
session=self.session, remoteip=self.address, key=key, server=server session=self.session, remoteip=self.address, key=key, server=server
) )
remote_tap.net_client.set_interface_master(node.brname, remote_tap.localname) remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
# save tunnels for shutdown # save tunnels for shutdown
tunnel = (local_tap, remote_tap) tunnel = (local_tap, remote_tap)
self.tunnels[key] = tunnel self.tunnels[key] = tunnel
return tunnel return tunnel
def tunnel_key(self, n1_id: int, n2_id: int) -> int: def tunnel_key(self, node1_id: int, node2_id: int) -> int:
""" """
Compute a 32-bit key used to uniquely identify a GRE tunnel. Compute a 32-bit key used to uniquely identify a GRE tunnel.
The hash(n1num), hash(n2num) values are used, so node numbers may be The hash(n1num), hash(n2num) values are used, so node numbers may be
None or string values (used for e.g. "ctrlnet"). None or string values (used for e.g. "ctrlnet").
:param n1_id: node one id :param node1_id: node one id
:param n2_id: node two id :param node2_id: node two id
:return: tunnel key for the node pair :return: tunnel key for the node pair
""" """
logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id) logging.debug("creating tunnel key for: %s, %s", node1_id, node2_id)
key = ( key = (
(self.session.id << 16) ^ utils.hashkey(n1_id) ^ (utils.hashkey(n2_id) << 8) (self.session.id << 16)
^ utils.hashkey(node1_id)
^ (utils.hashkey(node2_id) << 8)
) )
return key & 0xFFFFFFFF return key & 0xFFFFFFFF

View file

@ -1,206 +0,0 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional
import netaddr
from core import utils
from core.emulator.enumerations import LinkTypes
if TYPE_CHECKING:
from core.nodes.base import CoreNode
@dataclass
class NodeOptions:
"""
Options for creating and updating nodes within core.
"""
name: str = None
model: Optional[str] = "PC"
canvas: int = None
icon: str = None
opaque: str = None
services: List[str] = field(default_factory=list)
config_services: List[str] = field(default_factory=list)
x: float = None
y: float = None
lat: float = None
lon: float = None
alt: float = None
emulation_id: int = None
server: str = None
image: str = None
emane: str = None
def set_position(self, x: float, y: float) -> None:
"""
Convenience method for setting position.
:param x: x position
:param y: y position
:return: nothing
"""
self.x = x
self.y = y
def set_location(self, lat: float, lon: float, alt: float) -> None:
"""
Convenience method for setting location.
:param lat: latitude
:param lon: longitude
:param alt: altitude
:return: nothing
"""
self.lat = lat
self.lon = lon
self.alt = alt
@dataclass
class LinkOptions:
"""
Options for creating and updating links within core.
"""
type: LinkTypes = LinkTypes.WIRED
session: int = None
delay: int = None
bandwidth: int = None
per: float = None
dup: int = None
jitter: int = None
mer: int = None
burst: int = None
mburst: int = None
gui_attributes: str = None
unidirectional: bool = None
emulation_id: int = None
network_id: int = None
key: int = None
opaque: str = None
@dataclass
class InterfaceData:
"""
Convenience class for storing interface data.
"""
id: int = None
name: str = None
mac: str = None
ip4: str = None
ip4_mask: int = None
ip6: str = None
ip6_mask: int = None
def get_addresses(self) -> List[str]:
"""
Returns a list of ip4 and ip6 addresses when present.
:return: list of addresses
"""
addresses = []
if self.ip4 and self.ip4_mask:
addresses.append(f"{self.ip4}/{self.ip4_mask}")
if self.ip6 and self.ip6_mask:
addresses.append(f"{self.ip6}/{self.ip6_mask}")
return addresses
class IpPrefixes:
"""
Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
"""
def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
"""
Creates an IpPrefixes object.
:param ip4_prefix: ip4 prefix to use for generation
:param ip6_prefix: ip6 prefix to use for generation
:raises ValueError: when both ip4 and ip6 prefixes have not been provided
"""
if not ip4_prefix and not ip6_prefix:
raise ValueError("ip4 or ip6 must be provided")
self.ip4 = None
if ip4_prefix:
self.ip4 = netaddr.IPNetwork(ip4_prefix)
self.ip6 = None
if ip6_prefix:
self.ip6 = netaddr.IPNetwork(ip6_prefix)
def ip4_address(self, node_id: int) -> str:
"""
Convenience method to return the IP4 address for a node.
:param node_id: node id to get IP4 address for
:return: IP4 address or None
"""
if not self.ip4:
raise ValueError("ip4 prefixes have not been set")
return str(self.ip4[node_id])
def ip6_address(self, node_id: int) -> str:
"""
Convenience method to return the IP6 address for a node.
:param node_id: node id to get IP6 address for
:return: IP4 address or None
"""
if not self.ip6:
raise ValueError("ip6 prefixes have not been set")
return str(self.ip6[node_id])
def gen_interface(self, node_id: int, name: str = None, mac: str = None):
"""
Creates interface data for linking nodes, using the nodes unique id for
generation, along with a random mac address, unless provided.
:param node_id: node id to create an interface for
:param name: name to set for interface, default is eth{id}
:param mac: mac address to use for this interface, default is random
generation
:return: new interface data for the provided node
"""
# generate ip4 data
ip4 = None
ip4_mask = None
if self.ip4:
ip4 = self.ip4_address(node_id)
ip4_mask = self.ip4.prefixlen
# generate ip6 data
ip6 = None
ip6_mask = None
if self.ip6:
ip6 = self.ip6_address(node_id)
ip6_mask = self.ip6.prefixlen
# random mac
if not mac:
mac = utils.random_mac()
return InterfaceData(
name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac
)
def create_interface(
self, node: "CoreNode", name: str = None, mac: str = None
) -> InterfaceData:
"""
Creates interface data for linking nodes, using the nodes unique id for
generation, along with a random mac address, unless provided.
:param node: node to create interface for
:param name: name to set for interface, default is eth{id}
:param mac: mac address to use for this interface, default is random
generation
:return: new interface data for the provided node
"""
interface = self.gen_interface(node.id, name, mac)
interface.id = node.newifindex()
return interface

File diff suppressed because it is too large Load diff

View file

@ -56,6 +56,9 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
default=Sdt.DEFAULT_SDT_URL, default=Sdt.DEFAULT_SDT_URL,
label="SDT3D URL", label="SDT3D URL",
), ),
Configuration(
_id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS"
),
] ]
config_type: RegisterTlvs = RegisterTlvs.UTILITY config_type: RegisterTlvs = RegisterTlvs.UTILITY

View file

@ -0,0 +1,31 @@
from typing import List
VNODED: str = "vnoded"
VCMD: str = "vcmd"
SYSCTL: str = "sysctl"
IP: str = "ip"
ETHTOOL: str = "ethtool"
TC: str = "tc"
EBTABLES: str = "ebtables"
MOUNT: str = "mount"
UMOUNT: str = "umount"
OVS_VSCTL: str = "ovs-vsctl"
COMMON_REQUIREMENTS: List[str] = [SYSCTL, IP, ETHTOOL, TC, EBTABLES, MOUNT, UMOUNT]
VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD]
OVS_REQUIREMENTS: List[str] = [OVS_VSCTL]
def get_requirements(use_ovs: bool) -> List[str]:
"""
Retrieve executable requirements needed to run CORE.
:param use_ovs: True if OVS is being used, False otherwise
:return: list of executable requirements
"""
requirements = COMMON_REQUIREMENTS
if use_ovs:
requirements += OVS_REQUIREMENTS
else:
requirements += VCMD_REQUIREMENTS
return requirements

View file

@ -3,52 +3,60 @@ import math
import tkinter as tk import tkinter as tk
from tkinter import PhotoImage, font, ttk from tkinter import PhotoImage, font, ttk
from tkinter.ttk import Progressbar from tkinter.ttk import Progressbar
from typing import Any, Dict, Optional, Type
import grpc import grpc
from core.gui import appconfig, themes from core.gui import appconfig, themes
from core.gui.appconfig import GuiConfig
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.error import ErrorDialog
from core.gui.frames.base import InfoFrameBase
from core.gui.frames.default import DefaultInfoFrame
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.menubar import Menubar from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar from core.gui.statusbar import StatusBar
from core.gui.themes import PADY
from core.gui.toolbar import Toolbar from core.gui.toolbar import Toolbar
WIDTH = 1000 WIDTH: int = 1000
HEIGHT = 800 HEIGHT: int = 800
class Application(ttk.Frame): class Application(ttk.Frame):
def __init__(self, proxy: bool) -> None: def __init__(self, proxy: bool, session_id: int = None) -> None:
super().__init__() super().__init__()
# load node icons # load node icons
NodeUtils.setup() NodeUtils.setup()
# widgets # widgets
self.menubar = None self.menubar: Optional[Menubar] = None
self.toolbar = None self.toolbar: Optional[Toolbar] = None
self.right_frame = None self.right_frame: Optional[ttk.Frame] = None
self.canvas = None self.canvas: Optional[CanvasGraph] = None
self.statusbar = None self.statusbar: Optional[StatusBar] = None
self.progress = None self.progress: Optional[Progressbar] = None
self.infobar: Optional[ttk.Frame] = None
self.info_frame: Optional[InfoFrameBase] = None
self.show_infobar: tk.BooleanVar = tk.BooleanVar(value=False)
# fonts # fonts
self.fonts_size = None self.fonts_size: Dict[str, int] = {}
self.icon_text_font = None self.icon_text_font: Optional[font.Font] = None
self.edge_font = None self.edge_font: Optional[font.Font] = None
# setup # setup
self.guiconfig = appconfig.read() self.guiconfig: GuiConfig = appconfig.read()
self.app_scale = self.guiconfig.scale self.app_scale: float = self.guiconfig.scale
self.setup_scaling() self.setup_scaling()
self.style = ttk.Style() self.style: ttk.Style = ttk.Style()
self.setup_theme() self.setup_theme()
self.core = CoreClient(self, proxy) self.core: CoreClient = CoreClient(self, proxy)
self.setup_app() self.setup_app()
self.draw() self.draw()
self.core.setup() self.core.setup(session_id)
def setup_scaling(self) -> None: def setup_scaling(self) -> None:
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
@ -111,16 +119,27 @@ class Application(ttk.Frame):
self.right_frame.rowconfigure(0, weight=1) self.right_frame.rowconfigure(0, weight=1)
self.right_frame.grid(row=0, column=1, sticky="nsew") self.right_frame.grid(row=0, column=1, sticky="nsew")
self.draw_canvas() self.draw_canvas()
self.draw_infobar()
self.draw_status() self.draw_status()
self.progress = Progressbar(self.right_frame, mode="indeterminate") self.progress = Progressbar(self.right_frame, mode="indeterminate")
self.menubar = Menubar(self) self.menubar = Menubar(self)
self.master.config(menu=self.menubar) self.master.config(menu=self.menubar)
def draw_infobar(self) -> None:
self.infobar = ttk.Frame(self.right_frame, padding=5, relief=tk.RAISED)
self.infobar.columnconfigure(0, weight=1)
self.infobar.rowconfigure(1, weight=1)
label_font = font.Font(weight=font.BOLD, underline=tk.TRUE)
label = ttk.Label(
self.infobar, text="Details", anchor=tk.CENTER, font=label_font
)
label.grid(sticky=tk.EW, pady=PADY)
def draw_canvas(self) -> None: def draw_canvas(self) -> None:
canvas_frame = ttk.Frame(self.right_frame) canvas_frame = ttk.Frame(self.right_frame)
canvas_frame.rowconfigure(0, weight=1) canvas_frame.rowconfigure(0, weight=1)
canvas_frame.columnconfigure(0, weight=1) canvas_frame.columnconfigure(0, weight=1)
canvas_frame.grid(sticky="nsew", pady=1) canvas_frame.grid(row=0, column=0, sticky="nsew", pady=1)
self.canvas = CanvasGraph(canvas_frame, self, self.core) self.canvas = CanvasGraph(canvas_frame, self, self.core)
self.canvas.grid(sticky="nsew") self.canvas.grid(sticky="nsew")
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview) scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
@ -134,7 +153,31 @@ class Application(ttk.Frame):
def draw_status(self) -> None: def draw_status(self) -> None:
self.statusbar = StatusBar(self.right_frame, self) self.statusbar = StatusBar(self.right_frame, self)
self.statusbar.grid(sticky="ew") self.statusbar.grid(sticky="ew", columnspan=2)
def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None:
if not self.show_infobar.get():
return
self.clear_info()
self.info_frame = frame_class(self.infobar, **kwargs)
self.info_frame.draw()
self.info_frame.grid(sticky="nsew")
def clear_info(self) -> None:
if self.info_frame:
self.info_frame.destroy()
self.info_frame = None
def default_info(self) -> None:
self.clear_info()
self.display_info(DefaultInfoFrame, app=self)
def show_info(self) -> None:
self.default_info()
self.infobar.grid(row=0, column=1, sticky="nsew")
def hide_info(self) -> None:
self.infobar.grid_forget()
def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None: def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
logging.exception("app grpc exception", exc_info=e) logging.exception("app grpc exception", exc_info=e)

View file

@ -1,32 +1,32 @@
import os import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import Dict, List, Optional, Type
import yaml import yaml
from core.gui import themes from core.gui import themes
HOME_PATH = Path.home().joinpath(".coregui") HOME_PATH: Path = Path.home().joinpath(".coregui")
BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") BACKGROUNDS_PATH: Path = HOME_PATH.joinpath("backgrounds")
CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") CUSTOM_EMANE_PATH: Path = HOME_PATH.joinpath("custom_emane")
CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services") CUSTOM_SERVICE_PATH: Path = HOME_PATH.joinpath("custom_services")
ICONS_PATH = HOME_PATH.joinpath("icons") ICONS_PATH: Path = HOME_PATH.joinpath("icons")
MOBILITY_PATH = HOME_PATH.joinpath("mobility") MOBILITY_PATH: Path = HOME_PATH.joinpath("mobility")
XMLS_PATH = HOME_PATH.joinpath("xmls") XMLS_PATH: Path = HOME_PATH.joinpath("xmls")
CONFIG_PATH = HOME_PATH.joinpath("config.yaml") CONFIG_PATH: Path = HOME_PATH.joinpath("config.yaml")
LOG_PATH = HOME_PATH.joinpath("gui.log") LOG_PATH: Path = HOME_PATH.joinpath("gui.log")
SCRIPT_PATH = HOME_PATH.joinpath("scripts") SCRIPT_PATH: Path = HOME_PATH.joinpath("scripts")
# local paths # local paths
DATA_PATH = Path(__file__).parent.joinpath("data") DATA_PATH: Path = Path(__file__).parent.joinpath("data")
LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute() LOCAL_ICONS_PATH: Path = DATA_PATH.joinpath("icons").absolute()
LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute() LOCAL_BACKGROUND_PATH: Path = DATA_PATH.joinpath("backgrounds").absolute()
LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute() LOCAL_XMLS_PATH: Path = DATA_PATH.joinpath("xmls").absolute()
LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute() LOCAL_MOBILITY_PATH: Path = DATA_PATH.joinpath("mobility").absolute()
# configuration data # configuration data
TERMINALS = { TERMINALS: Dict[str, str] = {
"xterm": "xterm -e", "xterm": "xterm -e",
"aterm": "aterm -e", "aterm": "aterm -e",
"eterm": "eterm -e", "eterm": "eterm -e",
@ -36,45 +36,45 @@ TERMINALS = {
"xfce4-terminal": "xfce4-terminal -x", "xfce4-terminal": "xfce4-terminal -x",
"gnome-terminal": "gnome-terminal --window --", "gnome-terminal": "gnome-terminal --window --",
} }
EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] EDITORS: List[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
class IndentDumper(yaml.Dumper): class IndentDumper(yaml.Dumper):
def increase_indent(self, flow=False, indentless=False): def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
return super().increase_indent(flow, False) super().increase_indent(flow, False)
class CustomNode(yaml.YAMLObject): class CustomNode(yaml.YAMLObject):
yaml_tag = "!CustomNode" yaml_tag: str = "!CustomNode"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__(self, name: str, image: str, services: List[str]) -> None: def __init__(self, name: str, image: str, services: List[str]) -> None:
self.name = name self.name: str = name
self.image = image self.image: str = image
self.services = services self.services: List[str] = services
class CoreServer(yaml.YAMLObject): class CoreServer(yaml.YAMLObject):
yaml_tag = "!CoreServer" yaml_tag: str = "!CoreServer"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__(self, name: str, address: str) -> None: def __init__(self, name: str, address: str) -> None:
self.name = name self.name: str = name
self.address = address self.address: str = address
class Observer(yaml.YAMLObject): class Observer(yaml.YAMLObject):
yaml_tag = "!Observer" yaml_tag: str = "!Observer"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__(self, name: str, cmd: str) -> None: def __init__(self, name: str, cmd: str) -> None:
self.name = name self.name: str = name
self.cmd = cmd self.cmd: str = cmd
class PreferencesConfig(yaml.YAMLObject): class PreferencesConfig(yaml.YAMLObject):
yaml_tag = "!PreferencesConfig" yaml_tag: str = "!PreferencesConfig"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
@ -85,17 +85,17 @@ class PreferencesConfig(yaml.YAMLObject):
width: int = 1000, width: int = 1000,
height: int = 750, height: int = 750,
) -> None: ) -> None:
self.theme = theme self.theme: str = theme
self.editor = editor self.editor: str = editor
self.terminal = terminal self.terminal: str = terminal
self.gui3d = gui3d self.gui3d: str = gui3d
self.width = width self.width: int = width
self.height = height self.height: int = height
class LocationConfig(yaml.YAMLObject): class LocationConfig(yaml.YAMLObject):
yaml_tag = "!LocationConfig" yaml_tag: str = "!LocationConfig"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
@ -107,18 +107,18 @@ class LocationConfig(yaml.YAMLObject):
alt: float = 2.0, alt: float = 2.0,
scale: float = 150.0, scale: float = 150.0,
) -> None: ) -> None:
self.x = x self.x: float = x
self.y = y self.y: float = y
self.z = z self.z: float = z
self.lat = lat self.lat: float = lat
self.lon = lon self.lon: float = lon
self.alt = alt self.alt: float = alt
self.scale = scale self.scale: float = scale
class IpConfigs(yaml.YAMLObject): class IpConfigs(yaml.YAMLObject):
yaml_tag = "!IpConfigs" yaml_tag: str = "!IpConfigs"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
@ -129,21 +129,21 @@ class IpConfigs(yaml.YAMLObject):
) -> None: ) -> None:
if ip4s is None: if ip4s is None:
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"] ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
self.ip4s = ip4s self.ip4s: List[str] = ip4s
if ip6s is None: if ip6s is None:
ip6s = ["2001::", "2002::", "a::"] ip6s = ["2001::", "2002::", "a::"]
self.ip6s = ip6s self.ip6s: List[str] = ip6s
if ip4 is None: if ip4 is None:
ip4 = self.ip4s[0] ip4 = self.ip4s[0]
self.ip4 = ip4 self.ip4: str = ip4
if ip6 is None: if ip6 is None:
ip6 = self.ip6s[0] ip6 = self.ip6s[0]
self.ip6 = ip6 self.ip6: str = ip6
class GuiConfig(yaml.YAMLObject): class GuiConfig(yaml.YAMLObject):
yaml_tag = "!GuiConfig" yaml_tag: str = "!GuiConfig"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
@ -159,30 +159,30 @@ class GuiConfig(yaml.YAMLObject):
) -> None: ) -> None:
if preferences is None: if preferences is None:
preferences = PreferencesConfig() preferences = PreferencesConfig()
self.preferences = preferences self.preferences: PreferencesConfig = preferences
if location is None: if location is None:
location = LocationConfig() location = LocationConfig()
self.location = location self.location: LocationConfig = location
if servers is None: if servers is None:
servers = [] servers = []
self.servers = servers self.servers: List[CoreServer] = servers
if nodes is None: if nodes is None:
nodes = [] nodes = []
self.nodes = nodes self.nodes: List[CustomNode] = nodes
if recentfiles is None: if recentfiles is None:
recentfiles = [] recentfiles = []
self.recentfiles = recentfiles self.recentfiles: List[str] = recentfiles
if observers is None: if observers is None:
observers = [] observers = []
self.observers = observers self.observers: List[Observer] = observers
self.scale = scale self.scale: float = scale
if ips is None: if ips is None:
ips = IpConfigs() ips = IpConfigs()
self.ips = ips self.ips: IpConfigs = ips
self.mac = mac self.mac: str = mac
def copy_files(current_path, new_path) -> None: def copy_files(current_path: Path, new_path: Path) -> None:
for current_file in current_path.glob("*"): for current_file in current_path.glob("*"):
new_file = new_path.joinpath(current_file.name) new_file = new_path.joinpath(current_file.name)
shutil.copy(current_file, new_file) shutil.copy(current_file, new_file)

View file

@ -1,21 +1,46 @@
""" """
Incorporate grpc into python tkinter GUI Incorporate grpc into python tkinter GUI
""" """
import getpass
import json import json
import logging import logging
import os import os
import tkinter as tk
from pathlib import Path from pathlib import Path
from tkinter import messagebox from tkinter import messagebox
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
import grpc import grpc
from core.api.grpc import client, common_pb2, configservices_pb2, core_pb2 from core.api.grpc import client
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.configservices_pb2 import ConfigService, ConfigServiceConfig
from core.api.grpc.core_pb2 import (
CpuUsageEvent,
Event,
ExceptionEvent,
Hook,
Interface,
Link,
LinkEvent,
LinkType,
MessageType,
Node,
NodeEvent,
NodeType,
Position,
SessionLocation,
SessionState,
StartSessionResponse,
StopSessionResponse,
ThroughputsEvent,
)
from core.api.grpc.emane_pb2 import EmaneModelConfig from core.api.grpc.emane_pb2 import EmaneModelConfig
from core.api.grpc.mobility_pb2 import MobilityConfig from core.api.grpc.mobility_pb2 import MobilityConfig
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig
from core.api.grpc.wlan_pb2 import WlanConfig from core.api.grpc.wlan_pb2 import WlanConfig
from core.gui import appconfig from core.gui import appconfig
from core.gui.appconfig import CoreServer, Observer
from core.gui.dialogs.emaneinstall import EmaneInstallDialog from core.gui.dialogs.emaneinstall import EmaneInstallDialog
from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.error import ErrorDialog
from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.mobilityplayer import MobilityPlayer
@ -31,50 +56,52 @@ if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
GUI_SOURCE = "gui" GUI_SOURCE = "gui"
CPU_USAGE_DELAY = 3
class CoreClient: class CoreClient:
def __init__(self, app: "Application", proxy: bool): def __init__(self, app: "Application", proxy: bool) -> None:
""" """
Create a CoreGrpc instance Create a CoreGrpc instance
""" """
self._client = client.CoreGrpcClient(proxy=proxy) self.app: "Application" = app
self.session_id = None self.master: tk.Tk = app.master
self.node_ids = [] self._client: client.CoreGrpcClient = client.CoreGrpcClient(proxy=proxy)
self.app = app self.session_id: Optional[int] = None
self.master = app.master self.services: Dict[str, Set[str]] = {}
self.services = {} self.config_services_groups: Dict[str, Set[str]] = {}
self.config_services_groups = {} self.config_services: Dict[str, ConfigService] = {}
self.config_services = {} self.default_services: Dict[NodeType, Set[str]] = {}
self.default_services = {} self.emane_models: List[str] = []
self.emane_models = [] self.observer: Optional[str] = None
self.observer = None self.user = getpass.getuser()
# loaded configuration data # loaded configuration data
self.servers = {} self.servers: Dict[str, CoreServer] = {}
self.custom_nodes = {} self.custom_nodes: Dict[str, NodeDraw] = {}
self.custom_observers = {} self.custom_observers: Dict[str, Observer] = {}
self.read_config() self.read_config()
# helpers # helpers
self.interface_to_edge = {} self.iface_to_edge: Dict[Tuple[int, ...], Tuple[int, ...]] = {}
self.interfaces_manager = InterfaceManager(self.app) self.ifaces_manager: InterfaceManager = InterfaceManager(self.app)
# session data # session data
self.state = None self.state: Optional[SessionState] = None
self.canvas_nodes = {} self.canvas_nodes: Dict[int, CanvasNode] = {}
self.location = None self.location: Optional[SessionLocation] = None
self.links = {} self.links: Dict[Tuple[int, int], CanvasEdge] = {}
self.hooks = {} self.hooks: Dict[str, Hook] = {}
self.emane_config = None self.emane_config: Dict[str, ConfigOption] = {}
self.mobility_players = {} self.mobility_players: Dict[int, MobilityPlayer] = {}
self.handling_throughputs = None self.handling_throughputs: Optional[grpc.Future] = None
self.handling_events = None self.handling_cpu_usage: Optional[grpc.Future] = None
self.xml_dir = None self.handling_events: Optional[grpc.Future] = None
self.xml_file = None self.xml_dir: Optional[str] = None
self.xml_file: Optional[str] = None
@property @property
def client(self): def client(self) -> client.CoreGrpcClient:
if self.session_id: if self.session_id:
response = self._client.check_session(self.session_id) response = self._client.check_session(self.session_id)
if not response.result: if not response.result:
@ -87,12 +114,13 @@ class CoreClient:
) )
if throughputs_enabled: if throughputs_enabled:
self.enable_throughputs() self.enable_throughputs()
self.setup_cpu_usage()
return self._client return self._client
def reset(self): def reset(self) -> None:
# helpers # helpers
self.interfaces_manager.reset() self.ifaces_manager.reset()
self.interface_to_edge.clear() self.iface_to_edge.clear()
# session data # session data
self.canvas_nodes.clear() self.canvas_nodes.clear()
self.links.clear() self.links.clear()
@ -104,14 +132,14 @@ class CoreClient:
self.cancel_throughputs() self.cancel_throughputs()
self.cancel_events() self.cancel_events()
def close_mobility_players(self): def close_mobility_players(self) -> None:
for mobility_player in self.mobility_players.values(): for mobility_player in self.mobility_players.values():
mobility_player.close() mobility_player.close()
def set_observer(self, value: str): def set_observer(self, value: Optional[str]) -> None:
self.observer = value self.observer = value
def read_config(self): def read_config(self) -> None:
# read distributed servers # read distributed servers
for server in self.app.guiconfig.servers: for server in self.app.guiconfig.servers:
self.servers[server.name] = server self.servers[server.name] = server
@ -125,7 +153,9 @@ class CoreClient:
for observer in self.app.guiconfig.observers: for observer in self.app.guiconfig.observers:
self.custom_observers[observer.name] = observer self.custom_observers[observer.name] = observer
def handle_events(self, event: core_pb2.Event): def handle_events(self, event: Event) -> None:
if event.source == GUI_SOURCE:
return
if event.session_id != self.session_id: if event.session_id != self.session_id:
logging.warning( logging.warning(
"ignoring event session(%s) current(%s)", "ignoring event session(%s) current(%s)",
@ -139,7 +169,7 @@ class CoreClient:
elif event.HasField("session_event"): elif event.HasField("session_event"):
logging.info("session event: %s", event) logging.info("session event: %s", event)
session_event = event.session_event session_event = event.session_event
if session_event.event <= core_pb2.SessionState.SHUTDOWN: if session_event.event <= SessionState.SHUTDOWN:
self.state = event.session_event.event self.state = event.session_event.event
elif session_event.event in {7, 8, 9}: elif session_event.event in {7, 8, 9}:
node_id = session_event.node_id node_id = session_event.node_id
@ -162,56 +192,91 @@ class CoreClient:
else: else:
logging.info("unhandled event: %s", event) logging.info("unhandled event: %s", event)
def handle_link_event(self, event: core_pb2.LinkEvent): def handle_link_event(self, event: LinkEvent) -> None:
logging.debug("Link event: %s", event) logging.debug("Link event: %s", event)
node_one_id = event.link.node_one_id node1_id = event.link.node1_id
node_two_id = event.link.node_two_id node2_id = event.link.node2_id
if node_one_id == node_two_id: if node1_id == node2_id:
logging.warning("ignoring links with loops: %s", event) logging.warning("ignoring links with loops: %s", event)
return return
canvas_node_one = self.canvas_nodes[node_one_id] canvas_node1 = self.canvas_nodes[node1_id]
canvas_node_two = self.canvas_nodes[node_two_id] canvas_node2 = self.canvas_nodes[node2_id]
if event.message_type == core_pb2.MessageType.ADD: if event.link.type == LinkType.WIRELESS:
self.app.canvas.add_wireless_edge( if event.message_type == MessageType.ADD:
canvas_node_one, canvas_node_two, event.link self.app.canvas.add_wireless_edge(
) canvas_node1, canvas_node2, event.link
elif event.message_type == core_pb2.MessageType.DELETE: )
self.app.canvas.delete_wireless_edge( elif event.message_type == MessageType.DELETE:
canvas_node_one, canvas_node_two, event.link self.app.canvas.delete_wireless_edge(
) canvas_node1, canvas_node2, event.link
elif event.message_type == core_pb2.MessageType.NONE: )
self.app.canvas.update_wireless_edge( elif event.message_type == MessageType.NONE:
canvas_node_one, canvas_node_two, event.link self.app.canvas.update_wireless_edge(
) canvas_node1, canvas_node2, event.link
)
else:
logging.warning("unknown link event: %s", event)
else: else:
logging.warning("unknown link event: %s", event) if event.message_type == MessageType.ADD:
self.app.canvas.add_wired_edge(canvas_node1, canvas_node2, event.link)
self.app.canvas.organize()
elif event.message_type == MessageType.DELETE:
self.app.canvas.delete_wired_edge(canvas_node1, canvas_node2)
elif event.message_type == MessageType.NONE:
self.app.canvas.update_wired_edge(
canvas_node1, canvas_node2, event.link
)
else:
logging.warning("unknown link event: %s", event)
def handle_node_event(self, event: core_pb2.NodeEvent): def handle_node_event(self, event: NodeEvent) -> None:
logging.debug("node event: %s", event) logging.debug("node event: %s", event)
if event.source == GUI_SOURCE: if event.message_type == MessageType.NONE:
return canvas_node = self.canvas_nodes[event.node.id]
node_id = event.node.id x = event.node.position.x
x = event.node.position.x y = event.node.position.y
y = event.node.position.y canvas_node.move(x, y)
canvas_node = self.canvas_nodes[node_id] elif event.message_type == MessageType.DELETE:
canvas_node.move(x, y) canvas_node = self.canvas_nodes[event.node.id]
self.app.canvas.clear_selection()
self.app.canvas.select_object(canvas_node.id)
self.app.canvas.delete_selected_objects()
elif event.message_type == MessageType.ADD:
self.app.canvas.add_core_node(event.node)
else:
logging.warning("unknown node event: %s", event)
def enable_throughputs(self): def enable_throughputs(self) -> None:
self.handling_throughputs = self.client.throughputs( self.handling_throughputs = self.client.throughputs(
self.session_id, self.handle_throughputs self.session_id, self.handle_throughputs
) )
def cancel_throughputs(self): def cancel_throughputs(self) -> None:
if self.handling_throughputs: if self.handling_throughputs:
self.handling_throughputs.cancel() self.handling_throughputs.cancel()
self.handling_throughputs = None self.handling_throughputs = None
self.app.canvas.clear_throughputs()
def cancel_events(self): def cancel_events(self) -> None:
if self.handling_events: if self.handling_events:
self.handling_events.cancel() self.handling_events.cancel()
self.handling_events = None self.handling_events = None
def handle_throughputs(self, event: core_pb2.ThroughputsEvent): def cancel_cpu_usage(self) -> None:
if self.handling_cpu_usage:
self.handling_cpu_usage.cancel()
self.handling_cpu_usage = None
def setup_cpu_usage(self) -> None:
if self.handling_cpu_usage and self.handling_cpu_usage.running():
return
if self.handling_cpu_usage:
self.handling_cpu_usage.cancel()
self.handling_cpu_usage = self._client.cpu_usage(
CPU_USAGE_DELAY, self.handle_cpu_event
)
def handle_throughputs(self, event: ThroughputsEvent) -> None:
if event.session_id != self.session_id: if event.session_id != self.session_id:
logging.warning( logging.warning(
"ignoring throughput event session(%s) current(%s)", "ignoring throughput event session(%s) current(%s)",
@ -222,11 +287,14 @@ class CoreClient:
logging.debug("handling throughputs event: %s", event) logging.debug("handling throughputs event: %s", event)
self.app.after(0, self.app.canvas.set_throughputs, event) self.app.after(0, self.app.canvas.set_throughputs, event)
def handle_exception_event(self, event: core_pb2.ExceptionEvent): def handle_cpu_event(self, event: CpuUsageEvent) -> None:
logging.info("exception event: %s", event) self.app.after(0, self.app.statusbar.set_cpu, event.usage)
self.app.statusbar.core_alarms.append(event)
def join_session(self, session_id: int, query_location: bool = True): def handle_exception_event(self, event: ExceptionEvent) -> None:
logging.info("exception event: %s", event)
self.app.statusbar.add_alert(event)
def join_session(self, session_id: int, query_location: bool = True) -> None:
logging.info("join session(%s)", session_id) logging.info("join session(%s)", session_id)
# update session and title # update session and title
self.session_id = session_id self.session_id = session_id
@ -244,6 +312,9 @@ class CoreClient:
self.session_id, self.handle_events self.session_id, self.handle_events
) )
# set session user
self.client.set_session_user(self.session_id, self.user)
# get session service defaults # get session service defaults
response = self.client.get_service_defaults(self.session_id) response = self.client.get_service_defaults(self.session_id)
self.default_services = { self.default_services = {
@ -269,7 +340,7 @@ class CoreClient:
self.emane_config = response.config self.emane_config = response.config
# update interface manager # update interface manager
self.interfaces_manager.joined(session.links) self.ifaces_manager.joined(session.links)
# draw session # draw session
self.app.canvas.reset_and_redraw(session) self.app.canvas.reset_and_redraw(session)
@ -284,11 +355,11 @@ class CoreClient:
# get emane model config # get emane model config
response = self.client.get_emane_model_configs(self.session_id) response = self.client.get_emane_model_configs(self.session_id)
for config in response.configs: for config in response.configs:
interface = None iface_id = None
if config.interface != -1: if config.iface_id != -1:
interface = config.interface iface_id = config.iface_id
canvas_node = self.canvas_nodes[config.node_id] canvas_node = self.canvas_nodes[config.node_id]
canvas_node.emane_model_configs[(config.model, interface)] = dict( canvas_node.emane_model_configs[(config.model, iface_id)] = dict(
config.config config.config
) )
@ -332,14 +403,15 @@ class CoreClient:
# organize canvas # organize canvas
self.app.canvas.organize() self.app.canvas.organize()
if self.is_runtime():
self.show_mobility_players()
# update ui to represent current state # update ui to represent current state
self.app.after(0, self.app.joined_session_update) self.app.after(0, self.app.joined_session_update)
def is_runtime(self) -> bool: def is_runtime(self) -> bool:
return self.state == core_pb2.SessionState.RUNTIME return self.state == SessionState.RUNTIME
def parse_metadata(self, config: Dict[str, str]): def parse_metadata(self, config: Dict[str, str]) -> None:
# canvas setting # canvas setting
canvas_config = config.get("canvas") canvas_config = config.get("canvas")
logging.debug("canvas metadata: %s", canvas_config) logging.debug("canvas metadata: %s", canvas_config)
@ -392,7 +464,7 @@ class CoreClient:
except ValueError: except ValueError:
logging.exception("unknown shape: %s", shape_type) logging.exception("unknown shape: %s", shape_type)
def create_new_session(self): def create_new_session(self) -> None:
""" """
Create a new session Create a new session
""" """
@ -400,7 +472,7 @@ class CoreClient:
response = self.client.create_session() response = self.client.create_session()
logging.info("created session: %s", response) logging.info("created session: %s", response)
location_config = self.app.guiconfig.location location_config = self.app.guiconfig.location
self.location = core_pb2.SessionLocation( self.location = SessionLocation(
x=location_config.x, x=location_config.x,
y=location_config.y, y=location_config.y,
z=location_config.z, z=location_config.z,
@ -413,7 +485,7 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("New Session Error", e) self.app.show_grpc_exception("New Session Error", e)
def delete_session(self, session_id: int = None): def delete_session(self, session_id: int = None) -> None:
if session_id is None: if session_id is None:
session_id = self.session_id session_id = self.session_id
try: try:
@ -422,12 +494,14 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Delete Session Error", e) self.app.show_grpc_exception("Delete Session Error", e)
def setup(self): def setup(self, session_id: int = None) -> None:
""" """
Query sessions, if there exist any, prompt whether to join one Query sessions, if there exist any, prompt whether to join one
""" """
try: try:
self.client.connect() self.client.connect()
self.setup_cpu_usage()
# get service information # get service information
response = self.client.get_services() response = self.client.get_services()
for service in response.services: for service in response.services:
@ -443,21 +517,33 @@ class CoreClient:
) )
group_services.add(service.name) group_services.add(service.name)
# if there are no sessions, create a new session, else join a session # join provided session, create new session, or show dialog to select an
# existing session
response = self.client.get_sessions() response = self.client.get_sessions()
sessions = response.sessions sessions = response.sessions
if len(sessions) == 0: if session_id:
self.create_new_session() session_ids = set(x.id for x in sessions)
if session_id not in session_ids:
dialog = ErrorDialog(
self.app, "Join Session Error", f"{session_id} does not exist"
)
dialog.show()
self.app.close()
else:
self.join_session(session_id)
else: else:
dialog = SessionsDialog(self.app, True) if not sessions:
dialog.show() self.create_new_session()
else:
dialog = SessionsDialog(self.app, True)
dialog.show()
except grpc.RpcError as e: except grpc.RpcError as e:
logging.exception("core setup error") logging.exception("core setup error")
dialog = ErrorDialog(self.app, "Setup Error", e.details()) dialog = ErrorDialog(self.app, "Setup Error", e.details())
dialog.show() dialog.show()
self.app.close() self.app.close()
def edit_node(self, core_node: core_pb2.Node): def edit_node(self, core_node: Node) -> None:
try: try:
self.client.edit_node( self.client.edit_node(
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
@ -465,17 +551,21 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Edit Node Error", e) self.app.show_grpc_exception("Edit Node Error", e)
def start_session(self) -> core_pb2.StartSessionResponse: def send_servers(self) -> None:
self.interfaces_manager.reset_mac() for server in self.servers.values():
self.client.add_session_server(self.session_id, server.name, server.address)
def start_session(self) -> StartSessionResponse:
self.ifaces_manager.reset_mac()
nodes = [x.core_node for x in self.canvas_nodes.values()] nodes = [x.core_node for x in self.canvas_nodes.values()]
links = [] links = []
for edge in self.links.values(): for edge in self.links.values():
link = core_pb2.Link() link = Link()
link.CopyFrom(edge.link) link.CopyFrom(edge.link)
if link.HasField("interface_one") and not link.interface_one.mac: if link.HasField("iface1") and not link.iface1.mac:
link.interface_one.mac = self.interfaces_manager.next_mac() link.iface1.mac = self.ifaces_manager.next_mac()
if link.HasField("interface_two") and not link.interface_two.mac: if link.HasField("iface2") and not link.iface2.mac:
link.interface_two.mac = self.interfaces_manager.next_mac() link.iface2.mac = self.ifaces_manager.next_mac()
links.append(link) links.append(link)
wlan_configs = self.get_wlan_configs_proto() wlan_configs = self.get_wlan_configs_proto()
mobility_configs = self.get_mobility_configs_proto() mobility_configs = self.get_mobility_configs_proto()
@ -491,8 +581,9 @@ class CoreClient:
emane_config = {x: self.emane_config[x].value for x in self.emane_config} emane_config = {x: self.emane_config[x].value for x in self.emane_config}
else: else:
emane_config = None emane_config = None
response = core_pb2.StartSessionResponse(result=False) response = StartSessionResponse(result=False)
try: try:
self.send_servers()
response = self.client.start_session( response = self.client.start_session(
self.session_id, self.session_id,
nodes, nodes,
@ -517,10 +608,10 @@ class CoreClient:
self.app.show_grpc_exception("Start Session Error", e) self.app.show_grpc_exception("Start Session Error", e)
return response return response
def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse: def stop_session(self, session_id: int = None) -> StopSessionResponse:
if not session_id: if not session_id:
session_id = self.session_id session_id = self.session_id
response = core_pb2.StopSessionResponse(result=False) response = StopSessionResponse(result=False)
try: try:
response = self.client.stop_session(session_id) response = self.client.stop_session(session_id)
logging.info("stopped session(%s), result: %s", session_id, response) logging.info("stopped session(%s), result: %s", session_id, response)
@ -528,9 +619,9 @@ class CoreClient:
self.app.show_grpc_exception("Stop Session Error", e) self.app.show_grpc_exception("Stop Session Error", e)
return response return response
def show_mobility_players(self): def show_mobility_players(self) -> None:
for canvas_node in self.canvas_nodes.values(): for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
continue continue
if canvas_node.mobility_config: if canvas_node.mobility_config:
mobility_player = MobilityPlayer( mobility_player = MobilityPlayer(
@ -540,7 +631,7 @@ class CoreClient:
self.mobility_players[node_id] = mobility_player self.mobility_players[node_id] = mobility_player
mobility_player.show() mobility_player.show()
def set_metadata(self): def set_metadata(self) -> None:
# create canvas data # create canvas data
wallpaper = None wallpaper = None
if self.app.canvas.wallpaper_file: if self.app.canvas.wallpaper_file:
@ -564,7 +655,7 @@ class CoreClient:
response = self.client.set_session_metadata(self.session_id, metadata) response = self.client.set_session_metadata(self.session_id, metadata)
logging.info("set session metadata %s, result: %s", metadata, response) logging.info("set session metadata %s, result: %s", metadata, response)
def launch_terminal(self, node_id: int): def launch_terminal(self, node_id: int) -> None:
try: try:
terminal = self.app.guiconfig.preferences.terminal terminal = self.app.guiconfig.preferences.terminal
if not terminal: if not terminal:
@ -581,12 +672,12 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Node Terminal Error", e) self.app.show_grpc_exception("Node Terminal Error", e)
def save_xml(self, file_path: str): def save_xml(self, file_path: str) -> None:
""" """
Save core session as to an xml file Save core session as to an xml file
""" """
try: try:
if self.state != core_pb2.SessionState.RUNTIME: if self.state != SessionState.RUNTIME:
logging.debug("Send session data to the daemon") logging.debug("Send session data to the daemon")
self.send_data() self.send_data()
response = self.client.save_xml(self.session_id, file_path) response = self.client.save_xml(self.session_id, file_path)
@ -594,7 +685,7 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Save XML Error", e) self.app.show_grpc_exception("Save XML Error", e)
def open_xml(self, file_path: str): def open_xml(self, file_path: str) -> None:
""" """
Open core xml Open core xml
""" """
@ -633,7 +724,8 @@ class CoreClient:
shutdown=shutdowns, shutdown=shutdowns,
) )
logging.info( logging.info(
"Set %s service for node(%s), files: %s, Startup: %s, Validation: %s, Shutdown: %s, Result: %s", "Set %s service for node(%s), files: %s, Startup: %s, "
"Validation: %s, Shutdown: %s, Result: %s",
service_name, service_name,
node_id, node_id,
files, files,
@ -662,7 +754,7 @@ class CoreClient:
def set_node_service_file( def set_node_service_file(
self, node_id: int, service_name: str, file_name: str, data: str self, node_id: int, service_name: str, file_name: str, data: str
): ) -> None:
response = self.client.set_node_service_file( response = self.client.set_node_service_file(
self.session_id, node_id, service_name, file_name, data self.session_id, node_id, service_name, file_name, data
) )
@ -675,36 +767,35 @@ class CoreClient:
response, response,
) )
def create_nodes_and_links(self): def create_nodes_and_links(self) -> None:
""" """
create nodes and links that have not been created yet create nodes and links that have not been created yet
""" """
node_protos = [x.core_node for x in self.canvas_nodes.values()] node_protos = [x.core_node for x in self.canvas_nodes.values()]
link_protos = [x.link for x in self.links.values()] link_protos = [x.link for x in self.links.values()]
if self.state != core_pb2.SessionState.DEFINITION: if self.state != SessionState.DEFINITION:
self.client.set_session_state( self.client.set_session_state(self.session_id, SessionState.DEFINITION)
self.session_id, core_pb2.SessionState.DEFINITION
)
self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) self.client.set_session_state(self.session_id, SessionState.DEFINITION)
for node_proto in node_protos: for node_proto in node_protos:
response = self.client.add_node(self.session_id, node_proto) response = self.client.add_node(self.session_id, node_proto)
logging.debug("create node: %s", response) logging.debug("create node: %s", response)
for link_proto in link_protos: for link_proto in link_protos:
response = self.client.add_link( response = self.client.add_link(
self.session_id, self.session_id,
link_proto.node_one_id, link_proto.node1_id,
link_proto.node_two_id, link_proto.node2_id,
link_proto.interface_one, link_proto.iface1,
link_proto.interface_two, link_proto.iface2,
link_proto.options, link_proto.options,
) )
logging.debug("create link: %s", response) logging.debug("create link: %s", response)
def send_data(self): def send_data(self) -> None:
""" """
send to daemon all session info, but don't start the session Send to daemon all session info, but don't start the session
""" """
self.send_servers()
self.create_nodes_and_links() self.create_nodes_and_links()
for config_proto in self.get_wlan_configs_proto(): for config_proto in self.get_wlan_configs_proto():
self.client.set_wlan_config( self.client.set_wlan_config(
@ -739,15 +830,25 @@ class CoreClient:
config_proto.node_id, config_proto.node_id,
config_proto.model, config_proto.model,
config_proto.config, config_proto.config,
config_proto.interface_id, config_proto.iface_id,
) )
if self.emane_config: if self.emane_config:
config = {x: self.emane_config[x].value for x in self.emane_config} config = {x: self.emane_config[x].value for x in self.emane_config}
self.client.set_emane_config(self.session_id, config) self.client.set_emane_config(self.session_id, config)
if self.location:
self.client.set_session_location(
self.session_id,
self.location.x,
self.location.y,
self.location.z,
self.location.lat,
self.location.lon,
self.location.alt,
self.location.scale,
)
self.set_metadata() self.set_metadata()
def close(self): def close(self) -> None:
""" """
Clean ups when done using grpc Clean ups when done using grpc
""" """
@ -766,31 +867,31 @@ class CoreClient:
return i return i
def create_node( def create_node(
self, x: float, y: float, node_type: core_pb2.NodeType, model: str self, x: float, y: float, node_type: NodeType, model: str
) -> Optional[core_pb2.Node]: ) -> Optional[Node]:
""" """
Add node, with information filled in, to grpc manager Add node, with information filled in, to grpc manager
""" """
node_id = self.next_node_id() node_id = self.next_node_id()
position = core_pb2.Position(x=x, y=y) position = Position(x=x, y=y)
image = None image = None
if NodeUtils.is_image_node(node_type): if NodeUtils.is_image_node(node_type):
image = "ubuntu:latest" image = "ubuntu:latest"
emane = None emane = None
if node_type == core_pb2.NodeType.EMANE: if node_type == NodeType.EMANE:
if not self.emane_models: if not self.emane_models:
dialog = EmaneInstallDialog(self.app) dialog = EmaneInstallDialog(self.app)
dialog.show() dialog.show()
return return
emane = self.emane_models[0] emane = self.emane_models[0]
name = f"EMANE{node_id}" name = f"EMANE{node_id}"
elif node_type == core_pb2.NodeType.WIRELESS_LAN: elif node_type == NodeType.WIRELESS_LAN:
name = f"WLAN{node_id}" name = f"WLAN{node_id}"
elif node_type in [core_pb2.NodeType.RJ45, core_pb2.NodeType.TUNNEL]: elif node_type in [NodeType.RJ45, NodeType.TUNNEL]:
name = "UNASSIGNED" name = "UNASSIGNED"
else: else:
name = f"n{node_id}" name = f"n{node_id}"
node = core_pb2.Node( node = Node(
id=node_id, id=node_id,
type=node_type, type=node_type,
name=name, name=name,
@ -816,7 +917,7 @@ class CoreClient:
) )
return node return node
def deleted_graph_nodes(self, canvas_nodes: List[core_pb2.Node]): def deleted_graph_nodes(self, canvas_nodes: List[Node]) -> None:
""" """
remove the nodes selected by the user and anything related to that node remove the nodes selected by the user and anything related to that node
such as link, configurations, interfaces such as link, configurations, interfaces
@ -830,35 +931,35 @@ class CoreClient:
for edge in edges: for edge in edges:
del self.links[edge.token] del self.links[edge.token]
links.append(edge.link) links.append(edge.link)
self.interfaces_manager.removed(links) self.ifaces_manager.removed(links)
def create_interface(self, canvas_node: CanvasNode) -> core_pb2.Interface: def create_iface(self, canvas_node: CanvasNode) -> Interface:
node = canvas_node.core_node node = canvas_node.core_node
ip4, ip6 = self.interfaces_manager.get_ips(node) ip4, ip6 = self.ifaces_manager.get_ips(node)
ip4_mask = self.interfaces_manager.ip4_mask ip4_mask = self.ifaces_manager.ip4_mask
ip6_mask = self.interfaces_manager.ip6_mask ip6_mask = self.ifaces_manager.ip6_mask
interface_id = canvas_node.next_interface_id() iface_id = canvas_node.next_iface_id()
name = f"eth{interface_id}" name = f"eth{iface_id}"
interface = core_pb2.Interface( iface = Interface(
id=interface_id, id=iface_id,
name=name, name=name,
ip4=ip4, ip4=ip4,
ip4mask=ip4_mask, ip4_mask=ip4_mask,
ip6=ip6, ip6=ip6,
ip6mask=ip6_mask, ip6_mask=ip6_mask,
) )
logging.info( logging.info(
"create node(%s) interface(%s) IPv4(%s) IPv6(%s)", "create node(%s) interface(%s) IPv4(%s) IPv6(%s)",
node.name, node.name,
interface.name, iface.name,
interface.ip4, iface.ip4,
interface.ip6, iface.ip6,
) )
return interface return iface
def create_link( def create_link(
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
): ) -> None:
""" """
Create core link for a pair of canvas nodes, with token referencing Create core link for a pair of canvas nodes, with token referencing
the canvas edge. the canvas edge.
@ -867,34 +968,34 @@ class CoreClient:
dst_node = canvas_dst_node.core_node dst_node = canvas_dst_node.core_node
# determine subnet # determine subnet
self.interfaces_manager.determine_subnets(canvas_src_node, canvas_dst_node) self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)
src_interface = None src_iface = None
if NodeUtils.is_container_node(src_node.type): if NodeUtils.is_container_node(src_node.type):
src_interface = self.create_interface(canvas_src_node) src_iface = self.create_iface(canvas_src_node)
self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token self.iface_to_edge[(src_node.id, src_iface.id)] = edge.token
dst_interface = None dst_iface = None
if NodeUtils.is_container_node(dst_node.type): if NodeUtils.is_container_node(dst_node.type):
dst_interface = self.create_interface(canvas_dst_node) dst_iface = self.create_iface(canvas_dst_node)
self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token
link = core_pb2.Link( link = Link(
type=core_pb2.LinkType.WIRED, type=LinkType.WIRED,
node_one_id=src_node.id, node1_id=src_node.id,
node_two_id=dst_node.id, node2_id=dst_node.id,
interface_one=src_interface, iface1=src_iface,
interface_two=dst_interface, iface2=dst_iface,
) )
# assign after creating link proto, since interfaces are copied # assign after creating link proto, since interfaces are copied
if src_interface: if src_iface:
interface_one = link.interface_one iface1 = link.iface1
edge.src_interface = interface_one edge.src_iface = iface1
canvas_src_node.interfaces[interface_one.id] = interface_one canvas_src_node.ifaces[iface1.id] = iface1
if dst_interface: if dst_iface:
interface_two = link.interface_two iface2 = link.iface2
edge.dst_interface = interface_two edge.dst_iface = iface2
canvas_dst_node.interfaces[interface_two.id] = interface_two canvas_dst_node.ifaces[iface2.id] = iface2
edge.set_link(link) edge.set_link(link)
self.links[edge.token] = edge self.links[edge.token] = edge
logging.info("Add link between %s and %s", src_node.name, dst_node.name) logging.info("Add link between %s and %s", src_node.name, dst_node.name)
@ -902,7 +1003,7 @@ class CoreClient:
def get_wlan_configs_proto(self) -> List[WlanConfig]: def get_wlan_configs_proto(self) -> List[WlanConfig]:
configs = [] configs = []
for canvas_node in self.canvas_nodes.values(): for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
continue continue
if not canvas_node.wlan_config: if not canvas_node.wlan_config:
continue continue
@ -916,7 +1017,7 @@ class CoreClient:
def get_mobility_configs_proto(self) -> List[MobilityConfig]: def get_mobility_configs_proto(self) -> List[MobilityConfig]:
configs = [] configs = []
for canvas_node in self.canvas_nodes.values(): for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
continue continue
if not canvas_node.mobility_config: if not canvas_node.mobility_config:
continue continue
@ -930,16 +1031,16 @@ class CoreClient:
def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]: def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]:
configs = [] configs = []
for canvas_node in self.canvas_nodes.values(): for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.EMANE: if canvas_node.core_node.type != NodeType.EMANE:
continue continue
node_id = canvas_node.core_node.id node_id = canvas_node.core_node.id
for key, config in canvas_node.emane_model_configs.items(): for key, config in canvas_node.emane_model_configs.items():
model, interface = key model, iface_id = key
config = {x: config[x].value for x in config} config = {x: config[x].value for x in config}
if interface is None: if iface_id is None:
interface = -1 iface_id = -1
config_proto = EmaneModelConfig( config_proto = EmaneModelConfig(
node_id=node_id, interface_id=interface, model=model, config=config node_id=node_id, iface_id=iface_id, model=model, config=config
) )
configs.append(config_proto) configs.append(config_proto)
return configs return configs
@ -981,9 +1082,7 @@ class CoreClient:
configs.append(config_proto) configs.append(config_proto)
return configs return configs
def get_config_service_configs_proto( def get_config_service_configs_proto(self) -> List[ConfigServiceConfig]:
self
) -> List[configservices_pb2.ConfigServiceConfig]:
config_service_protos = [] config_service_protos = []
for canvas_node in self.canvas_nodes.values(): for canvas_node in self.canvas_nodes.values():
if not NodeUtils.is_container_node(canvas_node.core_node.type): if not NodeUtils.is_container_node(canvas_node.core_node.type):
@ -993,7 +1092,7 @@ class CoreClient:
node_id = canvas_node.core_node.id node_id = canvas_node.core_node.id
for name, service_config in canvas_node.config_service_configs.items(): for name, service_config in canvas_node.config_service_configs.items():
config = service_config.get("config", {}) config = service_config.get("config", {})
config_proto = configservices_pb2.ConfigServiceConfig( config_proto = ConfigServiceConfig(
node_id=node_id, node_id=node_id,
name=name, name=name,
templates=service_config["templates"], templates=service_config["templates"],
@ -1006,7 +1105,7 @@ class CoreClient:
logging.info("running node(%s) cmd: %s", node_id, self.observer) logging.info("running node(%s) cmd: %s", node_id, self.observer)
return self.client.node_command(self.session_id, node_id, self.observer).output return self.client.node_command(self.session_id, node_id, self.observer).output
def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]: def get_wlan_config(self, node_id: int) -> Dict[str, ConfigOption]:
response = self.client.get_wlan_config(self.session_id, node_id) response = self.client.get_wlan_config(self.session_id, node_id)
config = response.config config = response.config
logging.debug( logging.debug(
@ -1016,7 +1115,7 @@ class CoreClient:
) )
return dict(config) return dict(config)
def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]: def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]:
response = self.client.get_mobility_config(self.session_id, node_id) response = self.client.get_mobility_config(self.session_id, node_id)
config = response.config config = response.config
logging.debug( logging.debug(
@ -1027,24 +1126,25 @@ class CoreClient:
return dict(config) return dict(config)
def get_emane_model_config( def get_emane_model_config(
self, node_id: int, model: str, interface: int = None self, node_id: int, model: str, iface_id: int = None
) -> Dict[str, common_pb2.ConfigOption]: ) -> Dict[str, ConfigOption]:
if interface is None: if iface_id is None:
interface = -1 iface_id = -1
response = self.client.get_emane_model_config( response = self.client.get_emane_model_config(
self.session_id, node_id, model, interface self.session_id, node_id, model, iface_id
) )
config = response.config config = response.config
logging.debug( logging.debug(
"get emane model config: node id: %s, EMANE model: %s, interface: %s, config: %s", "get emane model config: node id: %s, EMANE model: %s, "
"interface: %s, config: %s",
node_id, node_id,
model, model,
interface, iface_id,
config, config,
) )
return dict(config) return dict(config)
def execute_script(self, script): def execute_script(self, script) -> None:
response = self.client.execute_script(script) response = self.client.execute_script(script)
logging.info("execute python script %s", response) logging.info("execute python script %s", response)
if response.session_id != -1: if response.session_id != -1:

View file

@ -188,7 +188,7 @@
<configuration name="error" value="0"/> <configuration name="error" value="0"/>
</mobility_configuration> </mobility_configuration>
<mobility_configuration node="10" model="ns2script"> <mobility_configuration node="10" model="ns2script">
<configuration name="file" value="/home/developer/.coretk/mobility/sample1.scen"/> <configuration name="file" value="sample1.scen"/>
<configuration name="refresh_ms" value="50"/> <configuration name="refresh_ms" value="50"/>
<configuration name="loop" value="1"/> <configuration name="loop" value="1"/>
<configuration name="autostart" value="5"/> <configuration name="autostart" value="5"/>

View file

@ -35,11 +35,11 @@ THE POSSIBILITY OF SUCH DAMAGE.\
class AboutDialog(Dialog): class AboutDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "About CORE") super().__init__(app, "About CORE")
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)

View file

@ -3,9 +3,9 @@ check engine light
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
from core.api.grpc.core_pb2 import ExceptionLevel from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import CodeText from core.gui.widgets import CodeText
@ -15,14 +15,14 @@ if TYPE_CHECKING:
class AlertsDialog(Dialog): class AlertsDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Alerts") super().__init__(app, "Alerts")
self.tree = None self.tree: Optional[ttk.Treeview] = None
self.codetext = None self.codetext: Optional[CodeText] = None
self.alarm_map = {} self.alarm_map: Dict[int, ExceptionEvent] = {}
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1) self.top.rowconfigure(1, weight=1)
@ -52,6 +52,7 @@ class AlertsDialog(Dialog):
for alarm in self.app.statusbar.core_alarms: for alarm in self.app.statusbar.core_alarms:
exception = alarm.exception_event exception = alarm.exception_event
level_name = ExceptionLevel.Enum.Name(exception.level) level_name = ExceptionLevel.Enum.Name(exception.level)
node_id = exception.node_id if exception.node_id else ""
insert_id = self.tree.insert( insert_id = self.tree.insert(
"", "",
tk.END, tk.END,
@ -60,7 +61,7 @@ class AlertsDialog(Dialog):
exception.date, exception.date,
level_name, level_name,
alarm.session_id, alarm.session_id,
exception.node_id, node_id,
exception.source, exception.source,
), ),
tags=(level_name,), tags=(level_name,),
@ -97,16 +98,18 @@ class AlertsDialog(Dialog):
button = ttk.Button(frame, text="Close", command=self.destroy) button = ttk.Button(frame, text="Close", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def reset_alerts(self): def reset_alerts(self) -> None:
self.codetext.text.delete("1.0", tk.END) self.codetext.text.config(state=tk.NORMAL)
self.codetext.text.delete(1.0, tk.END)
self.codetext.text.config(state=tk.DISABLED)
for item in self.tree.get_children(): for item in self.tree.get_children():
self.tree.delete(item) self.tree.delete(item)
self.app.statusbar.core_alarms.clear() self.app.statusbar.clear_alerts()
def click_select(self, event: tk.Event): def click_select(self, event: tk.Event) -> None:
current = self.tree.selection()[0] current = self.tree.selection()[0]
alarm = self.alarm_map[current] alarm = self.alarm_map[current]
self.codetext.text.config(state=tk.NORMAL) self.codetext.text.config(state=tk.NORMAL)
self.codetext.text.delete("1.0", "end") self.codetext.text.delete(1.0, tk.END)
self.codetext.text.insert("1.0", alarm.exception_event.text) self.codetext.text.insert(1.0, alarm.exception_event.text)
self.codetext.text.config(state=tk.DISABLED) self.codetext.text.config(state=tk.DISABLED)

View file

@ -7,38 +7,43 @@ from typing import TYPE_CHECKING
from core.gui import validation from core.gui import validation
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
PIXEL_SCALE = 100 PIXEL_SCALE: int = 100
class SizeAndScaleDialog(Dialog): class SizeAndScaleDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
""" """
create an instance for size and scale object create an instance for size and scale object
""" """
super().__init__(app, "Canvas Size and Scale") super().__init__(app, "Canvas Size and Scale")
self.canvas = self.app.canvas self.canvas: CanvasGraph = self.app.canvas
self.section_font = font.Font(weight="bold") self.section_font: font.Font = font.Font(weight="bold")
width, height = self.canvas.current_dimensions width, height = self.canvas.current_dimensions
self.pixel_width = tk.IntVar(value=width) self.pixel_width: tk.IntVar = tk.IntVar(value=width)
self.pixel_height = tk.IntVar(value=height) self.pixel_height: tk.IntVar = tk.IntVar(value=height)
location = self.app.core.location location = self.app.core.location
self.x = tk.DoubleVar(value=location.x) self.x: tk.DoubleVar = tk.DoubleVar(value=location.x)
self.y = tk.DoubleVar(value=location.y) self.y: tk.DoubleVar = tk.DoubleVar(value=location.y)
self.lat = tk.DoubleVar(value=location.lat) self.lat: tk.DoubleVar = tk.DoubleVar(value=location.lat)
self.lon = tk.DoubleVar(value=location.lon) self.lon: tk.DoubleVar = tk.DoubleVar(value=location.lon)
self.alt = tk.DoubleVar(value=location.alt) self.alt: tk.DoubleVar = tk.DoubleVar(value=location.alt)
self.scale = tk.DoubleVar(value=location.scale) self.scale: tk.DoubleVar = tk.DoubleVar(value=location.scale)
self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale) self.meters_width: tk.IntVar = tk.IntVar(
self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale) value=width / PIXEL_SCALE * location.scale
self.save_default = tk.BooleanVar(value=False) )
self.meters_height: tk.IntVar = tk.IntVar(
value=height / PIXEL_SCALE * location.scale
)
self.save_default: tk.BooleanVar = tk.BooleanVar(value=False)
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw_size() self.draw_size()
self.draw_scale() self.draw_scale()
@ -47,7 +52,7 @@ class SizeAndScaleDialog(Dialog):
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_size(self): def draw_size(self) -> None:
label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD)
label_frame.grid(sticky="ew") label_frame.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
@ -61,10 +66,12 @@ class SizeAndScaleDialog(Dialog):
label.grid(row=0, column=0, sticky="w", padx=PADX) label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width) entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
entry.grid(row=0, column=1, sticky="ew", padx=PADX) entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
label = ttk.Label(frame, text="x Height") label = ttk.Label(frame, text="x Height")
label.grid(row=0, column=2, sticky="w", padx=PADX) label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height) entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
entry.grid(row=0, column=3, sticky="ew", padx=PADX) entry.grid(row=0, column=3, sticky="ew", padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
label = ttk.Label(frame, text="Pixels") label = ttk.Label(frame, text="Pixels")
label.grid(row=0, column=4, sticky="w") label.grid(row=0, column=4, sticky="w")
@ -75,16 +82,20 @@ class SizeAndScaleDialog(Dialog):
frame.columnconfigure(3, weight=1) frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width") label = ttk.Label(frame, text="Width")
label.grid(row=0, column=0, sticky="w", padx=PADX) label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_width) entry = validation.PositiveFloatEntry(
frame, textvariable=self.meters_width, state=tk.DISABLED
)
entry.grid(row=0, column=1, sticky="ew", padx=PADX) entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="x Height") label = ttk.Label(frame, text="x Height")
label.grid(row=0, column=2, sticky="w", padx=PADX) label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_height) entry = validation.PositiveFloatEntry(
frame, textvariable=self.meters_height, state=tk.DISABLED
)
entry.grid(row=0, column=3, sticky="ew", padx=PADX) entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Meters") label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=4, sticky="w") label.grid(row=0, column=4, sticky="w")
def draw_scale(self): def draw_scale(self) -> None:
label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD)
label_frame.grid(sticky="ew") label_frame.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
@ -96,10 +107,11 @@ class SizeAndScaleDialog(Dialog):
label.grid(row=0, column=0, sticky="w", padx=PADX) label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = validation.PositiveFloatEntry(frame, textvariable=self.scale) entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
entry.grid(row=0, column=1, sticky="ew", padx=PADX) entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
label = ttk.Label(frame, text="Meters") label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=2, sticky="w") label.grid(row=0, column=2, sticky="w")
def draw_reference_point(self): def draw_reference_point(self) -> None:
label_frame = ttk.Labelframe( label_frame = ttk.Labelframe(
self.top, text="Reference Point", padding=FRAME_PAD self.top, text="Reference Point", padding=FRAME_PAD
) )
@ -150,13 +162,13 @@ class SizeAndScaleDialog(Dialog):
entry = validation.FloatEntry(frame, textvariable=self.alt) entry = validation.FloatEntry(frame, textvariable=self.alt)
entry.grid(row=0, column=5, sticky="ew") entry.grid(row=0, column=5, sticky="ew")
def draw_save_as_default(self): def draw_save_as_default(self) -> None:
button = ttk.Checkbutton( button = ttk.Checkbutton(
self.top, text="Save as default?", variable=self.save_default self.top, text="Save as default?", variable=self.save_default
) )
button.grid(sticky="w", pady=PADY) button.grid(sticky="w", pady=PADY)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -168,7 +180,14 @@ class SizeAndScaleDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_apply(self): def size_scale_keyup(self, _event: tk.Event) -> None:
scale = self.scale.get()
width = self.pixel_width.get()
height = self.pixel_height.get()
self.meters_width.set(width / PIXEL_SCALE * scale)
self.meters_height.set(height / PIXEL_SCALE * scale)
def click_apply(self) -> None:
width, height = self.pixel_width.get(), self.pixel_height.get() width, height = self.pixel_width.get(), self.pixel_height.get()
self.canvas.redraw_canvas((width, height)) self.canvas.redraw_canvas((width, height))
if self.canvas.wallpaper: if self.canvas.wallpaper:

View file

@ -4,10 +4,11 @@ set wallpaper
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, List, Optional
from core.gui.appconfig import BACKGROUNDS_PATH from core.gui.appconfig import BACKGROUNDS_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.images import Images from core.gui.images import Images
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import image_chooser from core.gui.widgets import image_chooser
@ -17,20 +18,22 @@ if TYPE_CHECKING:
class CanvasWallpaperDialog(Dialog): class CanvasWallpaperDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
""" """
create an instance of CanvasWallpaper object create an instance of CanvasWallpaper object
""" """
super().__init__(app, "Canvas Background") super().__init__(app, "Canvas Background")
self.canvas = self.app.canvas self.canvas: CanvasGraph = self.app.canvas
self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) self.scale_option: tk.IntVar = tk.IntVar(value=self.canvas.scale_option.get())
self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get()) self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(
self.filename = tk.StringVar(value=self.canvas.wallpaper_file) value=self.canvas.adjust_to_dim.get()
self.image_label = None )
self.options = [] self.filename: tk.StringVar = tk.StringVar(value=self.canvas.wallpaper_file)
self.image_label: Optional[ttk.Label] = None
self.options: List[ttk.Radiobutton] = []
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw_image() self.draw_image()
self.draw_image_label() self.draw_image_label()
@ -40,19 +43,19 @@ class CanvasWallpaperDialog(Dialog):
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_image(self): def draw_image(self) -> None:
self.image_label = ttk.Label( self.image_label = ttk.Label(
self.top, text="(image preview)", width=32, anchor=tk.CENTER self.top, text="(image preview)", width=32, anchor=tk.CENTER
) )
self.image_label.grid(pady=PADY) self.image_label.grid(pady=PADY)
def draw_image_label(self): def draw_image_label(self) -> None:
label = ttk.Label(self.top, text="Image filename: ") label = ttk.Label(self.top, text="Image filename: ")
label.grid(sticky="ew") label.grid(sticky="ew")
if self.filename.get(): if self.filename.get():
self.draw_preview() self.draw_preview()
def draw_image_selection(self): def draw_image_selection(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=2) frame.columnconfigure(0, weight=2)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -69,7 +72,7 @@ class CanvasWallpaperDialog(Dialog):
button = ttk.Button(frame, text="Clear", command=self.click_clear) button = ttk.Button(frame, text="Clear", command=self.click_clear)
button.grid(row=0, column=2, sticky="ew") button.grid(row=0, column=2, sticky="ew")
def draw_options(self): def draw_options(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -101,7 +104,7 @@ class CanvasWallpaperDialog(Dialog):
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
self.options.append(button) self.options.append(button)
def draw_additional_options(self): def draw_additional_options(self) -> None:
checkbutton = ttk.Checkbutton( checkbutton = ttk.Checkbutton(
self.top, self.top,
text="Adjust canvas size to image dimensions", text="Adjust canvas size to image dimensions",
@ -110,7 +113,7 @@ class CanvasWallpaperDialog(Dialog):
) )
checkbutton.grid(sticky="ew", padx=PADX) checkbutton.grid(sticky="ew", padx=PADX)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(pady=PADY, sticky="ew") frame.grid(pady=PADY, sticky="ew")
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
@ -122,18 +125,18 @@ class CanvasWallpaperDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_open_image(self): def click_open_image(self) -> None:
filename = image_chooser(self, BACKGROUNDS_PATH) filename = image_chooser(self, BACKGROUNDS_PATH)
if filename: if filename:
self.filename.set(filename) self.filename.set(filename)
self.draw_preview() self.draw_preview()
def draw_preview(self): def draw_preview(self) -> None:
image = Images.create(self.filename.get(), 250, 135) image = Images.create(self.filename.get(), 250, 135)
self.image_label.config(image=image) self.image_label.config(image=image)
self.image_label.image = image self.image_label.image = image
def click_clear(self): def click_clear(self) -> None:
""" """
delete like shown in image link entry if there is any delete like shown in image link entry if there is any
""" """
@ -143,7 +146,7 @@ class CanvasWallpaperDialog(Dialog):
self.image_label.config(image="", width=32) self.image_label.config(image="", width=32)
self.image_label.image = None self.image_label.image = None
def click_adjust_canvas(self): def click_adjust_canvas(self) -> None:
# deselect all radio buttons and grey them out # deselect all radio buttons and grey them out
if self.adjust_to_dim.get(): if self.adjust_to_dim.get():
self.scale_option.set(0) self.scale_option.set(0)
@ -155,7 +158,7 @@ class CanvasWallpaperDialog(Dialog):
for option in self.options: for option in self.options:
option.config(state=tk.NORMAL) option.config(state=tk.NORMAL)
def click_apply(self): def click_apply(self) -> None:
self.canvas.scale_option.set(self.scale_option.get()) self.canvas.scale_option.set(self.scale_option.get())
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
self.canvas.show_grid.click_handler() self.canvas.show_grid.click_handler()

View file

@ -3,7 +3,7 @@ custom color picker
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional, Tuple
from core.gui import validation from core.gui import validation
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -18,23 +18,23 @@ class ColorPickerDialog(Dialog):
self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000" self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000"
): ):
super().__init__(app, "Color Picker", master=master) super().__init__(app, "Color Picker", master=master)
self.red_entry = None self.red_entry: Optional[validation.RgbEntry] = None
self.blue_entry = None self.blue_entry: Optional[validation.RgbEntry] = None
self.green_entry = None self.green_entry: Optional[validation.RgbEntry] = None
self.hex_entry = None self.hex_entry: Optional[validation.HexEntry] = None
self.red_label = None self.red_label: Optional[ttk.Label] = None
self.green_label = None self.green_label: Optional[ttk.Label] = None
self.blue_label = None self.blue_label: Optional[ttk.Label] = None
self.display = None self.display: Optional[tk.Frame] = None
self.color = initcolor self.color: str = initcolor
red, green, blue = self.get_rgb(initcolor) red, green, blue = self.get_rgb(initcolor)
self.red = tk.IntVar(value=red) self.red: tk.IntVar = tk.IntVar(value=red)
self.blue = tk.IntVar(value=blue) self.blue: tk.IntVar = tk.IntVar(value=blue)
self.green = tk.IntVar(value=green) self.green: tk.IntVar = tk.IntVar(value=green)
self.hex = tk.StringVar(value=initcolor) self.hex: tk.StringVar = tk.StringVar(value=initcolor)
self.red_scale = tk.IntVar(value=red) self.red_scale: tk.IntVar = tk.IntVar(value=red)
self.green_scale = tk.IntVar(value=green) self.green_scale: tk.IntVar = tk.IntVar(value=green)
self.blue_scale = tk.IntVar(value=blue) self.blue_scale: tk.IntVar = tk.IntVar(value=blue)
self.draw() self.draw()
self.set_bindings() self.set_bindings()
@ -42,7 +42,7 @@ class ColorPickerDialog(Dialog):
self.show() self.show()
return self.color return self.color
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(3, weight=1) self.top.rowconfigure(3, weight=1)
@ -136,7 +136,7 @@ class ColorPickerDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def set_bindings(self): def set_bindings(self) -> None:
self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb")) self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
self.green_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb")) self.green_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
self.blue_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb")) self.blue_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
@ -146,7 +146,7 @@ class ColorPickerDialog(Dialog):
self.blue.trace_add("write", self.update_color) self.blue.trace_add("write", self.update_color)
self.hex.trace_add("write", self.update_color) self.hex.trace_add("write", self.update_color)
def button_ok(self): def button_ok(self) -> None:
self.color = self.hex.get() self.color = self.hex.get()
self.destroy() self.destroy()
@ -159,10 +159,10 @@ class ColorPickerDialog(Dialog):
green = self.green_entry.get() green = self.green_entry.get()
return "#%02x%02x%02x" % (int(red), int(green), int(blue)) return "#%02x%02x%02x" % (int(red), int(green), int(blue))
def current_focus(self, focus: str): def current_focus(self, focus: str) -> None:
self.focus = focus self.focus = focus
def update_color(self, arg1=None, arg2=None, arg3=None): def update_color(self, arg1=None, arg2=None, arg3=None) -> None:
if self.focus == "rgb": if self.focus == "rgb":
red = self.red_entry.get() red = self.red_entry.get()
blue = self.blue_entry.get() blue = self.blue_entry.get()
@ -184,7 +184,7 @@ class ColorPickerDialog(Dialog):
self.display.config(background=hex_code) self.display.config(background=hex_code)
self.set_label(str(red), str(green), str(blue)) self.set_label(str(red), str(green), str(blue))
def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar): def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar) -> None:
color_var.set(var.get()) color_var.set(var.get())
self.focus = "rgb" self.focus = "rgb"
self.update_color() self.update_color()
@ -194,17 +194,17 @@ class ColorPickerDialog(Dialog):
self.green_scale.set(green) self.green_scale.set(green)
self.blue_scale.set(blue) self.blue_scale.set(blue)
def set_entry(self, red: int, green: int, blue: int): def set_entry(self, red: int, green: int, blue: int) -> None:
self.red.set(red) self.red.set(red)
self.green.set(green) self.green.set(green)
self.blue.set(blue) self.blue.set(blue)
def set_label(self, red: str, green: str, blue: str): def set_label(self, red: str, green: str, blue: str) -> None:
self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0)) self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0))
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0)) self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue))) self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue)))
def get_rgb(self, hex_code: str) -> [int, int, int]: def get_rgb(self, hex_code: str) -> Tuple[int, int, int]:
""" """
convert a valid hex code to RGB values convert a valid hex code to RGB values
""" """

View file

@ -4,10 +4,11 @@ Service configuration dialog
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, List from typing import TYPE_CHECKING, Dict, List, Optional, Set
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.services_pb2 import ServiceValidationMode from core.api.grpc.services_pb2 import ServiceValidationMode
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
@ -16,6 +17,7 @@ from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
from core.gui.coreclient import CoreClient
class ConfigServiceConfigDialog(Dialog): class ConfigServiceConfigDialog(Dialog):
@ -26,56 +28,53 @@ class ConfigServiceConfigDialog(Dialog):
service_name: str, service_name: str,
canvas_node: "CanvasNode", canvas_node: "CanvasNode",
node_id: int, node_id: int,
): ) -> None:
title = f"{service_name} Config Service" title = f"{service_name} Config Service"
super().__init__(app, title, master=master) super().__init__(app, title, master=master)
self.core = app.core self.core: "CoreClient" = app.core
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node_id = node_id self.node_id: int = node_id
self.service_name = service_name self.service_name: str = service_name
self.radiovar = tk.IntVar() self.radiovar: tk.IntVar = tk.IntVar()
self.radiovar.set(2) self.radiovar.set(2)
self.directories = [] self.directories: List[str] = []
self.templates = [] self.templates: List[str] = []
self.dependencies = [] self.dependencies: List[str] = []
self.executables = [] self.executables: List[str] = []
self.startup_commands = [] self.startup_commands: List[str] = []
self.validation_commands = [] self.validation_commands: List[str] = []
self.shutdown_commands = [] self.shutdown_commands: List[str] = []
self.default_startup = [] self.default_startup: List[str] = []
self.default_validate = [] self.default_validate: List[str] = []
self.default_shutdown = [] self.default_shutdown: List[str] = []
self.validation_mode = None self.validation_mode: Optional[ServiceValidationMode] = None
self.validation_time = None self.validation_time: Optional[int] = None
self.validation_period = tk.StringVar() self.validation_period: tk.StringVar = tk.StringVar()
self.modes = [] self.modes: List[str] = []
self.mode_configs = {} self.mode_configs: Dict[str, str] = {}
self.notebook = None
self.templates_combobox = None
self.modes_combobox = None
self.startup_commands_listbox = None
self.shutdown_commands_listbox = None
self.validate_commands_listbox = None
self.validation_time_entry = None
self.validation_mode_entry = None
self.template_text = None
self.validation_period_entry = None
self.original_service_files = {}
self.temp_service_files = {}
self.modified_files = set()
self.config_frame = None
self.default_config = None
self.config = None
self.has_error = False
self.notebook: Optional[ttk.Notebook] = None
self.templates_combobox: Optional[ttk.Combobox] = None
self.modes_combobox: Optional[ttk.Combobox] = None
self.startup_commands_listbox: Optional[tk.Listbox] = None
self.shutdown_commands_listbox: Optional[tk.Listbox] = None
self.validate_commands_listbox: Optional[tk.Listbox] = None
self.validation_time_entry: Optional[ttk.Entry] = None
self.validation_mode_entry: Optional[ttk.Entry] = None
self.template_text: Optional[CodeText] = None
self.validation_period_entry: Optional[ttk.Entry] = None
self.original_service_files: Dict[str, str] = {}
self.temp_service_files: Dict[str, str] = {}
self.modified_files: Set[str] = set()
self.config_frame: Optional[ConfigFrame] = None
self.default_config: Dict[str, str] = {}
self.config: Dict[str, ConfigOption] = {}
self.has_error: bool = False
self.load() self.load()
if not self.has_error: if not self.has_error:
self.draw() self.draw()
def load(self): def load(self) -> None:
try: try:
self.core.create_nodes_and_links() self.core.create_nodes_and_links()
service = self.core.config_services[self.service_name] service = self.core.config_services[self.service_name]
@ -116,7 +115,7 @@ class ConfigServiceConfigDialog(Dialog):
self.app.show_grpc_exception("Get Config Service Error", e) self.app.show_grpc_exception("Get Config Service Error", e)
self.has_error = True self.has_error = True
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -130,7 +129,7 @@ class ConfigServiceConfigDialog(Dialog):
self.draw_tab_validation() self.draw_tab_validation()
self.draw_buttons() self.draw_buttons()
def draw_tab_files(self): def draw_tab_files(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew") tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1) tab.columnconfigure(0, weight=1)
@ -174,7 +173,7 @@ class ConfigServiceConfigDialog(Dialog):
) )
self.template_text.text.bind("<FocusOut>", self.update_template_file_data) self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
def draw_tab_config(self): def draw_tab_config(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew") tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1) tab.columnconfigure(0, weight=1)
@ -198,7 +197,7 @@ class ConfigServiceConfigDialog(Dialog):
self.config_frame.grid(sticky="nsew", pady=PADY) self.config_frame.grid(sticky="nsew", pady=PADY)
tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1) tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1)
def draw_tab_startstop(self): def draw_tab_startstop(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew") tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1) tab.columnconfigure(0, weight=1)
@ -239,7 +238,7 @@ class ConfigServiceConfigDialog(Dialog):
elif i == 2: elif i == 2:
self.validate_commands_listbox = listbox_scroll.listbox self.validate_commands_listbox = listbox_scroll.listbox
def draw_tab_validation(self): def draw_tab_validation(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="ew") tab.grid(sticky="ew")
tab.columnconfigure(0, weight=1) tab.columnconfigure(0, weight=1)
@ -298,7 +297,7 @@ class ConfigServiceConfigDialog(Dialog):
for dependency in self.dependencies: for dependency in self.dependencies:
listbox_scroll.listbox.insert("end", dependency) listbox_scroll.listbox.insert("end", dependency)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(4): for i in range(4):
@ -312,7 +311,7 @@ class ConfigServiceConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
def click_apply(self): def click_apply(self) -> None:
current_listbox = self.master.current.listbox current_listbox = self.master.current.listbox
if not self.is_custom(): if not self.is_custom():
self.canvas_node.config_service_configs.pop(self.service_name, None) self.canvas_node.config_service_configs.pop(self.service_name, None)
@ -333,18 +332,18 @@ class ConfigServiceConfigDialog(Dialog):
current_listbox.itemconfig(all_current.index(self.service_name), bg="green") current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
self.destroy() self.destroy()
def handle_template_changed(self, event: tk.Event): def handle_template_changed(self, event: tk.Event) -> None:
template = self.templates_combobox.get() template = self.templates_combobox.get()
self.template_text.text.delete(1.0, "end") self.template_text.text.delete(1.0, "end")
self.template_text.text.insert("end", self.temp_service_files[template]) self.template_text.text.insert("end", self.temp_service_files[template])
def handle_mode_changed(self, event: tk.Event): def handle_mode_changed(self, event: tk.Event) -> None:
mode = self.modes_combobox.get() mode = self.modes_combobox.get()
config = self.mode_configs[mode] config = self.mode_configs[mode]
logging.info("mode config: %s", config) logging.info("mode config: %s", config)
self.config_frame.set_values(config) self.config_frame.set_values(config)
def update_template_file_data(self, event: tk.Event): def update_template_file_data(self, event: tk.Event) -> None:
scrolledtext = event.widget scrolledtext = event.widget
template = self.templates_combobox.get() template = self.templates_combobox.get()
self.temp_service_files[template] = scrolledtext.get(1.0, "end") self.temp_service_files[template] = scrolledtext.get(1.0, "end")
@ -353,7 +352,7 @@ class ConfigServiceConfigDialog(Dialog):
else: else:
self.modified_files.discard(template) self.modified_files.discard(template)
def is_custom(self): def is_custom(self) -> bool:
has_custom_templates = len(self.modified_files) > 0 has_custom_templates = len(self.modified_files) > 0
has_custom_config = False has_custom_config = False
if self.config_frame: if self.config_frame:
@ -361,7 +360,7 @@ class ConfigServiceConfigDialog(Dialog):
has_custom_config = self.default_config != current has_custom_config = self.default_config != current
return has_custom_templates or has_custom_config return has_custom_templates or has_custom_config
def click_defaults(self): def click_defaults(self) -> None:
self.canvas_node.config_service_configs.pop(self.service_name, None) self.canvas_node.config_service_configs.pop(self.service_name, None)
logging.info( logging.info(
"cleared config service config: %s", self.canvas_node.config_service_configs "cleared config service config: %s", self.canvas_node.config_service_configs
@ -374,12 +373,12 @@ class ConfigServiceConfigDialog(Dialog):
logging.info("resetting defaults: %s", self.default_config) logging.info("resetting defaults: %s", self.default_config)
self.config_frame.set_values(self.default_config) self.config_frame.set_values(self.default_config)
def click_copy(self): def click_copy(self) -> None:
pass pass
def append_commands( def append_commands(
self, commands: List[str], listbox: tk.Listbox, to_add: List[str] self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
): ) -> None:
for cmd in to_add: for cmd in to_add:
commands.append(cmd) commands.append(cmd)
listbox.insert(tk.END, cmd) listbox.insert(tk.END, cmd)

View file

@ -4,81 +4,58 @@ copy service config dialog
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Tuple from typing import TYPE_CHECKING, Dict, Optional
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX from core.gui.themes import PADX, PADY
from core.gui.widgets import CodeText from core.gui.widgets import CodeText, ListboxScroll
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.dialogs.serviceconfig import ServiceConfigDialog
class CopyServiceConfigDialog(Dialog): class CopyServiceConfigDialog(Dialog):
def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int): def __init__(
super().__init__(app, f"Copy services to node {node_id}", master=master) self,
self.parent = master app: "Application",
self.node_id = node_id dialog: "ServiceConfigDialog",
self.service_configs = app.core.service_configs name: str,
self.file_configs = app.core.file_configs service: str,
file_name: str,
self.tree = None ) -> None:
super().__init__(app, f"Copy Custom File to {name}", master=dialog)
self.dialog: "ServiceConfigDialog" = dialog
self.service: str = service
self.file_name: str = file_name
self.listbox: Optional[tk.Listbox] = None
self.nodes: Dict[str, int] = {}
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.tree = ttk.Treeview(self.top) self.top.rowconfigure(1, weight=1)
self.tree.grid(row=0, column=0, sticky="ew", padx=PADX) label = ttk.Label(
self.tree["columns"] = () self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER
self.tree.column("#0", width=270, minwidth=270, stretch=tk.YES) )
self.tree.heading("#0", text="Service configuration items", anchor=tk.CENTER) label.grid(sticky="ew", pady=PADY)
custom_nodes = set(self.service_configs).union(set(self.file_configs))
for nid in custom_nodes: listbox_scroll = ListboxScroll(self.top)
treeid = self.tree.insert("", "end", text=f"n{nid}", tags="node") listbox_scroll.grid(sticky="nsew", pady=PADY)
services = self.service_configs.get(nid, None) self.listbox = listbox_scroll.listbox
files = self.file_configs.get(nid, None) for canvas_node in self.app.canvas.nodes.values():
tree_ids = {} file_configs = canvas_node.service_file_configs.get(self.service)
if services: if not file_configs:
for service, config in services.items(): continue
serviceid = self.tree.insert( data = file_configs.get(self.file_name)
treeid, "end", text=service, tags="service" if not data:
) continue
tree_ids[service] = serviceid name = canvas_node.core_node.name
cmdup = config.startup[:] self.nodes[name] = canvas_node.id
cmddown = config.shutdown[:] self.listbox.insert(tk.END, name)
cmdval = config.validate[:]
self.tree.insert(
serviceid,
"end",
text=f"cmdup=({str(cmdup)[1:-1]})",
tags=("cmd", "up"),
)
self.tree.insert(
serviceid,
"end",
text=f"cmddown=({str(cmddown)[1:-1]})",
tags=("cmd", "down"),
)
self.tree.insert(
serviceid,
"end",
text=f"cmdval=({str(cmdval)[1:-1]})",
tags=("cmd", "val"),
)
if files:
for service, configs in files.items():
if service in tree_ids:
serviceid = tree_ids[service]
else:
serviceid = self.tree.insert(
treeid, "end", text=service, tags="service"
)
tree_ids[service] = serviceid
for filename, data in configs.items():
self.tree.insert(serviceid, "end", text=filename, tags="file")
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(row=1, column=0) frame.grid(sticky="ew")
for i in range(3): for i in range(3):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Copy", command=self.click_copy) button = ttk.Button(frame, text="Copy", command=self.click_copy)
@ -86,118 +63,58 @@ class CopyServiceConfigDialog(Dialog):
button = ttk.Button(frame, text="View", command=self.click_view) button = ttk.Button(frame, text="View", command=self.click_view)
button.grid(row=0, column=1, sticky="ew", padx=PADX) button.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=2, sticky="ew", padx=PADX) button.grid(row=0, column=2, sticky="ew")
def click_copy(self): def click_copy(self) -> None:
selected = self.tree.selection() selection = self.listbox.curselection()
if selected: if not selection:
item = self.tree.item(selected[0]) return
if "file" in item["tags"]: name = self.listbox.get(selection)
filename = item["text"] canvas_node_id = self.nodes[name]
nid, service = self.get_node_service(selected) canvas_node = self.app.canvas.nodes[canvas_node_id]
data = self.file_configs[nid][service][filename] data = canvas_node.service_file_configs[self.service][self.file_name]
if service == self.parent.service_name: self.dialog.temp_service_files[self.file_name] = data
self.parent.temp_service_files[filename] = data self.dialog.modified_files.add(self.file_name)
self.parent.modified_files.add(filename) self.dialog.service_file_data.text.delete(1.0, tk.END)
if self.parent.filename_combobox.get() == filename: self.dialog.service_file_data.text.insert(tk.END, data)
self.parent.service_file_data.text.delete(1.0, "end")
self.parent.service_file_data.text.insert("end", data)
if "cmd" in item["tags"]:
nid, service = self.get_node_service(selected)
if service == self.master.service_name:
cmds = self.service_configs[nid][service]
if "up" in item["tags"]:
self.master.append_commands(
self.master.startup_commands,
self.master.startup_commands_listbox,
cmds.startup,
)
elif "down" in item["tags"]:
self.master.append_commands(
self.master.shutdown_commands,
self.master.shutdown_commands_listbox,
cmds.shutdown,
)
elif "val" in item["tags"]:
self.master.append_commands(
self.master.validate_commands,
self.master.validate_commands_listbox,
cmds.validate,
)
self.destroy() self.destroy()
def click_view(self): def click_view(self) -> None:
selected = self.tree.selection() selection = self.listbox.curselection()
data = "" if not selection:
if selected: return
item = self.tree.item(selected[0]) name = self.listbox.get(selection)
if "file" in item["tags"]: canvas_node_id = self.nodes[name]
nid, service = self.get_node_service(selected) canvas_node = self.app.canvas.nodes[canvas_node_id]
data = self.file_configs[nid][service][item["text"]] data = canvas_node.service_file_configs[self.service][self.file_name]
dialog = ViewConfigDialog( dialog = ViewConfigDialog(
self, self.app, nid, data, item["text"].split("/")[-1] self.app, self, name, self.service, self.file_name, data
) )
dialog.show() dialog.show()
if "cmd" in item["tags"]:
nid, service = self.get_node_service(selected)
cmds = self.service_configs[nid][service]
if "up" in item["tags"]:
data = f"({str(cmds.startup[:])[1:-1]})"
dialog = ViewConfigDialog(
self, self.app, self.node_id, data, "cmdup"
)
elif "down" in item["tags"]:
data = f"({str(cmds.shutdown[:])[1:-1]})"
dialog = ViewConfigDialog(
self, self.app, self.node_id, data, "cmdup"
)
elif "val" in item["tags"]:
data = f"({str(cmds.validate[:])[1:-1]})"
dialog = ViewConfigDialog(
self, self.app, self.node_id, data, "cmdup"
)
dialog.show()
def get_node_service(self, selected: Tuple[str]) -> [int, str]:
service_tree_id = self.tree.parent(selected[0])
service_name = self.tree.item(service_tree_id)["text"]
node_tree_id = self.tree.parent(service_tree_id)
node_id = int(self.tree.item(node_tree_id)["text"][1:])
return node_id, service_name
class ViewConfigDialog(Dialog): class ViewConfigDialog(Dialog):
def __init__( def __init__(
self, self,
master: tk.BaseWidget,
app: "Application", app: "Application",
node_id: int, master: tk.BaseWidget,
name: str,
service: str,
file_name: str,
data: str, data: str,
filename: str = None, ) -> None:
): title = f"{name} Service({service}) File({file_name})"
super().__init__(app, f"n{node_id} config data", master=master) super().__init__(app, title, master=master)
self.data = data self.data = data
self.service_data = None self.service_data = None
self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}")
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
frame = ttk.Frame(self.top, padding=FRAME_PAD) self.top.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=10)
frame.grid(row=0, column=0, sticky="ew")
label = ttk.Label(frame, text="File: ")
label.grid(row=0, column=0, sticky="ew", padx=PADX)
entry = ttk.Entry(frame, textvariable=self.filepath)
entry.config(state="disabled")
entry.grid(row=0, column=1, sticky="ew")
self.service_data = CodeText(self.top) self.service_data = CodeText(self.top)
self.service_data.grid(row=1, column=0, sticky="nsew") self.service_data.grid(sticky="nsew", pady=PADY)
self.service_data.text.insert("end", self.data) self.service_data.text.insert(tk.END, self.data)
self.service_data.text.config(state="disabled") self.service_data.text.config(state=tk.DISABLED)
button = ttk.Button(self.top, text="Close", command=self.destroy) button = ttk.Button(self.top, text="Close", command=self.destroy)
button.grid(row=2, column=0, sticky="ew", padx=PADX) button.grid(sticky="ew")

View file

@ -2,7 +2,9 @@ import logging
import tkinter as tk import tkinter as tk
from pathlib import Path from pathlib import Path
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Set from typing import TYPE_CHECKING, Optional, Set
from PIL.ImageTk import PhotoImage
from core.gui import nodeutils from core.gui import nodeutils
from core.gui.appconfig import ICONS_PATH, CustomNode from core.gui.appconfig import ICONS_PATH, CustomNode
@ -19,15 +21,15 @@ if TYPE_CHECKING:
class ServicesSelectDialog(Dialog): class ServicesSelectDialog(Dialog):
def __init__( def __init__(
self, master: tk.BaseWidget, app: "Application", current_services: Set[str] self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
): ) -> None:
super().__init__(app, "Node Services", master=master) super().__init__(app, "Node Services", master=master)
self.groups = None self.groups: Optional[ListboxScroll] = None
self.services = None self.services: Optional[CheckboxList] = None
self.current = None self.current: Optional[ListboxScroll] = None
self.current_services = set(current_services) self.current_services: Set[str] = current_services
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -77,7 +79,7 @@ class ServicesSelectDialog(Dialog):
# trigger group change # trigger group change
self.groups.listbox.event_generate("<<ListboxSelect>>") self.groups.listbox.event_generate("<<ListboxSelect>>")
def handle_group_change(self, event: tk.Event): def handle_group_change(self, event: tk.Event) -> None:
selection = self.groups.listbox.curselection() selection = self.groups.listbox.curselection()
if selection: if selection:
index = selection[0] index = selection[0]
@ -87,7 +89,7 @@ class ServicesSelectDialog(Dialog):
checked = name in self.current_services checked = name in self.current_services
self.services.add(name, checked) self.services.add(name, checked)
def service_clicked(self, name: str, var: tk.BooleanVar): def service_clicked(self, name: str, var: tk.BooleanVar) -> None:
if var.get() and name not in self.current_services: if var.get() and name not in self.current_services:
self.current_services.add(name) self.current_services.add(name)
elif not var.get() and name in self.current_services: elif not var.get() and name in self.current_services:
@ -96,34 +98,34 @@ class ServicesSelectDialog(Dialog):
for name in sorted(self.current_services): for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name) self.current.listbox.insert(tk.END, name)
def click_cancel(self): def click_cancel(self) -> None:
self.current_services = None self.current_services = None
self.destroy() self.destroy()
class CustomNodesDialog(Dialog): class CustomNodesDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Custom Nodes") super().__init__(app, "Custom Nodes")
self.edit_button = None self.edit_button: Optional[ttk.Button] = None
self.delete_button = None self.delete_button: Optional[ttk.Button] = None
self.nodes_list = None self.nodes_list: Optional[ListboxScroll] = None
self.name = tk.StringVar() self.name: tk.StringVar = tk.StringVar()
self.image_button = None self.image_button: Optional[ttk.Button] = None
self.image = None self.image: Optional[PhotoImage] = None
self.image_file = None self.image_file: Optional[str] = None
self.services = set() self.services: Set[str] = set()
self.selected = None self.selected: Optional[str] = None
self.selected_index = None self.selected_index: Optional[int] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.draw_node_config() self.draw_node_config()
self.draw_node_buttons() self.draw_node_buttons()
self.draw_buttons() self.draw_buttons()
def draw_node_config(self): def draw_node_config(self) -> None:
frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD) frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
frame.grid(sticky="nsew", pady=PADY) frame.grid(sticky="nsew", pady=PADY)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
@ -147,7 +149,7 @@ class CustomNodesDialog(Dialog):
button = ttk.Button(frame, text="Services", command=self.click_services) button = ttk.Button(frame, text="Services", command=self.click_services)
button.grid(sticky="ew") button.grid(sticky="ew")
def draw_node_buttons(self): def draw_node_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY) frame.grid(sticky="ew", pady=PADY)
for i in range(3): for i in range(3):
@ -166,7 +168,7 @@ class CustomNodesDialog(Dialog):
) )
self.delete_button.grid(row=0, column=2, sticky="ew") self.delete_button.grid(row=0, column=2, sticky="ew")
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
@ -178,14 +180,14 @@ class CustomNodesDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def reset_values(self): def reset_values(self) -> None:
self.name.set("") self.name.set("")
self.image = None self.image = None
self.image_file = None self.image_file = None
self.services = set() self.services = set()
self.image_button.config(image="") self.image_button.config(image="")
def click_icon(self): def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH) file_path = image_chooser(self, ICONS_PATH)
if file_path: if file_path:
image = Images.create(file_path, nodeutils.ICON_SIZE) image = Images.create(file_path, nodeutils.ICON_SIZE)
@ -193,24 +195,26 @@ class CustomNodesDialog(Dialog):
self.image_file = file_path self.image_file = file_path
self.image_button.config(image=self.image) self.image_button.config(image=self.image)
def click_services(self): def click_services(self) -> None:
dialog = ServicesSelectDialog(self, self.app, self.services) dialog = ServicesSelectDialog(self, self.app, self.services)
dialog.show() dialog.show()
if dialog.current_services is not None: if dialog.current_services is not None:
self.services.clear() self.services.clear()
self.services.update(dialog.current_services) self.services.update(dialog.current_services)
def click_save(self): def click_save(self) -> None:
self.app.guiconfig.nodes.clear() self.app.guiconfig.nodes.clear()
for name in self.app.core.custom_nodes: for name in self.app.core.custom_nodes:
node_draw = self.app.core.custom_nodes[name] node_draw = self.app.core.custom_nodes[name]
custom_node = CustomNode(name, node_draw.image_file, node_draw.services) custom_node = CustomNode(
name, node_draw.image_file, list(node_draw.services)
)
self.app.guiconfig.nodes.append(custom_node) self.app.guiconfig.nodes.append(custom_node)
logging.info("saving custom nodes: %s", self.app.guiconfig.nodes) logging.info("saving custom nodes: %s", self.app.guiconfig.nodes)
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()
def click_create(self): def click_create(self) -> None:
name = self.name.get() name = self.name.get()
if name not in self.app.core.custom_nodes: if name not in self.app.core.custom_nodes:
image_file = Path(self.image_file).stem image_file = Path(self.image_file).stem
@ -226,7 +230,7 @@ class CustomNodesDialog(Dialog):
self.nodes_list.listbox.insert(tk.END, name) self.nodes_list.listbox.insert(tk.END, name)
self.reset_values() self.reset_values()
def click_edit(self): def click_edit(self) -> None:
name = self.name.get() name = self.name.get()
if self.selected: if self.selected:
previous_name = self.selected previous_name = self.selected
@ -247,7 +251,7 @@ class CustomNodesDialog(Dialog):
self.nodes_list.listbox.insert(self.selected_index, name) self.nodes_list.listbox.insert(self.selected_index, name)
self.nodes_list.listbox.selection_set(self.selected_index) self.nodes_list.listbox.selection_set(self.selected_index)
def click_delete(self): def click_delete(self) -> None:
if self.selected and self.selected in self.app.core.custom_nodes: if self.selected and self.selected in self.app.core.custom_nodes:
self.nodes_list.listbox.delete(self.selected_index) self.nodes_list.listbox.delete(self.selected_index)
del self.app.core.custom_nodes[self.selected] del self.app.core.custom_nodes[self.selected]
@ -255,7 +259,7 @@ class CustomNodesDialog(Dialog):
self.nodes_list.listbox.selection_clear(0, tk.END) self.nodes_list.listbox.selection_clear(0, tk.END)
self.nodes_list.listbox.event_generate("<<ListboxSelect>>") self.nodes_list.listbox.event_generate("<<ListboxSelect>>")
def handle_node_select(self, event: tk.Event): def handle_node_select(self, event: tk.Event) -> None:
selection = self.nodes_list.listbox.curselection() selection = self.nodes_list.listbox.curselection()
if selection: if selection:
self.selected_index = selection[0] self.selected_index = selection[0]

View file

@ -16,23 +16,23 @@ class Dialog(tk.Toplevel):
title: str, title: str,
modal: bool = True, modal: bool = True,
master: tk.BaseWidget = None, master: tk.BaseWidget = None,
): ) -> None:
if master is None: if master is None:
master = app master = app
super().__init__(master) super().__init__(master)
self.withdraw() self.withdraw()
self.app = app self.app: "Application" = app
self.modal = modal self.modal: bool = modal
self.title(title) self.title(title)
self.protocol("WM_DELETE_WINDOW", self.destroy) self.protocol("WM_DELETE_WINDOW", self.destroy)
image = Images.get(ImageEnum.CORE, 16) image = Images.get(ImageEnum.CORE, 16)
self.tk.call("wm", "iconphoto", self._w, image) self.tk.call("wm", "iconphoto", self._w, image)
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1) self.rowconfigure(0, weight=1)
self.top = ttk.Frame(self, padding=DIALOG_PAD) self.top: ttk.Frame = ttk.Frame(self, padding=DIALOG_PAD)
self.top.grid(sticky="nsew") self.top.grid(sticky="nsew")
def show(self): def show(self) -> None:
self.transient(self.master) self.transient(self.master)
self.focus_force() self.focus_force()
self.update() self.update()
@ -42,7 +42,7 @@ class Dialog(tk.Toplevel):
self.grab_set() self.grab_set()
self.wait_window() self.wait_window()
def draw_spacer(self, row: int = None): def draw_spacer(self, row: int = None) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(row=row, sticky="nsew") frame.grid(row=row, sticky="nsew")
frame.rowconfigure(0, weight=1) frame.rowconfigure(0, weight=1)

View file

@ -4,10 +4,12 @@ emane configuration
import tkinter as tk import tkinter as tk
import webbrowser import webbrowser
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, List, Optional
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import Node
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
@ -19,32 +21,35 @@ if TYPE_CHECKING:
class GlobalEmaneDialog(Dialog): class GlobalEmaneDialog(Dialog):
def __init__(self, master: tk.BaseWidget, app: "Application"): def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
super().__init__(app, "EMANE Configuration", master=master) super().__init__(app, "EMANE Configuration", master=master)
self.config_frame = None self.config_frame: Optional[ConfigFrame] = None
self.enabled: bool = not self.app.core.is_runtime()
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config) self.config_frame = ConfigFrame(
self.top, self.app, self.app.core.emane_config, self.enabled
)
self.config_frame.draw_config() self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY) self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply) state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
button.grid(row=0, column=0, sticky="ew", padx=PADX) button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_apply(self): def click_apply(self) -> None:
self.config_frame.parse_config() self.config_frame.parse_config()
self.destroy() self.destroy()
@ -56,71 +61,77 @@ class EmaneModelDialog(Dialog):
app: "Application", app: "Application",
canvas_node: "CanvasNode", canvas_node: "CanvasNode",
model: str, model: str,
interface: int = None, iface_id: int = None,
): ) -> None:
super().__init__( super().__init__(
app, f"{canvas_node.core_node.name} {model} Configuration", master=master app, f"{canvas_node.core_node.name} {model} Configuration", master=master
) )
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.model = f"emane_{model}" self.model: str = f"emane_{model}"
self.interface = interface self.iface_id: int = iface_id
self.config_frame = None self.config_frame: Optional[ConfigFrame] = None
self.has_error = False self.enabled: bool = not self.app.core.is_runtime()
self.has_error: bool = False
try: try:
self.config = self.canvas_node.emane_model_configs.get( config = self.canvas_node.emane_model_configs.get(
(self.model, self.interface) (self.model, self.iface_id)
) )
if not self.config: if not config:
self.config = self.app.core.get_emane_model_config( config = self.app.core.get_emane_model_config(
self.node.id, self.model, self.interface self.node.id, self.model, self.iface_id
) )
self.config: Dict[str, ConfigOption] = config
self.draw() self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Get EMANE Config Error", e) self.app.show_grpc_exception("Get EMANE Config Error", e)
self.has_error = True self.has_error: bool = True
self.destroy() self.destroy()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled)
self.config_frame.draw_config() self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY) self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply) state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
button.grid(row=0, column=0, sticky="ew", padx=PADX) button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_apply(self): def click_apply(self) -> None:
self.config_frame.parse_config() self.config_frame.parse_config()
key = (self.model, self.interface) key = (self.model, self.iface_id)
self.canvas_node.emane_model_configs[key] = self.config self.canvas_node.emane_model_configs[key] = self.config
self.destroy() self.destroy()
class EmaneConfigDialog(Dialog): class EmaneConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration") super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration")
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.radiovar = tk.IntVar() self.radiovar: tk.IntVar = tk.IntVar()
self.radiovar.set(1) self.radiovar.set(1)
self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models] self.emane_models: List[str] = [
self.emane_model = tk.StringVar(value=self.node.emane.split("_")[1]) x.split("_")[1] for x in self.app.core.emane_models
self.emane_model_button = None ]
model = self.node.emane.split("_")[1]
self.emane_model: tk.StringVar = tk.StringVar(value=model)
self.emane_model_button: Optional[ttk.Button] = None
self.enabled: bool = not self.app.core.is_runtime()
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw_emane_configuration() self.draw_emane_configuration()
self.draw_emane_models() self.draw_emane_models()
@ -128,14 +139,15 @@ class EmaneConfigDialog(Dialog):
self.draw_spacer() self.draw_spacer()
self.draw_apply_and_cancel() self.draw_apply_and_cancel()
def draw_emane_configuration(self): def draw_emane_configuration(self) -> None:
""" """
draw the main frame for emane configuration draw the main frame for emane configuration
""" """
label = ttk.Label( label = ttk.Label(
self.top, self.top,
text="The EMANE emulation system provides more complex wireless radio emulation " text="The EMANE emulation system provides more complex wireless radio "
"\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", "emulation \nusing pluggable MAC and PHY modules. Refer to the wiki "
"for configuration option details",
justify=tk.CENTER, justify=tk.CENTER,
) )
label.grid(pady=PADY) label.grid(pady=PADY)
@ -153,7 +165,7 @@ class EmaneConfigDialog(Dialog):
button.image = image button.image = image
button.grid(sticky="ew", pady=PADY) button.grid(sticky="ew", pady=PADY)
def draw_emane_models(self): def draw_emane_models(self) -> None:
""" """
create a combobox that has all the known emane models create a combobox that has all the known emane models
""" """
@ -165,16 +177,14 @@ class EmaneConfigDialog(Dialog):
label.grid(row=0, column=0, sticky="w") label.grid(row=0, column=0, sticky="w")
# create combo box and its binding # create combo box and its binding
state = "readonly" if self.enabled else tk.DISABLED
combobox = ttk.Combobox( combobox = ttk.Combobox(
frame, frame, textvariable=self.emane_model, values=self.emane_models, state=state
textvariable=self.emane_model,
values=self.emane_models,
state="readonly",
) )
combobox.grid(row=0, column=1, sticky="ew") combobox.grid(row=0, column=1, sticky="ew")
combobox.bind("<<ComboboxSelected>>", self.emane_model_change) combobox.bind("<<ComboboxSelected>>", self.emane_model_change)
def draw_emane_buttons(self): def draw_emane_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY) frame.grid(sticky="ew", pady=PADY)
for i in range(2): for i in range(2):
@ -202,23 +212,22 @@ class EmaneConfigDialog(Dialog):
button.image = image button.image = image
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def draw_apply_and_cancel(self): def draw_apply_and_cancel(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Apply", command=self.click_apply) button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
button.grid(row=0, column=0, padx=PADX, sticky="ew") button.grid(row=0, column=0, padx=PADX, sticky="ew")
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_emane_config(self): def click_emane_config(self) -> None:
dialog = GlobalEmaneDialog(self, self.app) dialog = GlobalEmaneDialog(self, self.app)
dialog.show() dialog.show()
def click_model_config(self): def click_model_config(self) -> None:
""" """
draw emane model configuration draw emane model configuration
""" """
@ -227,13 +236,13 @@ class EmaneConfigDialog(Dialog):
if not dialog.has_error: if not dialog.has_error:
dialog.show() dialog.show()
def emane_model_change(self, event: tk.Event): def emane_model_change(self, event: tk.Event) -> None:
""" """
update emane model options button update emane model options button
""" """
model_name = self.emane_model.get() model_name = self.emane_model.get()
self.emane_model_button.config(text=f"{model_name} options") self.emane_model_button.config(text=f"{model_name} options")
def click_apply(self): def click_apply(self) -> None:
self.node.emane = f"emane_{self.emane_model.get()}" self.node.emane = f"emane_{self.emane_model.get()}"
self.destroy() self.destroy()

View file

@ -10,7 +10,7 @@ class EmaneInstallDialog(Dialog):
super().__init__(app, "EMANE Error") super().__init__(app, "EMANE Error")
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
label = ttk.Label(self.top, text="EMANE needs to be installed!") label = ttk.Label(self.top, text="EMANE needs to be installed!")
label.grid(sticky="ew", pady=PADY) label.grid(sticky="ew", pady=PADY)
@ -21,5 +21,5 @@ class EmaneInstallDialog(Dialog):
button = ttk.Button(self.top, text="Close", command=self.destroy) button = ttk.Button(self.top, text="Close", command=self.destroy)
button.grid(sticky="ew") button.grid(sticky="ew")
def click_doc(self): def click_doc(self) -> None:
webbrowser.open_new("https://coreemu.github.io/core/emane.html") webbrowser.open_new("https://coreemu.github.io/core/emane.html")

View file

@ -1,9 +1,10 @@
import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import PADY
from core.gui.widgets import CodeText from core.gui.widgets import CodeText
if TYPE_CHECKING: if TYPE_CHECKING:
@ -13,29 +14,23 @@ if TYPE_CHECKING:
class ErrorDialog(Dialog): class ErrorDialog(Dialog):
def __init__(self, app: "Application", title: str, details: str) -> None: def __init__(self, app: "Application", title: str, details: str) -> None:
super().__init__(app, "CORE Exception") super().__init__(app, "CORE Exception")
self.title = title self.title: str = title
self.details = details self.details: str = details
self.error_message = None self.error_message: Optional[CodeText] = None
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1) self.top.rowconfigure(1, weight=1)
image = Images.get(ImageEnum.ERROR, 24)
frame = ttk.Frame(self.top, padding=FRAME_PAD) label = ttk.Label(
frame.grid(pady=PADY, sticky="ew") self.top, text=self.title, image=image, compound=tk.LEFT, anchor=tk.CENTER
frame.columnconfigure(1, weight=1) )
image = Images.get(ImageEnum.ERROR, 36)
label = ttk.Label(frame, image=image)
label.image = image label.image = image
label.grid(row=0, column=0, padx=PADX) label.grid(sticky=tk.EW, pady=PADY)
label = ttk.Label(frame, text=self.title)
label.grid(row=0, column=1, sticky="ew")
self.error_message = CodeText(self.top) self.error_message = CodeText(self.top)
self.error_message.text.insert("1.0", self.details) self.error_message.text.insert("1.0", self.details)
self.error_message.text.config(state="disabled") self.error_message.text.config(state=tk.DISABLED)
self.error_message.grid(sticky="nsew", pady=PADY) self.error_message.grid(sticky=tk.NSEW, pady=PADY)
button = ttk.Button(self.top, text="Close", command=lambda: self.destroy()) button = ttk.Button(self.top, text="Close", command=lambda: self.destroy())
button.grid(sticky="ew") button.grid(sticky=tk.EW)

View file

@ -1,7 +1,7 @@
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import filedialog, ttk from tkinter import filedialog, ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from core.gui.appconfig import SCRIPT_PATH from core.gui.appconfig import SCRIPT_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -12,15 +12,15 @@ if TYPE_CHECKING:
class ExecutePythonDialog(Dialog): class ExecutePythonDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Execute Python Script") super().__init__(app, "Execute Python Script")
self.with_options = tk.IntVar(value=0) self.with_options: tk.IntVar = tk.IntVar(value=0)
self.options = tk.StringVar(value="") self.options: tk.StringVar = tk.StringVar(value="")
self.option_entry = None self.option_entry: Optional[ttk.Entry] = None
self.file_entry = None self.file_entry: Optional[ttk.Entry] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
i = 0 i = 0
frame = ttk.Frame(self.top, padding=FRAME_PAD) frame = ttk.Frame(self.top, padding=FRAME_PAD)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
@ -63,13 +63,13 @@ class ExecutePythonDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew", padx=PADX) button.grid(row=0, column=1, sticky="ew", padx=PADX)
def add_options(self): def add_options(self) -> None:
if self.with_options.get(): if self.with_options.get():
self.option_entry.configure(state="normal") self.option_entry.configure(state="normal")
else: else:
self.option_entry.configure(state="disabled") self.option_entry.configure(state="disabled")
def select_file(self): def select_file(self) -> None:
file = filedialog.askopenfilename( file = filedialog.askopenfilename(
parent=self.top, parent=self.top,
initialdir=str(SCRIPT_PATH), initialdir=str(SCRIPT_PATH),
@ -80,7 +80,7 @@ class ExecutePythonDialog(Dialog):
self.file_entry.delete(0, "end") self.file_entry.delete(0, "end")
self.file_entry.insert("end", file) self.file_entry.insert("end", file)
def script_execute(self): def script_execute(self) -> None:
file = self.file_entry.get() file = self.file_entry.get()
options = self.option_entry.get() options = self.option_entry.get()
logging.info("Execute %s with options %s", file, options) logging.info("Execute %s with options %s", file, options)

View file

@ -1,7 +1,7 @@
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
@ -13,8 +13,8 @@ if TYPE_CHECKING:
class FindDialog(Dialog): class FindDialog(Dialog):
def __init__(self, app: "Application") -> None: def __init__(self, app: "Application") -> None:
super().__init__(app, "Find", modal=False) super().__init__(app, "Find", modal=False)
self.find_text = tk.StringVar(value="") self.find_text: tk.StringVar = tk.StringVar(value="")
self.tree = None self.tree: Optional[ttk.Treeview] = None
self.draw() self.draw()
self.protocol("WM_DELETE_WINDOW", self.close_dialog) self.protocol("WM_DELETE_WINDOW", self.close_dialog)
self.bind("<Return>", self.find_node) self.bind("<Return>", self.find_node)

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -12,15 +12,15 @@ if TYPE_CHECKING:
class HookDialog(Dialog): class HookDialog(Dialog):
def __init__(self, master: tk.BaseWidget, app: "Application"): def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
super().__init__(app, "Hook", master=master) super().__init__(app, "Hook", master=master)
self.name = tk.StringVar() self.name: tk.StringVar = tk.StringVar()
self.codetext = None self.codetext: Optional[CodeText] = None
self.hook = core_pb2.Hook() self.hook: core_pb2.Hook = core_pb2.Hook()
self.state = tk.StringVar() self.state: tk.StringVar = tk.StringVar()
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1) self.top.rowconfigure(1, weight=1)
@ -66,11 +66,11 @@ class HookDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def state_change(self, event: tk.Event): def state_change(self, event: tk.Event) -> None:
state_name = self.state.get() state_name = self.state.get()
self.name.set(f"{state_name.lower()}_hook.sh") self.name.set(f"{state_name.lower()}_hook.sh")
def set(self, hook: core_pb2.Hook): def set(self, hook: core_pb2.Hook) -> None:
self.hook = hook self.hook = hook
self.name.set(hook.file) self.name.set(hook.file)
self.codetext.text.delete(1.0, tk.END) self.codetext.text.delete(1.0, tk.END)
@ -78,7 +78,7 @@ class HookDialog(Dialog):
state_name = core_pb2.SessionState.Enum.Name(hook.state) state_name = core_pb2.SessionState.Enum.Name(hook.state)
self.state.set(state_name) self.state.set(state_name)
def save(self): def save(self) -> None:
data = self.codetext.text.get("1.0", tk.END).strip() data = self.codetext.text.get("1.0", tk.END).strip()
state_value = core_pb2.SessionState.Enum.Value(self.state.get()) state_value = core_pb2.SessionState.Enum.Value(self.state.get())
self.hook.file = self.name.get() self.hook.file = self.name.get()
@ -88,15 +88,15 @@ class HookDialog(Dialog):
class HooksDialog(Dialog): class HooksDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Hooks") super().__init__(app, "Hooks")
self.listbox = None self.listbox: Optional[tk.Listbox] = None
self.edit_button = None self.edit_button: Optional[ttk.Button] = None
self.delete_button = None self.delete_button: Optional[ttk.Button] = None
self.selected = None self.selected: Optional[str] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -124,7 +124,7 @@ class HooksDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
def click_create(self): def click_create(self) -> None:
dialog = HookDialog(self, self.app) dialog = HookDialog(self, self.app)
dialog.show() dialog.show()
hook = dialog.hook hook = dialog.hook
@ -132,19 +132,19 @@ class HooksDialog(Dialog):
self.app.core.hooks[hook.file] = hook self.app.core.hooks[hook.file] = hook
self.listbox.insert(tk.END, hook.file) self.listbox.insert(tk.END, hook.file)
def click_edit(self): def click_edit(self) -> None:
hook = self.app.core.hooks[self.selected] hook = self.app.core.hooks[self.selected]
dialog = HookDialog(self, self.app) dialog = HookDialog(self, self.app)
dialog.set(hook) dialog.set(hook)
dialog.show() dialog.show()
def click_delete(self): def click_delete(self) -> None:
del self.app.core.hooks[self.selected] del self.app.core.hooks[self.selected]
self.listbox.delete(tk.ANCHOR) self.listbox.delete(tk.ANCHOR)
self.edit_button.config(state=tk.DISABLED) self.edit_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED)
def select(self, event: tk.Event): def select(self, event: tk.Event) -> None:
if self.listbox.curselection(): if self.listbox.curselection():
index = self.listbox.curselection()[0] index = self.listbox.curselection()[0]
self.selected = self.listbox.get(index) self.selected = self.listbox.get(index)

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, List, Optional
import netaddr import netaddr
@ -15,14 +15,14 @@ if TYPE_CHECKING:
class IpConfigDialog(Dialog): class IpConfigDialog(Dialog):
def __init__(self, app: "Application") -> None: def __init__(self, app: "Application") -> None:
super().__init__(app, "IP Configuration") super().__init__(app, "IP Configuration")
self.ip4 = self.app.guiconfig.ips.ip4 self.ip4: str = self.app.guiconfig.ips.ip4
self.ip6 = self.app.guiconfig.ips.ip6 self.ip6: str = self.app.guiconfig.ips.ip6
self.ip4s = self.app.guiconfig.ips.ip4s self.ip4s: List[str] = self.app.guiconfig.ips.ip4s
self.ip6s = self.app.guiconfig.ips.ip6s self.ip6s: List[str] = self.app.guiconfig.ips.ip6s
self.ip4_entry = None self.ip4_entry: Optional[ttk.Entry] = None
self.ip4_listbox = None self.ip4_listbox: Optional[ListboxScroll] = None
self.ip6_entry = None self.ip6_entry: Optional[ttk.Entry] = None
self.ip6_listbox = None self.ip6_listbox: Optional[ListboxScroll] = None
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:
@ -146,6 +146,6 @@ class IpConfigDialog(Dialog):
ip_config.ip6 = self.ip6 ip_config.ip6 = self.ip6
ip_config.ip4s = ip4s ip_config.ip4s = ip4s
ip_config.ip6s = ip6s ip_config.ip6s = ip6s
self.app.core.interfaces_manager.update_ips(self.ip4, self.ip6) self.app.core.ifaces_manager.update_ips(self.ip4, self.ip6)
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()

View file

@ -3,7 +3,7 @@ link configuration
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING, Optional
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.gui import validation from core.gui import validation
@ -16,7 +16,7 @@ if TYPE_CHECKING:
from core.gui.graph.graph import CanvasEdge from core.gui.graph.graph import CanvasEdge
def get_int(var: tk.StringVar) -> Union[int, None]: def get_int(var: tk.StringVar) -> Optional[int]:
value = var.get() value = var.get()
if value != "": if value != "":
return int(value) return int(value)
@ -24,7 +24,7 @@ def get_int(var: tk.StringVar) -> Union[int, None]:
return None return None
def get_float(var: tk.StringVar) -> Union[float, None]: def get_float(var: tk.StringVar) -> Optional[float]:
value = var.get() value = var.get()
if value != "": if value != "":
return float(value) return float(value)
@ -33,38 +33,39 @@ def get_float(var: tk.StringVar) -> Union[float, None]:
class LinkConfigurationDialog(Dialog): class LinkConfigurationDialog(Dialog):
def __init__(self, app: "Application", edge: "CanvasEdge"): def __init__(self, app: "Application", edge: "CanvasEdge") -> None:
super().__init__(app, "Link Configuration") super().__init__(app, "Link Configuration")
self.edge = edge self.edge: "CanvasEdge" = edge
self.is_symmetric = edge.link.options.unidirectional is False self.is_symmetric: bool = edge.link.options.unidirectional is False
if self.is_symmetric: if self.is_symmetric:
self.symmetry_var = tk.StringVar(value=">>") symmetry_var = tk.StringVar(value=">>")
else: else:
self.symmetry_var = tk.StringVar(value="<<") symmetry_var = tk.StringVar(value="<<")
self.symmetry_var: tk.StringVar = symmetry_var
self.bandwidth = tk.StringVar() self.bandwidth: tk.StringVar = tk.StringVar()
self.delay = tk.StringVar() self.delay: tk.StringVar = tk.StringVar()
self.jitter = tk.StringVar() self.jitter: tk.StringVar = tk.StringVar()
self.loss = tk.StringVar() self.loss: tk.StringVar = tk.StringVar()
self.duplicate = tk.StringVar() self.duplicate: tk.StringVar = tk.StringVar()
self.down_bandwidth = tk.StringVar() self.down_bandwidth: tk.StringVar = tk.StringVar()
self.down_delay = tk.StringVar() self.down_delay: tk.StringVar = tk.StringVar()
self.down_jitter = tk.StringVar() self.down_jitter: tk.StringVar = tk.StringVar()
self.down_loss = tk.StringVar() self.down_loss: tk.StringVar = tk.StringVar()
self.down_duplicate = tk.StringVar() self.down_duplicate: tk.StringVar = tk.StringVar()
self.color = tk.StringVar(value="#000000") self.color: tk.StringVar = tk.StringVar(value="#000000")
self.color_button = None self.color_button: Optional[tk.Button] = None
self.width = tk.DoubleVar() self.width: tk.DoubleVar = tk.DoubleVar()
self.load_link_config() self.load_link_config()
self.symmetric_frame = None self.symmetric_frame: Optional[ttk.Frame] = None
self.asymmetric_frame = None self.asymmetric_frame: Optional[ttk.Frame] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
source_name = self.app.canvas.nodes[self.edge.src].core_node.name source_name = self.app.canvas.nodes[self.edge.src].core_node.name
dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name
@ -207,13 +208,13 @@ class LinkConfigurationDialog(Dialog):
return frame return frame
def click_color(self): def click_color(self) -> None:
dialog = ColorPickerDialog(self, self.app, self.color.get()) dialog = ColorPickerDialog(self, self.app, self.color.get())
color = dialog.askcolor() color = dialog.askcolor()
self.color.set(color) self.color.set(color)
self.color_button.config(background=color) self.color_button.config(background=color)
def click_apply(self): def click_apply(self) -> None:
self.app.canvas.itemconfigure(self.edge.id, width=self.width.get()) self.app.canvas.itemconfigure(self.edge.id, width=self.width.get())
self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get()) self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get())
link = self.edge.link link = self.edge.link
@ -223,25 +224,25 @@ class LinkConfigurationDialog(Dialog):
duplicate = get_int(self.duplicate) duplicate = get_int(self.duplicate)
loss = get_float(self.loss) loss = get_float(self.loss)
options = core_pb2.LinkOptions( options = core_pb2.LinkOptions(
bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, per=loss bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, loss=loss
) )
link.options.CopyFrom(options) link.options.CopyFrom(options)
interface_one = None iface1_id = None
if link.HasField("interface_one"): if link.HasField("iface1"):
interface_one = link.interface_one.id iface1_id = link.iface1.id
interface_two = None iface2_id = None
if link.HasField("interface_two"): if link.HasField("iface2"):
interface_two = link.interface_two.id iface2_id = link.iface2.id
if not self.is_symmetric: if not self.is_symmetric:
link.options.unidirectional = True link.options.unidirectional = True
asym_interface_one = None asym_iface1 = None
if interface_one: if iface1_id:
asym_interface_one = core_pb2.Interface(id=interface_one) asym_iface1 = core_pb2.Interface(id=iface1_id)
asym_interface_two = None asym_iface2 = None
if interface_two: if iface2_id:
asym_interface_two = core_pb2.Interface(id=interface_two) asym_iface2 = core_pb2.Interface(id=iface2_id)
down_bandwidth = get_int(self.down_bandwidth) down_bandwidth = get_int(self.down_bandwidth)
down_jitter = get_int(self.down_jitter) down_jitter = get_int(self.down_jitter)
down_delay = get_int(self.down_delay) down_delay = get_int(self.down_delay)
@ -252,14 +253,14 @@ class LinkConfigurationDialog(Dialog):
jitter=down_jitter, jitter=down_jitter,
delay=down_delay, delay=down_delay,
dup=down_duplicate, dup=down_duplicate,
per=down_loss, loss=down_loss,
unidirectional=True, unidirectional=True,
) )
self.edge.asymmetric_link = core_pb2.Link( self.edge.asymmetric_link = core_pb2.Link(
node_one_id=link.node_two_id, node1_id=link.node2_id,
node_two_id=link.node_one_id, node2_id=link.node1_id,
interface_one=asym_interface_one, iface1=asym_iface1,
interface_two=asym_interface_two, iface2=asym_iface2,
options=options, options=options,
) )
else: else:
@ -270,25 +271,27 @@ class LinkConfigurationDialog(Dialog):
session_id = self.app.core.session_id session_id = self.app.core.session_id
self.app.core.client.edit_link( self.app.core.client.edit_link(
session_id, session_id,
link.node_one_id, link.node1_id,
link.node_two_id, link.node2_id,
link.options, link.options,
interface_one, iface1_id,
interface_two, iface2_id,
) )
if self.edge.asymmetric_link: if self.edge.asymmetric_link:
self.app.core.client.edit_link( self.app.core.client.edit_link(
session_id, session_id,
link.node_two_id, link.node2_id,
link.node_one_id, link.node1_id,
self.edge.asymmetric_link.options, self.edge.asymmetric_link.options,
interface_one, iface1_id,
interface_two, iface2_id,
) )
# update edge label
self.edge.draw_link_options()
self.destroy() self.destroy()
def change_symmetry(self): def change_symmetry(self) -> None:
if self.is_symmetric: if self.is_symmetric:
self.is_symmetric = False self.is_symmetric = False
self.symmetry_var.set("<<") self.symmetry_var.set("<<")
@ -304,7 +307,7 @@ class LinkConfigurationDialog(Dialog):
self.asymmetric_frame.grid_forget() self.asymmetric_frame.grid_forget()
self.symmetric_frame.grid(row=2, column=0) self.symmetric_frame.grid(row=2, column=0)
def load_link_config(self): def load_link_config(self) -> None:
""" """
populate link config to the table populate link config to the table
""" """
@ -317,12 +320,12 @@ class LinkConfigurationDialog(Dialog):
self.bandwidth.set(str(link.options.bandwidth)) self.bandwidth.set(str(link.options.bandwidth))
self.jitter.set(str(link.options.jitter)) self.jitter.set(str(link.options.jitter))
self.duplicate.set(str(link.options.dup)) self.duplicate.set(str(link.options.dup))
self.loss.set(str(link.options.per)) self.loss.set(str(link.options.loss))
self.delay.set(str(link.options.delay)) self.delay.set(str(link.options.delay))
if not self.is_symmetric: if not self.is_symmetric:
asym_link = self.edge.asymmetric_link asym_link = self.edge.asymmetric_link
self.down_bandwidth.set(str(asym_link.options.bandwidth)) self.down_bandwidth.set(str(asym_link.options.bandwidth))
self.down_jitter.set(str(asym_link.options.jitter)) self.down_jitter.set(str(asym_link.options.jitter))
self.down_duplicate.set(str(asym_link.options.dup)) self.down_duplicate.set(str(asym_link.options.dup))
self.down_loss.set(str(asym_link.options.per)) self.down_loss.set(str(asym_link.options.loss))
self.down_delay.set(str(asym_link.options.delay)) self.down_delay.set(str(asym_link.options.delay))

View file

@ -15,7 +15,7 @@ class MacConfigDialog(Dialog):
def __init__(self, app: "Application") -> None: def __init__(self, app: "Application") -> None:
super().__init__(app, "MAC Configuration") super().__init__(app, "MAC Configuration")
mac = self.app.guiconfig.mac mac = self.app.guiconfig.mac
self.mac_var = tk.StringVar(value=mac) self.mac_var: tk.StringVar = tk.StringVar(value=mac)
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:
@ -55,7 +55,7 @@ class MacConfigDialog(Dialog):
if not netaddr.valid_mac(mac): if not netaddr.valid_mac(mac):
messagebox.showerror("MAC Error", f"{mac} is an invalid mac") messagebox.showerror("MAC Error", f"{mac} is an invalid mac")
else: else:
self.app.core.interfaces_manager.mac = netaddr.EUI(mac) self.app.core.ifaces_manager.mac = netaddr.EUI(mac)
self.app.guiconfig.mac = mac self.app.guiconfig.mac = mac
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()

View file

@ -2,10 +2,12 @@
mobility configuration mobility configuration
""" """
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import Node
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame from core.gui.widgets import ConfigFrame
@ -16,23 +18,24 @@ if TYPE_CHECKING:
class MobilityConfigDialog(Dialog): class MobilityConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration") super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration")
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.config_frame = None self.config_frame: Optional[ConfigFrame] = None
self.has_error = False self.has_error: bool = False
try: try:
self.config = self.canvas_node.mobility_config config = self.canvas_node.mobility_config
if not self.config: if not config:
self.config = self.app.core.get_mobility_config(self.node.id) config = self.app.core.get_mobility_config(self.node.id)
self.config: Dict[str, ConfigOption] = config
self.draw() self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Get Mobility Config Error", e) self.app.show_grpc_exception("Get Mobility Config Error", e)
self.has_error = True self.has_error: bool = True
self.destroy() self.destroy()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame = ConfigFrame(self.top, self.app, self.config)
@ -40,7 +43,7 @@ class MobilityConfigDialog(Dialog):
self.config_frame.grid(sticky="nsew", pady=PADY) self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_apply_buttons() self.draw_apply_buttons()
def draw_apply_buttons(self): def draw_apply_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
@ -52,7 +55,7 @@ class MobilityConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_apply(self): def click_apply(self) -> None:
self.config_frame.parse_config() self.config_frame.parse_config()
self.canvas_node.mobility_config = self.config self.canvas_node.mobility_config = self.config
self.destroy() self.destroy()

View file

@ -1,9 +1,11 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import Node
from core.api.grpc.mobility_pb2 import MobilityAction from core.api.grpc.mobility_pb2 import MobilityAction
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum from core.gui.images import ImageEnum
@ -13,18 +15,23 @@ if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
ICON_SIZE = 16 ICON_SIZE: int = 16
class MobilityPlayer: class MobilityPlayer:
def __init__(self, app: "Application", canvas_node: "CanvasNode", config): def __init__(
self.app = app self,
self.canvas_node = canvas_node app: "Application",
self.config = config canvas_node: "CanvasNode",
self.dialog = None config: Dict[str, ConfigOption],
self.state = None ) -> None:
self.app: "Application" = app
self.canvas_node: "CanvasNode" = canvas_node
self.config: Dict[str, ConfigOption] = config
self.dialog: Optional[MobilityPlayerDialog] = None
self.state: Optional[MobilityAction] = None
def show(self): def show(self) -> None:
if self.dialog: if self.dialog:
self.dialog.destroy() self.dialog.destroy()
self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config) self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config)
@ -37,44 +44,49 @@ class MobilityPlayer:
self.set_stop() self.set_stop()
self.dialog.show() self.dialog.show()
def close(self): def close(self) -> None:
if self.dialog: if self.dialog:
self.dialog.destroy() self.dialog.destroy()
self.dialog = None self.dialog = None
def set_play(self): def set_play(self) -> None:
self.state = MobilityAction.START self.state = MobilityAction.START
if self.dialog: if self.dialog:
self.dialog.set_play() self.dialog.set_play()
def set_pause(self): def set_pause(self) -> None:
self.state = MobilityAction.PAUSE self.state = MobilityAction.PAUSE
if self.dialog: if self.dialog:
self.dialog.set_pause() self.dialog.set_pause()
def set_stop(self): def set_stop(self) -> None:
self.state = MobilityAction.STOP self.state = MobilityAction.STOP
if self.dialog: if self.dialog:
self.dialog.set_stop() self.dialog.set_stop()
class MobilityPlayerDialog(Dialog): class MobilityPlayerDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode", config): def __init__(
self,
app: "Application",
canvas_node: "CanvasNode",
config: Dict[str, ConfigOption],
) -> None:
super().__init__( super().__init__(
app, f"{canvas_node.core_node.name} Mobility Player", modal=False app, f"{canvas_node.core_node.name} Mobility Player", modal=False
) )
self.resizable(False, False) self.resizable(False, False)
self.geometry("") self.geometry("")
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.config = config self.config: Dict[str, ConfigOption] = config
self.play_button = None self.play_button: Optional[ttk.Button] = None
self.pause_button = None self.pause_button: Optional[ttk.Button] = None
self.stop_button = None self.stop_button: Optional[ttk.Button] = None
self.progressbar = None self.progressbar: Optional[ttk.Progressbar] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
file_name = self.config["file"].value file_name = self.config["file"].value
@ -114,27 +126,27 @@ class MobilityPlayerDialog(Dialog):
label = ttk.Label(frame, text=f"rate {rate} ms") label = ttk.Label(frame, text=f"rate {rate} ms")
label.grid(row=0, column=4) label.grid(row=0, column=4)
def clear_buttons(self): def clear_buttons(self) -> None:
self.play_button.state(["!pressed"]) self.play_button.state(["!pressed"])
self.pause_button.state(["!pressed"]) self.pause_button.state(["!pressed"])
self.stop_button.state(["!pressed"]) self.stop_button.state(["!pressed"])
def set_play(self): def set_play(self) -> None:
self.clear_buttons() self.clear_buttons()
self.play_button.state(["pressed"]) self.play_button.state(["pressed"])
self.progressbar.start() self.progressbar.start()
def set_pause(self): def set_pause(self) -> None:
self.clear_buttons() self.clear_buttons()
self.pause_button.state(["pressed"]) self.pause_button.state(["pressed"])
self.progressbar.stop() self.progressbar.stop()
def set_stop(self): def set_stop(self) -> None:
self.clear_buttons() self.clear_buttons()
self.stop_button.state(["pressed"]) self.stop_button.state(["pressed"])
self.progressbar.stop() self.progressbar.stop()
def click_play(self): def click_play(self) -> None:
self.set_play() self.set_play()
session_id = self.app.core.session_id session_id = self.app.core.session_id
try: try:
@ -144,7 +156,7 @@ class MobilityPlayerDialog(Dialog):
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Mobility Error", e) self.app.show_grpc_exception("Mobility Error", e)
def click_pause(self): def click_pause(self) -> None:
self.set_pause() self.set_pause()
session_id = self.app.core.session_id session_id = self.app.core.session_id
try: try:
@ -154,7 +166,7 @@ class MobilityPlayerDialog(Dialog):
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Mobility Error", e) self.app.show_grpc_exception("Mobility Error", e)
def click_stop(self): def click_stop(self) -> None:
self.set_stop() self.set_stop()
session_id = self.app.core.session_id session_id = self.app.core.session_id
try: try:

View file

@ -2,10 +2,12 @@ import logging
import tkinter as tk import tkinter as tk
from functools import partial from functools import partial
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
import netaddr import netaddr
from PIL.ImageTk import PhotoImage
from core.api.grpc.core_pb2 import Node
from core.gui import nodeutils, validation from core.gui import nodeutils, validation
from core.gui.appconfig import ICONS_PATH from core.gui.appconfig import ICONS_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -86,35 +88,35 @@ class InterfaceData:
mac: tk.StringVar, mac: tk.StringVar,
ip4: tk.StringVar, ip4: tk.StringVar,
ip6: tk.StringVar, ip6: tk.StringVar,
): ) -> None:
self.is_auto = is_auto self.is_auto: tk.BooleanVar = is_auto
self.mac = mac self.mac: tk.StringVar = mac
self.ip4 = ip4 self.ip4: tk.StringVar = ip4
self.ip6 = ip6 self.ip6: tk.StringVar = ip6
class NodeConfigDialog(Dialog): class NodeConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
""" """
create an instance of node configuration create an instance of node configuration
""" """
super().__init__(app, f"{canvas_node.core_node.name} Configuration") super().__init__(app, f"{canvas_node.core_node.name} Configuration")
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.image = canvas_node.image self.image: PhotoImage = canvas_node.image
self.image_file = None self.image_file: Optional[str] = None
self.image_button = None self.image_button: Optional[ttk.Button] = None
self.name = tk.StringVar(value=self.node.name) self.name: tk.StringVar = tk.StringVar(value=self.node.name)
self.type = tk.StringVar(value=self.node.model) self.type: tk.StringVar = tk.StringVar(value=self.node.model)
self.container_image = tk.StringVar(value=self.node.image) self.container_image: tk.StringVar = tk.StringVar(value=self.node.image)
server = "localhost" server = "localhost"
if self.node.server: if self.node.server:
server = self.node.server server = self.node.server
self.server = tk.StringVar(value=server) self.server: tk.StringVar = tk.StringVar(value=server)
self.interfaces = {} self.ifaces: Dict[int, InterfaceData] = {}
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
row = 0 row = 0
@ -183,53 +185,53 @@ class NodeConfigDialog(Dialog):
row += 1 row += 1
if NodeUtils.is_rj45_node(self.node.type): if NodeUtils.is_rj45_node(self.node.type):
response = self.app.core.client.get_interfaces() response = self.app.core.client.get_ifaces()
logging.debug("host machine available interfaces: %s", response) logging.debug("host machine available interfaces: %s", response)
interfaces = ListboxScroll(frame) ifaces = ListboxScroll(frame)
interfaces.listbox.config(state=state) ifaces.listbox.config(state=state)
interfaces.grid( ifaces.grid(
row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY
) )
for inf in sorted(response.interfaces[:]): for inf in sorted(response.ifaces[:]):
interfaces.listbox.insert(tk.END, inf) ifaces.listbox.insert(tk.END, inf)
row += 1 row += 1
interfaces.listbox.bind("<<ListboxSelect>>", self.interface_select) ifaces.listbox.bind("<<ListboxSelect>>", self.iface_select)
# interfaces # interfaces
if self.canvas_node.interfaces: if self.canvas_node.ifaces:
self.draw_interfaces() self.draw_ifaces()
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_interfaces(self): def draw_ifaces(self) -> None:
notebook = ttk.Notebook(self.top) notebook = ttk.Notebook(self.top)
notebook.grid(sticky="nsew", pady=PADY) notebook.grid(sticky="nsew", pady=PADY)
self.top.rowconfigure(notebook.grid_info()["row"], weight=1) self.top.rowconfigure(notebook.grid_info()["row"], weight=1)
state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
for interface_id in sorted(self.canvas_node.interfaces): for iface_id in sorted(self.canvas_node.ifaces):
interface = self.canvas_node.interfaces[interface_id] iface = self.canvas_node.ifaces[iface_id]
tab = ttk.Frame(notebook, padding=FRAME_PAD) tab = ttk.Frame(notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew", pady=PADY) tab.grid(sticky="nsew", pady=PADY)
tab.columnconfigure(1, weight=1) tab.columnconfigure(1, weight=1)
tab.columnconfigure(2, weight=1) tab.columnconfigure(2, weight=1)
notebook.add(tab, text=interface.name) notebook.add(tab, text=iface.name)
row = 0 row = 0
emane_node = self.canvas_node.has_emane_link(interface.id) emane_node = self.canvas_node.has_emane_link(iface.id)
if emane_node: if emane_node:
emane_model = emane_node.emane.split("_")[1] emane_model = emane_node.emane.split("_")[1]
button = ttk.Button( button = ttk.Button(
tab, tab,
text=f"Configure EMANE {emane_model}", text=f"Configure EMANE {emane_model}",
command=lambda: self.click_emane_config(emane_model, interface.id), command=lambda: self.click_emane_config(emane_model, iface.id),
) )
button.grid(row=row, sticky="ew", columnspan=3, pady=PADY) button.grid(row=row, sticky="ew", columnspan=3, pady=PADY)
row += 1 row += 1
label = ttk.Label(tab, text="MAC") label = ttk.Label(tab, text="MAC")
label.grid(row=row, column=0, padx=PADX, pady=PADY) label.grid(row=row, column=0, padx=PADX, pady=PADY)
auto_set = not interface.mac auto_set = not iface.mac
mac_state = tk.DISABLED if auto_set else tk.NORMAL mac_state = tk.DISABLED if auto_set else tk.NORMAL
is_auto = tk.BooleanVar(value=auto_set) is_auto = tk.BooleanVar(value=auto_set)
checkbutton = ttk.Checkbutton( checkbutton = ttk.Checkbutton(
@ -237,7 +239,7 @@ class NodeConfigDialog(Dialog):
) )
checkbutton.var = is_auto checkbutton.var = is_auto
checkbutton.grid(row=row, column=1, padx=PADX) checkbutton.grid(row=row, column=1, padx=PADX)
mac = tk.StringVar(value=interface.mac) mac = tk.StringVar(value=iface.mac)
entry = ttk.Entry(tab, textvariable=mac, state=mac_state) entry = ttk.Entry(tab, textvariable=mac, state=mac_state)
entry.grid(row=row, column=2, sticky="ew") entry.grid(row=row, column=2, sticky="ew")
func = partial(mac_auto, is_auto, entry, mac) func = partial(mac_auto, is_auto, entry, mac)
@ -247,8 +249,8 @@ class NodeConfigDialog(Dialog):
label = ttk.Label(tab, text="IPv4") label = ttk.Label(tab, text="IPv4")
label.grid(row=row, column=0, padx=PADX, pady=PADY) label.grid(row=row, column=0, padx=PADX, pady=PADY)
ip4_net = "" ip4_net = ""
if interface.ip4: if iface.ip4:
ip4_net = f"{interface.ip4}/{interface.ip4mask}" ip4_net = f"{iface.ip4}/{iface.ip4_mask}"
ip4 = tk.StringVar(value=ip4_net) ip4 = tk.StringVar(value=ip4_net)
entry = ttk.Entry(tab, textvariable=ip4, state=state) entry = ttk.Entry(tab, textvariable=ip4, state=state)
entry.grid(row=row, column=1, columnspan=2, sticky="ew") entry.grid(row=row, column=1, columnspan=2, sticky="ew")
@ -257,15 +259,15 @@ class NodeConfigDialog(Dialog):
label = ttk.Label(tab, text="IPv6") label = ttk.Label(tab, text="IPv6")
label.grid(row=row, column=0, padx=PADX, pady=PADY) label.grid(row=row, column=0, padx=PADX, pady=PADY)
ip6_net = "" ip6_net = ""
if interface.ip6: if iface.ip6:
ip6_net = f"{interface.ip6}/{interface.ip6mask}" ip6_net = f"{iface.ip6}/{iface.ip6_mask}"
ip6 = tk.StringVar(value=ip6_net) ip6 = tk.StringVar(value=ip6_net)
entry = ttk.Entry(tab, textvariable=ip6, state=state) entry = ttk.Entry(tab, textvariable=ip6, state=state)
entry.grid(row=row, column=1, columnspan=2, sticky="ew") entry.grid(row=row, column=1, columnspan=2, sticky="ew")
self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
@ -277,20 +279,20 @@ class NodeConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_emane_config(self, emane_model: str, interface_id: int): def click_emane_config(self, emane_model: str, iface_id: int) -> None:
dialog = EmaneModelDialog( dialog = EmaneModelDialog(
self, self.app, self.canvas_node, emane_model, interface_id self, self.app, self.canvas_node, emane_model, iface_id
) )
dialog.show() dialog.show()
def click_icon(self): def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH) file_path = image_chooser(self, ICONS_PATH)
if file_path: if file_path:
self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image = Images.create(file_path, nodeutils.ICON_SIZE)
self.image_button.config(image=self.image) self.image_button.config(image=self.image)
self.image_file = file_path self.image_file = file_path
def click_apply(self): def click_apply(self) -> None:
error = False error = False
# update core node # update core node
@ -309,54 +311,54 @@ class NodeConfigDialog(Dialog):
self.canvas_node.image = self.image self.canvas_node.image = self.image
# update node interface data # update node interface data
for interface in self.canvas_node.interfaces.values(): for iface in self.canvas_node.ifaces.values():
data = self.interfaces[interface.id] data = self.ifaces[iface.id]
# validate ip4 # validate ip4
ip4_net = data.ip4.get() ip4_net = data.ip4.get()
if not check_ip4(self, interface.name, ip4_net): if not check_ip4(self, iface.name, ip4_net):
error = True error = True
break break
if ip4_net: if ip4_net:
ip4, ip4mask = ip4_net.split("/") ip4, ip4_mask = ip4_net.split("/")
ip4mask = int(ip4mask) ip4_mask = int(ip4_mask)
else: else:
ip4, ip4mask = "", 0 ip4, ip4_mask = "", 0
interface.ip4 = ip4 iface.ip4 = ip4
interface.ip4mask = ip4mask iface.ip4_mask = ip4_mask
# validate ip6 # validate ip6
ip6_net = data.ip6.get() ip6_net = data.ip6.get()
if not check_ip6(self, interface.name, ip6_net): if not check_ip6(self, iface.name, ip6_net):
error = True error = True
break break
if ip6_net: if ip6_net:
ip6, ip6mask = ip6_net.split("/") ip6, ip6_mask = ip6_net.split("/")
ip6mask = int(ip6mask) ip6_mask = int(ip6_mask)
else: else:
ip6, ip6mask = "", 0 ip6, ip6_mask = "", 0
interface.ip6 = ip6 iface.ip6 = ip6
interface.ip6mask = ip6mask iface.ip6_mask = ip6_mask
mac = data.mac.get() mac = data.mac.get()
auto_mac = data.is_auto.get() auto_mac = data.is_auto.get()
if not auto_mac and not netaddr.valid_mac(mac): if not auto_mac and not netaddr.valid_mac(mac):
title = f"MAC Error for {interface.name}" title = f"MAC Error for {iface.name}"
messagebox.showerror(title, "Invalid MAC Address") messagebox.showerror(title, "Invalid MAC Address")
error = True error = True
break break
elif not auto_mac: elif not auto_mac:
mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded) mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded)
interface.mac = str(mac) iface.mac = str(mac)
# redraw # redraw
if not error: if not error:
self.canvas_node.redraw() self.canvas_node.redraw()
self.destroy() self.destroy()
def interface_select(self, event: tk.Event): def iface_select(self, event: tk.Event) -> None:
listbox = event.widget listbox = event.widget
cur = listbox.curselection() cur = listbox.curselection()
if cur: if cur:
interface = listbox.get(cur[0]) iface = listbox.get(cur[0])
self.name.set(interface) self.name.set(iface)

View file

@ -4,7 +4,7 @@ core node services
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, Set from typing import TYPE_CHECKING, Optional, Set
from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -19,20 +19,20 @@ if TYPE_CHECKING:
class NodeConfigServiceDialog(Dialog): class NodeConfigServiceDialog(Dialog):
def __init__( def __init__(
self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None
): ) -> None:
title = f"{canvas_node.core_node.name} Config Services" title = f"{canvas_node.core_node.name} Config Services"
super().__init__(app, title) super().__init__(app, title)
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node_id = canvas_node.core_node.id self.node_id: int = canvas_node.core_node.id
self.groups = None self.groups: Optional[ListboxScroll] = None
self.services = None self.services: Optional[CheckboxList] = None
self.current = None self.current: Optional[ListboxScroll] = None
if services is None: if services is None:
services = set(canvas_node.core_node.config_services) services = set(canvas_node.core_node.config_services)
self.current_services = services self.current_services: Set[str] = services
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -84,9 +84,9 @@ class NodeConfigServiceDialog(Dialog):
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
# trigger group change # trigger group change
self.groups.listbox.event_generate("<<ListboxSelect>>") self.handle_group_change()
def handle_group_change(self, event: tk.Event = None): def handle_group_change(self, event: tk.Event = None) -> None:
selection = self.groups.listbox.curselection() selection = self.groups.listbox.curselection()
if selection: if selection:
index = selection[0] index = selection[0]
@ -96,7 +96,7 @@ class NodeConfigServiceDialog(Dialog):
checked = name in self.current_services checked = name in self.current_services
self.services.add(name, checked) self.services.add(name, checked)
def service_clicked(self, name: str, var: tk.IntVar): def service_clicked(self, name: str, var: tk.IntVar) -> None:
if var.get() and name not in self.current_services: if var.get() and name not in self.current_services:
self.current_services.add(name) self.current_services.add(name)
elif not var.get() and name in self.current_services: elif not var.get() and name in self.current_services:
@ -104,7 +104,7 @@ class NodeConfigServiceDialog(Dialog):
self.draw_current_services() self.draw_current_services()
self.canvas_node.core_node.config_services[:] = self.current_services self.canvas_node.core_node.config_services[:] = self.current_services
def click_configure(self): def click_configure(self) -> None:
current_selection = self.current.listbox.curselection() current_selection = self.current.listbox.curselection()
if len(current_selection): if len(current_selection):
dialog = ConfigServiceConfigDialog( dialog = ConfigServiceConfigDialog(
@ -124,25 +124,25 @@ class NodeConfigServiceDialog(Dialog):
parent=self, parent=self,
) )
def draw_current_services(self): def draw_current_services(self) -> None:
self.current.listbox.delete(0, tk.END) self.current.listbox.delete(0, tk.END)
for name in sorted(self.current_services): for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name) self.current.listbox.insert(tk.END, name)
if self.is_custom_service(name): if self.is_custom_service(name):
self.current.listbox.itemconfig(tk.END, bg="green") self.current.listbox.itemconfig(tk.END, bg="green")
def click_save(self): def click_save(self) -> None:
self.canvas_node.core_node.config_services[:] = self.current_services self.canvas_node.core_node.config_services[:] = self.current_services
logging.info( logging.info(
"saved node config services: %s", self.canvas_node.core_node.config_services "saved node config services: %s", self.canvas_node.core_node.config_services
) )
self.destroy() self.destroy()
def click_cancel(self): def click_cancel(self) -> None:
self.current_services = None self.current_services = None
self.destroy() self.destroy()
def click_remove(self): def click_remove(self) -> None:
cur = self.current.listbox.curselection() cur = self.current.listbox.curselection()
if cur: if cur:
service = self.current.listbox.get(cur[0]) service = self.current.listbox.get(cur[0])

View file

@ -3,7 +3,7 @@ core node services
""" """
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional, Set
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.dialogs.serviceconfig import ServiceConfigDialog from core.gui.dialogs.serviceconfig import ServiceConfigDialog
@ -16,19 +16,19 @@ if TYPE_CHECKING:
class NodeServiceDialog(Dialog): class NodeServiceDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
title = f"{canvas_node.core_node.name} Services" title = f"{canvas_node.core_node.name} Services"
super().__init__(app, title) super().__init__(app, title)
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node_id = canvas_node.core_node.id self.node_id: int = canvas_node.core_node.id
self.groups = None self.groups: Optional[ListboxScroll] = None
self.services = None self.services: Optional[CheckboxList] = None
self.current = None self.current: Optional[ListboxScroll] = None
services = set(canvas_node.core_node.services) services = set(canvas_node.core_node.services)
self.current_services = services self.current_services: Set[str] = services
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -82,9 +82,9 @@ class NodeServiceDialog(Dialog):
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
# trigger group change # trigger group change
self.groups.listbox.event_generate("<<ListboxSelect>>") self.handle_group_change()
def handle_group_change(self, event: tk.Event = None): def handle_group_change(self, event: tk.Event = None) -> None:
selection = self.groups.listbox.curselection() selection = self.groups.listbox.curselection()
if selection: if selection:
index = selection[0] index = selection[0]
@ -94,7 +94,7 @@ class NodeServiceDialog(Dialog):
checked = name in self.current_services checked = name in self.current_services
self.services.add(name, checked) self.services.add(name, checked)
def service_clicked(self, name: str, var: tk.IntVar): def service_clicked(self, name: str, var: tk.IntVar) -> None:
if var.get() and name not in self.current_services: if var.get() and name not in self.current_services:
self.current_services.add(name) self.current_services.add(name)
elif not var.get() and name in self.current_services: elif not var.get() and name in self.current_services:
@ -106,7 +106,7 @@ class NodeServiceDialog(Dialog):
self.current.listbox.itemconfig(tk.END, bg="green") self.current.listbox.itemconfig(tk.END, bg="green")
self.canvas_node.core_node.services[:] = self.current_services self.canvas_node.core_node.services[:] = self.current_services
def click_configure(self): def click_configure(self) -> None:
current_selection = self.current.listbox.curselection() current_selection = self.current.listbox.curselection()
if len(current_selection): if len(current_selection):
dialog = ServiceConfigDialog( dialog = ServiceConfigDialog(
@ -127,12 +127,12 @@ class NodeServiceDialog(Dialog):
"Service Configuration", "Select a service to configure", parent=self "Service Configuration", "Select a service to configure", parent=self
) )
def click_save(self): def click_save(self) -> None:
core_node = self.canvas_node.core_node core_node = self.canvas_node.core_node
core_node.services[:] = self.current_services core_node.services[:] = self.current_services
self.destroy() self.destroy()
def click_remove(self): def click_remove(self) -> None:
cur = self.current.listbox.curselection() cur = self.current.listbox.curselection()
if cur: if cur:
service = self.current.listbox.get(cur[0]) service = self.current.listbox.get(cur[0])

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from core.gui.appconfig import Observer from core.gui.appconfig import Observer
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -12,18 +12,18 @@ if TYPE_CHECKING:
class ObserverDialog(Dialog): class ObserverDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Observer Widgets") super().__init__(app, "Observer Widgets")
self.observers = None self.observers: Optional[tk.Listbox] = None
self.save_button = None self.save_button: Optional[ttk.Button] = None
self.delete_button = None self.delete_button: Optional[ttk.Button] = None
self.selected = None self.selected: Optional[str] = None
self.selected_index = None self.selected_index: Optional[int] = None
self.name = tk.StringVar() self.name: tk.StringVar = tk.StringVar()
self.cmd = tk.StringVar() self.cmd: tk.StringVar = tk.StringVar()
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.draw_listbox() self.draw_listbox()
@ -31,7 +31,7 @@ class ObserverDialog(Dialog):
self.draw_config_buttons() self.draw_config_buttons()
self.draw_apply_buttons() self.draw_apply_buttons()
def draw_listbox(self): def draw_listbox(self) -> None:
listbox_scroll = ListboxScroll(self.top) listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(sticky="nsew", pady=PADY) listbox_scroll.grid(sticky="nsew", pady=PADY)
listbox_scroll.columnconfigure(0, weight=1) listbox_scroll.columnconfigure(0, weight=1)
@ -42,7 +42,7 @@ class ObserverDialog(Dialog):
for name in sorted(self.app.core.custom_observers): for name in sorted(self.app.core.custom_observers):
self.observers.insert(tk.END, name) self.observers.insert(tk.END, name)
def draw_form_fields(self): def draw_form_fields(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY) frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -57,7 +57,7 @@ class ObserverDialog(Dialog):
entry = ttk.Entry(frame, textvariable=self.cmd) entry = ttk.Entry(frame, textvariable=self.cmd)
entry.grid(row=1, column=1, sticky="ew") entry.grid(row=1, column=1, sticky="ew")
def draw_config_buttons(self): def draw_config_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY) frame.grid(sticky="ew", pady=PADY)
for i in range(3): for i in range(3):
@ -76,7 +76,7 @@ class ObserverDialog(Dialog):
) )
self.delete_button.grid(row=0, column=2, sticky="ew") self.delete_button.grid(row=0, column=2, sticky="ew")
def draw_apply_buttons(self): def draw_apply_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
@ -88,14 +88,14 @@ class ObserverDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_save_config(self): def click_save_config(self) -> None:
self.app.guiconfig.observers.clear() self.app.guiconfig.observers.clear()
for observer in self.app.core.custom_observers.values(): for observer in self.app.core.custom_observers.values():
self.app.guiconfig.observers.append(observer) self.app.guiconfig.observers.append(observer)
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()
def click_create(self): def click_create(self) -> None:
name = self.name.get() name = self.name.get()
if name not in self.app.core.custom_observers: if name not in self.app.core.custom_observers:
cmd = self.cmd.get() cmd = self.cmd.get()
@ -109,7 +109,7 @@ class ObserverDialog(Dialog):
else: else:
messagebox.showerror("Observer Error", f"{name} already exists") messagebox.showerror("Observer Error", f"{name} already exists")
def click_save(self): def click_save(self) -> None:
name = self.name.get() name = self.name.get()
if self.selected: if self.selected:
previous_name = self.selected previous_name = self.selected
@ -122,7 +122,7 @@ class ObserverDialog(Dialog):
self.observers.insert(self.selected_index, name) self.observers.insert(self.selected_index, name)
self.observers.selection_set(self.selected_index) self.observers.selection_set(self.selected_index)
def click_delete(self): def click_delete(self) -> None:
if self.selected: if self.selected:
self.observers.delete(self.selected_index) self.observers.delete(self.selected_index)
del self.app.core.custom_observers[self.selected] del self.app.core.custom_observers[self.selected]
@ -136,7 +136,7 @@ class ObserverDialog(Dialog):
self.app.menubar.observers_menu.draw_custom() self.app.menubar.observers_menu.draw_custom()
self.app.toolbar.observers_menu.draw_custom() self.app.toolbar.observers_menu.draw_custom()
def handle_observer_change(self, event: tk.Event): def handle_observer_change(self, event: tk.Event) -> None:
selection = self.observers.curselection() selection = self.observers.curselection()
if selection: if selection:
self.selected_index = selection[0] self.selected_index = selection[0]

View file

@ -12,27 +12,27 @@ from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
SCALE_INTERVAL = 0.01 SCALE_INTERVAL: float = 0.01
class PreferencesDialog(Dialog): class PreferencesDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Preferences") super().__init__(app, "Preferences")
self.gui_scale = tk.DoubleVar(value=self.app.app_scale) self.gui_scale: tk.DoubleVar = tk.DoubleVar(value=self.app.app_scale)
preferences = self.app.guiconfig.preferences preferences = self.app.guiconfig.preferences
self.editor = tk.StringVar(value=preferences.editor) self.editor: tk.StringVar = tk.StringVar(value=preferences.editor)
self.theme = tk.StringVar(value=preferences.theme) self.theme: tk.StringVar = tk.StringVar(value=preferences.theme)
self.terminal = tk.StringVar(value=preferences.terminal) self.terminal: tk.StringVar = tk.StringVar(value=preferences.terminal)
self.gui3d = tk.StringVar(value=preferences.gui3d) self.gui3d: tk.StringVar = tk.StringVar(value=preferences.gui3d)
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.draw_preferences() self.draw_preferences()
self.draw_buttons() self.draw_buttons()
def draw_preferences(self): def draw_preferences(self) -> None:
frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD) frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD)
frame.grid(sticky="nsew", pady=PADY) frame.grid(sticky="nsew", pady=PADY)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -88,7 +88,7 @@ class PreferencesDialog(Dialog):
scrollbar = ttk.Scrollbar(scale_frame, command=self.adjust_scale) scrollbar = ttk.Scrollbar(scale_frame, command=self.adjust_scale)
scrollbar.grid(row=0, column=2) scrollbar.grid(row=0, column=2)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
@ -100,12 +100,12 @@ class PreferencesDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def theme_change(self, event: tk.Event): def theme_change(self, event: tk.Event) -> None:
theme = self.theme.get() theme = self.theme.get()
logging.info("changing theme: %s", theme) logging.info("changing theme: %s", theme)
self.app.style.theme_use(theme) self.app.style.theme_use(theme)
def click_save(self): def click_save(self) -> None:
preferences = self.app.guiconfig.preferences preferences = self.app.guiconfig.preferences
preferences.terminal = self.terminal.get() preferences.terminal = self.terminal.get()
preferences.editor = self.editor.get() preferences.editor = self.editor.get()
@ -118,7 +118,7 @@ class PreferencesDialog(Dialog):
self.scale_adjust() self.scale_adjust()
self.destroy() self.destroy()
def scale_adjust(self): def scale_adjust(self) -> None:
app_scale = self.gui_scale.get() app_scale = self.gui_scale.get()
self.app.app_scale = app_scale self.app.app_scale = app_scale
self.app.master.tk.call("tk", "scaling", app_scale) self.app.master.tk.call("tk", "scaling", app_scale)
@ -136,7 +136,7 @@ class PreferencesDialog(Dialog):
self.app.toolbar.scale() self.app.toolbar.scale()
self.app.canvas.scale_graph() self.app.canvas.scale_graph()
def adjust_scale(self, arg1: str, arg2: str, arg3: str): def adjust_scale(self, arg1: str, arg2: str, arg3: str) -> None:
scale_value = self.gui_scale.get() scale_value = self.gui_scale.get()
if arg2 == "-1": if arg2 == "-1":
if scale_value <= LARGEST_SCALE - SCALE_INTERVAL: if scale_value <= LARGEST_SCALE - SCALE_INTERVAL:

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
@ -14,10 +14,10 @@ if TYPE_CHECKING:
class RunToolDialog(Dialog): class RunToolDialog(Dialog):
def __init__(self, app: "Application") -> None: def __init__(self, app: "Application") -> None:
super().__init__(app, "Run Tool") super().__init__(app, "Run Tool")
self.cmd = tk.StringVar(value="ps ax") self.cmd: tk.StringVar = tk.StringVar(value="ps ax")
self.result = None self.result: Optional[CodeText] = None
self.node_list = None self.node_list: Optional[ListboxScroll] = None
self.executable_nodes = {} self.executable_nodes: Dict[str, int] = {}
self.store_nodes() self.store_nodes()
self.draw() self.draw()

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from core.gui.appconfig import CoreServer from core.gui.appconfig import CoreServer
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -10,24 +10,24 @@ from core.gui.widgets import ListboxScroll
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
DEFAULT_NAME = "example" DEFAULT_NAME: str = "example"
DEFAULT_ADDRESS = "127.0.0.1" DEFAULT_ADDRESS: str = "127.0.0.1"
DEFAULT_PORT = 50051 DEFAULT_PORT: int = 50051
class ServersDialog(Dialog): class ServersDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "CORE Servers") super().__init__(app, "CORE Servers")
self.name = tk.StringVar(value=DEFAULT_NAME) self.name: tk.StringVar = tk.StringVar(value=DEFAULT_NAME)
self.address = tk.StringVar(value=DEFAULT_ADDRESS) self.address: tk.StringVar = tk.StringVar(value=DEFAULT_ADDRESS)
self.servers = None self.servers: Optional[tk.Listbox] = None
self.selected_index = None self.selected_index: Optional[int] = None
self.selected = None self.selected: Optional[str] = None
self.save_button = None self.save_button: Optional[ttk.Button] = None
self.delete_button = None self.delete_button: Optional[ttk.Button] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.draw_servers() self.draw_servers()
@ -35,7 +35,7 @@ class ServersDialog(Dialog):
self.draw_server_configuration() self.draw_server_configuration()
self.draw_apply_buttons() self.draw_apply_buttons()
def draw_servers(self): def draw_servers(self) -> None:
listbox_scroll = ListboxScroll(self.top) listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(pady=PADY, sticky="nsew") listbox_scroll.grid(pady=PADY, sticky="nsew")
listbox_scroll.columnconfigure(0, weight=1) listbox_scroll.columnconfigure(0, weight=1)
@ -48,7 +48,7 @@ class ServersDialog(Dialog):
for server in self.app.core.servers: for server in self.app.core.servers:
self.servers.insert(tk.END, server) self.servers.insert(tk.END, server)
def draw_server_configuration(self): def draw_server_configuration(self) -> None:
frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD) frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD)
frame.grid(pady=PADY, sticky="ew") frame.grid(pady=PADY, sticky="ew")
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -64,7 +64,7 @@ class ServersDialog(Dialog):
entry = ttk.Entry(frame, textvariable=self.address) entry = ttk.Entry(frame, textvariable=self.address)
entry.grid(row=0, column=3, sticky="ew") entry.grid(row=0, column=3, sticky="ew")
def draw_servers_buttons(self): def draw_servers_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(pady=PADY, sticky="ew") frame.grid(pady=PADY, sticky="ew")
for i in range(3): for i in range(3):
@ -83,7 +83,7 @@ class ServersDialog(Dialog):
) )
self.delete_button.grid(row=0, column=2, sticky="ew") self.delete_button.grid(row=0, column=2, sticky="ew")
def draw_apply_buttons(self): def draw_apply_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
@ -104,7 +104,7 @@ class ServersDialog(Dialog):
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()
def click_create(self): def click_create(self) -> None:
name = self.name.get() name = self.name.get()
if name not in self.app.core.servers: if name not in self.app.core.servers:
address = self.address.get() address = self.address.get()
@ -112,7 +112,7 @@ class ServersDialog(Dialog):
self.app.core.servers[name] = server self.app.core.servers[name] = server
self.servers.insert(tk.END, name) self.servers.insert(tk.END, name)
def click_save(self): def click_save(self) -> None:
name = self.name.get() name = self.name.get()
if self.selected: if self.selected:
previous_name = self.selected previous_name = self.selected
@ -125,7 +125,7 @@ class ServersDialog(Dialog):
self.servers.insert(self.selected_index, name) self.servers.insert(self.selected_index, name)
self.servers.selection_set(self.selected_index) self.servers.selection_set(self.selected_index)
def click_delete(self): def click_delete(self) -> None:
if self.selected: if self.selected:
self.servers.delete(self.selected_index) self.servers.delete(self.selected_index)
del self.app.core.servers[self.selected] del self.app.core.servers[self.selected]
@ -137,7 +137,7 @@ class ServersDialog(Dialog):
self.save_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED)
def handle_server_change(self, event: tk.Event): def handle_server_change(self, event: tk.Event) -> None:
selection = self.servers.curselection() selection = self.servers.curselection()
if selection: if selection:
self.selected_index = selection[0] self.selected_index = selection[0]

View file

@ -2,11 +2,12 @@ import logging
import os import os
import tkinter as tk import tkinter as tk
from tkinter import filedialog, ttk from tkinter import filedialog, ttk
from typing import TYPE_CHECKING, List from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
import grpc import grpc
from PIL.ImageTk import PhotoImage
from core.api.grpc.services_pb2 import ServiceValidationMode from core.api.grpc.services_pb2 import NodeServiceData, ServiceValidationMode
from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
@ -16,8 +17,9 @@ from core.gui.widgets import CodeText, ListboxScroll
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
from core.gui.coreclient import CoreClient
ICON_SIZE = 16 ICON_SIZE: int = 16
class ServiceConfigDialog(Dialog): class ServiceConfigDialog(Dialog):
@ -28,54 +30,57 @@ class ServiceConfigDialog(Dialog):
service_name: str, service_name: str,
canvas_node: "CanvasNode", canvas_node: "CanvasNode",
node_id: int, node_id: int,
): ) -> None:
title = f"{service_name} Service" title = f"{service_name} Service"
super().__init__(app, title, master=master) super().__init__(app, title, master=master)
self.core = app.core self.core: "CoreClient" = app.core
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node_id = node_id self.node_id: int = node_id
self.service_name = service_name self.service_name: str = service_name
self.radiovar = tk.IntVar() self.radiovar: tk.IntVar = tk.IntVar(value=2)
self.radiovar.set(2) self.metadata: str = ""
self.metadata = "" self.filenames: List[str] = []
self.filenames = [] self.dependencies: List[str] = []
self.dependencies = [] self.executables: List[str] = []
self.executables = [] self.startup_commands: List[str] = []
self.startup_commands = [] self.validation_commands: List[str] = []
self.validation_commands = [] self.shutdown_commands: List[str] = []
self.shutdown_commands = [] self.default_startup: List[str] = []
self.default_startup = [] self.default_validate: List[str] = []
self.default_validate = [] self.default_shutdown: List[str] = []
self.default_shutdown = [] self.validation_mode: Optional[ServiceValidationMode] = None
self.validation_mode = None self.validation_time: Optional[int] = None
self.validation_time = None self.validation_period: Optional[float] = None
self.validation_period = None self.directory_entry: Optional[ttk.Entry] = None
self.directory_entry = None self.default_directories: List[str] = []
self.default_directories = [] self.temp_directories: List[str] = []
self.temp_directories = [] self.documentnew_img: PhotoImage = self.app.get_icon(
self.documentnew_img = self.app.get_icon(ImageEnum.DOCUMENTNEW, ICON_SIZE) ImageEnum.DOCUMENTNEW, ICON_SIZE
self.editdelete_img = self.app.get_icon(ImageEnum.EDITDELETE, ICON_SIZE) )
self.notebook = None self.editdelete_img: PhotoImage = self.app.get_icon(
self.metadata_entry = None ImageEnum.EDITDELETE, ICON_SIZE
self.filename_combobox = None )
self.dir_list = None self.notebook: Optional[ttk.Notebook] = None
self.startup_commands_listbox = None self.metadata_entry: Optional[ttk.Entry] = None
self.shutdown_commands_listbox = None self.filename_combobox: Optional[ttk.Combobox] = None
self.validate_commands_listbox = None self.dir_list: Optional[ListboxScroll] = None
self.validation_time_entry = None self.startup_commands_listbox: Optional[tk.Listbox] = None
self.validation_mode_entry = None self.shutdown_commands_listbox: Optional[tk.Listbox] = None
self.service_file_data = None self.validate_commands_listbox: Optional[tk.Listbox] = None
self.validation_period_entry = None self.validation_time_entry: Optional[ttk.Entry] = None
self.original_service_files = {} self.validation_mode_entry: Optional[ttk.Entry] = None
self.default_config = None self.service_file_data: Optional[CodeText] = None
self.temp_service_files = {} self.validation_period_entry: Optional[ttk.Entry] = None
self.modified_files = set() self.original_service_files: Dict[str, str] = {}
self.has_error = False self.default_config: NodeServiceData = None
self.temp_service_files: Dict[str, str] = {}
self.modified_files: Set[str] = set()
self.has_error: bool = False
self.load() self.load()
if not self.has_error: if not self.has_error:
self.draw() self.draw()
def load(self): def load(self) -> None:
try: try:
self.app.core.create_nodes_and_links() self.app.core.create_nodes_and_links()
default_config = self.app.core.get_node_service( default_config = self.app.core.get_node_service(
@ -119,7 +124,7 @@ class ServiceConfigDialog(Dialog):
self.app.show_grpc_exception("Get Node Service Error", e) self.app.show_grpc_exception("Get Node Service Error", e)
self.has_error = True self.has_error = True
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1) self.top.rowconfigure(1, weight=1)
@ -142,7 +147,7 @@ class ServiceConfigDialog(Dialog):
self.draw_buttons() self.draw_buttons()
def draw_tab_files(self): def draw_tab_files(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew") tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1) tab.columnconfigure(0, weight=1)
@ -222,7 +227,7 @@ class ServiceConfigDialog(Dialog):
"<FocusOut>", self.update_temp_service_file_data "<FocusOut>", self.update_temp_service_file_data
) )
def draw_tab_directories(self): def draw_tab_directories(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew") tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1) tab.columnconfigure(0, weight=1)
@ -257,7 +262,7 @@ class ServiceConfigDialog(Dialog):
button = ttk.Button(frame, text="Remove", command=self.remove_directory) button = ttk.Button(frame, text="Remove", command=self.remove_directory)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def draw_tab_startstop(self): def draw_tab_startstop(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew") tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1) tab.columnconfigure(0, weight=1)
@ -311,7 +316,7 @@ class ServiceConfigDialog(Dialog):
elif i == 2: elif i == 2:
self.validate_commands_listbox = listbox_scroll.listbox self.validate_commands_listbox = listbox_scroll.listbox
def draw_tab_configuration(self): def draw_tab_configuration(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew") tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1) tab.columnconfigure(0, weight=1)
@ -370,7 +375,7 @@ class ServiceConfigDialog(Dialog):
for dependency in self.dependencies: for dependency in self.dependencies:
listbox_scroll.listbox.insert("end", dependency) listbox_scroll.listbox.insert("end", dependency)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(4): for i in range(4):
@ -384,7 +389,7 @@ class ServiceConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
def add_filename(self): def add_filename(self) -> None:
filename = self.filename_combobox.get() filename = self.filename_combobox.get()
if filename not in self.filename_combobox["values"]: if filename not in self.filename_combobox["values"]:
self.filename_combobox["values"] += (filename,) self.filename_combobox["values"] += (filename,)
@ -395,7 +400,7 @@ class ServiceConfigDialog(Dialog):
else: else:
logging.debug("file already existed") logging.debug("file already existed")
def delete_filename(self): def delete_filename(self) -> None:
cbb = self.filename_combobox cbb = self.filename_combobox
filename = cbb.get() filename = cbb.get()
if filename in cbb["values"]: if filename in cbb["values"]:
@ -407,7 +412,7 @@ class ServiceConfigDialog(Dialog):
self.modified_files.remove(filename) self.modified_files.remove(filename)
@classmethod @classmethod
def add_command(cls, event: tk.Event): def add_command(cls, event: tk.Event) -> None:
frame_contains_button = event.widget.master frame_contains_button = event.widget.master
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get() command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get()
@ -419,7 +424,7 @@ class ServiceConfigDialog(Dialog):
listbox.insert(tk.END, command_to_add) listbox.insert(tk.END, command_to_add)
@classmethod @classmethod
def update_entry(cls, event: tk.Event): def update_entry(cls, event: tk.Event) -> None:
listbox = event.widget listbox = event.widget
current_selection = listbox.curselection() current_selection = listbox.curselection()
if len(current_selection) > 0: if len(current_selection) > 0:
@ -431,7 +436,7 @@ class ServiceConfigDialog(Dialog):
entry.insert(0, cmd) entry.insert(0, cmd)
@classmethod @classmethod
def delete_command(cls, event: tk.Event): def delete_command(cls, event: tk.Event) -> None:
button = event.widget button = event.widget
frame_contains_button = button.master frame_contains_button = button.master
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
@ -441,7 +446,7 @@ class ServiceConfigDialog(Dialog):
entry = frame_contains_button.grid_slaves(row=0, column=0)[0] entry = frame_contains_button.grid_slaves(row=0, column=0)[0]
entry.delete(0, tk.END) entry.delete(0, tk.END)
def click_apply(self): def click_apply(self) -> None:
if ( if (
not self.is_custom_command() not self.is_custom_command()
and not self.is_custom_service_file() and not self.is_custom_service_file()
@ -484,12 +489,12 @@ class ServiceConfigDialog(Dialog):
self.app.show_grpc_exception("Save Service Config Error", e) self.app.show_grpc_exception("Save Service Config Error", e)
self.destroy() self.destroy()
def display_service_file_data(self, event: tk.Event): def display_service_file_data(self, event: tk.Event) -> None:
filename = self.filename_combobox.get() filename = self.filename_combobox.get()
self.service_file_data.text.delete(1.0, "end") self.service_file_data.text.delete(1.0, "end")
self.service_file_data.text.insert("end", self.temp_service_files[filename]) self.service_file_data.text.insert("end", self.temp_service_files[filename])
def update_temp_service_file_data(self, event: tk.Event): def update_temp_service_file_data(self, event: tk.Event) -> None:
filename = self.filename_combobox.get() filename = self.filename_combobox.get()
self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end") self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end")
if self.temp_service_files[filename] != self.original_service_files.get( if self.temp_service_files[filename] != self.original_service_files.get(
@ -499,7 +504,7 @@ class ServiceConfigDialog(Dialog):
else: else:
self.modified_files.discard(filename) self.modified_files.discard(filename)
def is_custom_command(self): def is_custom_command(self) -> bool:
startup, validate, shutdown = self.get_commands() startup, validate, shutdown = self.get_commands()
return ( return (
set(self.default_startup) != set(startup) set(self.default_startup) != set(startup)
@ -507,16 +512,16 @@ class ServiceConfigDialog(Dialog):
or set(self.default_shutdown) != set(shutdown) or set(self.default_shutdown) != set(shutdown)
) )
def has_new_files(self): def has_new_files(self) -> bool:
return set(self.filenames) != set(self.filename_combobox["values"]) return set(self.filenames) != set(self.filename_combobox["values"])
def is_custom_service_file(self): def is_custom_service_file(self) -> bool:
return len(self.modified_files) > 0 return len(self.modified_files) > 0
def is_custom_directory(self): def is_custom_directory(self) -> bool:
return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end")) return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end"))
def click_defaults(self): def click_defaults(self) -> None:
""" """
clears out any custom configuration permanently clears out any custom configuration permanently
""" """
@ -557,37 +562,41 @@ class ServiceConfigDialog(Dialog):
self.current_service_color("") self.current_service_color("")
def click_copy(self): def click_copy(self) -> None:
dialog = CopyServiceConfigDialog(self, self.app, self.node_id) file_name = self.filename_combobox.get()
name = self.canvas_node.core_node.name
dialog = CopyServiceConfigDialog(
self.app, self, name, self.service_name, file_name
)
dialog.show() dialog.show()
@classmethod @classmethod
def append_commands( def append_commands(
cls, commands: List[str], listbox: tk.Listbox, to_add: List[str] cls, commands: List[str], listbox: tk.Listbox, to_add: List[str]
): ) -> None:
for cmd in to_add: for cmd in to_add:
commands.append(cmd) commands.append(cmd)
listbox.insert(tk.END, cmd) listbox.insert(tk.END, cmd)
def get_commands(self): def get_commands(self) -> Tuple[List[str], List[str], List[str]]:
startup = self.startup_commands_listbox.get(0, "end") startup = self.startup_commands_listbox.get(0, "end")
shutdown = self.shutdown_commands_listbox.get(0, "end") shutdown = self.shutdown_commands_listbox.get(0, "end")
validate = self.validate_commands_listbox.get(0, "end") validate = self.validate_commands_listbox.get(0, "end")
return startup, validate, shutdown return startup, validate, shutdown
def find_directory_button(self): def find_directory_button(self) -> None:
d = filedialog.askdirectory(initialdir="/") d = filedialog.askdirectory(initialdir="/")
self.directory_entry.delete(0, "end") self.directory_entry.delete(0, "end")
self.directory_entry.insert("end", d) self.directory_entry.insert("end", d)
def add_directory(self): def add_directory(self) -> None:
d = self.directory_entry.get() d = self.directory_entry.get()
if os.path.isdir(d): if os.path.isdir(d):
if d not in self.temp_directories: if d not in self.temp_directories:
self.dir_list.listbox.insert("end", d) self.dir_list.listbox.insert("end", d)
self.temp_directories.append(d) self.temp_directories.append(d)
def remove_directory(self): def remove_directory(self) -> None:
d = self.directory_entry.get() d = self.directory_entry.get()
dirs = self.dir_list.listbox.get(0, "end") dirs = self.dir_list.listbox.get(0, "end")
if d and d in self.temp_directories: if d and d in self.temp_directories:
@ -599,14 +608,14 @@ class ServiceConfigDialog(Dialog):
logging.debug("directory is not in the list") logging.debug("directory is not in the list")
self.directory_entry.delete(0, "end") self.directory_entry.delete(0, "end")
def directory_select(self, event): def directory_select(self, event) -> None:
i = self.dir_list.listbox.curselection() i = self.dir_list.listbox.curselection()
if i: if i:
d = self.dir_list.listbox.get(i) d = self.dir_list.listbox.get(i)
self.directory_entry.delete(0, "end") self.directory_entry.delete(0, "end")
self.directory_entry.insert("end", d) self.directory_entry.insert("end", d)
def current_service_color(self, color=""): def current_service_color(self, color="") -> None:
""" """
change the current service label color change the current service label color
""" """

View file

@ -1,9 +1,11 @@
import logging import logging
import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame from core.gui.widgets import ConfigFrame
@ -13,15 +15,16 @@ if TYPE_CHECKING:
class SessionOptionsDialog(Dialog): class SessionOptionsDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Session Options") super().__init__(app, "Session Options")
self.config_frame = None self.config_frame: Optional[ConfigFrame] = None
self.has_error = False self.has_error: bool = False
self.config = self.get_config() self.config: Dict[str, ConfigOption] = self.get_config()
self.enabled: bool = not self.app.core.is_runtime()
if not self.has_error: if not self.has_error:
self.draw() self.draw()
def get_config(self): def get_config(self) -> Dict[str, ConfigOption]:
try: try:
session_id = self.app.core.session_id session_id = self.app.core.session_id
response = self.app.core.client.get_session_options(session_id) response = self.app.core.client.get_session_options(session_id)
@ -31,11 +34,10 @@ class SessionOptionsDialog(Dialog):
self.has_error = True self.has_error = True
self.destroy() self.destroy()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled)
self.config_frame = ConfigFrame(self.top, self.app, config=self.config)
self.config_frame.draw_config() self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY) self.config_frame.grid(sticky="nsew", pady=PADY)
@ -43,12 +45,13 @@ class SessionOptionsDialog(Dialog):
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.save) state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Save", command=self.save, state=state)
button.grid(row=0, column=0, padx=PADX, sticky="ew") button.grid(row=0, column=0, padx=PADX, sticky="ew")
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def save(self): def save(self) -> None:
config = self.config_frame.parse_config() config = self.config_frame.parse_config()
try: try:
session_id = self.app.core.session_id session_id = self.app.core.session_id

View file

@ -1,11 +1,12 @@
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, List from typing import TYPE_CHECKING, List, Optional
import grpc import grpc
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.api.grpc.core_pb2 import SessionSummary
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.task import ProgressTask from core.gui.task import ProgressTask
@ -18,17 +19,17 @@ if TYPE_CHECKING:
class SessionsDialog(Dialog): class SessionsDialog(Dialog):
def __init__(self, app: "Application", is_start_app: bool = False) -> None: def __init__(self, app: "Application", is_start_app: bool = False) -> None:
super().__init__(app, "Sessions") super().__init__(app, "Sessions")
self.is_start_app = is_start_app self.is_start_app: bool = is_start_app
self.selected_session = None self.selected_session: Optional[int] = None
self.selected_id = None self.selected_id: Optional[int] = None
self.tree = None self.tree: Optional[ttk.Treeview] = None
self.sessions = self.get_sessions() self.sessions: List[SessionSummary] = self.get_sessions()
self.connect_button = None self.connect_button: Optional[ttk.Button] = None
self.delete_button = None self.delete_button: Optional[ttk.Button] = None
self.protocol("WM_DELETE_WINDOW", self.on_closing) self.protocol("WM_DELETE_WINDOW", self.on_closing)
self.draw() self.draw()
def get_sessions(self) -> List[core_pb2.SessionSummary]: def get_sessions(self) -> List[SessionSummary]:
try: try:
response = self.app.core.client.get_sessions() response = self.app.core.client.get_sessions()
logging.info("sessions: %s", response) logging.info("sessions: %s", response)

View file

@ -3,7 +3,7 @@ shape input dialog
""" """
import tkinter as tk import tkinter as tk
from tkinter import font, ttk from tkinter import font, ttk
from typing import TYPE_CHECKING, List, Union from typing import TYPE_CHECKING, List, Optional, Union
from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -13,40 +13,41 @@ from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.shape import Shape from core.gui.graph.shape import Shape
FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] FONT_SIZES: List[int] = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] BORDER_WIDTH: List[int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
class ShapeDialog(Dialog): class ShapeDialog(Dialog):
def __init__(self, app: "Application", shape: "Shape"): def __init__(self, app: "Application", shape: "Shape") -> None:
if is_draw_shape(shape.shape_type): if is_draw_shape(shape.shape_type):
title = "Add Shape" title = "Add Shape"
else: else:
title = "Add Text" title = "Add Text"
super().__init__(app, title) super().__init__(app, title)
self.canvas = app.canvas self.canvas: "CanvasGraph" = app.canvas
self.fill = None self.fill: Optional[ttk.Label] = None
self.border = None self.border: Optional[ttk.Label] = None
self.shape = shape self.shape: "Shape" = shape
data = shape.shape_data data = shape.shape_data
self.shape_text = tk.StringVar(value=data.text) self.shape_text: tk.StringVar = tk.StringVar(value=data.text)
self.font = tk.StringVar(value=data.font) self.font: tk.StringVar = tk.StringVar(value=data.font)
self.font_size = tk.IntVar(value=data.font_size) self.font_size: tk.IntVar = tk.IntVar(value=data.font_size)
self.text_color = data.text_color self.text_color: str = data.text_color
fill_color = data.fill_color fill_color = data.fill_color
if not fill_color: if not fill_color:
fill_color = "#CFCFFF" fill_color = "#CFCFFF"
self.fill_color = fill_color self.fill_color: str = fill_color
self.border_color = data.border_color self.border_color: str = data.border_color
self.border_width = tk.IntVar(value=0) self.border_width: tk.IntVar = tk.IntVar(value=0)
self.bold = tk.BooleanVar(value=data.bold) self.bold: tk.BooleanVar = tk.BooleanVar(value=data.bold)
self.italic = tk.BooleanVar(value=data.italic) self.italic: tk.BooleanVar = tk.BooleanVar(value=data.italic)
self.underline = tk.BooleanVar(value=data.underline) self.underline: tk.BooleanVar = tk.BooleanVar(value=data.underline)
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw_label_options() self.draw_label_options()
if is_draw_shape(self.shape.shape_type): if is_draw_shape(self.shape.shape_type):
@ -54,7 +55,7 @@ class ShapeDialog(Dialog):
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_label_options(self): def draw_label_options(self) -> None:
label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD) label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD)
label_frame.grid(sticky="ew") label_frame.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
@ -94,7 +95,7 @@ class ShapeDialog(Dialog):
button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") button = ttk.Checkbutton(frame, variable=self.underline, text="Underline")
button.grid(row=0, column=2, sticky="ew") button.grid(row=0, column=2, sticky="ew")
def draw_shape_options(self): def draw_shape_options(self) -> None:
label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD) label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD)
label_frame.grid(sticky="ew", pady=PADY) label_frame.grid(sticky="ew", pady=PADY)
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
@ -129,7 +130,7 @@ class ShapeDialog(Dialog):
) )
combobox.grid(row=0, column=1, sticky="nsew") combobox.grid(row=0, column=1, sticky="nsew")
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="nsew") frame.grid(sticky="nsew")
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
@ -139,28 +140,28 @@ class ShapeDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.cancel) button = ttk.Button(frame, text="Cancel", command=self.cancel)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def choose_text_color(self): def choose_text_color(self) -> None:
color_picker = ColorPickerDialog(self, self.app, self.text_color) color_picker = ColorPickerDialog(self, self.app, self.text_color)
self.text_color = color_picker.askcolor() self.text_color = color_picker.askcolor()
def choose_fill_color(self): def choose_fill_color(self) -> None:
color_picker = ColorPickerDialog(self, self.app, self.fill_color) color_picker = ColorPickerDialog(self, self.app, self.fill_color)
color = color_picker.askcolor() color = color_picker.askcolor()
self.fill_color = color self.fill_color = color
self.fill.config(background=color, text=color) self.fill.config(background=color, text=color)
def choose_border_color(self): def choose_border_color(self) -> None:
color_picker = ColorPickerDialog(self, self.app, self.border_color) color_picker = ColorPickerDialog(self, self.app, self.border_color)
color = color_picker.askcolor() color = color_picker.askcolor()
self.border_color = color self.border_color = color
self.border.config(background=color, text=color) self.border.config(background=color, text=color)
def cancel(self): def cancel(self) -> None:
self.shape.delete() self.shape.delete()
self.canvas.shapes.pop(self.shape.id) self.canvas.shapes.pop(self.shape.id)
self.destroy() self.destroy()
def click_add(self): def click_add(self) -> None:
if is_draw_shape(self.shape.shape_type): if is_draw_shape(self.shape.shape_type):
self.add_shape() self.add_shape()
elif is_shape_text(self.shape.shape_type): elif is_shape_text(self.shape.shape_type):
@ -181,7 +182,7 @@ class ShapeDialog(Dialog):
text_font.append("underline") text_font.append("underline")
return text_font return text_font
def save_text(self): def save_text(self) -> None:
""" """
save info related to text or shape label save info related to text or shape label
""" """
@ -194,7 +195,7 @@ class ShapeDialog(Dialog):
data.italic = self.italic.get() data.italic = self.italic.get()
data.underline = self.underline.get() data.underline = self.underline.get()
def save_shape(self): def save_shape(self) -> None:
""" """
save info related to shape save info related to shape
""" """
@ -203,7 +204,7 @@ class ShapeDialog(Dialog):
data.border_color = self.border_color data.border_color = self.border_color
data.border_width = int(self.border_width.get()) data.border_width = int(self.border_width.get())
def add_text(self): def add_text(self) -> None:
""" """
add text to canvas add text to canvas
""" """
@ -214,7 +215,7 @@ class ShapeDialog(Dialog):
) )
self.save_text() self.save_text()
def add_shape(self): def add_shape(self) -> None:
self.canvas.itemconfig( self.canvas.itemconfig(
self.shape.id, self.shape.id,
fill=self.fill_color, fill=self.fill_color,

View file

@ -3,10 +3,11 @@ throughput dialog
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
@ -14,21 +15,23 @@ if TYPE_CHECKING:
class ThroughputDialog(Dialog): class ThroughputDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Throughput Config") super().__init__(app, "Throughput Config")
self.canvas = app.canvas self.canvas: CanvasGraph = app.canvas
self.show_throughput = tk.IntVar(value=1) self.show_throughput: tk.IntVar = tk.IntVar(value=1)
self.exponential_weight = tk.IntVar(value=1) self.exponential_weight: tk.IntVar = tk.IntVar(value=1)
self.transmission = tk.IntVar(value=1) self.transmission: tk.IntVar = tk.IntVar(value=1)
self.reception = tk.IntVar(value=1) self.reception: tk.IntVar = tk.IntVar(value=1)
self.threshold = tk.DoubleVar(value=self.canvas.throughput_threshold) self.threshold: tk.DoubleVar = tk.DoubleVar(
self.width = tk.IntVar(value=self.canvas.throughput_width) value=self.canvas.throughput_threshold
self.color = self.canvas.throughput_color )
self.color_button = None self.width: tk.IntVar = tk.IntVar(value=self.canvas.throughput_width)
self.color: str = self.canvas.throughput_color
self.color_button: Optional[tk.Button] = None
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw() self.draw()
def draw(self): def draw(self) -> None:
button = ttk.Checkbutton( button = ttk.Checkbutton(
self.top, self.top,
variable=self.show_throughput, variable=self.show_throughput,
@ -97,12 +100,12 @@ class ThroughputDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_color(self): def click_color(self) -> None:
color_picker = ColorPickerDialog(self, self.app, self.color) color_picker = ColorPickerDialog(self, self.app, self.color)
self.color = color_picker.askcolor() self.color = color_picker.askcolor()
self.color_button.config(bg=self.color, text=self.color, bd=0) self.color_button.config(bg=self.color, text=self.color, bd=0)
def click_save(self): def click_save(self) -> None:
self.canvas.throughput_threshold = self.threshold.get() self.canvas.throughput_threshold = self.threshold.get()
self.canvas.throughput_width = self.width.get() self.canvas.throughput_width = self.width.get()
self.canvas.throughput_color = self.color self.canvas.throughput_color = self.color

View file

@ -1,8 +1,10 @@
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import Node
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame from core.gui.widgets import ConfigFrame
@ -10,34 +12,36 @@ from core.gui.widgets import ConfigFrame
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
from core.gui.graph.graph import CanvasGraph
RANGE_COLOR = "#009933" RANGE_COLOR: str = "#009933"
RANGE_WIDTH = 3 RANGE_WIDTH: int = 3
class WlanConfigDialog(Dialog): class WlanConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration") super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration")
self.canvas_node = canvas_node self.canvas: "CanvasGraph" = app.canvas
self.node = canvas_node.core_node self.canvas_node: "CanvasNode" = canvas_node
self.config_frame = None self.node: Node = canvas_node.core_node
self.range_entry = None self.config_frame: Optional[ConfigFrame] = None
self.has_error = False self.range_entry: Optional[ttk.Entry] = None
self.canvas = app.canvas self.has_error: bool = False
self.ranges = {} self.ranges: Dict[int, int] = {}
self.positive_int = self.app.master.register(self.validate_and_update) self.positive_int: int = self.app.master.register(self.validate_and_update)
try: try:
self.config = self.canvas_node.wlan_config config = self.canvas_node.wlan_config
if not self.config: if not config:
self.config = self.app.core.get_wlan_config(self.node.id) config = self.app.core.get_wlan_config(self.node.id)
self.config: Dict[str, ConfigOption] = config
self.init_draw_range() self.init_draw_range()
self.draw() self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("WLAN Config Error", e) self.app.show_grpc_exception("WLAN Config Error", e)
self.has_error = True self.has_error: bool = True
self.destroy() self.destroy()
def init_draw_range(self): def init_draw_range(self) -> None:
if self.canvas_node.id in self.canvas.wireless_network: if self.canvas_node.id in self.canvas.wireless_network:
for cid in self.canvas.wireless_network[self.canvas_node.id]: for cid in self.canvas.wireless_network[self.canvas_node.id]:
x, y = self.canvas.coords(cid) x, y = self.canvas.coords(cid)
@ -46,7 +50,7 @@ class WlanConfigDialog(Dialog):
) )
self.ranges[cid] = range_id self.ranges[cid] = range_id
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame = ConfigFrame(self.top, self.app, self.config)
@ -55,7 +59,7 @@ class WlanConfigDialog(Dialog):
self.draw_apply_buttons() self.draw_apply_buttons()
self.top.bind("<Destroy>", self.remove_ranges) self.top.bind("<Destroy>", self.remove_ranges)
def draw_apply_buttons(self): def draw_apply_buttons(self) -> None:
""" """
create node configuration options create node configuration options
""" """
@ -75,7 +79,7 @@ class WlanConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_apply(self): def click_apply(self) -> None:
""" """
retrieve user's wlan configuration and store the new configuration values retrieve user's wlan configuration and store the new configuration values
""" """
@ -87,7 +91,7 @@ class WlanConfigDialog(Dialog):
self.remove_ranges() self.remove_ranges()
self.destroy() self.destroy()
def remove_ranges(self, event=None): def remove_ranges(self, event=None) -> None:
for cid in self.canvas.find_withtag("range"): for cid in self.canvas.find_withtag("range"):
self.canvas.delete(cid) self.canvas.delete(cid)
self.ranges.clear() self.ranges.clear()

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