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
with:
python-version: 3.6
- name: Install pipenv
- name: install poetry
run: |
python -m pip install --upgrade pip
pip install pipenv
pip install poetry
cd daemon
cp setup.py.in setup.py
cp core/constants.py.in core/constants.py
sed -i 's/True/False/g' core/constants.py
pipenv sync --dev
sed -i 's/required=True/required=False/g' core/emulator/coreemu.py
poetry install
- name: isort
run: |
cd daemon
pipenv run isort -c -df
poetry run isort -c -df
- name: black
run: |
cd daemon
pipenv run black --check --exclude ".+_pb2.*.py|doc|build|utm\.py|setup\.py" .
poetry run black --check .
- name: flake8
run: |
cd daemon
pipenv run flake8
poetry run flake8
- name: grpc
run: |
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
run: |
cd daemon
pipenv run test --mock
poetry run pytest --mock tests

4
.gitignore vendored
View file

@ -39,6 +39,7 @@ coverage.xml
# python files
*.egg-info
*.pyc
# ignore package files
*.rpm
@ -55,8 +56,5 @@ coverage.xml
netns/setup.py
daemon/setup.py
# ignore corefx build
corefx/target
# python
__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
* Breaking Changes
* 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
if WANT_DAEMON
DAEMON = scripts daemon
DAEMON = daemon
endif
if WANT_NETNS
@ -44,58 +44,6 @@ DISTCLEANFILES = aclocal.m4 \
MAINTAINERCLEANFILES = .version \
.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 =
fpm -s dir -t deb -n core-distributed \
-m "$(PACKAGE_MAINTAINERS)" \
@ -138,12 +86,6 @@ fpm -s dir -t rpm -n core-distributed \
-C $(DESTDIR)
endef
.PHONY: fpm
fpm: clean-local-fpm
$(MAKE) install DESTDIR=$(DESTDIR)
$(call fpm-deb)
$(call fpm-rpm)
.PHONY: fpm-distributed
fpm-distributed: clean-local-fpm
$(MAKE) -C netns install DESTDIR=$(DESTDIR)
@ -182,11 +124,8 @@ all: change-files
.PHONY: change-files
change-files:
$(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,netns/setup.py)
$(call change-files,daemon/setup.py)
CORE_DOC_SRC = core-python-$(PACKAGE_VERSION)
.PHONY: doc

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 6.5.0)
AC_INIT(core, 7.0.0)
# autoconf and automake initialization
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
AC_MSG_WARN([Could not locate ovs-ofctl cannot use OVS mode])
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
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])])
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
AM_CONDITIONAL(WANT_GUI, test x$enable_gui = xyes)
AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes)
AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes)
AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes)
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)
if test $cross_compiling = no; then
@ -249,7 +227,6 @@ AC_CONFIG_FILES([Makefile
gui/version.tcl
gui/Makefile
gui/icons/Makefile
scripts/Makefile
man/Makefile
docs/Makefile
daemon/Makefile
@ -279,9 +256,6 @@ Daemon:
Daemon path: ${bindir}
Daemon config: ${CORE_CONF_DIR}
Python: ${PYTHON}
Logs: ${CORE_STATE_DIR}/log
Startup: ${with_startup}
Features to build:
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
stages: [commit]
language: system
entry: bash -c 'cd daemon && pipenv run isort --atomic -y'
entry: bash -c 'cd daemon && poetry run isort --atomic -y'
types: [python]
- id: black
name: black
stages: [commit]
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]
- id: flake8
name: flake8
stages: [commit]
language: system
entry: bash -c 'cd daemon && pipenv run flake8'
entry: bash -c 'cd daemon && poetry run flake8'
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.
#
SETUPPY = setup.py
SETUPPYFLAGS = -v
if WANT_DOCS
DOCS = doc
endif
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
# the source control files
dist-hook:
@ -52,17 +21,15 @@ dist-hook:
distclean-local:
-rm -rf core.egg-info
DISTCLEANFILES = Makefile.in
# files to include with distribution tarball
EXTRA_DIST = $(SETUPPY) \
core \
EXTRA_DIST = core \
data \
doc/conf.py.in \
examples \
scripts \
tests \
test.py \
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 threading
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
@ -92,7 +92,7 @@ from core.api.grpc.wlan_pb2 import (
WlanLinkRequest,
WlanLinkResponse,
)
from core.emulator.emudata import IpPrefixes
from core.emulator.data import IpPrefixes
class InterfaceHelper:
@ -108,29 +108,29 @@ class InterfaceHelper:
:param ip6_prefix: ip6 prefix to use for generation
: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(
self, node_id: int, interface_id: int, name: str = None, mac: str = None
def create_iface(
self, node_id: int, iface_id: int, name: str = None, mac: str = None
) -> core_pb2.Interface:
"""
Create an interface protobuf object.
: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 mac: mac address for interface
: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(
id=interface_id,
name=interface_data.name,
ip4=interface_data.ip4,
ip4mask=interface_data.ip4_mask,
ip6=interface_data.ip6,
ip6mask=interface_data.ip6_mask,
mac=interface_data.mac,
id=iface_id,
name=iface_data.name,
ip4=iface_data.ip4,
ip4_mask=iface_data.ip4_mask,
ip6=iface_data.ip6,
ip6_mask=iface_data.ip6_mask,
mac=iface_data.mac,
)
@ -177,10 +177,10 @@ class CoreGrpcClient:
:param address: grpc server address to connect to
"""
self.address = address
self.stub = None
self.channel = None
self.proxy = proxy
self.address: str = address
self.stub: Optional[core_pb2_grpc.CoreApiStub] = None
self.channel: Optional[grpc.Channel] = None
self.proxy: bool = proxy
def start_session(
self,
@ -414,6 +414,20 @@ class CoreGrpcClient:
request = core_pb2.SetSessionStateRequest(session_id=session_id, state=state)
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(
self, session_id: int, name: str, host: str
) -> core_pb2.AddSessionServerResponse:
@ -431,12 +445,29 @@ class CoreGrpcClient:
)
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(
self,
session_id: int,
handler: Callable[[core_pb2.Event], None],
events: List[core_pb2.Event] = None,
) -> Any:
) -> grpc.Future:
"""
Listen for session events.
@ -453,7 +484,7 @@ class CoreGrpcClient:
def throughputs(
self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None]
) -> Any:
) -> grpc.Future:
"""
Listen for throughput events with information for interfaces and bridges.
@ -467,18 +498,36 @@ class CoreGrpcClient:
start_streamer(stream, handler)
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(
self, session_id: int, node: core_pb2.Node
self, session_id: int, node: core_pb2.Node, source: str = None
) -> core_pb2.AddNodeResponse:
"""
Add node to session.
:param session_id: session id
:param node: node to add
:param source: source application
:return: response with node id
: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)
def get_node(self, session_id: int, node_id: int) -> core_pb2.GetNodeResponse:
@ -499,8 +548,8 @@ class CoreGrpcClient:
node_id: int,
position: core_pb2.Position = None,
icon: str = None,
source: str = None,
geo: core_pb2.Geo = None,
source: str = None,
) -> core_pb2.EditNodeResponse:
"""
Edit a node, currently only changes position.
@ -509,8 +558,8 @@ class CoreGrpcClient:
:param node_id: node id
:param position: position to set node to
: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 source: application source
:return: response with result of success or failure
:raises grpc.RpcError: when session or node doesn't exist
"""
@ -536,16 +585,21 @@ class CoreGrpcClient:
"""
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.
:param session_id: session id
:param node_id: node id
:param source: application source
:return: response with result of success or failure
: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)
def node_command(
@ -609,91 +663,101 @@ class CoreGrpcClient:
def add_link(
self,
session_id: int,
node_one_id: int,
node_two_id: int,
interface_one: core_pb2.Interface = None,
interface_two: core_pb2.Interface = None,
node1_id: int,
node2_id: int,
iface1: core_pb2.Interface = None,
iface2: core_pb2.Interface = None,
options: core_pb2.LinkOptions = None,
source: str = None,
) -> core_pb2.AddLinkResponse:
"""
Add a link between nodes.
:param session_id: session id
:param node_one_id: node one id
:param node_two_id: node two id
:param interface_one: node one interface data
:param interface_two: node two interface data
:param node1_id: node one id
:param node2_id: node two id
:param iface1: node one interface data
:param iface2: node two interface data
:param options: options for link (jitter, bandwidth, etc)
:param source: application source
:return: response with result of success or failure
:raises grpc.RpcError: when session or one of the nodes don't exist
"""
link = core_pb2.Link(
node_one_id=node_one_id,
node_two_id=node_two_id,
node1_id=node1_id,
node2_id=node2_id,
type=core_pb2.LinkType.WIRED,
interface_one=interface_one,
interface_two=interface_two,
iface1=iface1,
iface2=iface2,
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)
def edit_link(
self,
session_id: int,
node_one_id: int,
node_two_id: int,
node1_id: int,
node2_id: int,
options: core_pb2.LinkOptions,
interface_one_id: int = None,
interface_two_id: int = None,
iface1_id: int = None,
iface2_id: int = None,
source: str = None,
) -> core_pb2.EditLinkResponse:
"""
Edit a link between nodes.
:param session_id: session id
:param node_one_id: node one id
:param node_two_id: node two id
:param node1_id: node one id
:param node2_id: node two id
:param options: options for link (jitter, bandwidth, etc)
:param interface_one_id: node one interface id
:param interface_two_id: node two interface id
:param iface1_id: node one interface id
:param iface2_id: node two interface id
:param source: application source
:return: response with result of success or failure
:raises grpc.RpcError: when session or one of the nodes don't exist
"""
request = core_pb2.EditLinkRequest(
session_id=session_id,
node_one_id=node_one_id,
node_two_id=node_two_id,
node1_id=node1_id,
node2_id=node2_id,
options=options,
interface_one_id=interface_one_id,
interface_two_id=interface_two_id,
iface1_id=iface1_id,
iface2_id=iface2_id,
source=source,
)
return self.stub.EditLink(request)
def delete_link(
self,
session_id: int,
node_one_id: int,
node_two_id: int,
interface_one_id: int = None,
interface_two_id: int = None,
node1_id: int,
node2_id: int,
iface1_id: int = None,
iface2_id: int = None,
source: str = None,
) -> core_pb2.DeleteLinkResponse:
"""
Delete a link between nodes.
:param session_id: session id
:param node_one_id: node one id
:param node_two_id: node two id
:param interface_one_id: node one interface id
:param interface_two_id: node two interface id
:param node1_id: node one id
:param node2_id: node two id
:param iface1_id: node one interface id
:param iface2_id: node two interface id
:param source: application source
:return: response with result of success or failure
:raises grpc.RpcError: when session doesn't exist
"""
request = core_pb2.DeleteLinkRequest(
session_id=session_id,
node_one_id=node_one_id,
node_two_id=node_two_id,
interface_one_id=interface_one_id,
interface_two_id=interface_two_id,
node1_id=node1_id,
node2_id=node2_id,
iface1_id=iface1_id,
iface2_id=iface2_id,
source=source,
)
return self.stub.DeleteLink(request)
@ -1028,7 +1092,7 @@ class CoreGrpcClient:
return self.stub.GetEmaneModels(request)
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:
"""
Get emane model configuration for a node or a node's interface.
@ -1036,12 +1100,12 @@ class CoreGrpcClient:
:param session_id: session id
:param node_id: node id
: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
:raises grpc.RpcError: when session doesn't exist
"""
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)
@ -1051,7 +1115,7 @@ class CoreGrpcClient:
node_id: int,
model: str,
config: Dict[str, str] = None,
interface_id: int = -1,
iface_id: int = -1,
) -> SetEmaneModelConfigResponse:
"""
Set emane model configuration for a node or a node's interface.
@ -1060,12 +1124,12 @@ class CoreGrpcClient:
:param node_id: node id
:param model: emane model name
: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
:raises grpc.RpcError: when session doesn't exist
"""
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(
session_id=session_id, emane_model_config=model_config
@ -1111,24 +1175,24 @@ class CoreGrpcClient:
return self.stub.OpenXml(request)
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:
"""
Helps broadcast wireless link/unlink between EMANE nodes.
:param session_id: session to emane link
:param nem_one: first nem for emane link
:param nem_two: second nem for emane link
:param nem1: first nem for emane link
:param nem2: second nem for emane link
:param linked: True to link, False to unlink
:return: get emane link response
:raises grpc.RpcError: when session or nodes related to nems do not exist
"""
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)
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
a part of a CORE session.
@ -1243,24 +1307,24 @@ class CoreGrpcClient:
return self.stub.ExecuteScript(request)
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:
"""
Links/unlinks nodes on the same WLAN.
:param session_id: session id containing wlan and nodes
:param wlan: wlan nodes must belong to
:param node_one: first node of pair to link/unlink
:param node_two: second node of pair to link/unlin
:param wlan_id: wlan nodes must belong to
:param node1_id: first node of pair to link/unlink
:param node2_id: second node of pair to link/unlin
:param linked: True to link, False to unlink
:return: wlan link response
:raises grpc.RpcError: when session or one of the nodes do not exist
"""
request = WlanLinkRequest(
session_id=session_id,
wlan=wlan,
node_one=node_one,
node_two=node_two,
wlan=wlan_id,
node1_id=node1_id,
node2_id=node2_id,
linked=linked,
)
return self.stub.WlanLink(request)

View file

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

View file

@ -1,9 +1,9 @@
import logging
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 netaddr
from grpc import ServicerContext
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.config import ConfigurableOptions
from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
from core.emulator.enumerations import LinkTypes, NodeTypes
from core.emulator.session import Session
from core.nodes.base import CoreNode, NodeBase
@ -22,6 +21,25 @@ from core.services.coreservices import CoreService
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]:
"""
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,
model=node_proto.model,
icon=node_proto.icon,
opaque=node_proto.opaque,
image=node_proto.image,
services=node_proto.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
def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData:
def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
"""
Create interface data from interface proto.
:param interface_proto: interface proto
:param iface_proto: interface proto
:return: interface data
"""
interface = None
if interface_proto:
name = interface_proto.name if interface_proto.name else None
mac = interface_proto.mac if interface_proto.mac else None
ip4 = interface_proto.ip4 if interface_proto.ip4 else None
ip6 = interface_proto.ip6 if interface_proto.ip6 else None
interface = InterfaceData(
id=interface_proto.id,
iface_data = None
if iface_proto:
name = iface_proto.name if iface_proto.name else None
mac = iface_proto.mac if iface_proto.mac else None
ip4 = iface_proto.ip4 if iface_proto.ip4 else None
ip6 = iface_proto.ip6 if iface_proto.ip6 else None
iface_data = InterfaceData(
id=iface_proto.id,
name=name,
mac=mac,
ip4=ip4,
ip4_mask=interface_proto.ip4mask,
ip4_mask=iface_proto.ip4_mask,
ip6=ip6,
ip6_mask=interface_proto.ip6mask,
ip6_mask=iface_proto.ip6_mask,
)
return interface
return iface_data
def add_link_data(
link_proto: core_pb2.Link
) -> Tuple[InterfaceData, InterfaceData, LinkOptions]:
) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]:
"""
Convert link proto to link interfaces and options data.
:param link_proto: link proto
:return: link interfaces and options
"""
interface_one = link_interface(link_proto.interface_one)
interface_two = link_interface(link_proto.interface_two)
iface1_data = link_iface(link_proto.iface1)
iface2_data = link_iface(link_proto.iface2)
link_type = LinkTypes(link_proto.type)
options = LinkOptions(type=link_type)
options_data = link_proto.options
if options_data:
options.delay = options_data.delay
options.bandwidth = options_data.bandwidth
options.per = options_data.per
options.dup = options_data.dup
options.jitter = options_data.jitter
options.mer = options_data.mer
options.burst = options_data.burst
options.mburst = options_data.mburst
options.unidirectional = options_data.unidirectional
options.key = options_data.key
options.opaque = options_data.opaque
return interface_one, interface_two, options
options = LinkOptions()
options_proto = link_proto.options
if options_proto:
options.delay = options_proto.delay
options.bandwidth = options_proto.bandwidth
options.loss = options_proto.loss
options.dup = options_proto.dup
options.jitter = options_proto.jitter
options.mer = options_proto.mer
options.burst = options_proto.burst
options.mburst = options_proto.mburst
options.unidirectional = options_proto.unidirectional
options.key = options_proto.key
return iface1_data, iface2_data, options, link_type
def create_nodes(
@ -141,10 +157,10 @@ def create_links(
"""
funcs = []
for link_proto in link_protos:
node_one_id = link_proto.node_one_id
node_two_id = link_proto.node_two_id
interface_one, interface_two, options = add_link_data(link_proto)
args = (node_one_id, node_two_id, interface_one, interface_two, options)
node1_id = link_proto.node1_id
node2_id = link_proto.node2_id
iface1, iface2, options, link_type = add_link_data(link_proto)
args = (node1_id, node2_id, iface1, iface2, options, link_type)
funcs.append((session.add_link, args, {}))
start = time.monotonic()
results, exceptions = utils.threadpool(funcs)
@ -165,10 +181,10 @@ def edit_links(
"""
funcs = []
for link_proto in link_protos:
node_one_id = link_proto.node_one_id
node_two_id = link_proto.node_two_id
interface_one, interface_two, options = add_link_data(link_proto)
args = (node_one_id, node_two_id, interface_one.id, interface_two.id, options)
node1_id = link_proto.node1_id
node2_id = link_proto.node2_id
iface1, iface2, options, link_type = add_link_data(link_proto)
args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type)
funcs.append((session.update_link, args, {}))
start = time.monotonic()
results, exceptions = utils.threadpool(funcs)
@ -190,7 +206,8 @@ def convert_value(value: Any) -> str:
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]:
"""
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
"""
links = []
for link_data in node.all_link_data():
link = convert_link(link_data)
links.append(link)
for link in node.links():
link_proto = convert_link(link)
links.append(link_proto)
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
:param node_id: node id
:param interface_id: interface id
:param iface_id: interface id
:return: EMANE model id
"""
if interface_id >= 0:
return node_id * 1000 + interface_id
if iface_id >= 0:
return node_id * 1000 + iface_id
else:
return node_id
@ -299,12 +316,39 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]:
:param _id: id to parse
:return: node id and interface id
"""
interface = -1
iface_id = -1
node_id = _id
if _id >= 1000:
interface = _id % 1000
iface_id = _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:
@ -314,47 +358,19 @@ def convert_link(link_data: LinkData) -> core_pb2.Link:
:param link_data: link to convert
:return: core protobuf Link
"""
interface_one = None
if link_data.interface1_id is not None:
interface_one = core_pb2.Interface(
id=link_data.interface1_id,
name=link_data.interface1_name,
mac=convert_value(link_data.interface1_mac),
ip4=convert_value(link_data.interface1_ip4),
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,
)
iface1 = None
if link_data.iface1 is not None:
iface1 = convert_iface(link_data.iface1)
iface2 = None
if link_data.iface2 is not None:
iface2 = convert_iface(link_data.iface2)
options = convert_link_options(link_data.options)
return core_pb2.Link(
type=link_data.link_type.value,
node_one_id=link_data.node1_id,
node_two_id=link_data.node2_id,
interface_one=interface_one,
interface_two=interface_two,
type=link_data.type.value,
node1_id=link_data.node1_id,
node2_id=link_data.node2_id,
iface1=iface1,
iface2=iface2,
options=options,
network_id=link_data.network_id,
label=link_data.label,
@ -418,7 +434,7 @@ def service_configuration(session: Session, config: ServiceConfig) -> None:
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.
@ -439,58 +455,84 @@ def get_service_configuration(service: Type[CoreService]) -> NodeServiceData:
)
def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface:
"""
Convenience for converting a core interface to the protobuf representation.
:param interface: interface to convert
:return: interface proto
"""
net_id = None
if interface.net:
net_id = interface.net.id
ip4 = None
ip4mask = None
ip6 = None
ip6mask = None
for addr in interface.addrlist:
network = netaddr.IPNetwork(addr)
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 iface_to_data(iface: CoreInterface) -> InterfaceData:
ip4 = iface.get_ip4()
ip4_addr = str(ip4.ip) if ip4 else None
ip4_mask = ip4.prefixlen if ip4 else None
ip6 = iface.get_ip6()
ip6_addr = str(ip6.ip) if ip6 else None
ip6_mask = ip6.prefixlen if ip6 else None
return InterfaceData(
id=iface.node_id,
name=iface.name,
mac=str(iface.mac),
ip4=ip4_addr,
ip4_mask=ip4_mask,
ip6=ip6_addr,
ip6_mask=ip6_mask,
)
def 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.
:param session: session node belongs to
: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
:return: nem id
"""
netif = node.netif(netif_id)
if not netif:
message = f"{node.name} missing interface {netif_id}"
iface = node.ifaces.get(iface_id)
if not iface:
message = f"{node.name} missing interface {iface_id}"
context.abort(grpc.StatusCode.NOT_FOUND, message)
net = netif.net
net = iface.net
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)
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 time
from concurrent import futures
from typing import Iterable, Type
from typing import Iterable, Optional, Pattern, Type
import grpc
from grpc import ServicerContext
@ -108,18 +108,22 @@ from core.api.grpc.wlan_pb2 import (
WlanLinkResponse,
)
from core.emulator.coreemu import CoreEmu
from core.emulator.data import LinkData
from core.emulator.emudata import LinkOptions, NodeOptions
from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
from core.emulator.enumerations import (
EventTypes,
ExceptionLevels,
LinkTypes,
MessageFlags,
)
from core.emulator.session import NT, Session
from core.errors import CoreCommandError, CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
from core.nodes.network import WlanNode
from core.nodes.network import PtpNet, WlanNode
from core.services.coreservices import ServiceManager
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
_INTERFACE_REGEX = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
_ONE_DAY_IN_SECONDS: int = 60 * 60 * 24
_INTERFACE_REGEX: Pattern = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
@ -131,9 +135,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
def __init__(self, coreemu: CoreEmu) -> None:
super().__init__()
self.coreemu = coreemu
self.running = True
self.server = None
self.coreemu: CoreEmu = coreemu
self.running: bool = True
self.server: Optional[grpc.Server] = None
atexit.register(self._exit_handler)
def _exit_handler(self) -> None:
@ -246,7 +250,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
config = session.emane.get_configs()
config.update(request.emane_config)
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)
# wlan configs
@ -449,6 +453,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
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(
self, request: core_pb2.GetSessionOptionsRequest, context: ServicerContext
) -> core_pb2.GetSessionOptionsResponse:
@ -544,10 +563,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
nodes = []
for _id in session.nodes:
node = session.nodes[_id]
if not isinstance(node.id, int):
continue
node_proto = grpcutils.get_node_proto(session, node)
nodes.append(node_proto)
if not isinstance(node, PtpNet):
node_proto = grpcutils.get_node_proto(session, node)
nodes.append(node_proto)
node_links = get_links(node)
links.extend(node_links)
@ -571,6 +589,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
session.distributed.add_server(request.name, request.host)
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:
session = self.get_session(request.session_id, context)
event_types = set(request.events)
@ -625,16 +652,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
key = key.split(".")
node_id = _INTERFACE_REGEX.search(key[0]).group("node")
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)
if session.id != session_id:
continue
interface_throughput = (
throughputs_event.interface_throughputs.add()
)
interface_throughput.node_id = node_id
interface_throughput.interface_id = interface_id
interface_throughput.throughput = throughput
iface_throughput = throughputs_event.iface_throughputs.add()
iface_throughput.node_id = node_id
iface_throughput.iface_id = iface_id
iface_throughput.throughput = throughput
elif key.startswith("b."):
try:
key = key.split(".")
@ -656,6 +681,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
last_stats = stats
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(
self, request: core_pb2.AddNodeRequest, context: ServicerContext
) -> core_pb2.AddNodeResponse:
@ -671,6 +705,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
_type, _id, options = grpcutils.add_node_data(request.node)
_class = session.get_node_class(_type)
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)
def GetNode(
@ -686,13 +722,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get node: %s", request)
session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context, NodeBase)
interfaces = []
for interface_id in node._netif:
interface = node._netif[interface_id]
interface_proto = grpcutils.interface_to_proto(interface)
interfaces.append(interface_proto)
ifaces = []
for iface_id in node.ifaces:
iface = node.ifaces[iface_id]
iface_proto = grpcutils.iface_to_proto(request.node_id, iface)
ifaces.append(iface_proto)
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(
self,
@ -778,7 +814,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("delete node: %s", request)
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)
def NodeCommand(
@ -845,27 +886,42 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:return: add-link response
"""
logging.debug("add link: %s", request)
# validate session and nodes
session = self.get_session(request.session_id, context)
self.get_node(session, request.link.node_one_id, context, NodeBase)
self.get_node(session, request.link.node_two_id, context, NodeBase)
node_one_id = request.link.node_one_id
node_two_id = request.link.node_two_id
interface_one, interface_two, options = grpcutils.add_link_data(request.link)
node_one_interface, node_two_interface = session.add_link(
node_one_id, node_two_id, interface_one, interface_two, options=options
node1_id = request.link.node1_id
node2_id = request.link.node2_id
self.get_node(session, node1_id, context, NodeBase)
self.get_node(session, node2_id, context, NodeBase)
iface1_data, iface2_data, options, link_type = grpcutils.add_link_data(
request.link
)
interface_one_proto = None
interface_two_proto = None
if node_one_interface:
interface_one_proto = grpcutils.interface_to_proto(node_one_interface)
if node_two_interface:
interface_two_proto = grpcutils.interface_to_proto(node_two_interface)
node1_iface, node2_iface = session.add_link(
node1_id, node2_id, iface1_data, iface2_data, options, link_type
)
iface1_data = None
if node1_iface:
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(
result=True,
interface_one=interface_one_proto,
interface_two=interface_two_proto,
result=True, iface1=iface1_proto, iface2=iface2_proto
)
def EditLink(
@ -880,26 +936,37 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("edit link: %s", request)
session = self.get_session(request.session_id, context)
node_one_id = request.node_one_id
node_two_id = request.node_two_id
interface_one_id = request.interface_one_id
interface_two_id = request.interface_two_id
options_data = request.options
link_options = LinkOptions()
link_options.delay = options_data.delay
link_options.bandwidth = options_data.bandwidth
link_options.per = options_data.per
link_options.dup = options_data.dup
link_options.jitter = options_data.jitter
link_options.mer = options_data.mer
link_options.burst = options_data.burst
link_options.mburst = options_data.mburst
link_options.unidirectional = options_data.unidirectional
link_options.key = options_data.key
link_options.opaque = options_data.opaque
session.update_link(
node_one_id, node_two_id, interface_one_id, interface_two_id, link_options
node1_id = request.node1_id
node2_id = request.node2_id
iface1_id = request.iface1_id
iface2_id = request.iface2_id
options_proto = request.options
options = LinkOptions(
delay=options_proto.delay,
bandwidth=options_proto.bandwidth,
loss=options_proto.loss,
dup=options_proto.dup,
jitter=options_proto.jitter,
mer=options_proto.mer,
burst=options_proto.burst,
mburst=options_proto.mburst,
unidirectional=options_proto.unidirectional,
key=options_proto.key,
)
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)
def DeleteLink(
@ -914,13 +981,23 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("delete link: %s", request)
session = self.get_session(request.session_id, context)
node_one_id = request.node_one_id
node_two_id = request.node_two_id
interface_one_id = request.interface_one_id
interface_two_id = request.interface_two_id
session.delete_link(
node_one_id, node_two_id, interface_one_id, interface_two_id
node1_id = request.node1_id
node2_id = request.node2_id
iface1_id = request.iface1_id
iface2_id = request.iface2_id
session.delete_link(node1_id, node2_id, iface1_id, iface2_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)
def GetHooks(
@ -936,8 +1013,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get hooks: %s", request)
session = self.get_session(request.session_id, context)
hooks = []
for state in session._hooks:
state_hooks = session._hooks[state]
for state in session.hooks:
state_hooks = session.hooks[state]
for file_name, file_data in state_hooks:
hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data)
hooks.append(hook)
@ -1304,13 +1381,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("set wlan config: %s", request)
session = self.get_session(request.session_id, context)
wlan_config = request.wlan_config
session.mobility.set_model_config(
wlan_config.node_id, BasicRangeModel.name, wlan_config.config
)
node_id = request.wlan_config.node_id
config = request.wlan_config.config
session.mobility.set_model_config(node_id, BasicRangeModel.name, config)
if session.state == EventTypes.RUNTIME_STATE:
node = self.get_node(session, wlan_config.node_id, context, WlanNode)
node.updatemodel(wlan_config.config)
node = self.get_node(session, node_id, context, WlanNode)
node.updatemodel(config)
return SetWlanConfigResponse(result=True)
def GetEmaneConfig(
@ -1378,7 +1454,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get emane model config: %s", request)
session = self.get_session(request.session_id, context)
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)
config = get_config_options(current_config, model)
return GetEmaneModelConfigResponse(config=config)
@ -1397,7 +1473,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("set emane model config: %s", request)
session = self.get_session(request.session_id, context)
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)
return SetEmaneModelConfigResponse(result=True)
@ -1426,12 +1502,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
model = session.emane.models[model_name]
current_config = session.emane.get_model_config(_id, model_name)
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(
node_id=node_id,
model=model_name,
interface=interface,
config=config,
node_id=node_id, model=model_name, iface_id=iface_id, config=config
)
configs.append(model_config)
return GetEmaneModelConfigsResponse(configs=configs)
@ -1496,16 +1569,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:param context: context object
:return: get-interfaces response that has all the system's interfaces
"""
interfaces = []
for interface in os.listdir("/sys/class/net"):
if (
interface.startswith("b.")
or interface.startswith("veth")
or interface == "lo"
):
ifaces = []
for iface in os.listdir("/sys/class/net"):
if iface.startswith("b.") or iface.startswith("veth") or iface == "lo":
continue
interfaces.append(interface)
return core_pb2.GetInterfacesResponse(interfaces=interfaces)
ifaces.append(iface)
return core_pb2.GetInterfacesResponse(ifaces=ifaces)
def EmaneLink(
self, request: EmaneLinkRequest, context: ServicerContext
@ -1519,30 +1588,30 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("emane link: %s", request)
session = self.get_session(request.session_id, context)
nem_one = request.nem_one
emane_one, netif = session.emane.nemlookup(nem_one)
if not emane_one or not netif:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem_one} not found")
node_one = netif.node
nem1 = request.nem1
iface1 = session.emane.get_iface(nem1)
if not iface1:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found")
node1 = iface1.node
nem_two = request.nem_two
emane_two, netif = session.emane.nemlookup(nem_two)
if not emane_two or not netif:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem_two} not found")
node_two = netif.node
nem2 = request.nem2
iface2 = session.emane.get_iface(nem2)
if not iface2:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found")
node2 = iface2.node
if emane_one.id == emane_two.id:
if iface1.net == iface2.net:
if request.linked:
flag = MessageFlags.ADD
else:
flag = MessageFlags.DELETE
color = session.get_link_color(emane_one.id)
color = session.get_link_color(iface1.net.id)
link = LinkData(
message_type=flag,
link_type=LinkTypes.WIRELESS,
node1_id=node_one.id,
node2_id=node_two.id,
network_id=emane_one.id,
type=LinkTypes.WIRELESS,
node1_id=node1.id,
node2_id=node2.id,
network_id=iface1.net.id,
color=color,
)
session.broadcast_link(link)
@ -1739,21 +1808,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
grpc.StatusCode.NOT_FOUND,
f"wlan node {request.wlan} does not using BasicRangeModel",
)
n1 = self.get_node(session, request.node_one, context, CoreNode)
n2 = self.get_node(session, request.node_two, context, CoreNode)
n1_netif, n2_netif = None, None
for net, netif1, netif2 in n1.commonnets(n2):
node1 = self.get_node(session, request.node1_id, context, CoreNode)
node2 = self.get_node(session, request.node2_id, context, CoreNode)
node1_iface, node2_iface = None, None
for net, iface1, iface2 in node1.commonnets(node2):
if net == wlan:
n1_netif = netif1
n2_netif = netif2
node1_iface = iface1
node2_iface = iface2
break
result = False
if n1_netif and n2_netif:
if node1_iface and node2_iface:
if request.linked:
wlan.link(n1_netif, n2_netif)
wlan.link(node1_iface, node2_iface)
else:
wlan.unlink(n1_netif, n2_netif)
wlan.model.sendlinkmsg(n1_netif, n2_netif, unlink=not request.linked)
wlan.unlink(node1_iface, node2_iface)
wlan.model.sendlinkmsg(node1_iface, node2_iface, unlink=not request.linked)
result = True
return WlanLinkResponse(result=result)
@ -1764,9 +1833,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
) -> EmanePathlossesResponse:
for request in request_iterator:
session = self.get_session(request.session_id, context)
n1 = self.get_node(session, request.node_one, context, CoreNode)
nem1 = grpcutils.get_nem_id(n1, request.interface_one_id, context)
n2 = self.get_node(session, request.node_two, context, CoreNode)
nem2 = grpcutils.get_nem_id(n2, request.interface_two_id, context)
session.emane.publish_pathloss(nem1, nem2, request.rx_one, request.rx_two)
node1 = self.get_node(session, request.node1_id, context, CoreNode)
nem1 = grpcutils.get_nem_id(session, node1, request.iface1_id, context)
node2 = self.get_node(session, request.node2_id, context, CoreNode)
nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context)
session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
return EmanePathlossesResponse()

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ Common support for configurable CORE objects.
import logging
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.emulator.enumerations import ConfigDataTypes
@ -29,9 +29,9 @@ class ConfigGroup:
:param start: configurations start index for this group
:param stop: configurations stop index for this group
"""
self.name = name
self.start = start
self.stop = stop
self.name: str = name
self.start: int = start
self.stop: int = stop
class Configuration:
@ -56,18 +56,21 @@ class Configuration:
:param default: default value for configuration
:param options: list options if this is a configuration with a combobox
"""
self.id = _id
self.type = _type
self.default = default
self.id: str = _id
self.type: ConfigDataTypes = _type
self.default: str = default
if not options:
options = []
self.options = options
self.options: List[str] = options
if not label:
label = _id
self.label = label
self.label: str = label
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:
@ -75,9 +78,9 @@ class ConfigurableOptions:
Provides a base for defining configuration options within CORE.
"""
name = None
bitmap = None
options = []
name: Optional[str] = None
bitmap: Optional[str] = None
options: List[Configuration] = []
@classmethod
def configurations(cls) -> List[Configuration]:
@ -115,8 +118,8 @@ class ConfigurableManager:
nodes.
"""
_default_node = -1
_default_type = _default_node
_default_node: int = -1
_default_type: int = _default_node
def __init__(self) -> None:
"""
@ -136,7 +139,8 @@ class ConfigurableManager:
"""
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
"""
if not node_id:
@ -222,7 +226,7 @@ class ConfigurableManager:
result = node_configs.get(config_type)
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.
@ -242,8 +246,8 @@ class ModelManager(ConfigurableManager):
Creates a ModelManager object.
"""
super().__init__()
self.models = {}
self.node_models = {}
self.models: Dict[str, Any] = {}
self.node_models: Dict[int, str] = {}
def set_model_config(
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.nodes.base import CoreNode
TEMPLATES_DIR = "templates"
TEMPLATES_DIR: str = "templates"
class ConfigServiceMode(enum.Enum):
@ -33,10 +33,10 @@ class ConfigService(abc.ABC):
"""
# 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
validation_timer = 5
validation_timer: int = 5
def __init__(self, node: CoreNode) -> None:
"""
@ -44,13 +44,13 @@ class ConfigService(abc.ABC):
:param node: node this service is assigned to
"""
self.node = node
self.node: CoreNode = node
class_file = inspect.getfile(self.__class__)
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
self.templates = TemplateLookup(directories=templates_path)
self.config = {}
self.custom_templates = {}
self.custom_config = {}
self.templates: TemplateLookup = TemplateLookup(directories=templates_path)
self.config: Dict[str, Configuration] = {}
self.custom_templates: Dict[str, str] = {}
self.custom_config: Dict[str, str] = {}
configs = self.default_configs[:]
self._define_config(configs)

View file

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

View file

@ -1,6 +1,6 @@
import logging
import pathlib
from typing import List, Type
from typing import Dict, List, Type
from core import utils
from core.configservice.base import ConfigService
@ -16,7 +16,7 @@ class ConfigServiceManager:
"""
Create a ConfigServiceManager instance.
"""
self.services = {}
self.services: Dict[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}")
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.
@ -40,7 +40,9 @@ class ConfigServiceManager:
:raises CoreError: when service is a duplicate or has unmet executables
"""
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
if name in self.services:
@ -50,10 +52,8 @@ class ConfigServiceManager:
for executable in service.executables:
try:
utils.which(executable, required=True)
except ValueError:
raise CoreError(
f"service({service.name}) missing executable {executable}"
)
except CoreError as e:
raise CoreError(f"config service({service.name}): {e}")
# make service available
self.services[name] = service
@ -73,7 +73,6 @@ class ConfigServiceManager:
logging.debug("loading config services from: %s", subdir)
services = utils.load_classes(str(subdir), ConfigService)
for service in services:
logging.debug("found service: %s", service)
try:
self.add(service)
except CoreError as e:

View file

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

View file

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

View file

@ -1,5 +1,5 @@
% for ifc, ip4s, ip6s, is_control in interfaces:
interface ${ifc.name}
% for iface, ip4s, ip6s, is_control in ifaces:
interface ${iface.name}
% if want_ip4:
% for addr in ip4s:
ip address ${addr}
@ -12,7 +12,7 @@ interface ${ifc.name}
% endif
% if not is_control:
% 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}
% endfor
% endfor

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
% for ifc, ip4s, ip6s, is_control in interfaces:
interface ${ifc.name}
% for iface, ip4s, ip6s, is_control in ifaces:
interface ${iface.name}
% if want_ip4:
% for addr in ip4s:
ip address ${addr}
@ -12,7 +12,7 @@ interface ${ifc.name}
% endif
% if not is_control:
% 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}
% 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.configservice.base import ConfigService, ConfigServiceMode
from core.emulator.enumerations import ConfigDataTypes
class SimpleService(ConfigService):
name = "Simple"
group = "SimpleGroup"
directories = ["/etc/quagga", "/usr/local/lib"]
files = ["test1.sh", "test2.sh"]
executables = []
dependencies = []
startup = []
validate = []
shutdown = []
validation_mode = ConfigServiceMode.BLOCKING
default_configs = [
name: str = "Simple"
group: str = "SimpleGroup"
directories: List[str] = ["/etc/quagga", "/usr/local/lib"]
files: List[str] = ["test1.sh", "test2.sh"]
executables: List[str] = []
dependencies: List[str] = []
startup: List[str] = []
validate: List[str] = []
shutdown: List[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
Configuration(
@ -24,7 +26,7 @@ class SimpleService(ConfigService):
options=["value1", "value2", "value3"],
),
]
modes = {
modes: Dict[str, Dict[str, str]] = {
"mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
"mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
"mode3": {"value1": "value3", "value2": "0", "value3": "value1"},

View file

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

View file

@ -5,8 +5,8 @@
<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>
<ul>
% for ifc in interfaces:
<li>${ifc.name} - ${ifc.addrlist}</li>
% for iface in ifaces:
<li>${iface.name} - ${iface.addrlist}</li>
% endfor
</ul>
</body>

View file

@ -1,19 +1,3 @@
from core.utils import which
COREDPY_VERSION = "@PACKAGE_VERSION@"
CORE_CONF_DIR = "@CORE_CONF_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
"""
from typing import List, Set
from core.config import Configuration
from core.emane import emanemodel
@ -8,14 +9,14 @@ from core.emulator.enumerations import ConfigDataTypes
class EmaneBypassModel(emanemodel.EmaneModel):
name = "emane_bypass"
name: str = "emane_bypass"
# values to ignore, when writing xml files
config_ignore = {"none"}
config_ignore: Set[str] = {"none"}
# mac definitions
mac_library = "bypassmaclayer"
mac_config = [
mac_library: str = "bypassmaclayer"
mac_config: List[Configuration] = [
Configuration(
_id="none",
_type=ConfigDataTypes.BOOL,
@ -25,8 +26,8 @@ class EmaneBypassModel(emanemodel.EmaneModel):
]
# phy definitions
phy_library = "bypassphylayer"
phy_config = []
phy_library: str = "bypassphylayer"
phy_config: List[Configuration] = []
@classmethod
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.emane import emanemanifest, emanemodel
from core.emane.nodes import EmaneNet
from core.emulator.emudata import LinkOptions
from core.emulator.enumerations import TransportType
from core.emulator.data import LinkOptions
from core.nodes.interface import CoreInterface
from core.xml import emanexml
@ -22,6 +20,7 @@ except ImportError:
try:
from emanesh.events.commeffectevent import CommEffectEvent
except ImportError:
CommEffectEvent = None
logging.debug("compatible emane python bindings not installed")
@ -38,16 +37,15 @@ def convert_none(x: float) -> int:
class EmaneCommEffectModel(emanemodel.EmaneModel):
name = "emane_commeffect"
shim_library = "commeffectshim"
shim_xml = "commeffectshim.xml"
shim_defaults = {}
config_shim = []
name: str = "emane_commeffect"
shim_library: str = "commeffectshim"
shim_xml: str = "commeffectshim.xml"
shim_defaults: Dict[str, str] = {}
config_shim: List[Configuration] = []
# comm effect does not need the default phy and external configurations
phy_config = []
external_config = []
phy_config: List[Configuration] = []
external_config: List[Configuration] = []
@classmethod
def load(cls, emane_prefix: str) -> None:
@ -62,9 +60,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
def config_groups(cls) -> List[ConfigGroup]:
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
def build_xml_files(
self, config: Dict[str, str], interface: CoreInterface = None
) -> None:
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
"""
Build the necessary nem and commeffect XMLs in the given path.
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.
: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
"""
# retrieve xml names
nem_name = emanexml.nem_file_name(self, interface)
shim_name = emanexml.shim_file_name(self, interface)
# create and write nem document
nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured")
transport_type = TransportType.VIRTUAL
if interface and interface.transport_type == TransportType.RAW:
transport_type = TransportType.RAW
transport_file = emanexml.transport_file_name(self.id, transport_type)
etree.SubElement(nem_element, "transport", definition=transport_file)
transport_name = emanexml.transport_file_name(iface)
etree.SubElement(nem_element, "transport", definition=transport_name)
# 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)
nem_file = os.path.join(self.session.session_dir, nem_name)
emanexml.create_file(nem_element, "nem", nem_file)
emanexml.create_iface_file(iface, nem_element, "nem", nem_name)
# create and write shim document
shim_element = etree.Element(
@ -110,12 +99,13 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
ff = config["filterfile"]
if ff.strip() != "":
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)
emanexml.create_file(shim_element, "shim", shim_file)
# create transport xml
emanexml.create_transport_xml(iface, config)
def linkconfig(
self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
) -> None:
"""
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)
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)
return
# TODO: batch these into multiple events per transmission
# TODO: may want to split out seconds portion of delay and jitter
event = CommEffectEvent()
emane_node = self.session.get_node(self.id, EmaneNet)
nemid = emane_node.getnemid(netif)
nemid2 = emane_node.getnemid(netif2)
nem1 = self.session.emane.get_nem_id(iface)
nem2 = self.session.emane.get_nem_id(iface2)
logging.info("sending comm effect event")
event.append(
nemid,
nem1,
latency=convert_none(options.delay),
jitter=convert_none(options.jitter),
loss=convert_none(options.per),
loss=convert_none(options.loss),
duplicate=convert_none(options.dup),
unicast=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 threading
from collections import OrderedDict
from enum import Enum
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
from core import utils
@ -28,9 +29,7 @@ from core.emulator.enumerations import (
)
from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNode, NodeBase
from core.nodes.interface import CoreInterface
from core.nodes.network import CtrlNet
from core.nodes.physical import Rj45Node
from core.nodes.interface import CoreInterface, TunTap
from core.xml import emanexml
if TYPE_CHECKING:
@ -63,6 +62,12 @@ DEFAULT_EMANE_PREFIX = "/usr"
DEFAULT_DEV = "ctrl0"
class EmaneState(Enum):
SUCCESS = 0
NOT_NEEDED = 1
NOT_READY = 2
class EmaneManager(ModelManager):
"""
EMANE controller object. Lives in a Session instance and is used for
@ -70,11 +75,11 @@ class EmaneManager(ModelManager):
controlling the EMANE daemons.
"""
name = "emane"
config_type = RegisterTlvs.EMULATION_SERVER
SUCCESS, NOT_NEEDED, NOT_READY = (0, 1, 2)
EVENTCFGVAR = "LIBEMANEEVENTSERVICECONFIG"
DEFAULT_LOG_LEVEL = 3
name: str = "emane"
config_type: RegisterTlvs = RegisterTlvs.EMULATION_SERVER
NOT_READY: int = 2
EVENTCFGVAR: str = "LIBEMANEEVENTSERVICECONFIG"
DEFAULT_LOG_LEVEL: int = 3
def __init__(self, session: "Session") -> None:
"""
@ -84,74 +89,71 @@ class EmaneManager(ModelManager):
:return: nothing
"""
super().__init__()
self.session = session
self._emane_nets = {}
self._emane_node_lock = threading.Lock()
self.session: "Session" = session
self.nems_to_ifaces: Dict[int, CoreInterface] = {}
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
self.platformport = self.session.options.get_config_int(
self.platformport: int = self.session.options.get_config_int(
"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
)
self.doeventloop = False
self.eventmonthread = None
self.doeventloop: bool = False
self.eventmonthread: Optional[threading.Thread] = None
# 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())
# link monitor
self.link_monitor = EmaneLinkMonitor(self)
self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self)
self.service = None
self.eventchannel = None
self.event_device = None
self.service: Optional[EventService] = None
self.eventchannel: Optional[Tuple[str, int, str]] = None
self.event_device: Optional[str] = None
self.emane_check()
def getifcconfig(
self, node_id: int, interface: CoreInterface, model_name: str
def next_nem_id(self) -> int:
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]:
"""
Retrieve interface configuration or node configuration if not provided.
Retrieve configuration for a given interface.
:param node_id: node id
:param interface: node interface
:param model_name: model to get configuration for
:return: node/interface model configuration
:param emane_net: emane network the interface is connected to
:param iface: interface running emane
:return: net, node, or interface model configuration
"""
# use the network-wide config values or interface(NEM)-specific values?
if interface is None:
return self.get_configs(node_id=node_id, config_type=model_name)
else:
# don"t use default values when interface config is the same as net
# note here that using ifc.node.id as key allows for only one type
# of each model per node;
# TODO: use both node and interface as key
# Adamson change: first check for iface config keyed by "node:ifc.name"
# (so that nodes w/ multiple interfaces of same conftype can have
# different configs for each separate interface)
key = 1000 * interface.node.id
if interface.netindex is not None:
key += interface.netindex
# try retrieve interface specific configuration, avoid getting defaults
config = self.get_configs(node_id=key, config_type=model_name)
# otherwise retrieve the interfaces node configuration, avoid using defaults
if not config:
config = self.get_configs(
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
model_name = emane_net.model.name
# don"t use default values when interface config is the same as net
# note here that using iface.node.id as key allows for only one type
# of each model per node;
# TODO: use both node and interface as key
# Adamson change: first check for iface config keyed by "node:iface.name"
# (so that nodes w/ multiple interfaces of same conftype can have
# different configs for each separate interface)
key = 1000 * iface.node.id
if iface.node_id is not None:
key += iface.node_id
# try retrieve interface specific configuration, avoid getting defaults
config = self.get_configs(node_id=key, config_type=model_name)
# otherwise retrieve the interfaces node configuration, avoid using defaults
if not config:
config = self.get_configs(node_id=iface.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=emane_net.id, config_type=model_name)
return config
def config_reset(self, node_id: int = None) -> None:
super().config_reset(node_id)
@ -163,23 +165,24 @@ class EmaneManager(ModelManager):
:return: nothing
"""
try:
# check for emane
args = "emane --version"
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:
# check for emane
path = utils.which("emane", required=False)
if not path:
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:
if self.service:
@ -250,8 +253,8 @@ class EmaneManager(ModelManager):
"""
with self._emane_node_lock:
if emane_net.id in self._emane_nets:
raise KeyError(
f"non-unique EMANE object id {emane_net.id} for {emane_net}"
raise CoreError(
f"duplicate emane network({emane_net.id}): {emane_net.name}"
)
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,
e.g. containers having one or more radio interfaces.
"""
# assumes self._objslock already held
nodes = set()
for emane_net in self._emane_nets.values():
for netif in emane_net.netifs():
nodes.add(netif.node)
for iface in emane_net.get_ifaces():
nodes.add(iface.node)
return nodes
def setup(self) -> int:
def setup(self) -> EmaneState:
"""
Setup duties for EMANE manager.
@ -275,9 +277,7 @@ class EmaneManager(ModelManager):
instantiation
"""
logging.debug("emane setup")
# TODO: drive this from the session object
with self.session._nodes_lock:
with self.session.nodes_lock:
for node_id in self.session.nodes:
node = self.session.nodes[node_id]
if isinstance(node, EmaneNet):
@ -285,10 +285,9 @@ class EmaneManager(ModelManager):
"adding emane node: id(%s) name(%s)", node.id, node.name
)
self.add_node(node)
if not self._emane_nets:
logging.debug("no emane nodes in session")
return EmaneManager.NOT_NEEDED
return EmaneState.NOT_NEEDED
# check if bindings were installed
if EventService is None:
@ -304,7 +303,7 @@ class EmaneManager(ModelManager):
"EMANE cannot start, check core config. invalid OTA device provided: %s",
otadev,
)
return EmaneManager.NOT_READY
return EmaneState.NOT_READY
self.session.add_remove_control_net(
net_index=netidx, remove=False, conf_required=False
@ -316,19 +315,18 @@ class EmaneManager(ModelManager):
logging.debug("emane event service device index: %s", netidx)
if netidx < 0:
logging.error(
"EMANE cannot start, check core config. invalid event service device: %s",
"emane cannot start due to invalid event service device: %s",
eventdev,
)
return EmaneManager.NOT_READY
return EmaneState.NOT_READY
self.session.add_remove_control_net(
net_index=netidx, remove=False, conf_required=False
)
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
and start the daemons.
@ -337,39 +335,63 @@ class EmaneManager(ModelManager):
instantiation
"""
self.reset()
r = self.setup()
# NOT_NEEDED or NOT_READY
if r != EmaneManager.SUCCESS:
return r
nems = []
status = self.setup()
if status != EmaneState.SUCCESS:
return status
self.starteventmonitor()
self.buildeventservicexml()
with self._emane_node_lock:
self.buildxml()
self.starteventmonitor()
if self.numnems() > 0:
self.startdaemons()
self.installnetifs()
for node_id in self._emane_nets:
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")
logging.info("emane building xmls...")
for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
if not emane_net.model:
logging.error("emane net(%s) has no model", emane_net.name)
continue
for iface in emane_net.get_ifaces():
self.start_iface(emane_net, iface)
if self.links_enabled():
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:
return self.get_config("link_enabled") == "1"
@ -380,18 +402,15 @@ class EmaneManager(ModelManager):
"""
if not self.genlocationevents():
return
with self._emane_node_lock:
for key in sorted(self._emane_nets.keys()):
emane_node = self._emane_nets[key]
for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
logging.debug(
"post startup for emane node: %s - %s",
emane_node.id,
emane_node.name,
"post startup for emane node: %s - %s", emane_net.id, emane_net.name
)
emane_node.model.post_startup()
for netif in emane_node.netifs():
netif.setposition()
emane_net.model.post_startup()
for iface in emane_net.get_ifaces():
iface.setposition()
def reset(self) -> None:
"""
@ -400,13 +419,8 @@ class EmaneManager(ModelManager):
"""
with self._emane_node_lock:
self._emane_nets.clear()
self.platformport = self.session.options.get_config_int(
"emane_platform_port", 8100
)
self.transformport = self.session.options.get_config_int(
"emane_transform_port", 8200
)
self.nems_to_ifaces.clear()
self.ifaces_to_nems.clear()
def shutdown(self) -> None:
"""
@ -418,44 +432,27 @@ class EmaneManager(ModelManager):
logging.info("stopping EMANE daemons")
if self.links_enabled():
self.link_monitor.stop()
self.deinstallnetifs()
self.deinstall_ifaces()
self.stopdaemons()
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:
"""
Associate EMANE model classes with EMANE network nodes.
"""
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)
# skip nodes that already have a model set
if emane_node.model:
if emane_net.model:
logging.debug(
"node(%s) already has model(%s)",
emane_node.id,
emane_node.model.name,
"node(%s) already has model(%s)", emane_net.id, emane_net.model.name
)
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)
if not model_name:
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)
logging.debug("setting emane model(%s) config(%s)", model_name, config)
model_class = self.models[model_name]
emane_node.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
emane_net.setmodel(model_class, config)
def get_nem_link(
self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE
) -> Optional[LinkData]:
emane1, netif = self.nemlookup(nem1)
if not emane1 or not netif:
iface1 = self.get_iface(nem1)
if not iface1:
logging.error("invalid nem: %s", nem1)
return None
node1 = netif.node
emane2, netif = self.nemlookup(nem2)
if not emane2 or not netif:
node1 = iface1.node
iface2 = self.get_iface(nem2)
if not iface2:
logging.error("invalid nem: %s", nem2)
return None
node2 = netif.node
color = self.session.get_link_color(emane1.id)
node2 = iface2.node
if iface1.net != iface2.net:
return None
emane_net = iface1.net
color = self.session.get_link_color(emane_net.id)
return LinkData(
message_type=flags,
type=LinkTypes.WIRELESS,
node1_id=node1.id,
node2_id=node2.id,
network_id=emane1.id,
link_type=LinkTypes.WIRELESS,
network_id=emane_net.id,
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:
"""
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.
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")
realtime = self.session.options.get_config_bool("emane_realtime", default=True)
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)
emanecmd = f"emane -d -l {loglevel}"
if realtime:
emanecmd += " -r"
otagroup, _otaport = self.get_config("otamanagergroup").split(":")
otadev = self.get_config("otamanagerdevice")
otanetidx = self.session.get_control_net_index(otadev)
eventgroup, _eventport = self.get_config("eventservicegroup").split(":")
eventdev = self.get_config("eventservicedevice")
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
node = iface.node
if iface.is_virtual():
otagroup, _otaport = self.get_config("otamanagergroup").split(":")
otadev = self.get_config("otamanagerdevice")
otanetidx = self.session.get_control_net_index(otadev)
eventgroup, _eventport = self.get_config("eventservicegroup").split(":")
eventdev = self.get_config("eventservicedevice")
eventservicenetidx = self.session.get_control_net_index(eventdev)
# 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
)
if otanetidx > 0:
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
)
if eventservicenetidx >= 0:
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
)
# 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)
# multicast route is also needed for event data if on control network
if eventservicenetidx >= 0 and eventgroup != otagroup:
node.node_net_client.create_route(eventgroup, eventdev)
# start emane
log_file = os.path.join(path, f"emane{n}.log")
platform_xml = os.path.join(path, f"platform{n}.xml")
log_file = os.path.join(node.nodedir, f"{iface.name}-emane.log")
platform_xml = os.path.join(node.nodedir, f"{iface.name}-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.debug("node(%s) emane daemon output: %s", node.name, output)
if not run_emane_on_host:
return
path = self.session.session_dir
log_file = os.path.join(path, "emane.log")
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)
else:
path = self.session.session_dir
log_file = os.path.join(path, f"{iface.name}-emane.log")
platform_xml = os.path.join(path, f"{iface.name}-platform.xml")
emanecmd += f" -f {log_file} {platform_xml}"
node.host_cmd(emanecmd, cwd=path)
logging.info("node(%s) host emane daemon running: %s", node.name, emanecmd)
def stopdaemons(self) -> None:
"""
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_transortd = "killall -q emanetransportd"
stop_emane_on_host = False
for node in self.getnodes():
if isinstance(node, Rj45Node):
stop_emane_on_host = True
continue
for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
for iface in emane_net.get_ifaces():
node = iface.node
if not node.up:
continue
if iface.is_raw():
node.host_cmd(kill_emaned, wait=False)
else:
node.cmd(kill_emaned, wait=False)
if node.up:
node.cmd(kill_emaned, wait=False)
# TODO: RJ45 node
def install_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
config = self.get_iface_config(emane_net, iface)
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:
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:
def deinstall_ifaces(self) -> None:
"""
Uninstall TUN/TAP virtual interfaces.
"""
for key in sorted(self._emane_nets.keys()):
emane_node = self._emane_nets[key]
emane_node.deinstallnetifs()
for key in sorted(self._emane_nets):
emane_net = self._emane_nets[key]
for iface in emane_net.get_ifaces():
if iface.is_virtual():
iface.shutdown()
iface.poshook = None
def doeventmonitor(self) -> bool:
"""
@ -718,7 +643,6 @@ class EmaneManager(ModelManager):
logging.info("emane start event monitor")
if not self.doeventmonitor():
return
if self.service is None:
logging.error(
"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.
"""
# convert nemid to node number
_emanenode, netif = self.nemlookup(nemid)
if netif is None:
iface = self.get_iface(nemid)
if iface is None:
logging.info("location event for unknown NEM %s", nemid)
return False
n = netif.node.id
n = iface.node.id
# convert from lat/long/alt to x,y,z coordinates
x, y, z = self.session.location.getxyz(lat, lon, alt)
x = int(x)
@ -890,12 +814,12 @@ class EmaneGlobalModel:
Global EMANE configuration options.
"""
name = "emane"
bitmap = None
name: str = "emane"
bitmap: Optional[str] = None
def __init__(self, session: "Session") -> None:
self.session = session
self.core_config = [
self.session: "Session" = session
self.core_config: List[Configuration] = [
Configuration(
_id="platform_id_start",
_type=ConfigDataTypes.INT32,

View file

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

View file

@ -3,13 +3,13 @@ Defines Emane Models used within CORE.
"""
import logging
import os
from typing import Dict, List
from typing import Dict, List, Optional, Set
from core.config import ConfigGroup, Configuration
from core.emane import emanemanifest
from core.emane.nodes import EmaneNet
from core.emulator.emudata import LinkOptions
from core.emulator.enumerations import ConfigDataTypes, TransportType
from core.emulator.data import LinkOptions
from core.emulator.enumerations import ConfigDataTypes
from core.errors import CoreError
from core.location.mobility import WirelessModel
from core.nodes.base import CoreNode
@ -25,19 +25,23 @@ class EmaneModel(WirelessModel):
"""
# default mac configuration settings
mac_library = None
mac_xml = None
mac_defaults = {}
mac_config = []
mac_library: Optional[str] = None
mac_xml: Optional[str] = None
mac_defaults: Dict[str, str] = {}
mac_config: List[Configuration] = []
# default phy configuration settings, using the universal model
phy_library = None
phy_xml = "emanephy.xml"
phy_defaults = {"subid": "1", "propagationmodel": "2ray", "noisemode": "none"}
phy_config = []
phy_library: Optional[str] = None
phy_xml: str = "emanephy.xml"
phy_defaults: Dict[str, str] = {
"subid": "1",
"propagationmodel": "2ray",
"noisemode": "none",
}
phy_config: List[Configuration] = []
# support for external configurations
external_config = [
external_config: List[Configuration] = [
Configuration("external", ConfigDataTypes.BOOL, default="0"),
Configuration(
"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
def load(cls, emane_prefix: str) -> None:
@ -92,45 +96,20 @@ class EmaneModel(WirelessModel):
ConfigGroup("External Parameters", phy_len + 1, config_len),
]
def build_xml_files(
self, config: Dict[str, str], interface: CoreInterface = None
) -> None:
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
"""
Builds xml files for this emane model. Creates a nem.xml file that points to
both mac.xml and phy.xml definitions.
: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
"""
nem_name = emanexml.nem_file_name(self, interface)
mac_name = emanexml.mac_file_name(self, interface)
phy_name = emanexml.phy_file_name(self, interface)
# remote server for file
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)
# create nem, mac, and phy xml files
emanexml.create_nem_xml(self, iface, config)
emanexml.create_mac_xml(self, iface, config)
emanexml.create_phy_xml(self, iface, config)
emanexml.create_transport_xml(iface, config)
def post_startup(self) -> None:
"""
@ -140,31 +119,31 @@ class EmaneModel(WirelessModel):
"""
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
emane location events to be generated for the nodes in the moved
list, making EmaneModels compatible with Ns2ScriptedMobility.
:param moved: moved nodes
:param moved_netifs: interfaces that were moved
:param moved_ifaces: interfaces that were moved
:return: nothing
"""
try:
wlan = self.session.get_node(self.id, EmaneNet)
wlan.setnempositions(moved_netifs)
wlan.setnempositions(moved_ifaces)
except CoreError:
logging.exception("error during update")
def linkconfig(
self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
) -> None:
"""
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 netif2: interface two
:param iface2: interface two
:return: nothing
"""
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):
# model name
name = "emane_ieee80211abg"
name: str = "emane_ieee80211abg"
# mac configuration
mac_library = "ieee80211abgmaclayer"
mac_xml = "ieee80211abgmaclayer.xml"
mac_library: str = "ieee80211abgmaclayer"
mac_xml: str = "ieee80211abgmaclayer.xml"
@classmethod
def load(cls, emane_prefix: str) -> None:

View file

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

View file

@ -6,23 +6,25 @@ share the same MAC+PHY model.
import logging
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.emudata import LinkOptions
from core.emulator.enumerations import (
EventTypes,
LinkTypes,
MessageFlags,
NodeTypes,
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
if TYPE_CHECKING:
from core.emane.emanemodel import EmaneModel
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]
try:
@ -31,6 +33,7 @@ except ImportError:
try:
from emanesh.events import LocationEvent
except ImportError:
LocationEvent = None
logging.debug("compatible emane python bindings not installed")
@ -41,60 +44,63 @@ class EmaneNet(CoreNetworkBase):
Emane controller object that exists in a session.
"""
apitype = NodeTypes.EMANE
linktype = LinkTypes.WIRED
type = "wlan"
is_emane = True
apitype: NodeTypes = NodeTypes.EMANE
linktype: LinkTypes = LinkTypes.WIRED
type: str = "wlan"
has_custom_iface: bool = True
def __init__(
self,
session: "Session",
_id: int = None,
name: str = None,
start: bool = True,
server: DistributedServer = None,
) -> None:
super().__init__(session, _id, name, start, server)
self.conf = ""
self.nemidmap = {}
self.model = None
self.mobility = None
super().__init__(session, _id, name, server)
self.conf: str = ""
self.model: "OptionalEmaneModel" = None
self.mobility: Optional[WayPointMobility] = None
def linkconfig(
self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
) -> None:
"""
The CommEffect model supports link configuration.
"""
if not self.model:
return
self.model.linkconfig(netif, options, netif2)
self.model.linkconfig(iface, options, iface2)
def config(self, conf: str) -> None:
self.conf = conf
def startup(self) -> None:
pass
def shutdown(self) -> None:
pass
def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass
def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
raise CoreError("emane networks cannot be linked to other networks")
def updatemodel(self, config: Dict[str, str]) -> None:
if not self.model:
raise 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(
"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:
"""
set the EmaneModel associated with this node
"""
logging.info("adding model: %s", model.name)
if model.config_type == RegisterTlvs.WIRELESS:
# EmaneModel really uses values from ConfigurableManager
# 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.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(
self, netif: CoreInterface
self, iface: CoreInterface
) -> Optional[Tuple[int, float, float, float]]:
"""
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
"""
nemid = self.getnemid(netif)
ifname = netif.localname
if nemid is None:
nem_id = self.session.emane.get_nem_id(iface)
ifname = iface.localname
if nem_id is None:
logging.info("nemid for %s is unknown", ifname)
return
node = netif.node
node = iface.node
x, y, z = node.getposition()
lat, lon, alt = self.session.location.getgeo(x, y, z)
if node.position.alt is not None:
@ -199,32 +132,31 @@ class EmaneNet(CoreNetworkBase):
node.position.set_geo(lon, lat, alt)
# altitude must be an integer or warning is printed
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.
:param netif: interface to set nem position for
:param iface: interface to set nem position for
"""
if self.session.emane.service is None:
logging.info("position service not available")
return
position = self._nem_position(netif)
position = self._nem_position(iface)
if position:
nemid, lon, lat, alt = position
event = LocationEvent()
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.session.emane.service.publish(0, event)
def setnempositions(self, moved_netifs: List[CoreInterface]) -> None:
def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None:
"""
Several NEMs have moved, from e.g. a WaypointMobilityModel
calculation. Generate an EMANE Location Event having several
entries for each netif that has moved.
entries for each interface that has moved.
"""
if len(moved_netifs) == 0:
if len(moved_ifaces) == 0:
return
if self.session.emane.service is None:
@ -232,18 +164,21 @@ class EmaneNet(CoreNetworkBase):
return
event = LocationEvent()
for netif in moved_netifs:
position = self._nem_position(netif)
for iface in moved_ifaces:
position = self._nem_position(iface)
if position:
nemid, lon, lat, alt = position
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.session.emane.service.publish(0, event)
def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
links = super().all_link_data(flags)
# gather current emane links
nem_ids = set(self.nemidmap.values())
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
links = super().links(flags)
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
considered = set()
for link_key in emane_links:
@ -262,3 +197,18 @@ class EmaneNet(CoreNetworkBase):
if link:
links.append(link)
return links
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
# TUN/TAP is not ready for addressing yet; the device may
# take some time to appear, and installing it into a
# namespace after it has been bound removes addressing;
# save addresses with the interface now
iface_id = node.newtuntap(iface_data.id, iface_data.name)
node.attachnet(iface_id, self)
iface = node.get_iface(iface_id)
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):
# model name
name = "emane_rfpipe"
name: str = "emane_rfpipe"
# mac configuration
mac_library = "rfpipemaclayer"
mac_xml = "rfpipemaclayer.xml"
mac_library: str = "rfpipemaclayer"
mac_xml: str = "rfpipemaclayer.xml"
@classmethod
def load(cls, emane_prefix: str) -> None:

View file

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

View file

@ -6,9 +6,10 @@ import sys
from typing import Dict, List, Type
import core.services
from core import configservices
from core import configservices, utils
from core.configservice.manager import ConfigServiceManager
from core.emulator.session import Session
from core.executables import get_requirements
from core.services.coreservices import ServiceManager
@ -65,10 +66,29 @@ class CoreEmu:
if custom_dir:
self.service_manager.load(custom_dir)
# check executables exist on path
self._validate_env()
# catch exit event
atexit.register(self.shutdown)
def _validate_env(self) -> None:
"""
Validates executables CORE depends on exist on path.
: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:
"""
Loads default and custom services for use within CORE.
:return: nothing
"""
# load default services
self.service_errors = core.services.load()

View file

@ -1,18 +1,22 @@
"""
CORE data objects.
"""
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional, Tuple
from dataclasses import dataclass
from typing import List, Tuple
import netaddr
from core import utils
from core.emulator.enumerations import (
EventTypes,
ExceptionLevels,
LinkTypes,
MessageFlags,
NodeTypes,
)
if TYPE_CHECKING:
from core.nodes.base import CoreNode, NodeBase
@dataclass
class ConfigData:
@ -27,7 +31,7 @@ class ConfigData:
possible_values: str = None
groups: str = None
session: int = None
interface_number: int = None
iface_id: int = None
network_id: int = None
opaque: str = None
@ -68,65 +72,218 @@ class FileData:
@dataclass
class NodeData:
message_type: MessageFlags = None
id: int = None
node_type: NodeTypes = None
class NodeOptions:
"""
Options for creating and updating nodes within core.
"""
name: str = None
ip_address: str = None
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
model: Optional[str] = "PC"
canvas: int = None
network_id: int = None
services: List[str] = None
latitude: float = None
longitude: float = None
altitude: float = 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
@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
class LinkData:
"""
Represents all data associated with a link.
"""
message_type: MessageFlags = None
type: LinkTypes = LinkTypes.WIRED
label: str = None
node1_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
key: int = None
interface1_id: int = None
interface1_name: str = None
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
iface1: InterfaceData = None
iface2: InterfaceData = None
options: LinkOptions = LinkOptions()
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 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.network import CoreNetwork, CtrlNet
@ -131,8 +132,17 @@ class DistributedController:
:param name: distributed server name
:param host: distributed server host address
:return: nothing
:raises CoreError: when there is an error validating server
"""
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
cmd = f"mkdir -p {self.session.session_dir}"
server.remote_cmd(cmd)
@ -208,7 +218,7 @@ class DistributedController:
"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.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
logging.info(
@ -217,25 +227,27 @@ class DistributedController:
remote_tap = GreTap(
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
tunnel = (local_tap, remote_tap)
self.tunnels[key] = 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.
The hash(n1num), hash(n2num) values are used, so node numbers may be
None or string values (used for e.g. "ctrlnet").
:param n1_id: node one id
:param n2_id: node two id
:param node1_id: node one id
:param node2_id: node two id
: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 = (
(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

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,
label="SDT3D URL",
),
Configuration(
_id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS"
),
]
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
from tkinter import PhotoImage, font, ttk
from tkinter.ttk import Progressbar
from typing import Any, Dict, Optional, Type
import grpc
from core.gui import appconfig, themes
from core.gui.appconfig import GuiConfig
from core.gui.coreclient import CoreClient
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.images import ImageEnum, Images
from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar
from core.gui.themes import PADY
from core.gui.toolbar import Toolbar
WIDTH = 1000
HEIGHT = 800
WIDTH: int = 1000
HEIGHT: int = 800
class Application(ttk.Frame):
def __init__(self, proxy: bool) -> None:
def __init__(self, proxy: bool, session_id: int = None) -> None:
super().__init__()
# load node icons
NodeUtils.setup()
# widgets
self.menubar = None
self.toolbar = None
self.right_frame = None
self.canvas = None
self.statusbar = None
self.progress = None
self.menubar: Optional[Menubar] = None
self.toolbar: Optional[Toolbar] = None
self.right_frame: Optional[ttk.Frame] = None
self.canvas: Optional[CanvasGraph] = None
self.statusbar: Optional[StatusBar] = 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
self.fonts_size = None
self.icon_text_font = None
self.edge_font = None
self.fonts_size: Dict[str, int] = {}
self.icon_text_font: Optional[font.Font] = None
self.edge_font: Optional[font.Font] = None
# setup
self.guiconfig = appconfig.read()
self.app_scale = self.guiconfig.scale
self.guiconfig: GuiConfig = appconfig.read()
self.app_scale: float = self.guiconfig.scale
self.setup_scaling()
self.style = ttk.Style()
self.style: ttk.Style = ttk.Style()
self.setup_theme()
self.core = CoreClient(self, proxy)
self.core: CoreClient = CoreClient(self, proxy)
self.setup_app()
self.draw()
self.core.setup()
self.core.setup(session_id)
def setup_scaling(self) -> None:
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.grid(row=0, column=1, sticky="nsew")
self.draw_canvas()
self.draw_infobar()
self.draw_status()
self.progress = Progressbar(self.right_frame, mode="indeterminate")
self.menubar = Menubar(self)
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:
canvas_frame = ttk.Frame(self.right_frame)
canvas_frame.rowconfigure(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.grid(sticky="nsew")
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
@ -134,7 +153,31 @@ class Application(ttk.Frame):
def draw_status(self) -> None:
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:
logging.exception("app grpc exception", exc_info=e)

View file

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

View file

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

View file

@ -188,7 +188,7 @@
<configuration name="error" value="0"/>
</mobility_configuration>
<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="loop" value="1"/>
<configuration name="autostart" value="5"/>

View file

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

View file

@ -3,9 +3,9 @@ check engine light
"""
import tkinter as tk
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.themes import PADX, PADY
from core.gui.widgets import CodeText
@ -15,14 +15,14 @@ if TYPE_CHECKING:
class AlertsDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Alerts")
self.tree = None
self.codetext = None
self.alarm_map = {}
self.tree: Optional[ttk.Treeview] = None
self.codetext: Optional[CodeText] = None
self.alarm_map: Dict[int, ExceptionEvent] = {}
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
@ -52,6 +52,7 @@ class AlertsDialog(Dialog):
for alarm in self.app.statusbar.core_alarms:
exception = alarm.exception_event
level_name = ExceptionLevel.Enum.Name(exception.level)
node_id = exception.node_id if exception.node_id else ""
insert_id = self.tree.insert(
"",
tk.END,
@ -60,7 +61,7 @@ class AlertsDialog(Dialog):
exception.date,
level_name,
alarm.session_id,
exception.node_id,
node_id,
exception.source,
),
tags=(level_name,),
@ -97,16 +98,18 @@ class AlertsDialog(Dialog):
button = ttk.Button(frame, text="Close", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def reset_alerts(self):
self.codetext.text.delete("1.0", tk.END)
def reset_alerts(self) -> None:
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():
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]
alarm = self.alarm_map[current]
self.codetext.text.config(state=tk.NORMAL)
self.codetext.text.delete("1.0", "end")
self.codetext.text.insert("1.0", alarm.exception_event.text)
self.codetext.text.delete(1.0, tk.END)
self.codetext.text.insert(1.0, alarm.exception_event.text)
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.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
from core.gui.app import Application
PIXEL_SCALE = 100
PIXEL_SCALE: int = 100
class SizeAndScaleDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
"""
create an instance for size and scale object
"""
super().__init__(app, "Canvas Size and Scale")
self.canvas = self.app.canvas
self.section_font = font.Font(weight="bold")
self.canvas: CanvasGraph = self.app.canvas
self.section_font: font.Font = font.Font(weight="bold")
width, height = self.canvas.current_dimensions
self.pixel_width = tk.IntVar(value=width)
self.pixel_height = tk.IntVar(value=height)
self.pixel_width: tk.IntVar = tk.IntVar(value=width)
self.pixel_height: tk.IntVar = tk.IntVar(value=height)
location = self.app.core.location
self.x = tk.DoubleVar(value=location.x)
self.y = tk.DoubleVar(value=location.y)
self.lat = tk.DoubleVar(value=location.lat)
self.lon = tk.DoubleVar(value=location.lon)
self.alt = tk.DoubleVar(value=location.alt)
self.scale = tk.DoubleVar(value=location.scale)
self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale)
self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale)
self.save_default = tk.BooleanVar(value=False)
self.x: tk.DoubleVar = tk.DoubleVar(value=location.x)
self.y: tk.DoubleVar = tk.DoubleVar(value=location.y)
self.lat: tk.DoubleVar = tk.DoubleVar(value=location.lat)
self.lon: tk.DoubleVar = tk.DoubleVar(value=location.lon)
self.alt: tk.DoubleVar = tk.DoubleVar(value=location.alt)
self.scale: tk.DoubleVar = tk.DoubleVar(value=location.scale)
self.meters_width: tk.IntVar = tk.IntVar(
value=width / PIXEL_SCALE * location.scale
)
self.meters_height: tk.IntVar = tk.IntVar(
value=height / PIXEL_SCALE * location.scale
)
self.save_default: tk.BooleanVar = tk.BooleanVar(value=False)
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.draw_size()
self.draw_scale()
@ -47,7 +52,7 @@ class SizeAndScaleDialog(Dialog):
self.draw_spacer()
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.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1)
@ -61,10 +66,12 @@ class SizeAndScaleDialog(Dialog):
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
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.grid(row=0, column=2, sticky="w", padx=PADX)
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
label = ttk.Label(frame, text="Pixels")
label.grid(row=0, column=4, sticky="w")
@ -75,16 +82,20 @@ class SizeAndScaleDialog(Dialog):
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width")
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)
label = ttk.Label(frame, text="x Height")
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)
label = ttk.Label(frame, text="Meters")
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.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1)
@ -96,10 +107,11 @@ class SizeAndScaleDialog(Dialog):
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=2, sticky="w")
def draw_reference_point(self):
def draw_reference_point(self) -> None:
label_frame = ttk.Labelframe(
self.top, text="Reference Point", padding=FRAME_PAD
)
@ -150,13 +162,13 @@ class SizeAndScaleDialog(Dialog):
entry = validation.FloatEntry(frame, textvariable=self.alt)
entry.grid(row=0, column=5, sticky="ew")
def draw_save_as_default(self):
def draw_save_as_default(self) -> None:
button = ttk.Checkbutton(
self.top, text="Save as default?", variable=self.save_default
)
button.grid(sticky="w", pady=PADY)
def draw_buttons(self):
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
@ -168,7 +180,14 @@ class SizeAndScaleDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy)
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()
self.canvas.redraw_canvas((width, height))
if self.canvas.wallpaper:

View file

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

View file

@ -3,7 +3,7 @@ custom color picker
"""
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional, Tuple
from core.gui import validation
from core.gui.dialogs.dialog import Dialog
@ -18,23 +18,23 @@ class ColorPickerDialog(Dialog):
self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000"
):
super().__init__(app, "Color Picker", master=master)
self.red_entry = None
self.blue_entry = None
self.green_entry = None
self.hex_entry = None
self.red_label = None
self.green_label = None
self.blue_label = None
self.display = None
self.color = initcolor
self.red_entry: Optional[validation.RgbEntry] = None
self.blue_entry: Optional[validation.RgbEntry] = None
self.green_entry: Optional[validation.RgbEntry] = None
self.hex_entry: Optional[validation.HexEntry] = None
self.red_label: Optional[ttk.Label] = None
self.green_label: Optional[ttk.Label] = None
self.blue_label: Optional[ttk.Label] = None
self.display: Optional[tk.Frame] = None
self.color: str = initcolor
red, green, blue = self.get_rgb(initcolor)
self.red = tk.IntVar(value=red)
self.blue = tk.IntVar(value=blue)
self.green = tk.IntVar(value=green)
self.hex = tk.StringVar(value=initcolor)
self.red_scale = tk.IntVar(value=red)
self.green_scale = tk.IntVar(value=green)
self.blue_scale = tk.IntVar(value=blue)
self.red: tk.IntVar = tk.IntVar(value=red)
self.blue: tk.IntVar = tk.IntVar(value=blue)
self.green: tk.IntVar = tk.IntVar(value=green)
self.hex: tk.StringVar = tk.StringVar(value=initcolor)
self.red_scale: tk.IntVar = tk.IntVar(value=red)
self.green_scale: tk.IntVar = tk.IntVar(value=green)
self.blue_scale: tk.IntVar = tk.IntVar(value=blue)
self.draw()
self.set_bindings()
@ -42,7 +42,7 @@ class ColorPickerDialog(Dialog):
self.show()
return self.color
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, 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.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.green_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.hex.trace_add("write", self.update_color)
def button_ok(self):
def button_ok(self) -> None:
self.color = self.hex.get()
self.destroy()
@ -159,10 +159,10 @@ class ColorPickerDialog(Dialog):
green = self.green_entry.get()
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
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":
red = self.red_entry.get()
blue = self.blue_entry.get()
@ -184,7 +184,7 @@ class ColorPickerDialog(Dialog):
self.display.config(background=hex_code)
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())
self.focus = "rgb"
self.update_color()
@ -194,17 +194,17 @@ class ColorPickerDialog(Dialog):
self.green_scale.set(green)
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.green.set(green)
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.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
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
"""

View file

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

View file

@ -4,81 +4,58 @@ copy service config dialog
import tkinter as tk
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.themes import FRAME_PAD, PADX
from core.gui.widgets import CodeText
from core.gui.themes import PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.dialogs.serviceconfig import ServiceConfigDialog
class CopyServiceConfigDialog(Dialog):
def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int):
super().__init__(app, f"Copy services to node {node_id}", master=master)
self.parent = master
self.node_id = node_id
self.service_configs = app.core.service_configs
self.file_configs = app.core.file_configs
self.tree = None
def __init__(
self,
app: "Application",
dialog: "ServiceConfigDialog",
name: str,
service: str,
file_name: str,
) -> 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()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.tree = ttk.Treeview(self.top)
self.tree.grid(row=0, column=0, sticky="ew", padx=PADX)
self.tree["columns"] = ()
self.tree.column("#0", width=270, minwidth=270, stretch=tk.YES)
self.tree.heading("#0", text="Service configuration items", anchor=tk.CENTER)
custom_nodes = set(self.service_configs).union(set(self.file_configs))
for nid in custom_nodes:
treeid = self.tree.insert("", "end", text=f"n{nid}", tags="node")
services = self.service_configs.get(nid, None)
files = self.file_configs.get(nid, None)
tree_ids = {}
if services:
for service, config in services.items():
serviceid = self.tree.insert(
treeid, "end", text=service, tags="service"
)
tree_ids[service] = serviceid
cmdup = config.startup[:]
cmddown = config.shutdown[:]
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")
self.top.rowconfigure(1, weight=1)
label = ttk.Label(
self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER
)
label.grid(sticky="ew", pady=PADY)
listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(sticky="nsew", pady=PADY)
self.listbox = listbox_scroll.listbox
for canvas_node in self.app.canvas.nodes.values():
file_configs = canvas_node.service_file_configs.get(self.service)
if not file_configs:
continue
data = file_configs.get(self.file_name)
if not data:
continue
name = canvas_node.core_node.name
self.nodes[name] = canvas_node.id
self.listbox.insert(tk.END, name)
frame = ttk.Frame(self.top)
frame.grid(row=1, column=0)
frame.grid(sticky="ew")
for i in range(3):
frame.columnconfigure(i, weight=1)
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.grid(row=0, column=1, sticky="ew", padx=PADX)
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):
selected = self.tree.selection()
if selected:
item = self.tree.item(selected[0])
if "file" in item["tags"]:
filename = item["text"]
nid, service = self.get_node_service(selected)
data = self.file_configs[nid][service][filename]
if service == self.parent.service_name:
self.parent.temp_service_files[filename] = data
self.parent.modified_files.add(filename)
if self.parent.filename_combobox.get() == filename:
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,
)
def click_copy(self) -> None:
selection = self.listbox.curselection()
if not selection:
return
name = self.listbox.get(selection)
canvas_node_id = self.nodes[name]
canvas_node = self.app.canvas.nodes[canvas_node_id]
data = canvas_node.service_file_configs[self.service][self.file_name]
self.dialog.temp_service_files[self.file_name] = data
self.dialog.modified_files.add(self.file_name)
self.dialog.service_file_data.text.delete(1.0, tk.END)
self.dialog.service_file_data.text.insert(tk.END, data)
self.destroy()
def click_view(self):
selected = self.tree.selection()
data = ""
if selected:
item = self.tree.item(selected[0])
if "file" in item["tags"]:
nid, service = self.get_node_service(selected)
data = self.file_configs[nid][service][item["text"]]
dialog = ViewConfigDialog(
self, self.app, nid, data, item["text"].split("/")[-1]
)
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
def click_view(self) -> None:
selection = self.listbox.curselection()
if not selection:
return
name = self.listbox.get(selection)
canvas_node_id = self.nodes[name]
canvas_node = self.app.canvas.nodes[canvas_node_id]
data = canvas_node.service_file_configs[self.service][self.file_name]
dialog = ViewConfigDialog(
self.app, self, name, self.service, self.file_name, data
)
dialog.show()
class ViewConfigDialog(Dialog):
def __init__(
self,
master: tk.BaseWidget,
app: "Application",
node_id: int,
master: tk.BaseWidget,
name: str,
service: str,
file_name: str,
data: str,
filename: str = None,
):
super().__init__(app, f"n{node_id} config data", master=master)
) -> None:
title = f"{name} Service({service}) File({file_name})"
super().__init__(app, title, master=master)
self.data = data
self.service_data = None
self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}")
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
frame = ttk.Frame(self.top, padding=FRAME_PAD)
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.top.rowconfigure(0, weight=1)
self.service_data = CodeText(self.top)
self.service_data.grid(row=1, column=0, sticky="nsew")
self.service_data.text.insert("end", self.data)
self.service_data.text.config(state="disabled")
self.service_data.grid(sticky="nsew", pady=PADY)
self.service_data.text.insert(tk.END, self.data)
self.service_data.text.config(state=tk.DISABLED)
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
from pathlib import Path
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.appconfig import ICONS_PATH, CustomNode
@ -19,15 +21,15 @@ if TYPE_CHECKING:
class ServicesSelectDialog(Dialog):
def __init__(
self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
):
) -> None:
super().__init__(app, "Node Services", master=master)
self.groups = None
self.services = None
self.current = None
self.current_services = set(current_services)
self.groups: Optional[ListboxScroll] = None
self.services: Optional[CheckboxList] = None
self.current: Optional[ListboxScroll] = None
self.current_services: Set[str] = current_services
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
@ -77,7 +79,7 @@ class ServicesSelectDialog(Dialog):
# trigger group change
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()
if selection:
index = selection[0]
@ -87,7 +89,7 @@ class ServicesSelectDialog(Dialog):
checked = name in self.current_services
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:
self.current_services.add(name)
elif not var.get() and name in self.current_services:
@ -96,34 +98,34 @@ class ServicesSelectDialog(Dialog):
for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name)
def click_cancel(self):
def click_cancel(self) -> None:
self.current_services = None
self.destroy()
class CustomNodesDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Custom Nodes")
self.edit_button = None
self.delete_button = None
self.nodes_list = None
self.name = tk.StringVar()
self.image_button = None
self.image = None
self.image_file = None
self.services = set()
self.selected = None
self.selected_index = None
self.edit_button: Optional[ttk.Button] = None
self.delete_button: Optional[ttk.Button] = None
self.nodes_list: Optional[ListboxScroll] = None
self.name: tk.StringVar = tk.StringVar()
self.image_button: Optional[ttk.Button] = None
self.image: Optional[PhotoImage] = None
self.image_file: Optional[str] = None
self.services: Set[str] = set()
self.selected: Optional[str] = None
self.selected_index: Optional[int] = None
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.draw_node_config()
self.draw_node_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.grid(sticky="nsew", pady=PADY)
frame.columnconfigure(0, weight=1)
@ -147,7 +149,7 @@ class CustomNodesDialog(Dialog):
button = ttk.Button(frame, text="Services", command=self.click_services)
button.grid(sticky="ew")
def draw_node_buttons(self):
def draw_node_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
for i in range(3):
@ -166,7 +168,7 @@ class CustomNodesDialog(Dialog):
)
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.grid(sticky="ew")
for i in range(2):
@ -178,14 +180,14 @@ class CustomNodesDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def reset_values(self):
def reset_values(self) -> None:
self.name.set("")
self.image = None
self.image_file = None
self.services = set()
self.image_button.config(image="")
def click_icon(self):
def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH)
if file_path:
image = Images.create(file_path, nodeutils.ICON_SIZE)
@ -193,24 +195,26 @@ class CustomNodesDialog(Dialog):
self.image_file = file_path
self.image_button.config(image=self.image)
def click_services(self):
def click_services(self) -> None:
dialog = ServicesSelectDialog(self, self.app, self.services)
dialog.show()
if dialog.current_services is not None:
self.services.clear()
self.services.update(dialog.current_services)
def click_save(self):
def click_save(self) -> None:
self.app.guiconfig.nodes.clear()
for name in self.app.core.custom_nodes:
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)
logging.info("saving custom nodes: %s", self.app.guiconfig.nodes)
self.app.save_config()
self.destroy()
def click_create(self):
def click_create(self) -> None:
name = self.name.get()
if name not in self.app.core.custom_nodes:
image_file = Path(self.image_file).stem
@ -226,7 +230,7 @@ class CustomNodesDialog(Dialog):
self.nodes_list.listbox.insert(tk.END, name)
self.reset_values()
def click_edit(self):
def click_edit(self) -> None:
name = self.name.get()
if 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.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:
self.nodes_list.listbox.delete(self.selected_index)
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.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()
if selection:
self.selected_index = selection[0]

View file

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

View file

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

View file

@ -10,7 +10,7 @@ class EmaneInstallDialog(Dialog):
super().__init__(app, "EMANE Error")
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
label = ttk.Label(self.top, text="EMANE needs to be installed!")
label.grid(sticky="ew", pady=PADY)
@ -21,5 +21,5 @@ class EmaneInstallDialog(Dialog):
button = ttk.Button(self.top, text="Close", command=self.destroy)
button.grid(sticky="ew")
def click_doc(self):
def click_doc(self) -> None:
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 typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from core.gui.dialogs.dialog import Dialog
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
if TYPE_CHECKING:
@ -13,29 +14,23 @@ if TYPE_CHECKING:
class ErrorDialog(Dialog):
def __init__(self, app: "Application", title: str, details: str) -> None:
super().__init__(app, "CORE Exception")
self.title = title
self.details = details
self.error_message = None
self.title: str = title
self.details: str = details
self.error_message: Optional[CodeText] = None
self.draw()
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
frame = ttk.Frame(self.top, padding=FRAME_PAD)
frame.grid(pady=PADY, sticky="ew")
frame.columnconfigure(1, weight=1)
image = Images.get(ImageEnum.ERROR, 36)
label = ttk.Label(frame, image=image)
image = Images.get(ImageEnum.ERROR, 24)
label = ttk.Label(
self.top, text=self.title, image=image, compound=tk.LEFT, anchor=tk.CENTER
)
label.image = image
label.grid(row=0, column=0, padx=PADX)
label = ttk.Label(frame, text=self.title)
label.grid(row=0, column=1, sticky="ew")
label.grid(sticky=tk.EW, pady=PADY)
self.error_message = CodeText(self.top)
self.error_message.text.insert("1.0", self.details)
self.error_message.text.config(state="disabled")
self.error_message.grid(sticky="nsew", pady=PADY)
self.error_message.text.config(state=tk.DISABLED)
self.error_message.grid(sticky=tk.NSEW, pady=PADY)
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 tkinter as tk
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.dialogs.dialog import Dialog
@ -12,15 +12,15 @@ if TYPE_CHECKING:
class ExecutePythonDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Execute Python Script")
self.with_options = tk.IntVar(value=0)
self.options = tk.StringVar(value="")
self.option_entry = None
self.file_entry = None
self.with_options: tk.IntVar = tk.IntVar(value=0)
self.options: tk.StringVar = tk.StringVar(value="")
self.option_entry: Optional[ttk.Entry] = None
self.file_entry: Optional[ttk.Entry] = None
self.draw()
def draw(self):
def draw(self) -> None:
i = 0
frame = ttk.Frame(self.top, padding=FRAME_PAD)
frame.columnconfigure(0, weight=1)
@ -63,13 +63,13 @@ class ExecutePythonDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
def add_options(self):
def add_options(self) -> None:
if self.with_options.get():
self.option_entry.configure(state="normal")
else:
self.option_entry.configure(state="disabled")
def select_file(self):
def select_file(self) -> None:
file = filedialog.askopenfilename(
parent=self.top,
initialdir=str(SCRIPT_PATH),
@ -80,7 +80,7 @@ class ExecutePythonDialog(Dialog):
self.file_entry.delete(0, "end")
self.file_entry.insert("end", file)
def script_execute(self):
def script_execute(self) -> None:
file = self.file_entry.get()
options = self.option_entry.get()
logging.info("Execute %s with options %s", file, options)

View file

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

View file

@ -1,6 +1,6 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog
@ -12,15 +12,15 @@ if TYPE_CHECKING:
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)
self.name = tk.StringVar()
self.codetext = None
self.hook = core_pb2.Hook()
self.state = tk.StringVar()
self.name: tk.StringVar = tk.StringVar()
self.codetext: Optional[CodeText] = None
self.hook: core_pb2.Hook = core_pb2.Hook()
self.state: tk.StringVar = tk.StringVar()
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, 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.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()
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.name.set(hook.file)
self.codetext.text.delete(1.0, tk.END)
@ -78,7 +78,7 @@ class HookDialog(Dialog):
state_name = core_pb2.SessionState.Enum.Name(hook.state)
self.state.set(state_name)
def save(self):
def save(self) -> None:
data = self.codetext.text.get("1.0", tk.END).strip()
state_value = core_pb2.SessionState.Enum.Value(self.state.get())
self.hook.file = self.name.get()
@ -88,15 +88,15 @@ class HookDialog(Dialog):
class HooksDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Hooks")
self.listbox = None
self.edit_button = None
self.delete_button = None
self.selected = None
self.listbox: Optional[tk.Listbox] = None
self.edit_button: Optional[ttk.Button] = None
self.delete_button: Optional[ttk.Button] = None
self.selected: Optional[str] = None
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(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.grid(row=0, column=3, sticky="ew")
def click_create(self):
def click_create(self) -> None:
dialog = HookDialog(self, self.app)
dialog.show()
hook = dialog.hook
@ -132,19 +132,19 @@ class HooksDialog(Dialog):
self.app.core.hooks[hook.file] = hook
self.listbox.insert(tk.END, hook.file)
def click_edit(self):
def click_edit(self) -> None:
hook = self.app.core.hooks[self.selected]
dialog = HookDialog(self, self.app)
dialog.set(hook)
dialog.show()
def click_delete(self):
def click_delete(self) -> None:
del self.app.core.hooks[self.selected]
self.listbox.delete(tk.ANCHOR)
self.edit_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():
index = self.listbox.curselection()[0]
self.selected = self.listbox.get(index)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ core node services
"""
import tkinter as tk
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.serviceconfig import ServiceConfigDialog
@ -16,19 +16,19 @@ if TYPE_CHECKING:
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"
super().__init__(app, title)
self.canvas_node = canvas_node
self.node_id = canvas_node.core_node.id
self.groups = None
self.services = None
self.current = None
self.canvas_node: "CanvasNode" = canvas_node
self.node_id: int = canvas_node.core_node.id
self.groups: Optional[ListboxScroll] = None
self.services: Optional[CheckboxList] = None
self.current: Optional[ListboxScroll] = None
services = set(canvas_node.core_node.services)
self.current_services = services
self.current_services: Set[str] = services
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
@ -82,9 +82,9 @@ class NodeServiceDialog(Dialog):
button.grid(row=0, column=3, sticky="ew")
# 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()
if selection:
index = selection[0]
@ -94,7 +94,7 @@ class NodeServiceDialog(Dialog):
checked = name in self.current_services
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:
self.current_services.add(name)
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.canvas_node.core_node.services[:] = self.current_services
def click_configure(self):
def click_configure(self) -> None:
current_selection = self.current.listbox.curselection()
if len(current_selection):
dialog = ServiceConfigDialog(
@ -127,12 +127,12 @@ class NodeServiceDialog(Dialog):
"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.services[:] = self.current_services
self.destroy()
def click_remove(self):
def click_remove(self) -> None:
cur = self.current.listbox.curselection()
if cur:
service = self.current.listbox.get(cur[0])

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,10 +3,11 @@ throughput dialog
"""
import tkinter as tk
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.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
@ -14,21 +15,23 @@ if TYPE_CHECKING:
class ThroughputDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Throughput Config")
self.canvas = app.canvas
self.show_throughput = tk.IntVar(value=1)
self.exponential_weight = tk.IntVar(value=1)
self.transmission = tk.IntVar(value=1)
self.reception = tk.IntVar(value=1)
self.threshold = tk.DoubleVar(value=self.canvas.throughput_threshold)
self.width = tk.IntVar(value=self.canvas.throughput_width)
self.color = self.canvas.throughput_color
self.color_button = None
self.canvas: CanvasGraph = app.canvas
self.show_throughput: tk.IntVar = tk.IntVar(value=1)
self.exponential_weight: tk.IntVar = tk.IntVar(value=1)
self.transmission: tk.IntVar = tk.IntVar(value=1)
self.reception: tk.IntVar = tk.IntVar(value=1)
self.threshold: tk.DoubleVar = tk.DoubleVar(
value=self.canvas.throughput_threshold
)
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.draw()
def draw(self):
def draw(self) -> None:
button = ttk.Checkbutton(
self.top,
variable=self.show_throughput,
@ -97,12 +100,12 @@ class ThroughputDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy)
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)
self.color = color_picker.askcolor()
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_width = self.width.get()
self.canvas.throughput_color = self.color

View file

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

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