diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index 854095680..52440467d 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -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 diff --git a/.gitignore b/.gitignore index bcfbadebc..2012df9d0 100644 --- a/.gitignore +++ b/.gitignore @@ -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__ diff --git a/CHANGELOG.md b/CHANGELOG.md index 96f7b30ac..375a76079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile.am b/Makefile.am index 4db054086..7a3799fc0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 diff --git a/configure.ac b/configure.ac index ae2d0c8d4..60f6709ec 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. # this defines the CORE version number, must be static for AC_INIT -AC_INIT(core, 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} diff --git a/daemon/.gitignore b/daemon/.gitignore deleted file mode 100644 index 27ffc2f17..000000000 --- a/daemon/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.pyc -build diff --git a/daemon/.pre-commit-config.yaml b/daemon/.pre-commit-config.yaml index 73566c9d7..bc9ead080 100644 --- a/daemon/.pre-commit-config.yaml +++ b/daemon/.pre-commit-config.yaml @@ -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] diff --git a/daemon/MANIFEST.in b/daemon/MANIFEST.in deleted file mode 100644 index c46dc828f..000000000 --- a/daemon/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -graft core/gui/data -graft core/configservices/*/templates diff --git a/daemon/Makefile.am b/daemon/Makefile.am index a56636549..7528dc01a 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -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 diff --git a/daemon/Pipfile b/daemon/Pipfile deleted file mode 100644 index 8bf52787d..000000000 --- a/daemon/Pipfile +++ /dev/null @@ -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 = "."} diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock deleted file mode 100644 index 2fb5c3b89..000000000 --- a/daemon/Pipfile.lock +++ /dev/null @@ -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" - } - } -} diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 0361a69b7..0674a0ebd 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -5,7 +5,7 @@ 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 @@ WlanLinkRequest, WlanLinkResponse, ) -from core.emulator.emudata import IpPrefixes +from core.emulator.data import IpPrefixes class InterfaceHelper: @@ -108,29 +108,29 @@ def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None: :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 @@ def __init__(self, address: str = "localhost:50051", proxy: bool = False) -> Non :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 @@ def set_session_state( 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 @@ def add_session_server( ) 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 @@ def events( 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 @@ def throughputs( 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 @@ def edit_node( 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 @@ def edit_node( :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 @@ def move_nodes( """ 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 @@ def get_node_links( 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 @@ def get_emane_models(self, session_id: int) -> GetEmaneModelsResponse: 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 @@ def get_emane_model_config( :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 @@ def set_emane_model_config( 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 @@ def set_emane_model_config( :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 @@ def open_xml(self, file_path: str, start: bool = False) -> core_pb2.OpenXmlRespo 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 @@ def execute_script(self, script: str) -> ExecuteScriptResponse: 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) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 837860e3a..fb6eaff8d 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -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.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 @@ def __init__( :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 @@ def add_handlers(self) -> None: 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: diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 5c6f3a80a..84b8ee6a0 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -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.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 @@ 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: +def iface_to_data(iface: CoreInterface) -> InterfaceData: + ip4 = iface.get_ip4() + ip4_addr = str(ip4.ip) if ip4 else None + ip4_mask = ip4.prefixlen if ip4 else None + ip6 = iface.get_ip6() + ip6_addr = str(ip6.ip) if ip6 else None + ip6_mask = ip6.prefixlen if ip6 else None + return InterfaceData( + id=iface.node_id, + name=iface.name, + mac=str(iface.mac), + ip4=ip4_addr, + ip4_mask=ip4_mask, + ip6=ip6_addr, + ip6_mask=ip6_mask, + ) + + +def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface: """ Convenience for converting a core interface to the protobuf representation. - :param interface: interface to convert + + :param node_id: id of node to convert interface for + :param iface: 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 + 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=interface.netindex, - netid=net_id, - name=interface.name, - mac=str(interface.hwaddr), - mtu=interface.mtu, - flowid=interface.flow_id, + 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, - ip4mask=ip4mask, + ip4_mask=ip4_mask, ip6=ip6, - ip6mask=ip6mask, + ip6_mask=ip6_mask, ) -def get_nem_id(node: CoreNode, netif_id: int, context: ServicerContext) -> int: +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) + 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 net.getnemid(netif) + return nem_id diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 7d7f7c806..38100e055 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -6,7 +6,7 @@ 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 @@ 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[0-9a-fA-F]+)") +_ONE_DAY_IN_SECONDS: int = 60 * 60 * 24 +_INTERFACE_REGEX: Pattern = re.compile(r"veth(?P[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 @@ def StartSession( 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 @@ def SetSessionState( 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 @@ def GetSession( 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 @@ def AddSessionServer( 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 @@ def Throughputs( 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 @@ def Throughputs( 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 @@ def AddNode( _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 @@ def GetNode( 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 @@ def DeleteNode( """ 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 @@ def AddLink( :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 + ) + node1_iface, node2_iface = session.add_link( + node1_id, node2_id, iface1_data, iface2_data, options, link_type ) - 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) + 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 @@ def EditLink( """ 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 @@ def DeleteLink( """ 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 @@ def GetHooks( 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 @@ def SetWlanConfig( """ 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 @@ def GetEmaneModelConfig( 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 @@ def SetEmaneModelConfig( 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 @@ def GetEmaneModelConfigs( 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 @@ def GetInterfaces( :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 @@ def EmaneLink( """ 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 - - 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 - - if emane_one.id == emane_two.id: + nem1 = request.nem1 + iface1 = session.emane.get_iface(nem1) + if not iface1: + context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found") + node1 = iface1.node + + nem2 = request.nem2 + iface2 = session.emane.get_iface(nem2) + if not iface2: + context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found") + node2 = iface2.node + + if iface1.net == iface2.net: if request.linked: flag = MessageFlags.ADD else: flag = MessageFlags.DELETE - color = session.get_link_color(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 @@ def WlanLink( 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 @@ def EmanePathlosses( ) -> 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() diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py index df60e3749..5d0b08e78 100644 --- a/daemon/core/api/tlv/coreapi.py +++ b/daemon/core/api/tlv/coreapi.py @@ -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, } diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index f3e1fbaaf..bb4f2ecd7 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -12,6 +12,7 @@ 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 @@ 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 @@ 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 @@ def __init__(self, request, client_address, server): 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 @@ def __init__(self, request, client_address, server): 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 @@ def session_message(self, flags=0): 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 @@ def handle_broadcast_node(self, node_data): """ logging.debug("handling broadcast node: %s", node_data) message = dataconversion.convert_node(node_data) - try: self.sendall(message) except IOError: @@ -334,46 +342,49 @@ def handle_broadcast_link(self, link_data): :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 @@ def handle_node_message(self, message): 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 @@ def handle_link_message(self, message): :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 @@ def handle_execute_message(self, message): :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 @@ def handle_execute_message(self, message): 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 @@ def handle_execute_message(self, message): 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 @@ def handle_execute_message(self, message): 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 @@ def handle_config_message(self, message): 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 @@ def handle_config_mobility_models(self, message_type, config_data): 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 @@ def handle_config_emane(self, message_type, config_data): 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 @@ def handle_config_emane_models(self, message_type, config_data): 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 @@ def handle_file_message(self, message): return () - def handle_interface_message(self, message): + def handle_iface_message(self, message): """ Interface Message handler. @@ -1555,11 +1550,11 @@ def handle_event_message(self, message): 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 @@ def handle_event_message(self, message): 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 @@ def send_objects(self): 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 @@ def send_objects(self): # 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 @@ def send_objects(self): 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 @@ def __init__(self, request, client_address, server): 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, } diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 876e72a52..8a26300ad 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -8,45 +8,39 @@ 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), ], diff --git a/daemon/core/api/tlv/enumerations.py b/daemon/core/api/tlv/enumerations.py index ed06bbe75..b4ec254a0 100644 --- a/daemon/core/api/tlv/enumerations.py +++ b/daemon/core/api/tlv/enumerations.py @@ -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 diff --git a/daemon/core/config.py b/daemon/core/config.py index 1f5bc3c0d..618e1273d 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -4,7 +4,7 @@ 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 @@ def __init__(self, name: str, start: int, stop: int) -> None: :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 @@ def __init__( :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 @@ def config_reset(self, node_id: int = None) -> None: """ 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 @@ def get_configs( 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 @@ def __init__(self) -> None: 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 diff --git a/daemon/core/configservice/base.py b/daemon/core/configservice/base.py index 825989885..bb97e3215 100644 --- a/daemon/core/configservice/base.py +++ b/daemon/core/configservice/base.py @@ -14,7 +14,7 @@ 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 @@ def __init__(self, node: CoreNode) -> None: :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) diff --git a/daemon/core/configservice/dependencies.py b/daemon/core/configservice/dependencies.py index 92eede79e..be1c45e7d 100644 --- a/daemon/core/configservice/dependencies.py +++ b/daemon/core/configservice/dependencies.py @@ -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 @@ def __init__(self, services: Dict[str, "ConfigService"]) -> None: :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 @@ def __init__(self, services: Dict[str, "ConfigService"]) -> None: 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"]]: """ diff --git a/daemon/core/configservice/manager.py b/daemon/core/configservice/manager.py index 1f806f7bb..836576557 100644 --- a/daemon/core/configservice/manager.py +++ b/daemon/core/configservice/manager.py @@ -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 @@ def __init__(self): """ Create a ConfigServiceManager instance. """ - self.services = {} + self.services: Dict[str, Type[ConfigService]] = {} def get_service(self, name: str) -> Type[ConfigService]: """ @@ -31,7 +31,7 @@ def get_service(self, name: str) -> Type[ConfigService]: 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 @@ def add(self, service: ConfigService) -> None: :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 @@ def add(self, service: ConfigService) -> None: 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 @@ def load(self, path: str) -> List[str]: 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: diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py index c4502f86d..720500778 100644 --- a/daemon/core/configservices/frrservices/services.py +++ b/daemon/core/configservices/frrservices/services.py @@ -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 @@ def data(self) -> Dict[str, Any]: 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 @@ def data(self) -> Dict[str, Any]: 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 @@ def frr_config(self) -> str: """ 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 @@ def frr_config(self) -> str: """ 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 @@ def frr_config(self) -> str: """ 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 @@ def frr_config(self) -> str: """ 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 @@ def frr_config(self) -> str: """ 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 @@ def frr_config(self) -> str: 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 @@ def frr_config(self) -> str: """ 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 diff --git a/daemon/core/configservices/frrservices/templates/daemons b/daemon/core/configservices/frrservices/templates/daemons index 0f6bda536..dbd421081 100644 --- a/daemon/core/configservices/frrservices/templates/daemons +++ b/daemon/core/configservices/frrservices/templates/daemons @@ -20,6 +20,7 @@ nhrpd=yes eigrpd=yes babeld=yes sharpd=yes +staticd=yes pbrd=yes bfdd=yes fabricd=yes diff --git a/daemon/core/configservices/frrservices/templates/frr.conf b/daemon/core/configservices/frrservices/templates/frr.conf index 748c86923..8e0361368 100644 --- a/daemon/core/configservices/frrservices/templates/frr.conf +++ b/daemon/core/configservices/frrservices/templates/frr.conf @@ -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 diff --git a/daemon/core/configservices/frrservices/templates/frrboot.sh b/daemon/core/configservices/frrservices/templates/frrboot.sh index 3c14cd1a5..db47b6d1a 100644 --- a/daemon/core/configservices/frrservices/templates/frrboot.sh +++ b/daemon/core/configservices/frrservices/templates/frrboot.sh @@ -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 diff --git a/daemon/core/configservices/nrlservices/services.py b/daemon/core/configservices/nrlservices/services.py index 3dddf1ba4..cf9b4c883 100644 --- a/daemon/core/configservices/nrlservices/services.py +++ b/daemon/core/configservices/nrlservices/services.py @@ -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 @@ def data(self) -> Dict[str, Any]: 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 @@ def data(self) -> Dict[str, Any]: 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) diff --git a/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh b/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh index 00b7e11d1..4513dfe9c 100644 --- a/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh +++ b/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh @@ -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} diff --git a/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh b/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh index d7a8d3b6d..81196e26e 100644 --- a/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh +++ b/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh @@ -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} diff --git a/daemon/core/configservices/nrlservices/templates/olsrd.sh b/daemon/core/configservices/nrlservices/templates/olsrd.sh index 076f049be..3040ca6b0 100644 --- a/daemon/core/configservices/nrlservices/templates/olsrd.sh +++ b/daemon/core/configservices/nrlservices/templates/olsrd.sh @@ -1,4 +1,4 @@ <% - interfaces = "-i " + " -i ".join(ifnames) + ifaces = "-i " + " -i ".join(ifnames) %> -olsrd ${interfaces} +olsrd ${ifaces} diff --git a/daemon/core/configservices/nrlservices/templates/startsmf.sh b/daemon/core/configservices/nrlservices/templates/startsmf.sh index 67fc0fe67..921568def 100644 --- a/daemon/core/configservices/nrlservices/templates/startsmf.sh +++ b/daemon/core/configservices/nrlservices/templates/startsmf.sh @@ -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 & diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index 32ce99be2..194306640 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -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 @@ def data(self) -> Dict[str, Any]: 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 @@ def data(self) -> Dict[str, Any]: 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 @@ def data(self) -> Dict[str, Any]: 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 @@ def quagga_interface_config(self, ifc: CoreInterface) -> str: 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 @@ def quagga_interface_config(self, ifc: CoreInterface) -> str: 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 @@ def quagga_config(self) -> str: """ 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 @@ def quagga_config(self) -> str: """ 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 @@ def quagga_config(self) -> str: 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 @@ def quagga_config(self) -> str: """ 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 diff --git a/daemon/core/configservices/quaggaservices/templates/Quagga.conf b/daemon/core/configservices/quaggaservices/templates/Quagga.conf index 853b17073..1d69838f0 100644 --- a/daemon/core/configservices/quaggaservices/templates/Quagga.conf +++ b/daemon/core/configservices/quaggaservices/templates/Quagga.conf @@ -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 diff --git a/daemon/core/configservices/sercurityservices/__init__.py b/daemon/core/configservices/securityservices/__init__.py similarity index 100% rename from daemon/core/configservices/sercurityservices/__init__.py rename to daemon/core/configservices/securityservices/__init__.py diff --git a/daemon/core/configservices/securityservices/services.py b/daemon/core/configservices/securityservices/services.py new file mode 100644 index 000000000..4a58fd8c3 --- /dev/null +++ b/daemon/core/configservices/securityservices/services.py @@ -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) diff --git a/daemon/core/configservices/sercurityservices/templates/firewall.sh b/daemon/core/configservices/securityservices/templates/firewall.sh similarity index 100% rename from daemon/core/configservices/sercurityservices/templates/firewall.sh rename to daemon/core/configservices/securityservices/templates/firewall.sh diff --git a/daemon/core/configservices/sercurityservices/templates/ipsec.sh b/daemon/core/configservices/securityservices/templates/ipsec.sh similarity index 100% rename from daemon/core/configservices/sercurityservices/templates/ipsec.sh rename to daemon/core/configservices/securityservices/templates/ipsec.sh diff --git a/daemon/core/configservices/sercurityservices/templates/nat.sh b/daemon/core/configservices/securityservices/templates/nat.sh similarity index 100% rename from daemon/core/configservices/sercurityservices/templates/nat.sh rename to daemon/core/configservices/securityservices/templates/nat.sh diff --git a/daemon/core/configservices/sercurityservices/templates/vpnclient.sh b/daemon/core/configservices/securityservices/templates/vpnclient.sh similarity index 100% rename from daemon/core/configservices/sercurityservices/templates/vpnclient.sh rename to daemon/core/configservices/securityservices/templates/vpnclient.sh diff --git a/daemon/core/configservices/sercurityservices/templates/vpnserver.sh b/daemon/core/configservices/securityservices/templates/vpnserver.sh similarity index 100% rename from daemon/core/configservices/sercurityservices/templates/vpnserver.sh rename to daemon/core/configservices/securityservices/templates/vpnserver.sh diff --git a/daemon/core/configservices/sercurityservices/services.py b/daemon/core/configservices/sercurityservices/services.py deleted file mode 100644 index 17f081cdd..000000000 --- a/daemon/core/configservices/sercurityservices/services.py +++ /dev/null @@ -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) diff --git a/daemon/core/configservices/simpleservice.py b/daemon/core/configservices/simpleservice.py index e727fe82c..c2e7242f4 100644 --- a/daemon/core/configservices/simpleservice.py +++ b/daemon/core/configservices/simpleservice.py @@ -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"}, diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index 8ddf1cc7b..b6bc0eb52 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -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 @@ def data(self) -> Dict[str, Any]: 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 @@ def data(self) -> Dict[str, Any]: 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) diff --git a/daemon/core/configservices/utilservices/templates/index.html b/daemon/core/configservices/utilservices/templates/index.html index aaf9d9fa3..bed270aea 100644 --- a/daemon/core/configservices/utilservices/templates/index.html +++ b/daemon/core/configservices/utilservices/templates/index.html @@ -5,8 +5,8 @@

${node.name} web server

This is the default web page for this server.

The web server software is running but no content has been added, yet.

diff --git a/daemon/core/constants.py.in b/daemon/core/constants.py.in index 54f3a1c3a..cb566e400 100644 --- a/daemon/core/constants.py.in +++ b/daemon/core/constants.py.in @@ -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) diff --git a/daemon/core/emane/bypass.py b/daemon/core/emane/bypass.py index 83f3b6e8b..8aabc3f98 100644 --- a/daemon/core/emane/bypass.py +++ b/daemon/core/emane/bypass.py @@ -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 @@ 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: diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index b7060e964..0fa70a92b 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -10,9 +10,7 @@ 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 @@ 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 @@ def configurations(cls) -> List[Configuration]: 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 @@ def build_xml_files( 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 @@ def build_xml_files( 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 @@ def linkconfig( 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) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 12b477f0c..ec39137d7 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -6,6 +6,7 @@ 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.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_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 @@ def __init__(self, session: "Session") -> None: :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 - ) -> Dict[str, str]: - """ - Retrieve interface configuration or node configuration if not provided. + 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 - :param node_id: node id - :param interface: node interface - :param model_name: model to get configuration for - :return: node/interface model configuration + def get_iface_config( + self, emane_net: EmaneNet, iface: CoreInterface + ) -> Dict[str, str]: """ - # 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 + Retrieve configuration for a given interface. + + :param emane_net: emane network the interface is connected to + :param iface: interface running emane + :return: net, node, or interface model configuration + """ + 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 @@ def emane_check(self) -> None: :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 @@ def add_node(self, emane_net: EmaneNet) -> None: """ 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 @@ def getnodes(self) -> Set[CoreNode]: 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 @@ def setup(self) -> int: 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 @@ def setup(self) -> int: "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 @@ def setup(self) -> int: "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 @@ def setup(self) -> int: 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 @@ def startup(self) -> int: 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 @@ def poststartup(self) -> None: """ 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 @@ def reset(self) -> None: """ 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 @@ def shutdown(self) -> None: 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 @@ def check_node_models(self) -> None: 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 @@ def buildeventservicexml(self) -> None: ) ) - 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 @@ def startdaemons(self) -> None: 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 - - if node.up: - node.cmd(kill_emaned, wait=False) - # TODO: RJ45 node - - 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: + 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) + + 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() + + 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 @@ def starteventmonitor(self) -> None: 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 @@ def handlelocationeventtoxyz( 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, diff --git a/daemon/core/emane/emanemanifest.py b/daemon/core/emane/emanemanifest.py index 914b4f830..41dc7beb5 100644 --- a/daemon/core/emane/emanemanifest.py +++ b/daemon/core/emane/emanemanifest.py @@ -11,6 +11,7 @@ try: from emanesh import manifest except ImportError: + manifest = None logging.debug("compatible emane python bindings not installed") diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 7b5ff417f..8672163d4 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -3,13 +3,13 @@ """ 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 @@ def config_groups(cls) -> List[ConfigGroup]: 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 @@ def post_startup(self) -> None: """ 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) diff --git a/daemon/core/emane/ieee80211abg.py b/daemon/core/emane/ieee80211abg.py index ecfd36941..0d58ec9e5 100644 --- a/daemon/core/emane/ieee80211abg.py +++ b/daemon/core/emane/ieee80211abg.py @@ -8,11 +8,11 @@ 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: diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 861c108c3..56473f62c 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -2,9 +2,8 @@ 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 @@ 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 @@ def _get_index(self, current_sinr: float) -> int: 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 @@ def __repr__(self) -> str: 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 @@ def stop(self) -> None: 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 @@ def get_addresses(self) -> List[str]: 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 @@ def check_links(self) -> None: 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 @@ def get_link_label(self, link_id: Tuple[int, int]) -> str: 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 @@ def send_message( 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) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index f4de8f47f..5791f46ad 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -6,23 +6,25 @@ 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 @@ 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 @@ def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None: 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 @@ def _nem_position( 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 @@ def setnempositions(self, moved_netifs: List[CoreInterface]) -> None: 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 @@ def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkDat 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 diff --git a/daemon/core/emane/rfpipe.py b/daemon/core/emane/rfpipe.py index 23790b3cf..068ef8002 100644 --- a/daemon/core/emane/rfpipe.py +++ b/daemon/core/emane/rfpipe.py @@ -8,11 +8,11 @@ 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: diff --git a/daemon/core/emane/tdma.py b/daemon/core/emane/tdma.py index 17f5328fa..ee80f3d72 100644 --- a/daemon/core/emane/tdma.py +++ b/daemon/core/emane/tdma.py @@ -4,6 +4,7 @@ import logging import os +from typing import Set from core import constants, utils from core.config import Configuration @@ -13,18 +14,18 @@ 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: diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 6a7f8b80c..c07d8c953 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -6,9 +6,10 @@ 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 @@ def __init__(self, config: Dict[str, str] = None) -> None: 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() diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index d3283974d..15d922a9e 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -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 diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 3753e1c23..a5e1009fd 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -14,7 +14,8 @@ 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 @@ def add_server(self, name: str, host: str) -> None: :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 @@ def create_gre_tunnel( "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 @@ def create_gre_tunnel( 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 diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py deleted file mode 100644 index b6dbd57cd..000000000 --- a/daemon/core/emulator/emudata.py +++ /dev/null @@ -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 diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 45c17743d..cad6ae3ce 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -6,28 +6,29 @@ import logging import os import pwd -import random import shutil import subprocess import tempfile import threading import time -from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar from core import constants, utils from core.configservice.manager import ConfigServiceManager -from core.emane.emanemanager import EmaneManager +from core.emane.emanemanager import EmaneManager, EmaneState from core.emane.nodes import EmaneNet from core.emulator.data import ( ConfigData, EventData, ExceptionData, FileData, + InterfaceData, LinkData, + LinkOptions, NodeData, + NodeOptions, ) from core.emulator.distributed import DistributedController -from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import ( EventTypes, ExceptionLevels, @@ -60,7 +61,7 @@ from core.xml.corexml import CoreXmlReader, CoreXmlWriter # maps for converting from API call node type values to classes and vice versa -NODES = { +NODES: Dict[NodeTypes, Type[NodeBase]] = { NodeTypes.DEFAULT: CoreNode, NodeTypes.PHYSICAL: PhysicalNode, NodeTypes.SWITCH: SwitchNode, @@ -75,11 +76,11 @@ NodeTypes.DOCKER: DockerNode, NodeTypes.LXC: LxcNode, } -NODES_TYPE = {NODES[x]: x for x in NODES} -CONTAINER_NODES = {DockerNode, LxcNode} -CTRL_NET_ID = 9001 -LINK_COLORS = ["green", "blue", "orange", "purple", "turquoise"] -NT = TypeVar("NT", bound=NodeBase) +NODES_TYPE: Dict[Type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES} +CONTAINER_NODES: Set[Type[NodeBase]] = {DockerNode, LxcNode} +CTRL_NET_ID: int = 9001 +LINK_COLORS: List[str] = ["green", "blue", "orange", "purple", "turquoise"] +NT: TypeVar = TypeVar("NT", bound=NodeBase) class Session: @@ -113,15 +114,13 @@ def __init__( # dict of nodes: all nodes and nets self.nodes: Dict[int, NodeBase] = {} - self._nodes_lock = threading.Lock() + self.nodes_lock = threading.Lock() + # states and hooks handlers self.state: EventTypes = EventTypes.DEFINITION_STATE - self._state_time: float = time.monotonic() - self._state_file: str = os.path.join(self.session_dir, "state") - - # hooks handlers - self._hooks: Dict[EventTypes, Tuple[str, str]] = {} - self._state_hooks: Dict[EventTypes, Callable[[int], None]] = {} + self.state_time: float = time.monotonic() + self.hooks: Dict[EventTypes, Tuple[str, str]] = {} + self.state_hooks: Dict[EventTypes, List[Callable[[EventTypes], None]]] = {} self.add_state_hook( state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook ) @@ -154,15 +153,6 @@ def __init__( self.emane: EmaneManager = EmaneManager(self) self.sdt: Sdt = Sdt(self) - # initialize default node services - self.services.default_services = { - "mdr": ("zebra", "OSPFv3MDR", "IPForward"), - "PC": ("DefaultRoute",), - "prouter": (), - "router": ("zebra", "OSPFv2", "OSPFv3", "IPForward"), - "host": ("DefaultRoute", "SSH"), - } - # config services self.service_manager: Optional[ConfigServiceManager] = None @@ -193,423 +183,286 @@ def get_node_type(cls, _class: Type[NodeBase]) -> NodeTypes: raise CoreError(f"invalid node class: {_class}") return node_type - def _link_nodes( - self, node_one_id: int, node_two_id: int - ) -> Tuple[ - Optional[CoreNode], - Optional[CoreNode], - Optional[CoreNetworkBase], - Optional[CoreNetworkBase], - ]: - """ - Convenience method for retrieving nodes within link data. - - :param node_one_id: node one id - :param node_two_id: node two id - :return: nodes, network nodes if present, and tunnel if present - """ - logging.debug( - "link message between node1(%s) and node2(%s)", node_one_id, node_two_id - ) - - # values to fill - net_one = None - net_two = None - - # retrieve node one - node_one = self.get_node(node_one_id, NodeBase) - node_two = self.get_node(node_two_id, NodeBase) - - if isinstance(node_one, CoreNetworkBase): - if not net_one: - net_one = node_one - else: - net_two = node_one - node_one = None - - if isinstance(node_two, CoreNetworkBase): - if not net_one: - net_one = node_two - else: - net_two = node_two - node_two = None - - logging.debug( - "link node types n1(%s) n2(%s) net1(%s) net2(%s)", - node_one, - node_two, - net_one, - net_two, - ) - return node_one, node_two, net_one, net_two - - def _link_wireless(self, objects: Iterable[CoreNodeBase], connect: bool) -> None: + def _link_wireless( + self, node1: CoreNodeBase, node2: CoreNodeBase, connect: bool + ) -> None: """ Objects to deal with when connecting/disconnecting wireless links. - :param objects: possible objects to deal with + :param node1: node one for wireless link + :param node2: node two for wireless link :param connect: link interfaces if True, unlink otherwise :return: nothing :raises core.CoreError: when objects to link is less than 2, or no common networks are found """ - objects = [x for x in objects if x] - if len(objects) < 2: - raise CoreError(f"wireless link failure: {objects}") - logging.debug( - "handling wireless linking objects(%s) connect(%s)", objects, connect + logging.info( + "handling wireless linking node1(%s) node2(%s): %s", + node1.name, + node2.name, + connect, ) - common_networks = objects[0].commonnets(objects[1]) + common_networks = node1.commonnets(node1) if not common_networks: raise CoreError("no common network found for wireless link/unlink") - - for common_network, interface_one, interface_two in common_networks: + for common_network, iface1, iface2 in common_networks: if not isinstance(common_network, (WlanNode, EmaneNet)): logging.info( "skipping common network that is not wireless/emane: %s", common_network, ) continue - - logging.info( - "wireless linking connect(%s): %s - %s", - connect, - interface_one, - interface_two, - ) if connect: - common_network.link(interface_one, interface_two) + common_network.link(iface1, iface2) else: - common_network.unlink(interface_one, interface_two) + common_network.unlink(iface1, iface2) + + def use_ovs(self) -> bool: + return self.options.get_config("ovs") == "1" def add_link( self, - node_one_id: int, - node_two_id: int, - interface_one: InterfaceData = None, - interface_two: InterfaceData = None, + node1_id: int, + node2_id: int, + iface1_data: InterfaceData = None, + iface2_data: InterfaceData = None, options: LinkOptions = None, + link_type: LinkTypes = LinkTypes.WIRED, ) -> Tuple[CoreInterface, CoreInterface]: """ Add a link between nodes. - :param node_one_id: node one id - :param node_two_id: node two id - :param interface_one: node one interface + :param node1_id: node one id + :param node2_id: node two id + :param iface1_data: node one interface data, defaults to none - :param interface_two: node two interface + :param iface2_data: node two interface data, defaults to none :param options: data for creating link, defaults to no options + :param link_type: type of link to add :return: tuple of created core interfaces, depending on link """ if not options: options = LinkOptions() - - # get node objects identified by link data - node_one, node_two, net_one, net_two = self._link_nodes( - node_one_id, node_two_id - ) - - if node_one: - node_one.lock.acquire() - if node_two: - node_two.lock.acquire() - - node_one_interface = None - node_two_interface = None - - try: - # wireless link - if options.type == LinkTypes.WIRELESS: - objects = [node_one, node_two, net_one, net_two] - self._link_wireless(objects, connect=True) - # wired link + node1 = self.get_node(node1_id, NodeBase) + node2 = self.get_node(node2_id, NodeBase) + iface1 = None + iface2 = None + + # wireless link + if link_type == LinkTypes.WIRELESS: + if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): + self._link_wireless(node1, node2, connect=True) else: - # 2 nodes being linked, ptp network - if all([node_one, node_two]) and not net_one: - logging.info( - "adding link for peer to peer nodes: %s - %s", - node_one.name, - node_two.name, - ) - start = self.state.should_start() - net_one = self.create_node(PtpNet, start=start) - - # node to network - if node_one and net_one: - logging.info( - "adding link from node to network: %s - %s", - node_one.name, - net_one.name, - ) - ifindex = node_one.newnetif(net_one, interface_one) - node_one_interface = node_one.netif(ifindex) - wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) - if not wireless_net: - net_one.linkconfig(node_one_interface, options) - - # network to node - if node_two and net_one: - logging.info( - "adding link from network to node: %s - %s", - node_two.name, - net_one.name, - ) - ifindex = node_two.newnetif(net_one, interface_two) - node_two_interface = node_two.netif(ifindex) - wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) - if not options.unidirectional and not wireless_net: - net_one.linkconfig(node_two_interface, options) - - # network to network - if net_one and net_two: - logging.info( - "adding link from network to network: %s - %s", - net_one.name, - net_two.name, - ) - interface = net_one.linknet(net_two) - node_one_interface = interface - net_one.linkconfig(interface, options) - if not options.unidirectional: - interface.swapparams("_params_up") - net_two.linkconfig(interface, options) - interface.swapparams("_params_up") - - # a tunnel node was found for the nodes - addresses = [] - if not node_one and all([net_one, interface_one]): - addresses.extend(interface_one.get_addresses()) - if not node_two and all([net_two, interface_two]): - addresses.extend(interface_two.get_addresses()) - - # tunnel node logic - key = options.key - if key and isinstance(net_one, TunnelNode): - logging.info("setting tunnel key for: %s", net_one.name) - net_one.setkey(key) - if addresses: - net_one.addrconfig(addresses) - if key and isinstance(net_two, TunnelNode): - logging.info("setting tunnel key for: %s", net_two.name) - net_two.setkey(key) - if addresses: - net_two.addrconfig(addresses) - finally: - if node_one: - node_one.lock.release() - if node_two: - node_two.lock.release() - - self.sdt.add_link(node_one_id, node_two_id) - return node_one_interface, node_two_interface + raise CoreError( + f"cannot wireless link node1({type(node1)}) node2({type(node2)})" + ) + # wired link + else: + # peer to peer link + if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): + logging.info("linking ptp: %s - %s", node1.name, node2.name) + start = self.state.should_start() + ptp = self.create_node(PtpNet, start) + iface1 = node1.new_iface(ptp, iface1_data) + iface2 = node2.new_iface(ptp, iface2_data) + ptp.linkconfig(iface1, options) + if not options.unidirectional: + ptp.linkconfig(iface2, options) + # link node to net + elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): + iface1 = node1.new_iface(node2, iface1_data) + if not isinstance(node2, (EmaneNet, WlanNode)): + node2.linkconfig(iface1, options) + # link net to node + elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): + iface2 = node2.new_iface(node1, iface2_data) + wireless_net = isinstance(node1, (EmaneNet, WlanNode)) + if not options.unidirectional and not wireless_net: + node1.linkconfig(iface2, options) + # network to network + elif isinstance(node1, CoreNetworkBase) and isinstance( + node2, CoreNetworkBase + ): + logging.info( + "linking network to network: %s - %s", node1.name, node2.name + ) + iface1 = node1.linknet(node2) + node1.linkconfig(iface1, options) + if not options.unidirectional: + iface1.swapparams("_params_up") + node2.linkconfig(iface1, options) + iface1.swapparams("_params_up") + else: + raise CoreError( + f"cannot link node1({type(node1)}) node2({type(node2)})" + ) + + # configure tunnel nodes + key = options.key + if isinstance(node1, TunnelNode): + logging.info("setting tunnel key for: %s", node1.name) + node1.setkey(key, iface1_data) + if isinstance(node2, TunnelNode): + logging.info("setting tunnel key for: %s", node2.name) + node2.setkey(key, iface2_data) + self.sdt.add_link(node1_id, node2_id) + return iface1, iface2 def delete_link( self, - node_one_id: int, - node_two_id: int, - interface_one_id: int, - interface_two_id: int, + node1_id: int, + node2_id: int, + iface1_id: int = None, + iface2_id: int = None, link_type: LinkTypes = LinkTypes.WIRED, ) -> None: """ Delete a link between nodes. - :param node_one_id: node one id - :param node_two_id: node two id - :param interface_one_id: interface id for node one - :param interface_two_id: interface id for node two + :param node1_id: node one id + :param node2_id: node two id + :param iface1_id: interface id for node one + :param iface2_id: interface id for node two :param link_type: link type to delete :return: nothing :raises core.CoreError: when no common network is found for link being deleted """ - # get node objects identified by link data - node_one, node_two, net_one, net_two = self._link_nodes( - node_one_id, node_two_id + node1 = self.get_node(node1_id, NodeBase) + node2 = self.get_node(node2_id, NodeBase) + logging.info( + "deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)", + link_type.name, + node1.name, + iface1_id, + node2.name, + iface2_id, ) - if node_one: - node_one.lock.acquire() - if node_two: - node_two.lock.acquire() - - try: - # wireless link - if link_type == LinkTypes.WIRELESS: - objects = [node_one, node_two, net_one, net_two] - self._link_wireless(objects, connect=False) - # wired link + # wireless link + if link_type == LinkTypes.WIRELESS: + if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): + self._link_wireless(node1, node2, connect=False) else: - if all([node_one, node_two]): - # TODO: fix this for the case where ifindex[1,2] are not specified - # a wired unlink event, delete the connecting bridge - interface_one = node_one.netif(interface_one_id) - interface_two = node_two.netif(interface_two_id) - - # get interfaces from common network, if no network node - # otherwise get interfaces between a node and network - if not interface_one and not interface_two: - common_networks = node_one.commonnets(node_two) - for ( - network, - common_interface_one, - common_interface_two, - ) in common_networks: - if (net_one and network == net_one) or not net_one: - interface_one = common_interface_one - interface_two = common_interface_two - break - - if all([interface_one, interface_two]) and any( - [interface_one.net, interface_two.net] - ): - if interface_one.net != interface_two.net and all( - [interface_one.up, interface_two.up] - ): - raise CoreError("no common network found") - - logging.info( - "deleting link node(%s):interface(%s) node(%s):interface(%s)", - node_one.name, - interface_one.name, - node_two.name, - interface_two.name, - ) - net_one = interface_one.net - interface_one.detachnet() - interface_two.detachnet() - if net_one.numnetif() == 0: - self.delete_node(net_one.id) - node_one.delnetif(interface_one.netindex) - node_two.delnetif(interface_two.netindex) - elif node_one and net_one: - interface = node_one.netif(interface_one_id) - if interface: - logging.info( - "deleting link node(%s):interface(%s) node(%s)", - node_one.name, - interface.name, - net_one.name, - ) - interface.detachnet() - node_one.delnetif(interface.netindex) - elif node_two and net_one: - interface = node_two.netif(interface_two_id) - if interface: - logging.info( - "deleting link node(%s):interface(%s) node(%s)", - node_two.name, - interface.name, - net_one.name, - ) - interface.detachnet() - node_two.delnetif(interface.netindex) - finally: - if node_one: - node_one.lock.release() - if node_two: - node_two.lock.release() - - self.sdt.delete_link(node_one_id, node_two_id) + raise CoreError( + "cannot delete wireless link " + f"node1({type(node1)}) node2({type(node2)})" + ) + # wired link + else: + if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): + iface1 = node1.get_iface(iface1_id) + iface2 = node2.get_iface(iface2_id) + if iface1.net != iface2.net: + raise CoreError( + f"node1({node1.name}) node2({node2.name}) " + "not connected to same net" + ) + ptp = iface1.net + node1.delete_iface(iface1_id) + node2.delete_iface(iface2_id) + self.delete_node(ptp.id) + elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): + node1.delete_iface(iface1_id) + elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): + node2.delete_iface(iface2_id) + self.sdt.delete_link(node1_id, node2_id) def update_link( self, - 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, options: LinkOptions = None, + link_type: LinkTypes = LinkTypes.WIRED, ) -> None: """ Update link information between nodes. - :param node_one_id: node one id - :param node_two_id: node two id - :param interface_one_id: interface id for node one - :param interface_two_id: interface id for node two + :param node1_id: node one id + :param node2_id: node two id + :param iface1_id: interface id for node one + :param iface2_id: interface id for node two :param options: data to update link with + :param link_type: type of link to update :return: nothing - :raises core.CoreError: when updating a wireless type link, when there is a unknown - link between networks + :raises core.CoreError: when updating a wireless type link, when there is a + unknown link between networks """ if not options: options = LinkOptions() - - # get node objects identified by link data - node_one, node_two, net_one, net_two = self._link_nodes( - node_one_id, node_two_id + node1 = self.get_node(node1_id, NodeBase) + node2 = self.get_node(node2_id, NodeBase) + logging.info( + "update link(%s) node(%s):interface(%s) node(%s):interface(%s)", + link_type.name, + node1.name, + iface1_id, + node2.name, + iface2_id, ) - if node_one: - node_one.lock.acquire() - if node_two: - node_two.lock.acquire() - - try: - # wireless link - if options.type == LinkTypes.WIRELESS: - raise CoreError("cannot update wireless link") - else: - if not node_one and not node_two: - if net_one and net_two: - # modify link between nets - interface = net_one.getlinknetif(net_two) - upstream = False - - if not interface: - upstream = True - interface = net_two.getlinknetif(net_one) - - if not interface: - raise CoreError("modify unknown link between nets") - - if upstream: - interface.swapparams("_params_up") - net_one.linkconfig(interface, options) - interface.swapparams("_params_up") - else: - net_one.linkconfig(interface, options) - - if not options.unidirectional: - if upstream: - net_two.linkconfig(interface, options) - else: - interface.swapparams("_params_up") - net_two.linkconfig(interface, options) - interface.swapparams("_params_up") - else: - raise CoreError("modify link for unknown nodes") - elif not node_one: - # node1 = layer 2node, node2 = layer3 node - interface = node_two.netif(interface_two_id) - net_one.linkconfig(interface, options) - elif not node_two: - # node2 = layer 2node, node1 = layer3 node - interface = node_one.netif(interface_one_id) - net_one.linkconfig(interface, options) + # wireless link + if link_type == LinkTypes.WIRELESS: + raise CoreError("cannot update wireless link") + else: + if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): + iface1 = node1.ifaces.get(iface1_id) + iface2 = node2.ifaces.get(iface2_id) + if not iface1: + raise CoreError( + f"node({node1.name}) missing interface({iface1_id})" + ) + if not iface2: + raise CoreError( + f"node({node2.name}) missing interface({iface2_id})" + ) + if iface1.net != iface2.net: + raise CoreError( + f"node1({node1.name}) node2({node2.name}) " + "not connected to same net" + ) + ptp = iface1.net + ptp.linkconfig(iface1, options, iface2) + if not options.unidirectional: + ptp.linkconfig(iface2, options, iface1) + elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): + iface = node1.get_iface(iface1_id) + node2.linkconfig(iface, options) + elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): + iface = node2.get_iface(iface2_id) + node1.linkconfig(iface, options) + elif isinstance(node1, CoreNetworkBase) and isinstance( + node2, CoreNetworkBase + ): + iface = node1.get_linked_iface(node2) + upstream = False + if not iface: + upstream = True + iface = node2.get_linked_iface(node1) + if not iface: + raise CoreError("modify unknown link between nets") + if upstream: + iface.swapparams("_params_up") + node1.linkconfig(iface, options) + iface.swapparams("_params_up") else: - common_networks = node_one.commonnets(node_two) - if not common_networks: - raise CoreError("no common network found") - - for net_one, interface_one, interface_two in common_networks: - if ( - interface_one_id is not None - and interface_one_id != node_one.getifindex(interface_one) - ): - continue - - net_one.linkconfig(interface_one, options, interface_two) - if not options.unidirectional: - net_one.linkconfig(interface_two, options, interface_one) - finally: - if node_one: - node_one.lock.release() - if node_two: - node_two.lock.release() - - def _next_node_id(self) -> int: + node1.linkconfig(iface, options) + if not options.unidirectional: + if upstream: + node2.linkconfig(iface, options) + else: + iface.swapparams("_params_up") + node2.linkconfig(iface, options) + iface.swapparams("_params_up") + else: + raise CoreError( + f"cannot update link node1({type(node1)}) node2({type(node2)})" + ) + + def next_node_id(self) -> int: """ Find the next valid node id, starting from 1. @@ -642,7 +495,7 @@ def add_node( # determine node id if not _id: - _id = self._next_node_id() + _id = self.next_node_id() # generate name if not provided if not options: @@ -665,21 +518,20 @@ def add_node( name, start, ) - kwargs = dict(_id=_id, name=name, start=start, server=server) + kwargs = dict(_id=_id, name=name, server=server) if _class in CONTAINER_NODES: kwargs["image"] = options.image - node = self.create_node(_class, **kwargs) + node = self.create_node(_class, start, **kwargs) # set node attributes node.icon = options.icon node.canvas = options.canvas - node.opaque = options.opaque # set node position and broadcast it self.set_node_position(node, options) # add services to needed nodes - if isinstance(node, (CoreNode, PhysicalNode, DockerNode, LxcNode)): + if isinstance(node, (CoreNode, PhysicalNode)): node.type = options.model logging.debug("set node type: %s", node.type) self.services.add_services(node, node.type, options.services) @@ -693,6 +545,8 @@ def add_node( # ensure default emane configuration if isinstance(node, EmaneNet) and options.emane: self.emane.set_model_config(_id, options.emane) + if self.state == EventTypes.RUNTIME_STATE: + self.emane.add_node(node) # set default wlan config if needed if isinstance(node, WlanNode): self.mobility.set_model_config(_id, BasicRangeModel.name) @@ -701,7 +555,7 @@ def add_node( is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node) if self.state == EventTypes.RUNTIME_STATE and is_boot_node: self.write_nodes() - self.add_remove_control_interface(node=node, remove=False) + self.add_remove_control_iface(node=node, remove=False) self.services.boot_services(node) self.sdt.add_node(node) @@ -788,19 +642,16 @@ def open_xml(self, file_name: str, start: bool = False) -> None: # clear out existing session self.clear() - if start: - state = EventTypes.CONFIGURATION_STATE - else: - state = EventTypes.DEFINITION_STATE + # set state and read xml + state = EventTypes.CONFIGURATION_STATE if start else EventTypes.DEFINITION_STATE self.set_state(state) self.name = os.path.basename(file_name) self.file_name = file_name - - # write out xml file CoreXmlReader(self).read(file_name) # start session if needed if start: + self.set_state(EventTypes.INSTANTIATION_STATE) self.instantiate() def save_xml(self, file_name: str) -> None: @@ -828,7 +679,7 @@ def add_hook( "setting state hook: %s - %s source(%s)", state, file_name, source_name ) hook = file_name, data - state_hooks = self._hooks.setdefault(state, []) + state_hooks = self.hooks.setdefault(state, []) state_hooks.append(hook) # immediately run a hook if it is in the current state @@ -863,7 +714,7 @@ def clear(self) -> None: self.emane.shutdown() self.delete_nodes() self.distributed.shutdown() - self.del_hooks() + self.hooks.clear() self.emane.reset() self.emane.config_reset() self.location.reset() @@ -931,7 +782,6 @@ def broadcast_event(self, event_data: EventData) -> None: :param event_data: event data to send out :return: nothing """ - for handler in self.event_handlers: handler(event_data) @@ -942,7 +792,6 @@ def broadcast_exception(self, exception_data: ExceptionData) -> None: :param exception_data: exception data to send out :return: nothing """ - for handler in self.exception_handlers: handler(exception_data) @@ -960,9 +809,9 @@ def broadcast_node( :param source: source of broadcast, None by default :return: nothing """ - node_data = node.data(message_type, source) - if not node_data: + if not node.apitype: return + node_data = NodeData(node=node, message_type=message_type, source=source) for handler in self.node_handlers: handler(node_data) @@ -973,7 +822,6 @@ def broadcast_file(self, file_data: FileData) -> None: :param file_data: file data to send out :return: nothing """ - for handler in self.file_handlers: handler(file_data) @@ -984,7 +832,6 @@ def broadcast_config(self, config_data: ConfigData) -> None: :param config_data: config data to send out :return: nothing """ - for handler in self.config_handlers: handler(config_data) @@ -995,7 +842,6 @@ def broadcast_link(self, link_data: LinkData) -> None: :param link_data: link data to send out :return: nothing """ - for handler in self.link_handlers: handler(link_data) @@ -1007,22 +853,14 @@ def set_state(self, state: EventTypes, send_event: bool = False) -> None: :param send_event: if true, generate core API event messages :return: nothing """ - state_name = state.name if self.state == state: - logging.info( - "session(%s) is already in state: %s, skipping change", - self.id, - state_name, - ) return - self.state = state - self._state_time = time.monotonic() - logging.info("changing session(%s) to state %s", self.id, state_name) + self.state_time = time.monotonic() + logging.info("changing session(%s) to state %s", self.id, state.name) self.write_state(state) self.run_hooks(state) self.run_state_hooks(state) - if send_event: event_data = EventData(event_type=state, time=str(time.monotonic())) self.broadcast_event(event_data) @@ -1034,10 +872,10 @@ def write_state(self, state: EventTypes) -> None: :param state: state to write to file :return: nothing """ + state_file = os.path.join(self.session_dir, "state") try: - state_file = open(self._state_file, "w") - state_file.write(f"{state.value} {state.name}\n") - state_file.close() + with open(state_file, "w") as f: + f.write(f"{state.value} {state.name}\n") except IOError: logging.exception("error writing state file: %s", state.name) @@ -1049,61 +887,10 @@ def run_hooks(self, state: EventTypes) -> None: :param state: state to run hooks for :return: nothing """ - - # check that state change hooks exist - if state not in self._hooks: - return - - # retrieve all state hooks - hooks = self._hooks.get(state, []) - - # execute all state hooks - if hooks: - for hook in hooks: - self.run_hook(hook) - else: - logging.info("no state hooks for %s", state) - - def set_hook( - self, hook_type: str, file_name: str, source_name: str, data: str - ) -> None: - """ - Store a hook from a received file message. - - :param hook_type: hook type - :param file_name: file name for hook - :param source_name: source name - :param data: hook data - :return: nothing - """ - logging.info( - "setting state hook: %s - %s from %s", hook_type, file_name, source_name - ) - - _hook_id, state = hook_type.split(":")[:2] - if not state.isdigit(): - logging.error("error setting hook having state '%s'", state) - return - - state = int(state) - hook = file_name, data - - # append hook to current state hooks - state_hooks = self._hooks.setdefault(state, []) - state_hooks.append(hook) - - # immediately run a hook if it is in the current state - # (this allows hooks in the definition and configuration states) - if self.state == state: - logging.info("immediately running new state hook") + hooks = self.hooks.get(state, []) + for hook in hooks: self.run_hook(hook) - def del_hooks(self) -> None: - """ - Clear the hook scripts dict. - """ - self._hooks.clear() - def run_hook(self, hook: Tuple[str, str]) -> None: """ Run a hook. @@ -1113,37 +900,23 @@ def run_hook(self, hook: Tuple[str, str]) -> None: """ file_name, data = hook logging.info("running hook %s", file_name) - - # write data to hook file + file_path = os.path.join(self.session_dir, file_name) + log_path = os.path.join(self.session_dir, f"{file_name}.log") try: - hook_file = open(os.path.join(self.session_dir, file_name), "w") - hook_file.write(data) - hook_file.close() - except IOError: - logging.exception("error writing hook '%s'", file_name) - - # setup hook stdout and stderr - try: - stdout = open(os.path.join(self.session_dir, file_name + ".log"), "w") - stderr = subprocess.STDOUT - except IOError: - logging.exception("error setting up hook stderr and stdout") - stdout = None - stderr = None - - # execute hook file - try: - args = ["/bin/sh", file_name] - subprocess.check_call( - args, - stdout=stdout, - stderr=stderr, - close_fds=True, - cwd=self.session_dir, - env=self.get_environment(), - ) - except (OSError, subprocess.CalledProcessError): - logging.exception("error running hook: %s", file_name) + with open(file_path, "w") as f: + f.write(data) + with open(log_path, "w") as f: + args = ["/bin/sh", file_name] + subprocess.check_call( + args, + stdout=f, + stderr=subprocess.STDOUT, + close_fds=True, + cwd=self.session_dir, + env=self.get_environment(), + ) + except (IOError, subprocess.CalledProcessError): + logging.exception("error running hook: %s", file_path) def run_state_hooks(self, state: EventTypes) -> None: """ @@ -1152,17 +925,16 @@ def run_state_hooks(self, state: EventTypes) -> None: :param state: state to run hooks for :return: nothing """ - for hook in self._state_hooks.get(state, []): - try: - hook(state) - except Exception: - message = ( - f"exception occured when running {state.name} state hook: {hook}" - ) - logging.exception(message) - self.exception( - ExceptionLevels.ERROR, "Session.run_state_hooks", message - ) + for hook in self.state_hooks.get(state, []): + self.run_state_hook(state, hook) + + def run_state_hook(self, state: EventTypes, hook: Callable[[EventTypes], None]): + try: + hook(state) + except Exception: + message = f"exception occurred when running {state.name} state hook: {hook}" + logging.exception(message) + self.exception(ExceptionLevels.ERROR, "Session.run_state_hooks", message) def add_state_hook( self, state: EventTypes, hook: Callable[[EventTypes], None] @@ -1174,15 +946,16 @@ def add_state_hook( :param hook: hook callback for the state :return: nothing """ - hooks = self._state_hooks.setdefault(state, []) + hooks = self.state_hooks.setdefault(state, []) if hook in hooks: raise CoreError("attempting to add duplicate state hook") hooks.append(hook) - if self.state == state: - hook(state) + self.run_state_hook(state, hook) - def del_state_hook(self, state: int, hook: Callable[[int], None]) -> None: + def del_state_hook( + self, state: EventTypes, hook: Callable[[EventTypes], None] + ) -> None: """ Delete a state hook. @@ -1190,24 +963,23 @@ def del_state_hook(self, state: int, hook: Callable[[int], None]) -> None: :param hook: hook to delete :return: nothing """ - hooks = self._state_hooks.setdefault(state, []) - hooks.remove(hook) + hooks = self.state_hooks.get(state, []) + if hook in hooks: + hooks.remove(hook) - def runtime_state_hook(self, state: EventTypes) -> None: + def runtime_state_hook(self, _state: EventTypes) -> None: """ Runtime state hook check. - :param state: state to check + :param _state: state to check :return: nothing """ - if state == EventTypes.RUNTIME_STATE: - self.emane.poststartup() - - # create session deployed xml - xml_file_name = os.path.join(self.session_dir, "session-deployed.xml") - xml_writer = corexml.CoreXmlWriter(self) - corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario) - xml_writer.write(xml_file_name) + self.emane.poststartup() + # create session deployed xml + xml_file_name = os.path.join(self.session_dir, "session-deployed.xml") + xml_writer = corexml.CoreXmlWriter(self) + corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario) + xml_writer.write(xml_file_name) def get_environment(self, state: bool = True) -> Dict[str, str]: """ @@ -1225,11 +997,8 @@ def get_environment(self, state: bool = True) -> Dict[str, str]: env["SESSION_NAME"] = str(self.name) env["SESSION_FILENAME"] = str(self.file_name) env["SESSION_USER"] = str(self.user) - env["SESSION_NODE_COUNT"] = str(self.get_node_count()) - if state: env["SESSION_STATE"] = str(self.state) - # attempt to read and add environment config file environment_config_file = os.path.join(constants.CORE_CONF_DIR, "environment") try: @@ -1240,7 +1009,6 @@ def get_environment(self, state: bool = True) -> Dict[str, str]: "environment configuration file does not exist: %s", environment_config_file, ) - # attempt to read and add user environment file if self.user: environment_user_file = os.path.join( @@ -1253,7 +1021,6 @@ def get_environment(self, state: bool = True) -> Dict[str, str]: "user core environment settings file not present: %s", environment_user_file, ) - return env def set_thumbnail(self, thumb_file: str) -> None: @@ -1267,7 +1034,6 @@ def set_thumbnail(self, thumb_file: str) -> None: logging.error("thumbnail file to set does not exist: %s", thumb_file) self.thumbnail = None return - destination_file = os.path.join(self.session_dir, os.path.basename(thumb_file)) shutil.copy(thumb_file, destination_file) self.thumbnail = destination_file @@ -1287,36 +1053,29 @@ def set_user(self, user: str) -> None: os.chown(self.session_dir, uid, gid) except IOError: logging.exception("failed to set permission on %s", self.session_dir) - self.user = user - def get_node_id(self) -> int: - """ - Return a unique, new node id. - """ - with self._nodes_lock: - while True: - node_id = random.randint(1, 0xFFFF) - if node_id not in self.nodes: - break - return node_id - - def create_node(self, _class: Type[NT], *args: Any, **kwargs: Any) -> NT: + def create_node( + self, _class: Type[NT], start: bool, *args: Any, **kwargs: Any + ) -> NT: """ Create an emulation node. :param _class: node class to create + :param start: True to start node, False otherwise :param args: list of arguments for the class to create :param kwargs: dictionary of arguments for the class to create :return: the created node instance :raises core.CoreError: when id of the node to create already exists """ - node = _class(self, *args, **kwargs) - with self._nodes_lock: + with self.nodes_lock: + node = _class(self, *args, **kwargs) if node.id in self.nodes: node.shutdown() raise CoreError(f"duplicate node id {node.id} for {node.name}") self.nodes[node.id] = node + if start: + node.startup() return node def get_node(self, _id: int, _class: Type[NT]) -> NT: @@ -1328,9 +1087,9 @@ def get_node(self, _id: int, _class: Type[NT]) -> NT: :return: node for the given id :raises core.CoreError: when node does not exist """ - if _id not in self.nodes: + node = self.nodes.get(_id) + if node is None: raise CoreError(f"unknown node id {_id}") - node = self.nodes[_id] if not isinstance(node, _class): actual = node.__class__.__name__ expected = _class.__name__ @@ -1345,24 +1104,22 @@ def delete_node(self, _id: int) -> bool: :return: True if node deleted, False otherwise """ # delete node and check for session shutdown if a node was removed - logging.info("deleting node(%s)", _id) node = None - with self._nodes_lock: + with self.nodes_lock: if _id in self.nodes: node = self.nodes.pop(_id) - + logging.info("deleted node(%s)", node.name) if node: node.shutdown() self.sdt.delete_node(_id) self.check_shutdown() - return node is not None def delete_nodes(self) -> None: """ Clear the nodes dictionary, and call shutdown for each node. """ - with self._nodes_lock: + with self.nodes_lock: funcs = [] while self.nodes: _, node = self.nodes.popitem() @@ -1375,29 +1132,15 @@ def write_nodes(self) -> None: Write nodes to a 'nodes' file in the session dir. The 'nodes' file lists: number, name, api-type, class-type """ + file_path = os.path.join(self.session_dir, "nodes") try: - with self._nodes_lock: - file_path = os.path.join(self.session_dir, "nodes") + with self.nodes_lock: with open(file_path, "w") as f: - for _id in self.nodes.keys(): - node = self.nodes[_id] + for _id, node in self.nodes.items(): f.write(f"{_id} {node.name} {node.apitype} {type(node)}\n") except IOError: logging.exception("error writing nodes file") - def dump_session(self) -> None: - """ - Log information about the session in its current state. - """ - logging.info("session id=%s name=%s state=%s", self.id, self.name, self.state) - logging.info( - "file=%s thumbnail=%s node_count=%s/%s", - self.file_name, - self.thumbnail, - self.get_node_count(), - len(self.nodes), - ) - def exception( self, level: ExceptionLevels, source: str, text: str, node_id: int = None ) -> None: @@ -1440,7 +1183,7 @@ def instantiate(self) -> List[Exception]: self.distributed.start() # instantiate will be invoked again upon emane configure - if self.emane.startup() == self.emane.NOT_READY: + if self.emane.startup() == EmaneState.NOT_READY: return [] # boot node services and then start mobility @@ -1465,17 +1208,15 @@ def get_node_count(self) -> int: :return: created node count """ - with self._nodes_lock: + with self.nodes_lock: count = 0 - for node_id in self.nodes: - node = self.nodes[node_id] + for node in self.nodes.values(): is_p2p_ctrlnet = isinstance(node, (PtpNet, CtrlNet)) is_tap = isinstance(node, GreTapBridge) and not isinstance( node, TunnelNode ) if is_p2p_ctrlnet or is_tap: continue - count += 1 return count @@ -1497,7 +1238,6 @@ def check_runtime(self) -> None: if self.state == EventTypes.RUNTIME_STATE: logging.info("valid runtime state found, returning") return - # start event loop and set to runtime self.event_loop.run() self.set_state(EventTypes.RUNTIME_STATE, send_event=True) @@ -1513,7 +1253,7 @@ def data_collect(self) -> None: self.event_loop.stop() # stop node services - with self._nodes_lock: + with self.nodes_lock: funcs = [] for node_id in self.nodes: node = self.nodes[node_id] @@ -1527,7 +1267,7 @@ def data_collect(self) -> None: self.emane.shutdown() # update control interface hosts - self.update_control_interface_hosts(remove=True) + self.update_control_iface_hosts(remove=True) # remove all four possible control networks self.add_remove_control_net(0, remove=True) @@ -1573,7 +1313,7 @@ def boot_node(self, node: CoreNode) -> None: :return: nothing """ logging.info("booting node(%s): %s", node.name, [x.name for x in node.services]) - self.add_remove_control_interface(node=node, remove=False) + self.add_remove_control_iface(node=node, remove=False) self.services.boot_services(node) node.start_config_services() @@ -1585,7 +1325,7 @@ def boot_nodes(self) -> List[Exception]: :return: service boot exceptions """ - with self._nodes_lock: + with self.nodes_lock: funcs = [] start = time.monotonic() for _id in self.nodes: @@ -1597,7 +1337,7 @@ def boot_nodes(self) -> List[Exception]: total = time.monotonic() - start logging.debug("boot run time: %s", total) if not exceptions: - self.update_control_interface_hosts() + self.update_control_iface_hosts() return exceptions def get_control_net_prefixes(self) -> List[str]: @@ -1615,7 +1355,7 @@ def get_control_net_prefixes(self) -> List[str]: p0 = p return [p0, p1, p2, p3] - def get_control_net_server_interfaces(self) -> List[str]: + def get_control_net_server_ifaces(self) -> List[str]: """ Retrieve control net server interfaces. @@ -1683,8 +1423,7 @@ def add_remove_control_net( else: prefix_spec = CtrlNet.DEFAULT_PREFIX_LIST[net_index] logging.debug("prefix spec: %s", prefix_spec) - - server_interface = self.get_control_net_server_interfaces()[net_index] + server_iface = self.get_control_net_server_ifaces()[net_index] # return any existing controlnet bridge try: @@ -1725,18 +1464,19 @@ def add_remove_control_net( _id, prefix, updown_script, - server_interface, + server_iface, ) control_net = self.create_node( CtrlNet, + True, prefix, _id=_id, updown_script=updown_script, - serverintf=server_interface, + serverintf=server_iface, ) return control_net - def add_remove_control_interface( + def add_remove_control_iface( self, node: CoreNode, net_index: int = 0, @@ -1762,27 +1502,27 @@ def add_remove_control_interface( if not node: return # ctrl# already exists - if node.netif(control_net.CTRLIF_IDX_BASE + net_index): + if node.ifaces.get(control_net.CTRLIF_IDX_BASE + net_index): return try: ip4 = control_net.prefix[node.id] ip4_mask = control_net.prefix.prefixlen - interface = InterfaceData( + iface_data = InterfaceData( id=control_net.CTRLIF_IDX_BASE + net_index, name=f"ctrl{net_index}", mac=utils.random_mac(), ip4=ip4, ip4_mask=ip4_mask, ) - ifindex = node.newnetif(control_net, interface) - node.netif(ifindex).control = True + iface = node.new_iface(control_net, iface_data) + iface.control = True except ValueError: msg = f"Control interface not added to node {node.id}. " msg += f"Invalid control network prefix ({control_net.prefix}). " msg += "A longer prefix length may be required for this many nodes." logging.exception(msg) - def update_control_interface_hosts( + def update_control_iface_hosts( self, net_index: int = 0, remove: bool = False ) -> None: """ @@ -1808,11 +1548,10 @@ def update_control_interface_hosts( return entries = [] - for interface in control_net.netifs(): - name = interface.node.name - for address in interface.addrlist: - address = address.split("/")[0] - entries.append(f"{address} {name}") + for iface in control_net.get_ifaces(): + name = iface.node.name + for ip in iface.ips(): + entries.append(f"{ip.ip} {name}") logging.info("Adding %d /etc/hosts file entries.", len(entries)) utils.file_munge("/etc/hosts", header, "\n".join(entries) + "\n") @@ -1823,30 +1562,24 @@ def runtime(self) -> float: if not in runtime. """ if self.state == EventTypes.RUNTIME_STATE: - return time.monotonic() - self._state_time + return time.monotonic() - self.state_time else: return 0.0 def add_event( - self, - event_time: float, - node: CoreNode = None, - name: str = None, - data: str = None, + self, event_time: float, node_id: int = None, name: str = None, data: str = None ) -> None: """ Add an event to the event queue, with a start time relative to the start of the runtime state. :param event_time: event time - :param node: node to add event for + :param node_id: node to add event for :param name: name of event :param data: data for event :return: nothing """ - event_time = float(event_time) current_time = self.runtime() - if current_time > 0: if event_time <= current_time: logging.warning( @@ -1856,11 +1589,9 @@ def add_event( ) return event_time = event_time - current_time - self.event_loop.add_event( - event_time, self.run_event, node=node, name=name, data=data + event_time, self.run_event, node_id=node_id, name=name, data=data ) - if not name: name = "" logging.info( @@ -1870,8 +1601,6 @@ def add_event( data, ) - # TODO: if data is None, this blows up, but this ties into how event functions - # are ran, need to clean that up def run_event( self, node_id: int = None, name: str = None, data: str = None ) -> None: @@ -1883,10 +1612,12 @@ def run_event( :param data: event data :return: nothing """ + if data is None: + logging.warning("no data for event node(%s) name(%s)", node_id, name) + return now = self.runtime() if not name: name = "" - logging.info("running event %s at time %s cmd=%s", name, now, data) if not node_id: utils.mute_detach(data) diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index e22e852e8..9b22bcc73 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -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 diff --git a/daemon/core/executables.py b/daemon/core/executables.py new file mode 100644 index 000000000..6eb0214a1 --- /dev/null +++ b/daemon/core/executables.py @@ -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 diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index c795a46a1..176b31e36 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -3,52 +3,60 @@ 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 @@ def draw(self) -> None: 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 @@ def draw_canvas(self) -> None: 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) diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index 077f938d7..6bc213eb1 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -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 @@ "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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( ) -> 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 @@ def __init__( ) -> 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) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 2b565e7f6..fc0bd5209 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -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 @@ 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 @@ def client(self): ) 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 @@ def reset(self): 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 @@ def read_config(self): 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 @@ def handle_events(self, event: core_pb2.Event): 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 @@ def handle_events(self, event: core_pb2.Event): 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 @@ def handle_throughputs(self, event: core_pb2.ThroughputsEvent): 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): + def handle_cpu_event(self, event: CpuUsageEvent) -> None: + self.app.after(0, self.app.statusbar.set_cpu, event.usage) + + def handle_exception_event(self, event: ExceptionEvent) -> None: logging.info("exception event: %s", event) - self.app.statusbar.core_alarms.append(event) + self.app.statusbar.add_alert(event) - def join_session(self, session_id: int, query_location: bool = True): + 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 @@ def join_session(self, session_id: int, query_location: bool = True): 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 @@ def join_session(self, session_id: int, query_location: bool = True): 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 @@ def join_session(self, session_id: int, query_location: bool = True): # 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 @@ def join_session(self, session_id: int, query_location: bool = True): # 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 @@ def parse_metadata(self, config: Dict[str, str]): 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 @@ def create_new_session(self): 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 @@ def create_new_session(self): 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 @@ def delete_session(self, session_id: int = None): 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 @@ def setup(self): ) 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 @@ def edit_node(self, core_node: core_pb2.Node): 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 @@ def start_session(self) -> core_pb2.StartSessionResponse: 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 @@ def start_session(self) -> core_pb2.StartSessionResponse: 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 @@ def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse: 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 @@ def show_mobility_players(self): 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 @@ def set_metadata(self): 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 @@ def launch_terminal(self, node_id: int): 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 @@ def save_xml(self, file_path: str): 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 @@ def set_node_service( 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 @@ def get_node_service_file( 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 @@ def set_node_service_file( 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 @@ def send_data(self): 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 @@ def next_node_id(self) -> int: 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 @@ def create_node( ) return node - def deleted_graph_nodes(self, canvas_nodes: List[core_pb2.Node]): + def deleted_graph_nodes(self, canvas_nodes: List[Node]) -> None: """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces @@ -830,35 +931,35 @@ def deleted_graph_edges(self, edges: Iterable[CanvasEdge]) -> None: 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 @@ def create_link( 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 - - 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, + dst_iface = self.create_iface(canvas_dst_node) + self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token + + 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 @@ def create_link( 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 @@ def get_wlan_configs_proto(self) -> List[WlanConfig]: 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 @@ def get_mobility_configs_proto(self) -> List[MobilityConfig]: 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 @@ def get_service_file_configs_proto(self) -> List[ServiceFileConfig]: 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 @@ def get_config_service_configs_proto( 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 @@ def run(self, node_id: int) -> str: 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 @@ def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]: ) 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 @@ def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption 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: diff --git a/daemon/core/gui/data/xmls/sample1.xml b/daemon/core/gui/data/xmls/sample1.xml index afec88742..5055c2254 100644 --- a/daemon/core/gui/data/xmls/sample1.xml +++ b/daemon/core/gui/data/xmls/sample1.xml @@ -188,7 +188,7 @@ - + diff --git a/daemon/core/gui/dialogs/about.py b/daemon/core/gui/dialogs/about.py index 2e6491691..fa96e218a 100644 --- a/daemon/core/gui/dialogs/about.py +++ b/daemon/core/gui/dialogs/about.py @@ -35,11 +35,11 @@ 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) diff --git a/daemon/core/gui/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py index a0c3e68bd..8e0aa02e9 100644 --- a/daemon/core/gui/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -3,9 +3,9 @@ """ 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 @@ 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 @@ def draw(self): 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 @@ def draw(self): exception.date, level_name, alarm.session_id, - exception.node_id, + node_id, exception.source, ), tags=(level_name,), @@ -97,16 +98,18 @@ def draw(self): 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) diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index 6a63a1aee..38cecc830 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -7,38 +7,43 @@ 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 @@ def draw(self): 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 @@ def draw_size(self): 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("", 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("", self.size_scale_keyup) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -75,16 +82,20 @@ def draw_size(self): 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 @@ def draw_scale(self): 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("", 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 @@ def draw_reference_point(self): 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 @@ def draw_buttons(self): 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: diff --git a/daemon/core/gui/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py index 5e8460bec..8a1e71d87 100644 --- a/daemon/core/gui/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -4,10 +4,11 @@ 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 @@ 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 @@ def draw(self): 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 @@ def draw_image_selection(self): 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 @@ def draw_options(self): 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 @@ def draw_additional_options(self): ) 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 @@ def draw_buttons(self): 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 @@ def click_clear(self): 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 @@ def click_adjust_canvas(self): 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() diff --git a/daemon/core/gui/dialogs/colorpicker.py b/daemon/core/gui/dialogs/colorpicker.py index b1968cd43..908b8acb7 100644 --- a/daemon/core/gui/dialogs/colorpicker.py +++ b/daemon/core/gui/dialogs/colorpicker.py @@ -3,7 +3,7 @@ """ 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 @@ def __init__( 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 @@ def askcolor(self) -> str: 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 @@ def draw(self): 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("", lambda x: self.current_focus("rgb")) self.green_entry.bind("", lambda x: self.current_focus("rgb")) self.blue_entry.bind("", lambda x: self.current_focus("rgb")) @@ -146,7 +146,7 @@ def set_bindings(self): 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 @@ def get_hex(self) -> str: 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 @@ def update_color(self, arg1=None, arg2=None, arg3=None): 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 @@ def set_scale(self, red: int, green: int, blue: int): 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 """ diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 42041a8e5..c2d42ee43 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -4,10 +4,11 @@ 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 @@ 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 @@ def __init__( 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 @@ def load(self): 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 @@ def draw(self): 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 @@ def draw_tab_files(self): ) self.template_text.text.bind("", 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 @@ def draw_tab_config(self): 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 @@ def draw_tab_startstop(self): 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 @@ def draw_tab_validation(self): 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 @@ def draw_buttons(self): 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 @@ def click_apply(self): 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 @@ def update_template_file_data(self, event: tk.Event): 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 @@ def is_custom(self): 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 @@ def click_defaults(self): 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) diff --git a/daemon/core/gui/dialogs/copyserviceconfig.py b/daemon/core/gui/dialogs/copyserviceconfig.py index ff75a59a8..2a01249da 100644 --- a/daemon/core/gui/dialogs/copyserviceconfig.py +++ b/daemon/core/gui/dialogs/copyserviceconfig.py @@ -4,81 +4,58 @@ 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 @@ def draw(self): 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) - - 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, - ) + button.grid(row=0, column=2, sticky="ew") + + 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") diff --git a/daemon/core/gui/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py index 56012780b..df3bafa77 100644 --- a/daemon/core/gui/dialogs/customnodes.py +++ b/daemon/core/gui/dialogs/customnodes.py @@ -2,7 +2,9 @@ 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 @@ 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 @@ def draw(self): # trigger group change self.groups.listbox.event_generate("<>") - 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 @@ def handle_group_change(self, event: tk.Event): 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 @@ def service_clicked(self, name: str, var: tk.BooleanVar): 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 @@ def draw_node_config(self): 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 @@ def draw_node_buttons(self): ) 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 @@ def draw_buttons(self): 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 @@ def click_icon(self): 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 @@ def click_create(self): 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 @@ def click_edit(self): 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 @@ def click_delete(self): self.nodes_list.listbox.selection_clear(0, tk.END) self.nodes_list.listbox.event_generate("<>") - 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] diff --git a/daemon/core/gui/dialogs/dialog.py b/daemon/core/gui/dialogs/dialog.py index f3742c509..962170e7f 100644 --- a/daemon/core/gui/dialogs/dialog.py +++ b/daemon/core/gui/dialogs/dialog.py @@ -16,23 +16,23 @@ def __init__( 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 @@ def show(self): 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) diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 000ebb05c..bb3347572 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -4,10 +4,12 @@ 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 @@ 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 @@ def __init__( 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 @@ def draw(self): 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 @@ def draw_emane_configuration(self): 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 @@ def draw_emane_models(self): 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("<>", 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 @@ def draw_emane_buttons(self): 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 @@ def click_model_config(self): 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() diff --git a/daemon/core/gui/dialogs/emaneinstall.py b/daemon/core/gui/dialogs/emaneinstall.py index 93cf2ac48..3ad9396b2 100644 --- a/daemon/core/gui/dialogs/emaneinstall.py +++ b/daemon/core/gui/dialogs/emaneinstall.py @@ -10,7 +10,7 @@ def __init__(self, app) -> None: 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 @@ def draw(self): 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") diff --git a/daemon/core/gui/dialogs/error.py b/daemon/core/gui/dialogs/error.py index 5ff1dbc58..9d215e82b 100644 --- a/daemon/core/gui/dialogs/error.py +++ b/daemon/core/gui/dialogs/error.py @@ -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 @@ 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) diff --git a/daemon/core/gui/dialogs/executepython.py b/daemon/core/gui/dialogs/executepython.py index dd60c778e..a4516df15 100644 --- a/daemon/core/gui/dialogs/executepython.py +++ b/daemon/core/gui/dialogs/executepython.py @@ -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 @@ 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 @@ def draw(self): 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 @@ def select_file(self): 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) diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 25da4b19b..328f673e3 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -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 @@ 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("", self.find_node) diff --git a/daemon/core/gui/dialogs/hooks.py b/daemon/core/gui/dialogs/hooks.py index 5895a2e1d..08d666ba0 100644 --- a/daemon/core/gui/dialogs/hooks.py +++ b/daemon/core/gui/dialogs/hooks.py @@ -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 @@ 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 @@ def draw(self): 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 @@ def set(self, hook: core_pb2.Hook): 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 @@ def save(self): 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 @@ def draw(self): 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 @@ def click_create(self): 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) diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py index 62f5d0ba7..351bfffcd 100644 --- a/daemon/core/gui/dialogs/ipdialog.py +++ b/daemon/core/gui/dialogs/ipdialog.py @@ -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 @@ 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 @@ def click_save(self) -> None: 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() diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 92361ed4a..28798ec10 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -3,7 +3,7 @@ """ 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 @@ 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 @@ def get_frame(self) -> ttk.Frame: 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 @@ def click_apply(self): 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 @@ def click_apply(self): 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 @@ def click_apply(self): 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 @@ def change_symmetry(self): 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 @@ def load_link_config(self): 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)) diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py index caca9fd08..4d89439b5 100644 --- a/daemon/core/gui/dialogs/macdialog.py +++ b/daemon/core/gui/dialogs/macdialog.py @@ -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 @@ def click_save(self) -> None: 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() diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index dced5e444..daaf9ea52 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -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 @@ 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 @@ def draw(self): 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 @@ def draw_apply_buttons(self): 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() diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index b4801bcf2..e6ef62ea1 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -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 @@ 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 show(self): + 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) -> None: if self.dialog: self.dialog.destroy() self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config) @@ -37,44 +44,49 @@ def show(self): 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 @@ def draw(self): 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 @@ def click_play(self): 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 @@ def click_pause(self): 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: diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 0d46ae063..9e9582839 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -2,10 +2,12 @@ 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 @@ def __init__( 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 @@ def draw(self): 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("<>", self.interface_select) + ifaces.listbox.bind("<>", 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 @@ def draw_interfaces(self): ) 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 @@ def draw_interfaces(self): 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 @@ def draw_interfaces(self): 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 @@ def draw_buttons(self): 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 @@ def click_apply(self): 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) diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index 5f77ece34..b9a9a1f58 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -4,7 +4,7 @@ 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 @@ 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 @@ def draw(self): button.grid(row=0, column=3, sticky="ew") # trigger group change - self.groups.listbox.event_generate("<>") + 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 @@ def handle_group_change(self, event: tk.Event = None): 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 @@ def service_clicked(self, name: str, var: tk.IntVar): 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 @@ def click_configure(self): 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]) diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 13490d8cd..6fcc29128 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -3,7 +3,7 @@ """ 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 @@ 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 @@ def draw(self): button.grid(row=0, column=3, sticky="ew") # trigger group change - self.groups.listbox.event_generate("<>") + 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 @@ def handle_group_change(self, event: tk.Event = None): 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 @@ def service_clicked(self, name: str, var: tk.IntVar): 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 @@ def click_configure(self): "Service Configuration", "Select a service to configure", parent=self ) - def click_save(self): + def click_save(self) -> None: core_node = self.canvas_node.core_node core_node.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]) diff --git a/daemon/core/gui/dialogs/observers.py b/daemon/core/gui/dialogs/observers.py index d1812b644..286fc2c9f 100644 --- a/daemon/core/gui/dialogs/observers.py +++ b/daemon/core/gui/dialogs/observers.py @@ -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 @@ 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 @@ def draw(self): 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 @@ def draw_listbox(self): 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 @@ def draw_form_fields(self): 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 @@ def draw_config_buttons(self): ) 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 @@ def draw_apply_buttons(self): 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 @@ def click_create(self): 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 @@ def click_save(self): 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 @@ def click_delete(self): 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] diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 11d1ba95a..839ebd3b5 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -12,27 +12,27 @@ 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 @@ def draw_preferences(self): 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 @@ def draw_buttons(self): 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 @@ def click_save(self): 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 @@ def scale_adjust(self): 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: diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index 98be730f6..c66fea8f7 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -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 @@ 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() diff --git a/daemon/core/gui/dialogs/servers.py b/daemon/core/gui/dialogs/servers.py index 7ca96e9fc..45121a201 100644 --- a/daemon/core/gui/dialogs/servers.py +++ b/daemon/core/gui/dialogs/servers.py @@ -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 @@ 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 @@ def draw(self): 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 @@ def draw_servers(self): 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 @@ def draw_server_configuration(self): 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 @@ def draw_servers_buttons(self): ) 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 @@ def click_save_configuration(self): 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 @@ def click_create(self): 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 @@ def click_save(self): 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 @@ def click_delete(self): 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] diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index efeefa090..4e615db04 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -2,11 +2,12 @@ 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 @@ 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 @@ def __init__( 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 @@ def load(self): 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 @@ def draw(self): 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 @@ def draw_tab_files(self): "", 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 @@ def draw_tab_directories(self): 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 @@ def draw_tab_startstop(self): 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 @@ def draw_tab_configuration(self): 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 @@ def draw_buttons(self): 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 @@ def add_filename(self): 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 @@ def delete_filename(self): 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 @@ def add_command(cls, event: tk.Event): 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 @@ def update_entry(cls, event: tk.Event): 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 @@ def delete_command(cls, event: tk.Event): 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 @@ def click_apply(self): 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 @@ def update_temp_service_file_data(self, event: tk.Event): 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 @@ def is_custom_command(self): 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 @@ def click_defaults(self): 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 @@ def remove_directory(self): 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 """ diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index d31a5fb58..fd021fee1 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -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 @@ 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 @@ def get_config(self): 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 @@ def draw(self): 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 diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 9aa71a131..a7d702ebc 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -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 @@ 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) diff --git a/daemon/core/gui/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py index 4c84991bc..2ca067721 100644 --- a/daemon/core/gui/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -3,7 +3,7 @@ """ 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 @@ 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 @@ def draw(self): 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 @@ def draw_label_options(self): 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 @@ def draw_shape_options(self): ) 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 @@ def draw_buttons(self): 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 @@ def make_font(self) -> List[Union[int, str]]: 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 @@ def save_text(self): 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 @@ def save_shape(self): 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 @@ def add_text(self): ) self.save_text() - def add_shape(self): + def add_shape(self) -> None: self.canvas.itemconfig( self.shape.id, fill=self.fill_color, diff --git a/daemon/core/gui/dialogs/throughput.py b/daemon/core/gui/dialogs/throughput.py index 5210fe597..5b3cc9b34 100644 --- a/daemon/core/gui/dialogs/throughput.py +++ b/daemon/core/gui/dialogs/throughput.py @@ -3,10 +3,11 @@ """ 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 @@ 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 @@ def draw(self): 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 diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index b0435a2fa..326b3195d 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -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 @@ 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 @@ def init_draw_range(self): ) 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 @@ def draw(self): self.draw_apply_buttons() self.top.bind("", self.remove_ranges) - def draw_apply_buttons(self): + def draw_apply_buttons(self) -> None: """ create node configuration options """ @@ -75,7 +79,7 @@ def draw_apply_buttons(self): 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 @@ def click_apply(self): 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() diff --git a/daemon/core/gui/frames/__init__.py b/daemon/core/gui/frames/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/daemon/core/gui/frames/base.py b/daemon/core/gui/frames/base.py new file mode 100644 index 000000000..8db952f16 --- /dev/null +++ b/daemon/core/gui/frames/base.py @@ -0,0 +1,36 @@ +import tkinter as tk +from tkinter import ttk +from typing import TYPE_CHECKING + +from core.gui.themes import FRAME_PAD, PADX, PADY + +if TYPE_CHECKING: + from core.gui.app import Application + + +class InfoFrameBase(ttk.Frame): + def __init__(self, master: tk.BaseWidget, app: "Application") -> None: + super().__init__(master, padding=FRAME_PAD) + self.app: "Application" = app + + def draw(self) -> None: + raise NotImplementedError + + +class DetailsFrame(ttk.Frame): + def __init__(self, master: tk.BaseWidget) -> None: + super().__init__(master) + self.columnconfigure(1, weight=1) + self.row = 0 + + def add_detail(self, label: str, value: str) -> None: + label = ttk.Label(self, text=label, anchor=tk.W) + label.grid(row=self.row, sticky=tk.EW, column=0, padx=PADX) + label = ttk.Label(self, text=value, anchor=tk.W, state=tk.DISABLED) + label.grid(row=self.row, sticky=tk.EW, column=1) + self.row += 1 + + def add_separator(self) -> None: + separator = ttk.Separator(self) + separator.grid(row=self.row, sticky=tk.EW, columnspan=2, pady=PADY) + self.row += 1 diff --git a/daemon/core/gui/frames/default.py b/daemon/core/gui/frames/default.py new file mode 100644 index 000000000..e84edb87a --- /dev/null +++ b/daemon/core/gui/frames/default.py @@ -0,0 +1,19 @@ +import tkinter as tk +from tkinter import ttk +from typing import TYPE_CHECKING + +from core.gui.frames.base import InfoFrameBase + +if TYPE_CHECKING: + from core.gui.app import Application + + +class DefaultInfoFrame(InfoFrameBase): + def __init__(self, master: tk.BaseWidget, app: "Application") -> None: + super().__init__(master, app) + + def draw(self) -> None: + label = ttk.Label(self, text="Click a Node/Link", anchor=tk.CENTER) + label.grid(sticky=tk.EW) + label = ttk.Label(self, text="to see details", anchor=tk.CENTER) + label.grid(sticky=tk.EW) diff --git a/daemon/core/gui/frames/link.py b/daemon/core/gui/frames/link.py new file mode 100644 index 000000000..57b1bf66c --- /dev/null +++ b/daemon/core/gui/frames/link.py @@ -0,0 +1,113 @@ +import tkinter as tk +from typing import TYPE_CHECKING, Optional + +from core.api.grpc.core_pb2 import Interface +from core.gui.frames.base import DetailsFrame, InfoFrameBase +from core.gui.utils import bandwidth_text + +if TYPE_CHECKING: + from core.gui.app import Application + from core.gui.graph.edges import CanvasEdge + from core.gui.graph.node import CanvasNode + from core.gui.graph.edges import CanvasWirelessEdge + + +def get_iface(canvas_node: "CanvasNode", net_id: int) -> Optional[Interface]: + iface = None + for edge in canvas_node.edges: + link = edge.link + if link.node1_id == net_id: + iface = link.iface2 + elif link.node2_id == net_id: + iface = link.iface1 + return iface + + +class EdgeInfoFrame(InfoFrameBase): + def __init__( + self, master: tk.BaseWidget, app: "Application", edge: "CanvasEdge" + ) -> None: + super().__init__(master, app) + self.edge: "CanvasEdge" = edge + + def draw(self) -> None: + self.columnconfigure(0, weight=1) + link = self.edge.link + options = link.options + src_canvas_node = self.app.core.canvas_nodes[link.node1_id] + src_node = src_canvas_node.core_node + dst_canvas_node = self.app.core.canvas_nodes[link.node2_id] + dst_node = dst_canvas_node.core_node + + frame = DetailsFrame(self) + frame.grid(sticky="ew") + frame.add_detail("Source", src_node.name) + iface1 = link.iface1 + if iface1: + mac = iface1.mac if iface1.mac else "auto" + frame.add_detail("MAC", mac) + ip4 = f"{iface1.ip4}/{iface1.ip4_mask}" if iface1.ip4 else "" + frame.add_detail("IP4", ip4) + ip6 = f"{iface1.ip6}/{iface1.ip6_mask}" if iface1.ip6 else "" + frame.add_detail("IP6", ip6) + + frame.add_separator() + frame.add_detail("Destination", dst_node.name) + iface2 = link.iface2 + if iface2: + mac = iface2.mac if iface2.mac else "auto" + frame.add_detail("MAC", mac) + ip4 = f"{iface2.ip4}/{iface2.ip4_mask}" if iface2.ip4 else "" + frame.add_detail("IP4", ip4) + ip6 = f"{iface2.ip6}/{iface2.ip6_mask}" if iface2.ip6 else "" + frame.add_detail("IP6", ip6) + + if link.HasField("options"): + frame.add_separator() + bandwidth = bandwidth_text(options.bandwidth) + frame.add_detail("Bandwidth", bandwidth) + frame.add_detail("Delay", f"{options.delay} us") + frame.add_detail("Jitter", f"\u00B1{options.jitter} us") + frame.add_detail("Loss", f"{options.loss}%") + frame.add_detail("Duplicate", f"{options.dup}%") + + +class WirelessEdgeInfoFrame(InfoFrameBase): + def __init__( + self, master: tk.BaseWidget, app: "Application", edge: "CanvasWirelessEdge" + ) -> None: + super().__init__(master, app) + self.edge: "CanvasWirelessEdge" = edge + + def draw(self) -> None: + link = self.edge.link + src_canvas_node = self.app.core.canvas_nodes[link.node1_id] + src_node = src_canvas_node.core_node + dst_canvas_node = self.app.core.canvas_nodes[link.node2_id] + dst_node = dst_canvas_node.core_node + + # find interface for each node connected to network + net_id = link.network_id + iface1 = get_iface(src_canvas_node, net_id) + iface2 = get_iface(dst_canvas_node, net_id) + + frame = DetailsFrame(self) + frame.grid(sticky="ew") + frame.add_detail("Source", src_node.name) + if iface1: + mac = iface1.mac if iface1.mac else "auto" + frame.add_detail("MAC", mac) + ip4 = f"{iface1.ip4}/{iface1.ip4_mask}" if iface1.ip4 else "" + frame.add_detail("IP4", ip4) + ip6 = f"{iface1.ip6}/{iface1.ip6_mask}" if iface1.ip6 else "" + frame.add_detail("IP6", ip6) + + frame.add_separator() + frame.add_detail("Destination", dst_node.name) + if iface2: + mac = iface2.mac if iface2.mac else "auto" + frame.add_detail("MAC", mac) + ip4 = f"{iface2.ip4}/{iface2.ip4_mask}" if iface2.ip4 else "" + frame.add_detail("IP4", ip4) + ip6 = f"{iface2.ip6}/{iface2.ip6_mask}" if iface2.ip6 else "" + frame.add_detail("IP6", ip6) diff --git a/daemon/core/gui/frames/node.py b/daemon/core/gui/frames/node.py new file mode 100644 index 000000000..7480e056d --- /dev/null +++ b/daemon/core/gui/frames/node.py @@ -0,0 +1,39 @@ +from typing import TYPE_CHECKING + +from core.api.grpc.core_pb2 import NodeType +from core.gui.frames.base import DetailsFrame, InfoFrameBase +from core.gui.nodeutils import NodeUtils + +if TYPE_CHECKING: + from core.gui.app import Application + from core.gui.graph.node import CanvasNode + + +class NodeInfoFrame(InfoFrameBase): + def __init__(self, master, app: "Application", canvas_node: "CanvasNode") -> None: + super().__init__(master, app) + self.canvas_node: "CanvasNode" = canvas_node + + def draw(self) -> None: + self.columnconfigure(0, weight=1) + node = self.canvas_node.core_node + frame = DetailsFrame(self) + frame.grid(sticky="ew") + frame.add_detail("ID", node.id) + frame.add_detail("Name", node.name) + if NodeUtils.is_model_node(node.type): + frame.add_detail("Type", node.model) + if NodeUtils.is_container_node(node.type): + for index, service in enumerate(sorted(node.services)): + if index == 0: + frame.add_detail("Services", service) + else: + frame.add_detail("", service) + if node.type == NodeType.EMANE: + emane = node.emane.split("_")[1:] + frame.add_detail("EMANE", emane) + if NodeUtils.is_image_node(node.type): + frame.add_detail("Image", node.image) + if NodeUtils.is_container_node(node.type): + server = node.server if node.server else "localhost" + frame.add_detail("Server", server) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 00268c885..d90859103 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -1,23 +1,26 @@ import logging import math import tkinter as tk -from typing import TYPE_CHECKING, Any, Tuple +from typing import TYPE_CHECKING, Optional, Tuple from core.api.grpc import core_pb2 +from core.api.grpc.core_pb2 import Interface, Link from core.gui import themes from core.gui.dialogs.linkconfig import LinkConfigurationDialog +from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.graph import tags from core.gui.nodeutils import NodeUtils +from core.gui.utils import bandwidth_text if TYPE_CHECKING: from core.gui.graph.graph import CanvasGraph -TEXT_DISTANCE = 0.30 -EDGE_WIDTH = 3 -EDGE_COLOR = "#ff0000" -WIRELESS_WIDTH = 1.5 -WIRELESS_COLOR = "#009933" -ARC_DISTANCE = 50 +TEXT_DISTANCE: float = 0.30 +EDGE_WIDTH: int = 3 +EDGE_COLOR: str = "#ff0000" +WIRELESS_WIDTH: float = 3 +WIRELESS_COLOR: str = "#009933" +ARC_DISTANCE: int = 50 def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]: @@ -57,20 +60,20 @@ def arc_edges(edges) -> None: class Edge: - tag = tags.EDGE + tag: str = tags.EDGE def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None: self.canvas = canvas - self.id = None - self.src = src - self.dst = dst - self.arc = 0 - self.token = None - self.src_label = None - self.middle_label = None - self.dst_label = None - self.color = EDGE_COLOR - self.width = EDGE_WIDTH + self.id: Optional[int] = None + self.src: int = src + self.dst: int = dst + self.arc: int = 0 + self.token: Optional[Tuple[int, ...]] = None + self.src_label: Optional[int] = None + self.middle_label: Optional[int] = None + self.dst_label: Optional[int] = None + self.color: str = EDGE_COLOR + self.width: int = EDGE_WIDTH @classmethod def create_token(cls, src: int, dst: int) -> Tuple[int, ...]: @@ -120,7 +123,7 @@ def draw(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> No fill=self.color, ) - def redraw(self): + def redraw(self) -> None: self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color) src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) src_pos = src_x, src_y @@ -139,11 +142,16 @@ def middle_label_text(self, text: str) -> None: font=self.canvas.app.edge_font, text=text, tags=tags.LINK_LABEL, + justify=tk.CENTER, state=self.canvas.show_link_labels.state(), ) else: self.canvas.itemconfig(self.middle_label, text=text) + def clear_middle_label(self) -> None: + self.canvas.delete(self.middle_label) + self.middle_label = None + def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]: src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id) v1 = dst_x - src_x @@ -215,11 +223,10 @@ def delete(self) -> None: logging.debug("deleting canvas edge, id: %s", self.id) self.canvas.delete(self.id) self.canvas.delete(self.src_label) - self.canvas.delete(self.middle_label) self.canvas.delete(self.dst_label) + self.clear_middle_label() self.id = None self.src_label = None - self.middle_label = None self.dst_label = None @@ -233,14 +240,28 @@ def __init__( dst: int, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], - token: Tuple[Any, ...], + token: Tuple[int, ...], + link: Link, ) -> None: logging.debug("drawing wireless link from node %s to node %s", src, dst) super().__init__(canvas, src, dst) - self.token = token - self.width = WIRELESS_WIDTH - self.color = WIRELESS_COLOR + self.link: Link = link + self.token: Tuple[int, ...] = token + self.width: float = WIRELESS_WIDTH + color = link.color if link.color else WIRELESS_COLOR + self.color: str = color self.draw(src_pos, dst_pos) + if link.label: + self.middle_label_text(link.label) + self.set_binding() + + def set_binding(self) -> None: + self.canvas.tag_bind(self.id, "", self.show_info) + + def show_info(self, _event: tk.Event) -> None: + self.canvas.app.display_info( + WirelessEdgeInfoFrame, app=self.canvas.app, edge=self + ) class CanvasEdge(Edge): @@ -259,55 +280,57 @@ def __init__( Create an instance of canvas edge object """ super().__init__(canvas, src) - self.src_interface = None - self.dst_interface = None - self.text_src = None - self.text_dst = None - self.link = None - self.asymmetric_link = None - self.throughput = None + self.src_iface: Optional[Interface] = None + self.dst_iface: Optional[Interface] = None + self.text_src: Optional[int] = None + self.text_dst: Optional[int] = None + self.link: Optional[Link] = None + self.asymmetric_link: Optional[Link] = None + self.throughput: Optional[float] = None self.draw(src_pos, dst_pos) self.set_binding() - self.context = tk.Menu(self.canvas) + self.context: tk.Menu = tk.Menu(self.canvas) self.create_context() - def create_context(self): + def create_context(self) -> None: themes.style_menu(self.context) self.context.add_command(label="Configure", command=self.click_configure) self.context.add_command(label="Delete", command=self.click_delete) def set_binding(self) -> None: self.canvas.tag_bind(self.id, "", self.show_context) + self.canvas.tag_bind(self.id, "", self.show_info) - def set_link(self, link) -> None: + def set_link(self, link: Link) -> None: self.link = link self.draw_labels() - def interface_label(self, interface: core_pb2.Interface) -> str: + def iface_label(self, iface: core_pb2.Interface) -> str: label = "" - if interface.name and self.canvas.show_interface_names.get(): - label = f"{interface.name}" - if interface.ip4 and self.canvas.show_ip4s.get(): + if iface.name and self.canvas.show_iface_names.get(): + label = f"{iface.name}" + if iface.ip4 and self.canvas.show_ip4s.get(): label = f"{label}\n" if label else "" - label += f"{interface.ip4}/{interface.ip4mask}" - if interface.ip6 and self.canvas.show_ip6s.get(): + label += f"{iface.ip4}/{iface.ip4_mask}" + if iface.ip6 and self.canvas.show_ip6s.get(): label = f"{label}\n" if label else "" - label += f"{interface.ip6}/{interface.ip6mask}" + label += f"{iface.ip6}/{iface.ip6_mask}" return label def create_node_labels(self) -> Tuple[str, str]: - label_one = None - if self.link.HasField("interface_one"): - label_one = self.interface_label(self.link.interface_one) - label_two = None - if self.link.HasField("interface_two"): - label_two = self.interface_label(self.link.interface_two) - return label_one, label_two + label1 = None + if self.link.HasField("iface1"): + label1 = self.iface_label(self.link.iface1) + label2 = None + if self.link.HasField("iface2"): + label2 = self.iface_label(self.link.iface2) + return label1, label2 def draw_labels(self) -> None: src_text, dst_text = self.create_node_labels() self.src_label_text(src_text) self.dst_label_text(dst_text) + self.draw_link_options() def redraw(self) -> None: super().redraw() @@ -378,14 +401,38 @@ def reset(self) -> None: self.middle_label = None self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width()) + def show_info(self, _event: tk.Event) -> None: + self.canvas.app.display_info(EdgeInfoFrame, app=self.canvas.app, edge=self) + def show_context(self, event: tk.Event) -> None: state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL self.context.entryconfigure(1, state=state) self.context.tk_popup(event.x_root, event.y_root) - def click_delete(self): + def click_delete(self) -> None: self.canvas.delete_edge(self) def click_configure(self) -> None: dialog = LinkConfigurationDialog(self.canvas.app, self) dialog.show() + + def draw_link_options(self): + options = self.link.options + lines = [] + bandwidth = options.bandwidth + if bandwidth > 0: + lines.append(bandwidth_text(bandwidth)) + delay = options.delay + jitter = options.jitter + if delay > 0 and jitter > 0: + lines.append(f"{delay} us (\u00B1{jitter} us)") + elif jitter > 0: + lines.append(f"0 us (\u00B1{jitter} us)") + loss = options.loss + if loss > 0: + lines.append(f"loss={loss}%") + dup = options.dup + if dup > 0: + lines.append(f"dup={dup}%") + label = "\n".join(lines) + self.middle_label_text(label) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 3d6fd369a..56a31c3f6 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -2,11 +2,19 @@ import tkinter as tk from copy import deepcopy from tkinter import BooleanVar -from typing import TYPE_CHECKING, Tuple - -from PIL import Image, ImageTk - -from core.api.grpc import core_pb2 +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple + +from PIL import Image +from PIL.ImageTk import PhotoImage + +from core.api.grpc.core_pb2 import ( + Interface, + Link, + LinkType, + Node, + Session, + ThroughputsEvent, +) from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags from core.gui.graph.edges import ( @@ -21,7 +29,7 @@ from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker from core.gui.images import ImageEnum, TypeToImage -from core.gui.nodeutils import NodeUtils +from core.gui.nodeutils import NodeDraw, NodeUtils if TYPE_CHECKING: from core.gui.app import Application @@ -48,58 +56,59 @@ def click_handler(self) -> None: class CanvasGraph(tk.Canvas): - def __init__(self, master: tk.Widget, app: "Application", core: "CoreClient"): + def __init__( + self, master: tk.BaseWidget, app: "Application", core: "CoreClient" + ) -> None: super().__init__(master, highlightthickness=0, background="#cccccc") - self.app = app - self.core = core - self.mode = GraphMode.SELECT - self.annotation_type = None - self.selection = {} - self.select_box = None - self.selected = None - self.node_draw = None - self.nodes = {} - self.edges = {} - self.shapes = {} - self.wireless_edges = {} + self.app: "Application" = app + self.core: "CoreClient" = core + self.mode: GraphMode = GraphMode.SELECT + self.annotation_type: Optional[ShapeType] = None + self.selection: Dict[int, int] = {} + self.select_box: Optional[Shape] = None + self.selected: Optional[int] = None + self.node_draw: Optional[NodeDraw] = None + self.nodes: Dict[int, CanvasNode] = {} + self.edges: Dict[int, CanvasEdge] = {} + self.shapes: Dict[int, Shape] = {} + self.wireless_edges: Dict[Tuple[int, ...], CanvasWirelessEdge] = {} # map wireless/EMANE node to the set of MDRs connected to that node - self.wireless_network = {} + self.wireless_network: Dict[int, Set[int]] = {} - self.drawing_edge = None - self.rect = None - self.shape_drawing = False + self.drawing_edge: Optional[CanvasEdge] = None + self.rect: Optional[int] = None + self.shape_drawing: bool = False width = self.app.guiconfig.preferences.width height = self.app.guiconfig.preferences.height - self.default_dimensions = (width, height) - self.current_dimensions = self.default_dimensions - self.ratio = 1.0 - self.offset = (0, 0) - self.cursor = (0, 0) - self.marker_tool = None - self.to_copy = [] + self.default_dimensions: Tuple[int, int] = (width, height) + self.current_dimensions: Tuple[int, int] = self.default_dimensions + self.ratio: float = 1.0 + self.offset: Tuple[int, int] = (0, 0) + self.cursor: Tuple[int, int] = (0, 0) + self.to_copy: List[CanvasNode] = [] # background related - self.wallpaper_id = None - self.wallpaper = None - self.wallpaper_drawn = None - self.wallpaper_file = "" - self.scale_option = tk.IntVar(value=1) - self.adjust_to_dim = tk.BooleanVar(value=False) + self.wallpaper_id: Optional[int] = None + self.wallpaper: Optional[Image.Image] = None + self.wallpaper_drawn: Optional[PhotoImage] = None + self.wallpaper_file: str = "" + self.scale_option: tk.IntVar = tk.IntVar(value=1) + self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False) # throughput related - self.throughput_threshold = 250.0 - self.throughput_width = 10 - self.throughput_color = "#FF0000" + self.throughput_threshold: float = 250.0 + self.throughput_width: int = 10 + self.throughput_color: str = "#FF0000" # drawing related - self.show_node_labels = ShowVar(self, tags.NODE_LABEL, value=True) - self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True) - self.show_grid = ShowVar(self, tags.GRIDLINE, value=True) - self.show_annotations = ShowVar(self, tags.ANNOTATION, value=True) - self.show_interface_names = BooleanVar(value=False) - self.show_ip4s = BooleanVar(value=True) - self.show_ip6s = BooleanVar(value=True) + self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True) + self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True) + self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True) + self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True) + self.show_iface_names: BooleanVar = BooleanVar(value=False) + self.show_ip4s: BooleanVar = BooleanVar(value=True) + self.show_ip6s: BooleanVar = BooleanVar(value=True) # bindings self.setup_bindings() @@ -108,7 +117,7 @@ def __init__(self, master: tk.Widget, app: "Application", core: "CoreClient"): self.draw_canvas() self.draw_grid() - def draw_canvas(self, dimensions: Tuple[int, int] = None): + def draw_canvas(self, dimensions: Tuple[int, int] = None) -> None: if self.rect is not None: self.delete(self.rect) if not dimensions: @@ -125,7 +134,7 @@ def draw_canvas(self, dimensions: Tuple[int, int] = None): ) self.configure(scrollregion=self.bbox(tk.ALL)) - def reset_and_redraw(self, session: core_pb2.Session): + def reset_and_redraw(self, session: Session) -> None: """ Reset the private variables CanvasGraph object, redraw nodes given the new grpc client. @@ -136,7 +145,7 @@ def reset_and_redraw(self, session: core_pb2.Session): self.show_link_labels.set(True) self.show_grid.set(True) self.show_annotations.set(True) - self.show_interface_names.set(False) + self.show_iface_names.set(False) self.show_ip4s.set(True) self.show_ip6s.set(True) @@ -157,7 +166,7 @@ def reset_and_redraw(self, session: core_pb2.Session): self.drawing_edge = None self.draw_session(session) - def setup_bindings(self): + def setup_bindings(self) -> None: """ Bind any mouse events or hot keys to the matching action """ @@ -173,43 +182,43 @@ def setup_bindings(self): self.bind("", lambda e: self.scan_mark(e.x, e.y)) self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) - def get_actual_coords(self, x: float, y: float) -> [float, float]: + def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]: actual_x = (x - self.offset[0]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio return actual_x, actual_y - def get_scaled_coords(self, x: float, y: float) -> [float, float]: + def get_scaled_coords(self, x: float, y: float) -> Tuple[float, float]: scaled_x = (x * self.ratio) + self.offset[0] scaled_y = (y * self.ratio) + self.offset[1] return scaled_x, scaled_y - def inside_canvas(self, x: float, y: float) -> [bool, bool]: + def inside_canvas(self, x: float, y: float) -> Tuple[bool, bool]: x1, y1, x2, y2 = self.bbox(self.rect) valid_x = x1 <= x <= x2 valid_y = y1 <= y <= y2 return valid_x and valid_y - def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> [bool, bool]: + def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> Tuple[bool, bool]: valid_topleft = self.inside_canvas(x1, y1) valid_bottomright = self.inside_canvas(x2, y2) return valid_topleft and valid_bottomright - def set_throughputs(self, throughputs_event: core_pb2.ThroughputsEvent): - for interface_throughput in throughputs_event.interface_throughputs: - node_id = interface_throughput.node_id - interface_id = interface_throughput.interface_id - throughput = interface_throughput.throughput - interface_to_edge_id = (node_id, interface_id) - token = self.core.interface_to_edge.get(interface_to_edge_id) + def set_throughputs(self, throughputs_event: ThroughputsEvent) -> None: + for iface_throughput in throughputs_event.iface_throughputs: + node_id = iface_throughput.node_id + iface_id = iface_throughput.iface_id + throughput = iface_throughput.throughput + iface_to_edge_id = (node_id, iface_id) + token = self.core.iface_to_edge.get(iface_to_edge_id) if not token: continue edge = self.edges.get(token) if edge: edge.set_throughput(throughput) else: - del self.core.interface_to_edge[interface_to_edge_id] + del self.core.iface_to_edge[iface_to_edge_id] - def draw_grid(self): + def draw_grid(self) -> None: """ Create grid. """ @@ -223,9 +232,51 @@ def draw_grid(self): self.tag_lower(tags.GRIDLINE) self.tag_lower(self.rect) - def add_wireless_edge( - self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link - ) -> None: + def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: + token = create_edge_token(src.id, dst.id) + if token in self.edges and link.options.unidirectional: + edge = self.edges[token] + edge.asymmetric_link = link + elif token not in self.edges: + node1 = src.core_node + node2 = dst.core_node + src_pos = (node1.position.x, node1.position.y) + dst_pos = (node2.position.x, node2.position.y) + edge = CanvasEdge(self, src.id, src_pos, dst_pos) + edge.token = token + edge.dst = dst.id + edge.set_link(link) + edge.check_wireless() + src.edges.add(edge) + dst.edges.add(edge) + self.edges[edge.token] = edge + self.core.links[edge.token] = edge + if link.HasField("iface1"): + iface1 = link.iface1 + self.core.iface_to_edge[(node1.id, iface1.id)] = token + src.ifaces[iface1.id] = iface1 + edge.src_iface = iface1 + if link.HasField("iface2"): + iface2 = link.iface2 + self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token + dst.ifaces[iface2.id] = iface2 + edge.dst_iface = iface2 + + def delete_wired_edge(self, src: CanvasNode, dst: CanvasNode) -> None: + token = create_edge_token(src.id, dst.id) + edge = self.edges.get(token) + if not edge: + return + self.delete_edge(edge) + + def update_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: + token = create_edge_token(src.id, dst.id) + edge = self.edges.get(token) + if not edge: + return + edge.link.options.CopyFrom(link.options) + + def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: network_id = link.network_id if link.network_id else None token = create_edge_token(src.id, dst.id, network_id) if token in self.wireless_edges: @@ -233,11 +284,7 @@ def add_wireless_edge( return src_pos = self.coords(src.id) dst_pos = self.coords(dst.id) - edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token) - if link.label: - edge.middle_label_text(link.label) - if link.color: - edge.color = link.color + edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token, link) self.wireless_edges[token] = edge src.wireless_edges.add(edge) dst.wireless_edges.add(edge) @@ -248,7 +295,7 @@ def add_wireless_edge( arc_edges(common_edges) def delete_wireless_edge( - self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link + self, src: CanvasNode, dst: CanvasNode, link: Link ) -> None: network_id = link.network_id if link.network_id else None token = create_edge_token(src.id, dst.id, network_id) @@ -263,7 +310,7 @@ def delete_wireless_edge( arc_edges(common_edges) def update_wireless_edge( - self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link + self, src: CanvasNode, dst: CanvasNode, link: Link ) -> None: if not link.label: return @@ -275,73 +322,43 @@ def update_wireless_edge( edge = self.wireless_edges[token] edge.middle_label_text(link.label) - def draw_session(self, session: core_pb2.Session): + def add_core_node(self, core_node: Node) -> None: + if core_node.id in self.core.canvas_nodes: + logging.error("core node already exists: %s", core_node) + return + logging.debug("adding node %s", core_node) + # if the gui can't find node's image, default to the "edit-node" image + image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale) + if not image: + image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE) + x = core_node.position.x + y = core_node.position.y + node = CanvasNode(self.app, x, y, core_node, image) + self.nodes[node.id] = node + self.core.canvas_nodes[core_node.id] = node + + def draw_session(self, session: Session) -> None: """ Draw existing session. """ # draw existing nodes for core_node in session.nodes: - logging.debug("drawing node %s", core_node) # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue - image = NodeUtils.node_image( - core_node, self.app.guiconfig, self.app.app_scale - ) - # if the gui can't find node's image, default to the "edit-node" image - if not image: - image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE) - x = core_node.position.x - y = core_node.position.y - node = CanvasNode(self.app, x, y, core_node, image) - self.nodes[node.id] = node - self.core.canvas_nodes[core_node.id] = node + self.add_core_node(core_node) - # draw existing links + # draw existing links for link in session.links: logging.debug("drawing link: %s", link) - canvas_node_one = self.core.canvas_nodes[link.node_one_id] - node_one = canvas_node_one.core_node - canvas_node_two = self.core.canvas_nodes[link.node_two_id] - node_two = canvas_node_two.core_node - token = create_edge_token(canvas_node_one.id, canvas_node_two.id) - - if link.type == core_pb2.LinkType.WIRELESS: - self.add_wireless_edge(canvas_node_one, canvas_node_two, link) + canvas_node1 = self.core.canvas_nodes[link.node1_id] + canvas_node2 = self.core.canvas_nodes[link.node2_id] + if link.type == LinkType.WIRELESS: + self.add_wireless_edge(canvas_node1, canvas_node2, link) else: - if token not in self.edges: - src_pos = (node_one.position.x, node_one.position.y) - dst_pos = (node_two.position.x, node_two.position.y) - edge = CanvasEdge(self, canvas_node_one.id, src_pos, dst_pos) - edge.token = token - edge.dst = canvas_node_two.id - edge.set_link(link) - edge.check_wireless() - canvas_node_one.edges.add(edge) - canvas_node_two.edges.add(edge) - self.edges[edge.token] = edge - self.core.links[edge.token] = edge - if link.HasField("interface_one"): - interface_one = link.interface_one - self.core.interface_to_edge[ - (node_one.id, interface_one.id) - ] = token - canvas_node_one.interfaces[interface_one.id] = interface_one - edge.src_interface = interface_one - if link.HasField("interface_two"): - interface_two = link.interface_two - self.core.interface_to_edge[ - (node_two.id, interface_two.id) - ] = edge.token - canvas_node_two.interfaces[interface_two.id] = interface_two - edge.dst_interface = interface_two - elif link.options.unidirectional: - edge = self.edges[token] - edge.asymmetric_link = link - else: - logging.error("duplicate link received: %s", link) + self.add_wired_edge(canvas_node1, canvas_node2, link) - def stopped_session(self): + def stopped_session(self) -> None: # clear wireless edges for edge in self.wireless_edges.values(): edge.delete() @@ -351,11 +368,10 @@ def stopped_session(self): dst_node.wireless_edges.remove(edge) self.wireless_edges.clear() - # clear all middle edge labels - for edge in self.edges.values(): - edge.reset() + # clear throughputs + self.clear_throughputs() - def canvas_xy(self, event: tk.Event) -> [float, float]: + def canvas_xy(self, event: tk.Event) -> Tuple[float, float]: """ Convert window coordinate to canvas coordinate """ @@ -383,7 +399,7 @@ def get_selected(self, event: tk.Event) -> int: return selected - def click_release(self, event: tk.Event): + def click_release(self, event: tk.Event) -> None: """ Draw a node or finish drawing an edge according to the current graph mode """ @@ -422,7 +438,7 @@ def click_release(self, event: tk.Event): self.mode = GraphMode.NODE self.selected = None - def handle_edge_release(self, _event: tk.Event): + def handle_edge_release(self, _event: tk.Event) -> None: edge = self.drawing_edge self.drawing_edge = None @@ -432,8 +448,9 @@ def handle_edge_release(self, _event: tk.Event): # edge dst must be a node logging.debug("current selected: %s", self.selected) + src_node = self.nodes.get(edge.src) dst_node = self.nodes.get(self.selected) - if not dst_node: + if not dst_node or not src_node: edge.delete() return @@ -448,17 +465,23 @@ def handle_edge_release(self, _event: tk.Event): edge.delete() return + # rj45 nodes can only support one link + if NodeUtils.is_rj45_node(src_node.core_node.type) and src_node.edges: + edge.delete() + return + if NodeUtils.is_rj45_node(dst_node.core_node.type) and dst_node.edges: + edge.delete() + return + # set dst node and snap edge to center edge.complete(self.selected) self.edges[edge.token] = edge - node_src = self.nodes[edge.src] - node_src.edges.add(edge) - node_dst = self.nodes[edge.dst] - node_dst.edges.add(edge) - self.core.create_link(edge, node_src, node_dst) + src_node.edges.add(edge) + dst_node.edges.add(edge) + self.core.create_link(edge, src_node, dst_node) - def select_object(self, object_id: int, choose_multiple: bool = False): + def select_object(self, object_id: int, choose_multiple: bool = False) -> None: """ create a bounding box when a node is selected """ @@ -479,7 +502,7 @@ def select_object(self, object_id: int, choose_multiple: bool = False): selection_id = self.selection.pop(object_id) self.delete(selection_id) - def clear_selection(self): + def clear_selection(self) -> None: """ Clear current selection boxes. """ @@ -487,7 +510,7 @@ def clear_selection(self): self.delete(_id) self.selection.clear() - def move_selection(self, object_id: int, x_offset: float, y_offset: float): + def move_selection(self, object_id: int, x_offset: float, y_offset: float) -> None: select_id = self.selection.get(object_id) if select_id is not None: self.move(select_id, x_offset, y_offset) @@ -515,14 +538,14 @@ def delete_selected_objects(self) -> None: edge.delete() # update node connected to edge being deleted other_id = edge.src - other_interface = edge.src_interface + other_iface = edge.src_iface if edge.src == object_id: other_id = edge.dst - other_interface = edge.dst_interface + other_iface = edge.dst_iface other_node = self.nodes[other_id] other_node.edges.remove(edge) - if other_interface: - del other_node.interfaces[other_interface.id] + if other_iface: + del other_node.ifaces[other_iface.id] if is_wireless: other_node.delete_antenna() @@ -535,17 +558,17 @@ def delete_selected_objects(self) -> None: self.core.deleted_graph_nodes(nodes) self.core.deleted_graph_edges(edges) - def delete_edge(self, edge: CanvasEdge): + def delete_edge(self, edge: CanvasEdge) -> None: edge.delete() del self.edges[edge.token] src_node = self.nodes[edge.src] src_node.edges.discard(edge) - if edge.src_interface: - del src_node.interfaces[edge.src_interface.id] + if edge.src_iface: + del src_node.ifaces[edge.src_iface.id] dst_node = self.nodes[edge.dst] dst_node.edges.discard(edge) - if edge.dst_interface: - del dst_node.interfaces[edge.dst_interface.id] + if edge.dst_iface: + del dst_node.ifaces[edge.dst_iface.id] src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) if src_wireless: dst_node.delete_antenna() @@ -554,7 +577,7 @@ def delete_edge(self, edge: CanvasEdge): src_node.delete_antenna() self.core.deleted_graph_edges([edge]) - def zoom(self, event: tk.Event, factor: float = None): + def zoom(self, event: tk.Event, factor: float = None) -> None: if not factor: factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT event.x, event.y = self.canvasx(event.x), self.canvasy(event.y) @@ -567,12 +590,11 @@ def zoom(self, event: tk.Event, factor: float = None): ) logging.debug("ratio: %s", self.ratio) logging.debug("offset: %s", self.offset) - zoom_label = f"{self.ratio * 100:.0f}%" - self.app.statusbar.zoom.config(text=zoom_label) + self.app.statusbar.set_zoom(self.ratio) if self.wallpaper: self.redraw_wallpaper() - def click_press(self, event: tk.Event): + def click_press(self, event: tk.Event) -> None: """ Start drawing an edge if mouse click is on a node """ @@ -634,7 +656,7 @@ def click_press(self, event: tk.Event): self.select_box = shape self.clear_selection() - def ctrl_click(self, event: tk.Event): + def ctrl_click(self, event: tk.Event) -> None: # update cursor location x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): @@ -652,7 +674,7 @@ def ctrl_click(self, event: tk.Event): ): self.select_object(selected, choose_multiple=True) - def click_motion(self, event: tk.Event): + def click_motion(self, event: tk.Event) -> None: x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): if self.select_box: @@ -705,17 +727,18 @@ def click_motion(self, event: tk.Event): if self.select_box and self.mode == GraphMode.SELECT: self.select_box.shape_motion(x, y) - def press_delete(self, _event: tk.Event): + def press_delete(self, _event: tk.Event) -> None: """ delete selected nodes and any data that relates to it """ logging.debug("press delete key") if not self.app.core.is_runtime(): self.delete_selected_objects() + self.app.default_info() else: logging.debug("node deletion is disabled during runtime state") - def double_click(self, event: tk.Event): + def double_click(self, event: tk.Event) -> None: selected = self.get_selected(event) if selected is not None and selected in self.shapes: shape = self.shapes[selected] @@ -741,7 +764,7 @@ def add_node(self, x: float, y: float) -> None: self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node - def width_and_height(self): + def width_and_height(self) -> Tuple[int, int]: """ retrieve canvas width and height in pixels """ @@ -757,8 +780,8 @@ def get_wallpaper_image(self) -> Image.Image: return image def draw_wallpaper( - self, image: ImageTk.PhotoImage, x: float = None, y: float = None - ): + self, image: PhotoImage, x: float = None, y: float = None + ) -> None: if x is None and y is None: x1, y1, x2, y2 = self.bbox(self.rect) x = (x1 + x2) / 2 @@ -766,7 +789,7 @@ def draw_wallpaper( self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER) self.wallpaper_drawn = image - def wallpaper_upper_left(self): + def wallpaper_upper_left(self) -> None: self.delete(self.wallpaper_id) # create new scaled image, cropped if needed @@ -779,7 +802,7 @@ def wallpaper_upper_left(self): if image.height > height: cropy = image.height cropped = image.crop((0, 0, cropx, cropy)) - image = ImageTk.PhotoImage(cropped) + image = PhotoImage(cropped) # draw on canvas x1, y1, _, _ = self.bbox(self.rect) @@ -787,7 +810,7 @@ def wallpaper_upper_left(self): y = (cropy / 2) + y1 self.draw_wallpaper(image, x, y) - def wallpaper_center(self): + def wallpaper_center(self) -> None: """ place the image at the center of canvas """ @@ -807,26 +830,26 @@ def wallpaper_center(self): x2 = image.width - cropx y2 = image.height - cropy cropped = image.crop((x1, y1, x2, y2)) - image = ImageTk.PhotoImage(cropped) + image = PhotoImage(cropped) self.draw_wallpaper(image) - def wallpaper_scaled(self): + def wallpaper_scaled(self) -> None: """ scale image based on canvas dimension """ self.delete(self.wallpaper_id) canvas_w, canvas_h = self.width_and_height() image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) - image = ImageTk.PhotoImage(image) + image = PhotoImage(image) self.draw_wallpaper(image) - def resize_to_wallpaper(self): + def resize_to_wallpaper(self) -> None: self.delete(self.wallpaper_id) - image = ImageTk.PhotoImage(self.wallpaper) + image = PhotoImage(self.wallpaper) self.redraw_canvas((image.width(), image.height())) self.draw_wallpaper(image) - def redraw_canvas(self, dimensions: Tuple[int, int] = None): + def redraw_canvas(self, dimensions: Tuple[int, int] = None) -> None: logging.debug("redrawing canvas to dimensions: %s", dimensions) # reset scale and move back to original position @@ -847,7 +870,7 @@ def redraw_canvas(self, dimensions: Tuple[int, int] = None): self.draw_grid() self.app.canvas.show_grid.click_handler() - def redraw_wallpaper(self): + def redraw_wallpaper(self) -> None: if self.adjust_to_dim.get(): logging.debug("drawing wallpaper to canvas dimensions") self.resize_to_wallpaper() @@ -868,7 +891,7 @@ def organize(self) -> None: for tag in tags.ORGANIZE_TAGS: self.tag_raise(tag) - def set_wallpaper(self, filename: str): + def set_wallpaper(self, filename: Optional[str]) -> None: logging.debug("setting wallpaper: %s", filename) if filename: img = Image.open(filename) @@ -884,7 +907,7 @@ def set_wallpaper(self, filename: str): def is_selection_mode(self) -> bool: return self.mode == GraphMode.SELECT - def create_edge(self, source: CanvasNode, dest: CanvasNode): + def create_edge(self, source: CanvasNode, dest: CanvasNode) -> None: """ create an edge between source node and destination node """ @@ -898,7 +921,7 @@ def create_edge(self, source: CanvasNode, dest: CanvasNode): self.nodes[dest.id].edges.add(edge) self.core.create_link(edge, source, dest) - def copy(self): + def copy(self) -> None: if self.core.is_runtime(): logging.debug("copy is disabled during runtime state") return @@ -909,7 +932,7 @@ def copy(self): canvas_node = self.nodes[node_id] self.to_copy.append(canvas_node) - def paste(self): + def paste(self) -> None: if self.core.is_runtime(): logging.debug("paste is disabled during runtime state") return @@ -965,26 +988,26 @@ def paste(self): copy_link = copy_edge.link options = edge.link.options copy_link.options.CopyFrom(options) - interface_one = None - if copy_link.HasField("interface_one"): - interface_one = copy_link.interface_one.id - interface_two = None - if copy_link.HasField("interface_two"): - interface_two = copy_link.interface_two.id + iface1_id = None + if copy_link.HasField("iface1"): + iface1_id = copy_link.iface1.id + iface2_id = None + if copy_link.HasField("iface2"): + iface2_id = copy_link.iface2.id if not options.unidirectional: copy_edge.asymmetric_link = None else: - 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) - copy_edge.asymmetric_link = core_pb2.Link( - node_one_id=copy_link.node_two_id, - node_two_id=copy_link.node_one_id, - interface_one=asym_interface_one, - interface_two=asym_interface_two, + asym_iface1 = None + if iface1_id: + asym_iface1 = Interface(id=iface1_id) + asym_iface2 = None + if iface2_id: + asym_iface2 = Interface(id=iface2_id) + copy_edge.asymmetric_link = Link( + node1_id=copy_link.node2_id, + node2_id=copy_link.node1_id, + iface1=asym_iface1, + iface2=asym_iface2, options=edge.asymmetric_link.options, ) self.itemconfig( @@ -994,7 +1017,12 @@ def paste(self): ) self.tag_raise(tags.NODE) - def scale_graph(self): + def clear_throughputs(self) -> None: + for edge in self.edges.values(): + edge.clear_middle_label() + edge.draw_link_options() + + def scale_graph(self) -> None: for nid, canvas_node in self.nodes.items(): img = None if NodeUtils.is_custom( diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 8ad3f02ab..7b5cd2f3d 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -1,12 +1,14 @@ import functools import logging import tkinter as tk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple import grpc +from PIL.ImageTk import PhotoImage -from core.api.grpc import core_pb2 -from core.api.grpc.core_pb2 import NodeType +from core.api.grpc.common_pb2 import ConfigOption +from core.api.grpc.core_pb2 import Interface, Node, NodeType +from core.api.grpc.services_pb2 import NodeServiceData from core.gui import themes from core.gui.dialogs.emaneconfig import EmaneConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog @@ -14,81 +16,81 @@ from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog +from core.gui.frames.node import NodeInfoFrame from core.gui.graph import tags -from core.gui.graph.edges import CanvasEdge +from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.tooltip import CanvasTooltip from core.gui.images import ImageEnum from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils if TYPE_CHECKING: from core.gui.app import Application - from PIL.ImageTk import PhotoImage + from core.gui.graph.graph import CanvasGraph -NODE_TEXT_OFFSET = 5 +NODE_TEXT_OFFSET: int = 5 class CanvasNode: def __init__( - self, - app: "Application", - x: float, - y: float, - core_node: core_pb2.Node, - image: "PhotoImage", + self, app: "Application", x: float, y: float, core_node: Node, image: PhotoImage ): - self.app = app - self.canvas = app.canvas - self.image = image - self.core_node = core_node - self.id = self.canvas.create_image( + self.app: "Application" = app + self.canvas: "CanvasGraph" = app.canvas + self.image: PhotoImage = image + self.core_node: Node = core_node + self.id: int = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) label_y = self._get_label_y() - self.text_id = self.canvas.create_text( + label = self.get_label() + self.text_id: int = self.canvas.create_text( x, label_y, - text=self.core_node.name, + text=label, tags=tags.NODE_LABEL, font=self.app.icon_text_font, fill="#0000CD", state=self.canvas.show_node_labels.state(), ) - self.tooltip = CanvasTooltip(self.canvas) - self.edges = set() - self.interfaces = {} - self.wireless_edges = set() - self.antennas = [] - self.antenna_images = {} + self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas) + self.edges: Set[CanvasEdge] = set() + self.ifaces: Dict[int, Interface] = {} + self.wireless_edges: Set[CanvasWirelessEdge] = set() + self.antennas: List[int] = [] + self.antenna_images: Dict[int, PhotoImage] = {} # possible configurations - self.emane_model_configs = {} - self.wlan_config = {} - self.mobility_config = {} - self.service_configs = {} - self.service_file_configs = {} - self.config_service_configs = {} + self.emane_model_configs: Dict[ + Tuple[str, Optional[int]], Dict[str, ConfigOption] + ] = {} + self.wlan_config: Dict[str, ConfigOption] = {} + self.mobility_config: Dict[str, ConfigOption] = {} + self.service_configs: Dict[str, NodeServiceData] = {} + self.service_file_configs: Dict[str, Dict[str, str]] = {} + self.config_service_configs: Dict[str, Any] = {} self.setup_bindings() - self.context = tk.Menu(self.canvas) + self.context: tk.Menu = tk.Menu(self.canvas) themes.style_menu(self.context) - def next_interface_id(self) -> int: + def next_iface_id(self) -> int: i = 0 - while i in self.interfaces: + while i in self.ifaces: i += 1 return i - def setup_bindings(self): + def setup_bindings(self) -> None: self.canvas.tag_bind(self.id, "", self.double_click) self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) self.canvas.tag_bind(self.id, "", self.show_context) + self.canvas.tag_bind(self.id, "", self.show_info) - def delete(self): + def delete(self) -> None: logging.debug("Delete canvas node for %s", self.core_node) self.canvas.delete(self.id) self.canvas.delete(self.text_id) self.delete_antennas() - def add_antenna(self): + def add_antenna(self) -> None: x, y = self.canvas.coords(self.id) offset = len(self.antennas) * 8 * self.app.app_scale img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE) @@ -102,7 +104,7 @@ def add_antenna(self): self.antennas.append(antenna_id) self.antenna_images[antenna_id] = img - def delete_antenna(self): + def delete_antenna(self) -> None: """ delete one antenna """ @@ -112,7 +114,7 @@ def delete_antenna(self): self.canvas.delete(antenna_id) self.antenna_images.pop(antenna_id, None) - def delete_antennas(self): + def delete_antennas(self) -> None: """ delete all antennas """ @@ -122,30 +124,37 @@ def delete_antennas(self): self.antennas.clear() self.antenna_images.clear() - def redraw(self): + def get_label(self) -> str: + label = self.core_node.name + if self.core_node.server: + label = f"{self.core_node.name}({self.core_node.server})" + return label + + def redraw(self) -> None: self.canvas.itemconfig(self.id, image=self.image) - self.canvas.itemconfig(self.text_id, text=self.core_node.name) + label = self.get_label() + self.canvas.itemconfig(self.text_id, text=label) for edge in self.edges: edge.redraw() - def _get_label_y(self): + def _get_label_y(self) -> int: image_box = self.canvas.bbox(self.id) return image_box[3] + NODE_TEXT_OFFSET - def scale_text(self): + def scale_text(self) -> None: text_bound = self.canvas.bbox(self.text_id) prev_y = (text_bound[3] + text_bound[1]) / 2 new_y = self._get_label_y() self.canvas.move(self.text_id, 0, new_y - prev_y) - def move(self, x: int, y: int): + def move(self, x: float, y: float) -> None: x, y = self.canvas.get_scaled_coords(x, y) current_x, current_y = self.canvas.coords(self.id) x_offset = x - current_x y_offset = y - current_y self.motion(x_offset, y_offset, update=False) - def motion(self, x_offset: int, y_offset: int, update: bool = True): + def motion(self, x_offset: float, y_offset: float, update: bool = True) -> None: original_position = self.canvas.coords(self.id) self.canvas.move(self.id, x_offset, y_offset) pos = self.canvas.coords(self.id) @@ -177,8 +186,11 @@ def motion(self, x_offset: int, y_offset: int, update: bool = True): if self.app.core.is_runtime() and update: self.app.core.edit_node(self.core_node) - def on_enter(self, event: tk.Event): - if self.app.core.is_runtime() and self.app.core.observer: + def on_enter(self, event: tk.Event) -> None: + is_runtime = self.app.core.is_runtime() + has_observer = self.app.core.observer is not None + is_container = NodeUtils.is_container_node(self.core_node.type) + if is_runtime and has_observer and is_container: self.tooltip.text.set("waiting...") self.tooltip.on_enter(event) try: @@ -187,15 +199,19 @@ def on_enter(self, event: tk.Event): except grpc.RpcError as e: self.app.show_grpc_exception("Observer Error", e) - def on_leave(self, event: tk.Event): + def on_leave(self, event: tk.Event) -> None: self.tooltip.on_leave(event) - def double_click(self, event: tk.Event): + def double_click(self, event: tk.Event) -> None: if self.app.core.is_runtime(): - self.canvas.core.launch_terminal(self.core_node.id) + if NodeUtils.is_container_node(self.core_node.type): + self.canvas.core.launch_terminal(self.core_node.id) else: self.show_config() + def show_info(self, _event: tk.Event) -> None: + self.app.display_info(NodeInfoFrame, app=self.app, canvas_node=self) + def show_context(self, event: tk.Event) -> None: # clear existing menu self.context.delete(0, tk.END) @@ -203,6 +219,10 @@ def show_context(self, event: tk.Event) -> None: is_emane = self.core_node.type == NodeType.EMANE if self.app.core.is_runtime(): self.context.add_command(label="Configure", command=self.show_config) + if is_emane: + self.context.add_command( + label="EMANE Config", command=self.show_emane_config + ) if is_wlan: self.context.add_command( label="WLAN Config", command=self.show_wlan_config @@ -259,57 +279,58 @@ def click_cut(self) -> None: def click_unlink(self, edge: CanvasEdge) -> None: self.canvas.delete_edge(edge) + self.app.default_info() def canvas_delete(self) -> None: self.canvas.clear_selection() - self.canvas.selection[self.id] = self + self.canvas.select_object(self.id) self.canvas.delete_selected_objects() def canvas_copy(self) -> None: self.canvas.clear_selection() - self.canvas.selection[self.id] = self + self.canvas.select_object(self.id) self.canvas.copy() - def show_config(self): + def show_config(self) -> None: dialog = NodeConfigDialog(self.app, self) dialog.show() - def show_wlan_config(self): + def show_wlan_config(self) -> None: dialog = WlanConfigDialog(self.app, self) if not dialog.has_error: dialog.show() - def show_mobility_config(self): + def show_mobility_config(self) -> None: dialog = MobilityConfigDialog(self.app, self) if not dialog.has_error: dialog.show() - def show_mobility_player(self): + def show_mobility_player(self) -> None: mobility_player = self.app.core.mobility_players[self.core_node.id] mobility_player.show() - def show_emane_config(self): + def show_emane_config(self) -> None: dialog = EmaneConfigDialog(self.app, self) dialog.show() - def show_services(self): + def show_services(self) -> None: dialog = NodeServiceDialog(self.app, self) dialog.show() - def show_config_services(self): + def show_config_services(self) -> None: dialog = NodeConfigServiceDialog(self.app, self) dialog.show() - def has_emane_link(self, interface_id: int) -> core_pb2.Node: + def has_emane_link(self, iface_id: int) -> Node: result = None for edge in self.edges: if self.id == edge.src: other_id = edge.dst - edge_interface_id = edge.src_interface.id + edge_iface_id = edge.src_iface.id else: other_id = edge.src - edge_interface_id = edge.dst_interface.id - if edge_interface_id != interface_id: + edge_iface_id = edge.dst_iface.id + if edge_iface_id != iface_id: continue other_node = self.canvas.nodes[other_id] if other_node.core_node.type == NodeType.EMANE: @@ -317,14 +338,14 @@ def has_emane_link(self, interface_id: int) -> core_pb2.Node: break return result - def wireless_link_selected(self): + def wireless_link_selected(self) -> None: nodes = [x for x in self.canvas.selection if x in self.canvas.nodes] for node_id in nodes: canvas_node = self.canvas.nodes[node_id] self.canvas.create_edge(self, canvas_node) self.canvas.clear_selection() - def scale_antennas(self): + def scale_antennas(self) -> None: for i in range(len(self.antennas)): antenna_id = self.antennas[i] image = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE) diff --git a/daemon/core/gui/graph/shape.py b/daemon/core/gui/graph/shape.py index 70f67d14e..36298655e 100644 --- a/daemon/core/gui/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, Dict, List, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Union from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags @@ -23,17 +23,17 @@ def __init__( bold: bool = False, italic: bool = False, underline: bool = False, - ): - self.text = text - self.font = font - self.font_size = font_size - self.text_color = text_color - self.fill_color = fill_color - self.border_color = border_color - self.border_width = border_width - self.bold = bold - self.italic = italic - self.underline = underline + ) -> None: + self.text: str = text + self.font: str = font + self.font_size: int = font_size + self.text_color: str = text_color + self.fill_color: str = fill_color + self.border_color: str = border_color + self.border_width: int = border_width + self.bold: bool = bold + self.italic: bool = italic + self.underline: bool = underline class Shape: @@ -47,29 +47,29 @@ def __init__( x2: float = None, y2: float = None, data: AnnotationData = None, - ): - self.app = app - self.canvas = canvas - self.shape_type = shape_type - self.id = None - self.text_id = None - self.x1 = x1 - self.y1 = y1 + ) -> None: + self.app: "Application" = app + self.canvas: "CanvasGraph" = canvas + self.shape_type: ShapeType = shape_type + self.id: Optional[int] = None + self.text_id: Optional[int] = None + self.x1: float = x1 + self.y1: float = y1 if x2 is None: x2 = x1 - self.x2 = x2 + self.x2: float = x2 if y2 is None: y2 = y1 - self.y2 = y2 + self.y2: float = y2 if data is None: - self.created = False - self.shape_data = AnnotationData() + self.created: bool = False + self.shape_data: AnnotationData = AnnotationData() else: - self.created = True + self.created: bool = True self.shape_data = data self.draw() - def draw(self): + def draw(self) -> None: if self.created: dash = None else: @@ -127,7 +127,7 @@ def get_font(self) -> List[Union[int, str]]: font.append("underline") return font - def draw_shape_text(self): + def draw_shape_text(self) -> None: if self.shape_data.text: x = (self.x1 + self.x2) / 2 y = self.y1 + 1.5 * self.shape_data.font_size @@ -142,18 +142,18 @@ def draw_shape_text(self): state=self.canvas.show_annotations.state(), ) - def shape_motion(self, x1: float, y1: float): + def shape_motion(self, x1: float, y1: float) -> None: self.canvas.coords(self.id, self.x1, self.y1, x1, y1) - def shape_complete(self, x: float, y: float): + def shape_complete(self, x: float, y: float) -> None: self.canvas.organize() s = ShapeDialog(self.app, self) s.show() - def disappear(self): + def disappear(self) -> None: self.canvas.delete(self.id) - def motion(self, x_offset: float, y_offset: float): + def motion(self, x_offset: float, y_offset: float) -> None: original_position = self.canvas.coords(self.id) self.canvas.move(self.id, x_offset, y_offset) coords = self.canvas.coords(self.id) @@ -166,7 +166,7 @@ def motion(self, x_offset: float, y_offset: float): if self.text_id is not None: self.canvas.move(self.text_id, x_offset, y_offset) - def delete(self): + def delete(self) -> None: logging.debug("Delete shape, id(%s)", self.id) self.canvas.delete(self.id) self.canvas.delete(self.text_id) diff --git a/daemon/core/gui/graph/shapeutils.py b/daemon/core/gui/graph/shapeutils.py index ce2b7f96f..2b62a46c2 100644 --- a/daemon/core/gui/graph/shapeutils.py +++ b/daemon/core/gui/graph/shapeutils.py @@ -1,4 +1,5 @@ import enum +from typing import Set class ShapeType(enum.Enum): @@ -8,7 +9,7 @@ class ShapeType(enum.Enum): TEXT = "text" -SHAPES = {ShapeType.OVAL, ShapeType.RECTANGLE} +SHAPES: Set[ShapeType] = {ShapeType.OVAL, ShapeType.RECTANGLE} def is_draw_shape(shape_type: ShapeType) -> bool: diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index c07211931..b7b355172 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -1,17 +1,19 @@ -ANNOTATION = "annotation" -GRIDLINE = "gridline" -SHAPE = "shape" -SHAPE_TEXT = "shapetext" -EDGE = "edge" -LINK_LABEL = "linklabel" -WIRELESS_EDGE = "wireless" -ANTENNA = "antenna" -NODE_LABEL = "nodename" -NODE = "node" -WALLPAPER = "wallpaper" -SELECTION = "selectednodes" -MARKER = "marker" -ORGANIZE_TAGS = [ +from typing import List + +ANNOTATION: str = "annotation" +GRIDLINE: str = "gridline" +SHAPE: str = "shape" +SHAPE_TEXT: str = "shapetext" +EDGE: str = "edge" +LINK_LABEL: str = "linklabel" +WIRELESS_EDGE: str = "wireless" +ANTENNA: str = "antenna" +NODE_LABEL: str = "nodename" +NODE: str = "node" +WALLPAPER: str = "wallpaper" +SELECTION: str = "selectednodes" +MARKER: str = "marker" +ORGANIZE_TAGS: List[str] = [ WALLPAPER, GRIDLINE, SHAPE, @@ -25,7 +27,7 @@ SELECTION, MARKER, ] -RESET_TAGS = [ +RESET_TAGS: List[str] = [ EDGE, NODE, NODE_LABEL, diff --git a/daemon/core/gui/graph/tooltip.py b/daemon/core/gui/graph/tooltip.py index a2193901b..6e4aa62f5 100644 --- a/daemon/core/gui/graph/tooltip.py +++ b/daemon/core/gui/graph/tooltip.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Tuple from core.gui.themes import Styles @@ -27,39 +27,45 @@ def __init__( self, canvas: "CanvasGraph", *, - pad=(5, 3, 5, 3), + pad: Tuple[int, int, int, int] = (5, 3, 5, 3), waittime: int = 400, wraplength: int = 600 - ): + ) -> None: # in miliseconds, originally 500 - self.waittime = waittime + self.waittime: int = waittime # in pixels, originally 180 - self.wraplength = wraplength - self.canvas = canvas - self.text = tk.StringVar() - self.pad = pad - self.id = None - self.tw = None - - def on_enter(self, event: tk.Event = None): + self.wraplength: int = wraplength + self.canvas: "CanvasGraph" = canvas + self.text: tk.StringVar = tk.StringVar() + self.pad: Tuple[int, int, int, int] = pad + self.id: Optional[str] = None + self.tw: Optional[tk.Toplevel] = None + + def on_enter(self, event: tk.Event = None) -> None: self.schedule() - def on_leave(self, event: tk.Event = None): + def on_leave(self, event: tk.Event = None) -> None: self.unschedule() self.hide() - def schedule(self): + def schedule(self) -> None: self.unschedule() self.id = self.canvas.after(self.waittime, self.show) - def unschedule(self): + def unschedule(self) -> None: id_ = self.id self.id = None if id_: self.canvas.after_cancel(id_) - def show(self, event: tk.Event = None): - def tip_pos_calculator(canvas, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): + def show(self, event: tk.Event = None) -> None: + def tip_pos_calculator( + canvas: "CanvasGraph", + label: ttk.Label, + *, + tip_delta: Tuple[int, int] = (10, 5), + pad: Tuple[int, int, int, int] = (5, 3, 5, 3) + ): c = canvas s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight() width, height = ( @@ -108,7 +114,7 @@ def tip_pos_calculator(canvas, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): x, y = tip_pos_calculator(canvas, label, pad=pad) self.tw.wm_geometry("+%d+%d" % (x, y)) - def hide(self): + def hide(self) -> None: if self.tw: self.tw.destroy() self.tw = None diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index 3a9530544..227194578 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -1,46 +1,44 @@ from enum import Enum from tkinter import messagebox +from typing import Dict, Optional, Tuple -from PIL import Image, ImageTk +from PIL import Image +from PIL.ImageTk import PhotoImage -from core.api.grpc import core_pb2 +from core.api.grpc.core_pb2 import NodeType from core.gui.appconfig import LOCAL_ICONS_PATH class Images: - images = {} + images: Dict[str, str] = {} @classmethod - def create(cls, file_path: str, width: int, height: int = None): + def create(cls, file_path: str, width: int, height: int = None) -> PhotoImage: if height is None: height = width image = Image.open(file_path) image = image.resize((width, height), Image.ANTIALIAS) - return ImageTk.PhotoImage(image) + return PhotoImage(image) @classmethod - def load_all(cls): + def load_all(cls) -> None: for image in LOCAL_ICONS_PATH.glob("*"): cls.images[image.stem] = str(image) @classmethod - def get( - cls, image_enum: Enum, width: int, height: int = None - ) -> ImageTk.PhotoImage: + def get(cls, image_enum: Enum, width: int, height: int = None) -> PhotoImage: file_path = cls.images[image_enum.value] return cls.create(file_path, width, height) @classmethod def get_with_image_file( cls, stem: str, width: int, height: int = None - ) -> ImageTk.PhotoImage: + ) -> PhotoImage: file_path = cls.images[stem] return cls.create(file_path, width, height) @classmethod - def get_custom( - cls, name: str, width: int, height: int = None - ) -> ImageTk.PhotoImage: + def get_custom(cls, name: str, width: int, height: int = None) -> PhotoImage: try: file_path = cls.images[name] return cls.create(file_path, width, height) @@ -95,22 +93,22 @@ class ImageEnum(Enum): class TypeToImage: - type_to_image = { - (core_pb2.NodeType.DEFAULT, "router"): ImageEnum.ROUTER, - (core_pb2.NodeType.DEFAULT, "PC"): ImageEnum.PC, - (core_pb2.NodeType.DEFAULT, "host"): ImageEnum.HOST, - (core_pb2.NodeType.DEFAULT, "mdr"): ImageEnum.MDR, - (core_pb2.NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER, - (core_pb2.NodeType.HUB, ""): ImageEnum.HUB, - (core_pb2.NodeType.SWITCH, ""): ImageEnum.SWITCH, - (core_pb2.NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN, - (core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE, - (core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45, - (core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL, - (core_pb2.NodeType.DOCKER, ""): ImageEnum.DOCKER, - (core_pb2.NodeType.LXC, ""): ImageEnum.LXC, + type_to_image: Dict[Tuple[NodeType, str], ImageEnum] = { + (NodeType.DEFAULT, "router"): ImageEnum.ROUTER, + (NodeType.DEFAULT, "PC"): ImageEnum.PC, + (NodeType.DEFAULT, "host"): ImageEnum.HOST, + (NodeType.DEFAULT, "mdr"): ImageEnum.MDR, + (NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER, + (NodeType.HUB, ""): ImageEnum.HUB, + (NodeType.SWITCH, ""): ImageEnum.SWITCH, + (NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN, + (NodeType.EMANE, ""): ImageEnum.EMANE, + (NodeType.RJ45, ""): ImageEnum.RJ45, + (NodeType.TUNNEL, ""): ImageEnum.TUNNEL, + (NodeType.DOCKER, ""): ImageEnum.DOCKER, + (NodeType.LXC, ""): ImageEnum.LXC, } @classmethod - def get(cls, node_type, model): - return cls.type_to_image.get((node_type, model), None) + def get(cls, node_type, model) -> Optional[ImageEnum]: + return cls.type_to_image.get((node_type, model)) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 1973fe995..f4f2e3cc8 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -1,21 +1,21 @@ import logging -from typing import TYPE_CHECKING, Any, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple import netaddr from netaddr import EUI, IPNetwork +from core.api.grpc.core_pb2 import Interface, Link, Node +from core.gui.graph.node import CanvasNode from core.gui.nodeutils import NodeUtils if TYPE_CHECKING: from core.gui.app import Application - from core.api.grpc import core_pb2 - from core.gui.graph.node import CanvasNode -def get_index(interface: "core_pb2.Interface") -> Optional[int]: - if not interface.ip4: +def get_index(iface: Interface) -> Optional[int]: + if not iface.ip4: return None - net = netaddr.IPNetwork(f"{interface.ip4}/{interface.ip4mask}") + net = netaddr.IPNetwork(f"{iface.ip4}/{iface.ip4_mask}") ip_value = net.value cidr_value = net.cidr.value return ip_value - cidr_value @@ -44,18 +44,18 @@ def next(self) -> "Subnets": class InterfaceManager: def __init__(self, app: "Application") -> None: - self.app = app + self.app: "Application" = app ip4 = self.app.guiconfig.ips.ip4 ip6 = self.app.guiconfig.ips.ip6 - self.ip4_mask = 24 - self.ip6_mask = 64 - self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}") - self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}") + self.ip4_mask: int = 24 + self.ip6_mask: int = 64 + self.ip4_subnets: IPNetwork = IPNetwork(f"{ip4}/{self.ip4_mask}") + self.ip6_subnets: IPNetwork = IPNetwork(f"{ip6}/{self.ip6_mask}") mac = self.app.guiconfig.mac - self.mac = EUI(mac, dialect=netaddr.mac_unix_expanded) - self.current_mac = None - self.current_subnets = None - self.used_subnets = {} + self.mac: EUI = EUI(mac, dialect=netaddr.mac_unix_expanded) + self.current_mac: Optional[EUI] = None + self.current_subnets: Optional[Subnets] = None + self.used_subnets: Dict[Tuple[IPNetwork, IPNetwork], Subnets] = {} def update_ips(self, ip4: str, ip6: str) -> None: self.reset() @@ -84,55 +84,55 @@ def reset(self) -> None: self.current_subnets = None self.used_subnets.clear() - def removed(self, links: List["core_pb2.Link"]) -> None: + def removed(self, links: List[Link]) -> None: # get remaining subnets remaining_subnets = set() for edge in self.app.core.links.values(): link = edge.link - if link.HasField("interface_one"): - subnets = self.get_subnets(link.interface_one) + if link.HasField("iface1"): + subnets = self.get_subnets(link.iface1) remaining_subnets.add(subnets) - if link.HasField("interface_two"): - subnets = self.get_subnets(link.interface_two) + if link.HasField("iface2"): + subnets = self.get_subnets(link.iface2) remaining_subnets.add(subnets) # remove all subnets from used subnets when no longer present # or remove used indexes from subnet - interfaces = [] + ifaces = [] for link in links: - if link.HasField("interface_one"): - interfaces.append(link.interface_one) - if link.HasField("interface_two"): - interfaces.append(link.interface_two) - for interface in interfaces: - subnets = self.get_subnets(interface) + if link.HasField("iface1"): + ifaces.append(link.iface1) + if link.HasField("iface2"): + ifaces.append(link.iface2) + for iface in ifaces: + subnets = self.get_subnets(iface) if subnets not in remaining_subnets: self.used_subnets.pop(subnets.key(), None) else: - index = get_index(interface) + index = get_index(iface) if index is not None: subnets.used_indexes.discard(index) self.current_subnets = None - def joined(self, links: List["core_pb2.Link"]) -> None: - interfaces = [] + def joined(self, links: List[Link]) -> None: + ifaces = [] for link in links: - if link.HasField("interface_one"): - interfaces.append(link.interface_one) - if link.HasField("interface_two"): - interfaces.append(link.interface_two) + if link.HasField("iface1"): + ifaces.append(link.iface1) + if link.HasField("iface2"): + ifaces.append(link.iface2) # add to used subnets and mark used indexes - for interface in interfaces: - subnets = self.get_subnets(interface) - index = get_index(interface) + for iface in ifaces: + subnets = self.get_subnets(iface) + index = get_index(iface) if index is None: continue subnets.used_indexes.add(index) if subnets.key() not in self.used_subnets: self.used_subnets[subnets.key()] = subnets - def next_index(self, node: "core_pb2.Node") -> int: + def next_index(self, node: Node) -> int: if NodeUtils.is_router_node(node): index = 1 else: @@ -144,24 +144,24 @@ def next_index(self, node: "core_pb2.Node") -> int: index += 1 return index - def get_ips(self, node: "core_pb2.Node") -> [str, str]: + def get_ips(self, node: Node) -> [str, str]: index = self.next_index(node) ip4 = self.current_subnets.ip4[index] ip6 = self.current_subnets.ip6[index] return str(ip4), str(ip6) - def get_subnets(self, interface: "core_pb2.Interface") -> Subnets: + def get_subnets(self, iface: Interface) -> Subnets: ip4_subnet = self.ip4_subnets - if interface.ip4: - ip4_subnet = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr + if iface.ip4: + ip4_subnet = IPNetwork(f"{iface.ip4}/{iface.ip4_mask}").cidr ip6_subnet = self.ip6_subnets - if interface.ip6: - ip6_subnet = IPNetwork(f"{interface.ip6}/{interface.ip6mask}").cidr + if iface.ip6: + ip6_subnet = IPNetwork(f"{iface.ip6}/{iface.ip6_mask}").cidr subnets = Subnets(ip4_subnet, ip6_subnet) return self.used_subnets.get(subnets.key(), subnets) def determine_subnets( - self, canvas_src_node: "CanvasNode", canvas_dst_node: "CanvasNode" + self, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode ) -> None: src_node = canvas_src_node.core_node dst_node = canvas_dst_node.core_node @@ -185,7 +185,7 @@ def determine_subnets( logging.info("ignoring subnet change for link between network nodes") def find_subnets( - self, canvas_node: "CanvasNode", visited: Set[int] = None + self, canvas_node: CanvasNode, visited: Set[int] = None ) -> Optional[IPNetwork]: logging.info("finding subnet for node: %s", canvas_node.core_node.name) canvas = self.app.canvas @@ -196,16 +196,16 @@ def find_subnets( for edge in canvas_node.edges: src_node = canvas.nodes[edge.src] dst_node = canvas.nodes[edge.dst] - interface = edge.src_interface + iface = edge.src_iface check_node = src_node if src_node == canvas_node: - interface = edge.dst_interface + iface = edge.dst_iface check_node = dst_node if check_node.core_node.id in visited: continue visited.add(check_node.core_node.id) - if interface: - subnets = self.get_subnets(interface) + if iface: + subnets = self.get_subnets(iface) else: subnets = self.find_subnets(check_node, visited) if subnets: diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 62a9ceaef..3b85ac6f5 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -4,9 +4,10 @@ import webbrowser from functools import partial from tkinter import filedialog, messagebox -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from core.gui.appconfig import XMLS_PATH +from core.gui.coreclient import CoreClient from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog @@ -22,6 +23,7 @@ from core.gui.dialogs.sessionoptions import SessionOptionsDialog from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.throughput import ThroughputDialog +from core.gui.graph.graph import CanvasGraph from core.gui.nodeutils import ICON_SIZE from core.gui.observers import ObserversMenu from core.gui.task import ProgressTask @@ -29,7 +31,7 @@ if TYPE_CHECKING: from core.gui.app import Application -MAX_FILES = 3 +MAX_FILES: int = 3 class Menubar(tk.Menu): @@ -42,12 +44,12 @@ def __init__(self, app: "Application") -> None: Create a CoreMenubar instance """ super().__init__(app) - self.app = app - self.core = app.core - self.canvas = app.canvas - self.recent_menu = None - self.edit_menu = None - self.observers_menu = None + self.app: "Application" = app + self.core: CoreClient = app.core + self.canvas: CanvasGraph = app.canvas + self.recent_menu: Optional[tk.Menu] = None + self.edit_menu: Optional[tk.Menu] = None + self.observers_menu: Optional[ObserversMenu] = None self.draw() def draw(self) -> None: @@ -136,10 +138,15 @@ def draw_view_menu(self) -> None: Create view menu """ menu = tk.Menu(self) + menu.add_checkbutton( + label="Details Panel", + command=self.click_infobar_change, + variable=self.app.show_infobar, + ) menu.add_checkbutton( label="Interface Names", command=self.click_edge_label_change, - variable=self.canvas.show_interface_names, + variable=self.canvas.show_iface_names, ) menu.add_checkbutton( label="IPv4 Addresses", @@ -441,6 +448,12 @@ def click_autogrid(self) -> None: y = (row * layout_size) + padding node.move(x, y) + def click_infobar_change(self) -> None: + if self.app.show_infobar.get(): + self.app.show_info() + else: + self.app.hide_info() + def click_edge_label_change(self) -> None: for edge in self.canvas.edges.values(): edge.draw_labels() diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 402046621..08c8f31ce 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -1,38 +1,36 @@ import logging -from typing import TYPE_CHECKING, List, Optional, Set +from typing import List, Optional, Set + +from PIL.ImageTk import PhotoImage from core.api.grpc.core_pb2 import Node, NodeType from core.gui.appconfig import CustomNode, GuiConfig from core.gui.images import ImageEnum, Images, TypeToImage -if TYPE_CHECKING: - from core.api.grpc import core_pb2 - from PIL import ImageTk - -ICON_SIZE = 48 -ANTENNA_SIZE = 32 +ICON_SIZE: int = 48 +ANTENNA_SIZE: int = 32 class NodeDraw: - def __init__(self): + def __init__(self) -> None: self.custom: bool = False - self.image = None + self.image: Optional[PhotoImage] = None self.image_enum: Optional[ImageEnum] = None - self.image_file = None - self.node_type: core_pb2.NodeType = None + self.image_file: Optional[str] = None + self.node_type: NodeType = None self.model: Optional[str] = None self.services: Set[str] = set() - self.label = None + self.label: Optional[str] = None @classmethod def from_setup( cls, image_enum: ImageEnum, - node_type: "core_pb2.NodeType", + node_type: NodeType, label: str, model: str = None, - tooltip=None, - ): + tooltip: str = None, + ) -> "NodeDraw": node_draw = NodeDraw() node_draw.image_enum = image_enum node_draw.image = Images.get(image_enum, ICON_SIZE) @@ -43,7 +41,7 @@ def from_setup( return node_draw @classmethod - def from_custom(cls, custom_node: CustomNode): + def from_custom(cls, custom_node: CustomNode) -> "NodeDraw": node_draw = NodeDraw() node_draw.custom = True node_draw.image_file = custom_node.image @@ -57,17 +55,17 @@ def from_custom(cls, custom_node: CustomNode): class NodeUtils: - NODES = [] - NETWORK_NODES = [] + NODES: List[NodeDraw] = [] + NETWORK_NODES: List[NodeDraw] = [] NODE_ICONS = {} - CONTAINER_NODES = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} - IMAGE_NODES = {NodeType.DOCKER, NodeType.LXC} - WIRELESS_NODES = {NodeType.WIRELESS_LAN, NodeType.EMANE} - RJ45_NODES = {NodeType.RJ45} - IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} - NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} - ROUTER_NODES = {"router", "mdr"} - ANTENNA_ICON = None + CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} + IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC} + WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} + RJ45_NODES: Set[NodeType] = {NodeType.RJ45} + IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET} + NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"} + ROUTER_NODES: Set[str] = {"router", "mdr"} + ANTENNA_ICON: PhotoImage = None @classmethod def is_router_node(cls, node: Node) -> bool: @@ -99,8 +97,8 @@ def is_rj45_node(cls, node_type: NodeType) -> bool: @classmethod def node_icon( - cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale=1.0 - ) -> "ImageTk.PhotoImage": + cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale: float = 1.0 + ) -> PhotoImage: image_enum = TypeToImage.get(node_type, model) if image_enum: @@ -112,8 +110,8 @@ def node_icon( @classmethod def node_image( - cls, core_node: "core_pb2.Node", gui_config: GuiConfig, scale=1.0 - ) -> "ImageTk.PhotoImage": + cls, core_node: Node, gui_config: GuiConfig, scale: float = 1.0 + ) -> PhotoImage: image = cls.node_icon(core_node.type, core_node.model, gui_config, scale) if core_node.icon: try: @@ -141,7 +139,7 @@ def get_image_file(cls, gui_config: GuiConfig, name: str) -> Optional[str]: return None @classmethod - def setup(cls): + def setup(cls) -> None: nodes = [ (ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"), (ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"), diff --git a/daemon/core/gui/observers.py b/daemon/core/gui/observers.py index 27d0a26ed..7879494b5 100644 --- a/daemon/core/gui/observers.py +++ b/daemon/core/gui/observers.py @@ -1,13 +1,13 @@ import tkinter as tk from functools import partial -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict from core.gui.dialogs.observers import ObserverDialog if TYPE_CHECKING: from core.gui.app import Application -OBSERVERS = { +OBSERVERS: Dict[str, str] = { "List Processes": "ps", "Show Interfaces": "ip address", "IPV4 Routes": "ip -4 route", @@ -23,9 +23,9 @@ class ObserversMenu(tk.Menu): def __init__(self, master: tk.BaseWidget, app: "Application") -> None: super().__init__(master) - self.app = app - self.observer = tk.StringVar(value=tk.NONE) - self.custom_index = 0 + self.app: "Application" = app + self.observer: tk.StringVar = tk.StringVar(value=tk.NONE) + self.custom_index: int = 0 self.draw() def draw(self) -> None: diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index 3f58e7a03..6989593e5 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -3,8 +3,9 @@ """ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional +from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel from core.gui.dialogs.alerts import AlertsDialog from core.gui.themes import Styles @@ -13,20 +14,20 @@ class StatusBar(ttk.Frame): - def __init__(self, master: tk.Widget, app: "Application"): + def __init__(self, master: tk.Widget, app: "Application") -> None: super().__init__(master) - self.app = app - self.status = None - self.statusvar = tk.StringVar() - self.zoom = None - self.cpu_usage = None - self.memory = None - self.alerts_button = None - self.running = False - self.core_alarms = [] + self.app: "Application" = app + self.status: Optional[ttk.Label] = None + self.statusvar: tk.StringVar = tk.StringVar() + self.zoom: Optional[ttk.Label] = None + self.cpu_label: Optional[ttk.Label] = None + self.alerts_button: Optional[ttk.Button] = None + self.alert_style = Styles.no_alert + self.running: bool = False + self.core_alarms: List[ExceptionEvent] = [] self.draw() - def draw(self): + def draw(self) -> None: self.columnconfigure(0, weight=7) self.columnconfigure(1, weight=1) self.columnconfigure(2, weight=1) @@ -45,28 +46,50 @@ def draw(self): ) self.status.grid(row=0, column=0, sticky="ew") - self.zoom = ttk.Label( - self, - text="%s" % (int(self.app.canvas.ratio * 100)) + "%", - anchor=tk.CENTER, - borderwidth=1, - relief=tk.RIDGE, - ) + self.zoom = ttk.Label(self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE) self.zoom.grid(row=0, column=1, sticky="ew") + self.set_zoom(self.app.canvas.ratio) - self.cpu_usage = ttk.Label( - self, text="CPU TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + self.cpu_label = ttk.Label( + self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE ) - self.cpu_usage.grid(row=0, column=2, sticky="ew") + self.cpu_label.grid(row=0, column=2, sticky="ew") + self.set_cpu(0.0) self.alerts_button = ttk.Button( - self, text="Alerts", command=self.click_alerts, style=Styles.green_alert + self, text="Alerts", command=self.click_alerts, style=self.alert_style ) self.alerts_button.grid(row=0, column=3, sticky="ew") - def click_alerts(self): + def set_cpu(self, usage: float) -> None: + self.cpu_label.config(text=f"CPU {usage * 100:.2f}%") + + def set_zoom(self, zoom: float) -> None: + self.zoom.config(text=f"ZOOM {zoom * 100:.0f}%") + + def add_alert(self, event: ExceptionEvent) -> None: + self.core_alarms.append(event) + level = event.exception_event.level + self._set_alert_style(level) + label = f"Alerts ({len(self.core_alarms)})" + self.alerts_button.config(text=label, style=self.alert_style) + + def _set_alert_style(self, level: ExceptionLevel) -> None: + if level in [ExceptionLevel.FATAL, ExceptionLevel.ERROR]: + self.alert_style = Styles.red_alert + elif level == ExceptionLevel.WARNING and self.alert_style != Styles.red_alert: + self.alert_style = Styles.yellow_alert + elif self.alert_style == Styles.no_alert: + self.alert_style = Styles.green_alert + + def clear_alerts(self): + self.core_alarms.clear() + self.alert_style = Styles.no_alert + self.alerts_button.config(text="Alerts", style=self.alert_style) + + def click_alerts(self) -> None: dialog = AlertsDialog(self.app) dialog.show() - def set_status(self, message: str): + def set_status(self, message: str) -> None: self.statusvar.set(message) diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index 2f055a903..c60350f94 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -1,7 +1,7 @@ import logging import threading import time -from typing import TYPE_CHECKING, Any, Callable, Tuple +from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple if TYPE_CHECKING: from core.gui.app import Application @@ -16,17 +16,17 @@ def __init__( callback: Callable = None, args: Tuple[Any] = None, ): - self.app = app - self.title = title - self.task = task - self.callback = callback - self.args = args - if self.args is None: - self.args = () - self.time = None + self.app: "Application" = app + self.title: str = title + self.task: Callable = task + self.callback: Callable = callback + if args is None: + args = () + self.args: Tuple[Any] = args + self.time: Optional[float] = None def start(self) -> None: - self.app.progress.grid(sticky="ew") + self.app.progress.grid(sticky="ew", columnspan=2) self.app.progress.start() self.time = time.perf_counter() thread = threading.Thread(target=self.run, daemon=True) @@ -49,7 +49,7 @@ def run(self) -> None: finally: self.app.after(0, self.complete) - def complete(self): + def complete(self) -> None: self.app.progress.stop() self.app.progress.grid_forget() total = time.perf_counter() - self.time diff --git a/daemon/core/gui/themes.py b/daemon/core/gui/themes.py index 141a7a5c3..45b109f0c 100644 --- a/daemon/core/gui/themes.py +++ b/daemon/core/gui/themes.py @@ -1,39 +1,41 @@ import tkinter as tk from tkinter import font, ttk +from typing import Dict, Tuple -THEME_DARK = "black" -PADX = (0, 5) -PADY = (0, 5) -FRAME_PAD = 5 -DIALOG_PAD = 5 +THEME_DARK: str = "black" +PADX: Tuple[int, int] = (0, 5) +PADY: Tuple[int, int] = (0, 5) +FRAME_PAD: int = 5 +DIALOG_PAD: int = 5 class Styles: - tooltip = "Tooltip.TLabel" - tooltip_frame = "Tooltip.TFrame" - service_checkbutton = "Service.TCheckbutton" - picker_button = "Picker.TButton" - green_alert = "GAlert.TButton" - red_alert = "RAlert.TButton" - yellow_alert = "YAlert.TButton" + tooltip: str = "Tooltip.TLabel" + tooltip_frame: str = "Tooltip.TFrame" + service_checkbutton: str = "Service.TCheckbutton" + picker_button: str = "Picker.TButton" + no_alert: str = "NAlert.TButton" + green_alert: str = "GAlert.TButton" + red_alert: str = "RAlert.TButton" + yellow_alert: str = "YAlert.TButton" class Colors: - disabledfg = "DarkGrey" - frame = "#424242" - dark = "#222222" - darker = "#121212" - darkest = "black" - lighter = "#626262" - lightest = "#ffffff" - selectbg = "#4a6984" - selectfg = "#ffffff" - white = "white" - black = "black" - listboxbg = "#f2f1f0" - - -def load(style: ttk.Style): + disabledfg: str = "DarkGrey" + frame: str = "#424242" + dark: str = "#222222" + darker: str = "#121212" + darkest: str = "black" + lighter: str = "#626262" + lightest: str = "#ffffff" + selectbg: str = "#4a6984" + selectfg: str = "#ffffff" + white: str = "white" + black: str = "black" + listboxbg: str = "#f2f1f0" + + +def load(style: ttk.Style) -> None: style.theme_create( THEME_DARK, "clam", @@ -139,13 +141,13 @@ def load(style: ttk.Style): ) -def theme_change_menu(event: tk.Event): +def theme_change_menu(event: tk.Event) -> None: if not isinstance(event.widget, tk.Menu): return style_menu(event.widget) -def style_menu(widget: tk.Widget): +def style_menu(widget: tk.Widget) -> None: style = ttk.Style() bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") @@ -157,7 +159,7 @@ def style_menu(widget: tk.Widget): ) -def style_listbox(widget: tk.Widget): +def style_listbox(widget: tk.Widget) -> None: style = ttk.Style() bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") @@ -174,36 +176,32 @@ def style_listbox(widget: tk.Widget): ) -def theme_change(event: tk.Event): - style = ttk.Style() - style.configure(Styles.picker_button, font="TkSmallCaptionFont") - style.configure( - Styles.green_alert, - background="green", - padding=0, - relief=tk.RIDGE, - borderwidth=1, - font="TkDefaultFont", - ) +def _alert_style(style: ttk.Style, name: str, background: str): style.configure( - Styles.yellow_alert, - background="yellow", + name, + background=background, padding=0, relief=tk.RIDGE, borderwidth=1, font="TkDefaultFont", + foreground="black", + highlightbackground="white", ) + style.map(name, background=[("!active", background), ("active", "white")]) + + +def theme_change(event: tk.Event) -> None: + style = ttk.Style() + style.configure(Styles.picker_button, font="TkSmallCaptionFont") style.configure( - Styles.red_alert, - background="red", - padding=0, - relief=tk.RIDGE, - borderwidth=1, - font="TkDefaultFont", + Styles.no_alert, padding=0, relief=tk.RIDGE, borderwidth=1, font="TkDefaultFont" ) + _alert_style(style, Styles.green_alert, "green") + _alert_style(style, Styles.yellow_alert, "yellow") + _alert_style(style, Styles.red_alert, "red") -def scale_fonts(fonts_size, scale): +def scale_fonts(fonts_size: Dict[str, int], scale: float) -> None: for name in font.names(): f = font.nametofont(name) if name in fonts_size: diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 54fac126f..968b447d7 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -3,7 +3,7 @@ from enum import Enum from functools import partial from tkinter import ttk -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, List, Optional from PIL.ImageTk import PhotoImage @@ -23,8 +23,8 @@ if TYPE_CHECKING: from core.gui.app import Application -TOOLBAR_SIZE = 32 -PICKER_SIZE = 24 +TOOLBAR_SIZE: int = 32 +PICKER_SIZE: int = 24 class NodeTypeEnum(Enum): @@ -42,8 +42,8 @@ def enable_buttons(frame: ttk.Frame, enabled: bool) -> None: class PickerFrame(ttk.Frame): def __init__(self, app: "Application", button: ttk.Button) -> None: super().__init__(app) - self.app = app - self.button = button + self.app: "Application" = app + self.button: ttk.Button = button def create_node_button(self, node_draw: NodeDraw, func: Callable) -> None: self.create_button( @@ -85,10 +85,10 @@ def _show(self) -> None: class ButtonBar(ttk.Frame): - def __init__(self, master: tk.Widget, app: "Application"): + def __init__(self, master: tk.Widget, app: "Application") -> None: super().__init__(master) - self.app = app - self.radio_buttons = [] + self.app: "Application" = app + self.radio_buttons: List[ttk.Button] = [] def create_button( self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False @@ -109,14 +109,14 @@ def select_radio(self, selected: ttk.Button) -> None: class MarkerFrame(ttk.Frame): - PAD = 3 + PAD: int = 3 def __init__(self, master: tk.BaseWidget, app: "Application") -> None: super().__init__(master, padding=self.PAD) - self.app = app - self.color = "#000000" - self.size = tk.DoubleVar() - self.color_frame = None + self.app: "Application" = app + self.color: str = "#000000" + self.size: tk.DoubleVar = tk.DoubleVar() + self.color_frame: Optional[tk.Frame] = None self.draw() def draw(self) -> None: @@ -144,7 +144,7 @@ def draw(self) -> None: self.color_frame.bind("", self.click_color) Tooltip(self.color_frame, "Marker Color") - def click_clear(self): + def click_clear(self) -> None: self.app.canvas.delete(tags.MARKER) def click_color(self, _event: tk.Event) -> None: @@ -163,37 +163,37 @@ def __init__(self, app: "Application") -> None: Create a CoreToolbar instance """ super().__init__(app) - self.app = app + self.app: "Application" = app # design buttons - self.play_button = None - self.select_button = None - self.link_button = None - self.node_button = None - self.network_button = None - self.annotation_button = None + self.play_button: Optional[ttk.Button] = None + self.select_button: Optional[ttk.Button] = None + self.link_button: Optional[ttk.Button] = None + self.node_button: Optional[ttk.Button] = None + self.network_button: Optional[ttk.Button] = None + self.annotation_button: Optional[ttk.Button] = None # runtime buttons - self.runtime_select_button = None - self.stop_button = None - self.runtime_marker_button = None - self.run_command_button = None + self.runtime_select_button: Optional[ttk.Button] = None + self.stop_button: Optional[ttk.Button] = None + self.runtime_marker_button: Optional[ttk.Button] = None + self.run_command_button: Optional[ttk.Button] = None # frames - self.design_frame = None - self.runtime_frame = None - self.marker_frame = None - self.picker = None + self.design_frame: Optional[ButtonBar] = None + self.runtime_frame: Optional[ButtonBar] = None + self.marker_frame: Optional[MarkerFrame] = None + self.picker: Optional[PickerFrame] = None # observers - self.observers_menu = None + self.observers_menu: Optional[ObserversMenu] = None # these variables help keep track of what images being drawn so that scaling # is possible since PhotoImage does not have resize method - self.current_node = NodeUtils.NODES[0] - self.current_network = NodeUtils.NETWORK_NODES[0] - self.current_annotation = ShapeType.MARKER - self.annotation_enum = ImageEnum.MARKER + self.current_node: NodeDraw = NodeUtils.NODES[0] + self.current_network: NodeDraw = NodeUtils.NETWORK_NODES[0] + self.current_annotation: ShapeType = ShapeType.MARKER + self.annotation_enum: ImageEnum = ImageEnum.MARKER # draw components self.draw() @@ -307,8 +307,9 @@ def start_callback(self, response: core_pb2.StartSessionResponse) -> None: self.app.core.show_mobility_players() else: enable_buttons(self.design_frame, enabled=True) - message = "\n".join(response.exceptions) - self.app.show_error("Start Session Error", message) + if response.exceptions: + message = "\n".join(response.exceptions) + self.app.show_error("Start Session Error", message) def set_runtime(self) -> None: enable_buttons(self.runtime_frame, enabled=True) diff --git a/daemon/core/gui/tooltip.py b/daemon/core/gui/tooltip.py index bc1ed9b5e..c29785106 100644 --- a/daemon/core/gui/tooltip.py +++ b/daemon/core/gui/tooltip.py @@ -1,5 +1,6 @@ import tkinter as tk from tkinter import ttk +from typing import Optional from core.gui.themes import Styles @@ -9,19 +10,19 @@ class Tooltip(object): Create tool tip for a given widget """ - def __init__(self, widget: tk.Widget, text: str = "widget info"): - self.widget = widget - self.text = text + def __init__(self, widget: tk.BaseWidget, text: str = "widget info") -> None: + self.widget: tk.BaseWidget = widget + self.text: str = text self.widget.bind("", self.on_enter) self.widget.bind("", self.on_leave) - self.waittime = 400 - self.id = None - self.tw = None + self.waittime: int = 400 + self.id: Optional[str] = None + self.tw: Optional[tk.Toplevel] = None - def on_enter(self, event: tk.Event = None): + def on_enter(self, event: tk.Event = None) -> None: self.schedule() - def on_leave(self, event: tk.Event = None): + def on_leave(self, event: tk.Event = None) -> None: self.unschedule() self.close(event) @@ -39,7 +40,6 @@ def enter(self, event: tk.Event = None): x, y, cx, cy = self.widget.bbox("insert") x += self.widget.winfo_rootx() y += self.widget.winfo_rooty() + 32 - self.tw = tk.Toplevel(self.widget) self.tw.wm_overrideredirect(True) self.tw.wm_geometry("+%d+%d" % (x, y)) diff --git a/daemon/core/gui/utils.py b/daemon/core/gui/utils.py new file mode 100644 index 000000000..ee5ad8cba --- /dev/null +++ b/daemon/core/gui/utils.py @@ -0,0 +1,10 @@ +def bandwidth_text(bandwidth: int) -> str: + size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"} + unit = 1000 + i = 0 + while bandwidth > unit: + bandwidth /= unit + i += 1 + if i == 3: + break + return f"{bandwidth} {size[i]}" diff --git a/daemon/core/gui/validation.py b/daemon/core/gui/validation.py index 873db1893..22f12bb8b 100644 --- a/daemon/core/gui/validation.py +++ b/daemon/core/gui/validation.py @@ -4,16 +4,23 @@ import re import tkinter as tk from tkinter import ttk +from typing import Any, Optional, Pattern -SMALLEST_SCALE = 0.5 -LARGEST_SCALE = 5.0 -HEX_REGEX = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$") +SMALLEST_SCALE: float = 0.5 +LARGEST_SCALE: float = 5.0 +HEX_REGEX: Pattern = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$") class ValidationEntry(ttk.Entry): - empty = None - - def __init__(self, master=None, widget=None, empty_enabled=True, **kwargs) -> None: + empty: Optional[str] = None + + def __init__( + self, + master: tk.BaseWidget = None, + widget: tk.BaseWidget = None, + empty_enabled: bool = True, + **kwargs: Any + ) -> None: super().__init__(master, widget, **kwargs) cmd = self.register(self.is_valid) self.configure(validate="key", validatecommand=(cmd, "%P")) @@ -30,7 +37,7 @@ def focus_out(self, _event: tk.Event) -> None: class PositiveIntEntry(ValidationEntry): - empty = "0" + empty: str = "0" def is_valid(self, s: str) -> bool: if not s: @@ -92,7 +99,7 @@ def is_valid(self, s: str) -> bool: class NodeNameEntry(ValidationEntry): - empty = "noname" + empty: str = "noname" def is_valid(self, s: str) -> bool: if len(s) < 0: diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 6f51bd8c0..81bad0f56 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -1,53 +1,63 @@ import logging import tkinter as tk from functools import partial -from pathlib import PosixPath +from pathlib import Path from tkinter import filedialog, font, ttk -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type -from core.api.grpc import common_pb2, core_pb2 +from core.api.grpc import core_pb2 +from core.api.grpc.common_pb2 import ConfigOption +from core.api.grpc.core_pb2 import ConfigOptionType from core.gui import themes, validation +from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY if TYPE_CHECKING: from core.gui.app import Application - from core.gui.dialogs.dialog import Dialog - -INT_TYPES = { - core_pb2.ConfigOptionType.UINT8, - core_pb2.ConfigOptionType.UINT16, - core_pb2.ConfigOptionType.UINT32, - core_pb2.ConfigOptionType.UINT64, - core_pb2.ConfigOptionType.INT8, - core_pb2.ConfigOptionType.INT16, - core_pb2.ConfigOptionType.INT32, - core_pb2.ConfigOptionType.INT64, + +INT_TYPES: Set[ConfigOptionType] = { + ConfigOptionType.UINT8, + ConfigOptionType.UINT16, + ConfigOptionType.UINT32, + ConfigOptionType.UINT64, + ConfigOptionType.INT8, + ConfigOptionType.INT16, + ConfigOptionType.INT32, + ConfigOptionType.INT64, } -def file_button_click(value: tk.StringVar, parent: tk.Widget): +def file_button_click(value: tk.StringVar, parent: tk.Widget) -> None: file_path = filedialog.askopenfilename(title="Select File", parent=parent) if file_path: value.set(file_path) class FrameScroll(ttk.Frame): - def __init__(self, master: tk.Widget, app: "Application", _cls=ttk.Frame, **kw): + def __init__( + self, + master: tk.Widget, + app: "Application", + _cls: Type[ttk.Frame] = ttk.Frame, + **kw: Any + ) -> None: super().__init__(master, **kw) - self.app = app + self.app: "Application" = app self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) bg = self.app.style.lookup(".", "background") - self.canvas = tk.Canvas(self, highlightthickness=0, background=bg) + self.canvas: tk.Canvas = tk.Canvas(self, highlightthickness=0, background=bg) self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2) self.canvas.columnconfigure(0, weight=1) self.canvas.rowconfigure(0, weight=1) - self.scrollbar = ttk.Scrollbar( + self.scrollbar: ttk.Scrollbar = ttk.Scrollbar( self, orient="vertical", command=self.canvas.yview ) self.scrollbar.grid(row=0, column=1, sticky="ns") - self.frame = _cls(self.canvas) - self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) + self.frame: ttk.Frame = _cls(self.canvas) + self.frame_id: int = self.canvas.create_window( + 0, 0, anchor="nw", window=self.frame + ) self.canvas.update_idletasks() self.canvas.configure( scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set @@ -55,16 +65,16 @@ def __init__(self, master: tk.Widget, app: "Application", _cls=ttk.Frame, **kw): self.frame.bind("", self._configure_frame) self.canvas.bind("", self._configure_canvas) - def _configure_frame(self, event: tk.Event): + def _configure_frame(self, event: tk.Event) -> None: req_width = self.frame.winfo_reqwidth() if req_width != self.canvas.winfo_reqwidth(): self.canvas.configure(width=req_width) self.canvas.configure(scrollregion=self.canvas.bbox("all")) - def _configure_canvas(self, event: tk.Event): + def _configure_canvas(self, event: tk.Event) -> None: self.canvas.itemconfig(self.frame_id, width=event.width) - def clear(self): + def clear(self) -> None: for widget in self.frame.winfo_children(): widget.destroy() @@ -74,15 +84,17 @@ def __init__( self, master: tk.Widget, app: "Application", - config: Dict[str, common_pb2.ConfigOption], - **kw - ): + config: Dict[str, ConfigOption], + enabled: bool = True, + **kw: Any + ) -> None: super().__init__(master, **kw) - self.app = app - self.config = config - self.values = {} + self.app: "Application" = app + self.config: Dict[str, ConfigOption] = config + self.values: Dict[str, tk.StringVar] = {} + self.enabled: bool = enabled - def draw_config(self): + def draw_config(self) -> None: group_mapping = {} for key in self.config: option = self.config[key] @@ -100,8 +112,9 @@ def draw_config(self): value = tk.StringVar() if option.type == core_pb2.ConfigOptionType.BOOL: select = ("On", "Off") + state = "readonly" if self.enabled else tk.DISABLED combobox = ttk.Combobox( - tab.frame, textvariable=value, values=select, state="readonly" + tab.frame, textvariable=value, values=select, state=state ) combobox.grid(row=index, column=1, sticky="ew") if option.value == "1": @@ -111,38 +124,47 @@ def draw_config(self): elif option.select: value.set(option.value) select = tuple(option.select) + state = "readonly" if self.enabled else tk.DISABLED combobox = ttk.Combobox( - tab.frame, textvariable=value, values=select, state="readonly" + tab.frame, textvariable=value, values=select, state=state ) combobox.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) + state = tk.NORMAL if self.enabled else tk.DISABLED if "file" in option.label: file_frame = ttk.Frame(tab.frame) file_frame.grid(row=index, column=1, sticky="ew") file_frame.columnconfigure(0, weight=1) - entry = ttk.Entry(file_frame, textvariable=value) + entry = ttk.Entry(file_frame, textvariable=value, state=state) entry.grid(row=0, column=0, sticky="ew", padx=PADX) func = partial(file_button_click, value, self) - button = ttk.Button(file_frame, text="...", command=func) + button = ttk.Button( + file_frame, text="...", command=func, state=state + ) button.grid(row=0, column=1) else: - entry = ttk.Entry(tab.frame, textvariable=value) + entry = ttk.Entry(tab.frame, textvariable=value, state=state) entry.grid(row=index, column=1, sticky="ew") - elif option.type in INT_TYPES: value.set(option.value) - entry = validation.PositiveIntEntry(tab.frame, textvariable=value) + state = tk.NORMAL if self.enabled else tk.DISABLED + entry = validation.PositiveIntEntry( + tab.frame, textvariable=value, state=state + ) entry.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) - entry = validation.PositiveFloatEntry(tab.frame, textvariable=value) + state = tk.NORMAL if self.enabled else tk.DISABLED + entry = validation.PositiveFloatEntry( + tab.frame, textvariable=value, state=state + ) entry.grid(row=index, column=1, sticky="ew") else: logging.error("unhandled config option type: %s", option.type) self.values[option.name] = value - def parse_config(self): + def parse_config(self) -> Dict[str, str]: for key in self.config: option = self.config[key] value = self.values[key] @@ -169,13 +191,13 @@ def set_values(self, config: Dict[str, str]) -> None: class ListboxScroll(ttk.Frame): - def __init__(self, master: tk.Widget = None, **kw): + def __init__(self, master: tk.BaseWidget = None, **kw: Any) -> None: super().__init__(master, **kw) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) + self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) self.scrollbar.grid(row=0, column=1, sticky="ns") - self.listbox = tk.Listbox( + self.listbox: tk.Listbox = tk.Listbox( self, selectmode=tk.BROWSE, yscrollcommand=self.scrollbar.set, @@ -187,12 +209,18 @@ def __init__(self, master: tk.Widget = None, **kw): class CheckboxList(FrameScroll): - def __init__(self, master: ttk.Widget, app: "Application", clicked=None, **kw): + def __init__( + self, + master: ttk.Widget, + app: "Application", + clicked: Callable = None, + **kw: Any + ) -> None: super().__init__(master, app, **kw) - self.clicked = clicked + self.clicked: Callable = clicked self.frame.columnconfigure(0, weight=1) - def add(self, name: str, checked: bool): + def add(self, name: str, checked: bool) -> None: var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) @@ -200,16 +228,16 @@ def add(self, name: str, checked: bool): class CodeFont(font.Font): - def __init__(self): + def __init__(self) -> None: super().__init__(font="TkFixedFont", color="green") class CodeText(ttk.Frame): - def __init__(self, master: tk.Widget, **kwargs): + def __init__(self, master: tk.BaseWidget, **kwargs: Any) -> None: super().__init__(master, **kwargs) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) - self.text = tk.Text( + self.text: tk.Text = tk.Text( self, bd=0, bg="black", @@ -229,14 +257,14 @@ def __init__(self, master: tk.Widget, **kwargs): class Spinbox(ttk.Entry): - def __init__(self, master: tk.Widget = None, **kwargs): + def __init__(self, master: tk.BaseWidget = None, **kwargs: Any) -> None: super().__init__(master, "ttk::spinbox", **kwargs) - def set(self, value): + def set(self, value: str) -> None: self.tk.call(self._w, "set", value) -def image_chooser(parent: "Dialog", path: PosixPath): +def image_chooser(parent: Dialog, path: Path) -> str: return filedialog.askopenfilename( parent=parent, initialdir=str(path), diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index e9efa16bb..e982c5c1c 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -5,16 +5,15 @@ import heapq import logging import math -import os import threading import time from functools import total_ordering +from pathlib import Path from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from core import utils from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager -from core.emulator.data import EventData, LinkData -from core.emulator.emudata import LinkOptions +from core.emulator.data import EventData, LinkData, LinkOptions from core.emulator.enumerations import ( ConfigDataTypes, EventTypes, @@ -178,7 +177,7 @@ def sendevent(self, model: "WayPointMobility") -> None: self.session.broadcast_event(event_data) def updatewlans( - self, moved: List[CoreNode], moved_netifs: List[CoreInterface] + self, moved: List[CoreNode], moved_ifaces: List[CoreInterface] ) -> None: """ A mobility script has caused nodes in the 'moved' list to move. @@ -186,7 +185,7 @@ def updatewlans( were to recalculate for each individual node movement. :param moved: moved nodes - :param moved_netifs: moved network interfaces + :param moved_ifaces: moved network interfaces :return: nothing """ for node_id in self.nodes(): @@ -195,7 +194,7 @@ def updatewlans( except CoreError: continue if node.model: - node.model.update(moved, moved_netifs) + node.model.update(moved, moved_ifaces) class WirelessModel(ConfigurableOptions): @@ -218,7 +217,7 @@ def __init__(self, session: "Session", _id: int) -> None: self.session: "Session" = session self.id: int = _id - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ May be used if the model can populate the GUI with wireless (green) link lines. @@ -228,12 +227,12 @@ def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkDat """ return [] - def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None: + def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None: """ Update this wireless model. :param moved: moved nodes - :param moved_netifs: moved network interfaces + :param moved_ifaces: moved network interfaces :return: nothing """ raise NotImplementedError @@ -301,8 +300,8 @@ def __init__(self, session: "Session", _id: int) -> None: super().__init__(session, _id) self.session: "Session" = session self.wlan: WlanNode = session.get_node(_id, WlanNode) - self._netifs: Dict[CoreInterface, Tuple[float, float, float]] = {} - self._netifslock: threading.Lock = threading.Lock() + self.iface_to_pos: Dict[CoreInterface, Tuple[float, float, float]] = {} + self.iface_lock: threading.Lock = threading.Lock() self.range: int = 0 self.bw: Optional[int] = None self.delay: Optional[int] = None @@ -333,48 +332,48 @@ def setlinkparams(self) -> None: Apply link parameters to all interfaces. This is invoked from WlanNode.setmodel() after the position callback has been set. """ - with self._netifslock: - for netif in self._netifs: + with self.iface_lock: + for iface in self.iface_to_pos: options = LinkOptions( bandwidth=self.bw, delay=self.delay, - per=self.loss, + loss=self.loss, jitter=self.jitter, ) - self.wlan.linkconfig(netif, options) + self.wlan.linkconfig(iface, options) - def get_position(self, netif: CoreInterface) -> Tuple[float, float, float]: + def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]: """ Retrieve network interface position. - :param netif: network interface position to retrieve + :param iface: network interface position to retrieve :return: network interface position """ - with self._netifslock: - return self._netifs[netif] + with self.iface_lock: + return self.iface_to_pos[iface] - def set_position(self, netif: CoreInterface) -> None: + def set_position(self, iface: CoreInterface) -> None: """ A node has moved; given an interface, a new (x,y,z) position has been set; calculate the new distance between other nodes and link or unlink node pairs based on the configured range. - :param netif: network interface to set position for + :param iface: network interface to set position for :return: nothing """ - x, y, z = netif.node.position.get() - self._netifslock.acquire() - self._netifs[netif] = (x, y, z) + x, y, z = iface.node.position.get() + self.iface_lock.acquire() + self.iface_to_pos[iface] = (x, y, z) if x is None or y is None: - self._netifslock.release() + self.iface_lock.release() return - for netif2 in self._netifs: - self.calclink(netif, netif2) - self._netifslock.release() + for iface2 in self.iface_to_pos: + self.calclink(iface, iface2) + self.iface_lock.release() position_callback = set_position - def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None: + def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None: """ Node positions have changed without recalc. Update positions from node.position, then re-calculate links for those that have moved. @@ -382,37 +381,37 @@ def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> No one of the nodes has moved. :param moved: moved nodes - :param moved_netifs: moved network interfaces + :param moved_ifaces: moved network interfaces :return: nothing """ - with self._netifslock: - while len(moved_netifs): - netif = moved_netifs.pop() - nx, ny, nz = netif.node.getposition() - if netif in self._netifs: - self._netifs[netif] = (nx, ny, nz) - for netif2 in self._netifs: - if netif2 in moved_netifs: + with self.iface_lock: + while len(moved_ifaces): + iface = moved_ifaces.pop() + nx, ny, nz = iface.node.getposition() + if iface in self.iface_to_pos: + self.iface_to_pos[iface] = (nx, ny, nz) + for iface2 in self.iface_to_pos: + if iface2 in moved_ifaces: continue - self.calclink(netif, netif2) + self.calclink(iface, iface2) - def calclink(self, netif: CoreInterface, netif2: CoreInterface) -> None: + def calclink(self, iface: CoreInterface, iface2: CoreInterface) -> None: """ Helper used by set_position() and update() to calculate distance between two interfaces and perform linking/unlinking. Sends link/unlink messages and updates the WlanNode's linked dict. - :param netif: interface one - :param netif2: interface two + :param iface: interface one + :param iface2: interface two :return: nothing """ - if netif == netif2: + if iface == iface2: return try: - x, y, z = self._netifs[netif] - x2, y2, z2 = self._netifs[netif2] + x, y, z = self.iface_to_pos[iface] + x2, y2, z2 = self.iface_to_pos[iface2] if x2 is None or y2 is None: return @@ -420,8 +419,8 @@ def calclink(self, netif: CoreInterface, netif2: CoreInterface) -> None: d = self.calcdistance((x, y, z), (x2, y2, z2)) # ordering is important, to keep the wlan._linked dict organized - a = min(netif, netif2) - b = max(netif, netif2) + a = min(iface, iface2) + b = max(iface, iface2) with self.wlan._linked_lock: linked = self.wlan.linked(a, b) @@ -475,45 +474,42 @@ def update_config(self, config: Dict[str, str]) -> None: self.setlinkparams() def create_link_data( - self, - interface1: CoreInterface, - interface2: CoreInterface, - message_type: MessageFlags, + self, iface1: CoreInterface, iface2: CoreInterface, message_type: MessageFlags ) -> LinkData: """ Create a wireless link/unlink data message. - :param interface1: interface one - :param interface2: interface two + :param iface1: interface one + :param iface2: interface two :param message_type: link message type :return: link data """ color = self.session.get_link_color(self.wlan.id) return LinkData( message_type=message_type, - node1_id=interface1.node.id, - node2_id=interface2.node.id, + type=LinkTypes.WIRELESS, + node1_id=iface1.node.id, + node2_id=iface2.node.id, network_id=self.wlan.id, - link_type=LinkTypes.WIRELESS, color=color, ) def sendlinkmsg( - self, netif: CoreInterface, netif2: CoreInterface, unlink: bool = False + self, iface: CoreInterface, iface2: CoreInterface, unlink: bool = False ) -> None: """ Send a wireless link/unlink API message to the GUI. - :param netif: interface one - :param netif2: interface two + :param iface: interface one + :param iface2: interface two :param unlink: unlink or not :return: nothing """ message_type = MessageFlags.DELETE if unlink else MessageFlags.ADD - link_data = self.create_link_data(netif, netif2, message_type) + link_data = self.create_link_data(iface, iface2, message_type) self.session.broadcast_link(link_data) - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Return a list of wireless link messages for when the GUI reconnects. @@ -643,17 +639,17 @@ def runround(self) -> None: return return self.run() - # only move netifs attached to self.wlan, or all nodenum in script? + # only move interfaces attached to self.wlan, or all nodenum in script? moved = [] - moved_netifs = [] - for netif in self.wlan.netifs(): - node = netif.node + moved_ifaces = [] + for iface in self.wlan.get_ifaces(): + node = iface.node if self.movenode(node, dt): moved.append(node) - moved_netifs.append(netif) + moved_ifaces.append(iface) # calculate all ranges after moving nodes; this saves calculations - self.session.mobility.updatewlans(moved, moved_netifs) + self.session.mobility.updatewlans(moved, moved_ifaces) # TODO: check session state self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround) @@ -725,16 +721,16 @@ def movenodesinitial(self) -> None: :return: nothing """ moved = [] - moved_netifs = [] - for netif in self.wlan.netifs(): - node = netif.node + moved_ifaces = [] + for iface in self.wlan.get_ifaces(): + node = iface.node if node.id not in self.initial: continue x, y, z = self.initial[node.id].coords self.setnodeposition(node, x, y, z) moved.append(node) - moved_netifs.append(netif) - self.session.mobility.updatewlans(moved, moved_netifs) + moved_ifaces.append(iface) + self.session.mobility.updatewlans(moved, moved_ifaces) def addwaypoint( self, @@ -1034,30 +1030,28 @@ def readscriptfile(self) -> None: def findfile(self, file_name: str) -> str: """ Locate a script file. If the specified file doesn't exist, look in the - same directory as the scenario file, or in the default - configs directory (~/.core/configs). This allows for sample files without - absolute path names. + same directory as the scenario file, or in gui directories. :param file_name: file name to find :return: absolute path to the file - """ - if os.path.exists(file_name): - return file_name - - if self.session.file_name is not None: - d = os.path.dirname(self.session.file_name) - sessfn = os.path.join(d, file_name) - if os.path.exists(sessfn): - return sessfn - - if self.session.user is not None: - userfn = os.path.join( - "/home", self.session.user, ".core", "configs", file_name - ) - if os.path.exists(userfn): - return userfn - - return file_name + :raises CoreError: when file is not found + """ + file_path = Path(file_name).expanduser() + if file_path.exists(): + return str(file_path) + if self.session.file_name: + file_path = Path(self.session.file_name).parent / file_name + if file_path.exists(): + return str(file_path) + if self.session.user: + user_path = Path(f"~{self.session.user}").expanduser() + file_path = user_path / ".core" / "configs" / file_name + if file_path.exists(): + return str(file_path) + file_path = user_path / ".coregui" / "mobility" / file_name + if file_path.exists(): + return str(file_path) + raise CoreError(f"invalid file: {file_name}") def parsemap(self, mapstr: str) -> None: """ diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 0c76d6a23..cea1e81bd 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1,23 +1,22 @@ """ Defines the base logic for nodes used within core. """ - +import abc import logging import os import shutil import threading from threading import RLock -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union import netaddr from core import utils from core.configservice.dependencies import ConfigServiceDependencies -from core.constants import MOUNT_BIN, VNODED_BIN -from core.emulator.data import LinkData, NodeData -from core.emulator.emudata import InterfaceData, LinkOptions +from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError +from core.executables import MOUNT, VNODED from core.nodes.client import VnodeClient from core.nodes.interface import CoreInterface, TunTap, Veth from core.nodes.netclient import LinuxNetClient, get_net_client @@ -28,26 +27,24 @@ from core.configservice.base import ConfigService from core.services.coreservices import CoreService - CoreServices = List[CoreService] + CoreServices = List[Union[CoreService, Type[CoreService]]] ConfigServiceType = Type[ConfigService] _DEFAULT_MTU = 1500 -class NodeBase: +class NodeBase(abc.ABC): """ Base class for CORE nodes (nodes and networks) """ apitype: Optional[NodeTypes] = None - # TODO: appears start has no usage, verify and remove def __init__( self, session: "Session", _id: int = None, name: str = None, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ @@ -56,14 +53,13 @@ def __init__( :param session: CORE session object :param _id: id :param name: object name - :param start: start value :param server: remote server node will run on, default is None for localhost """ self.session: "Session" = session if _id is None: - _id = session.get_node_id() + _id = session.next_node_id() self.id: int = _id if name is None: name = f"o{self.id}" @@ -71,16 +67,17 @@ def __init__( self.server: "DistributedServer" = server self.type: Optional[str] = None self.services: CoreServices = [] - self._netif: Dict[int, CoreInterface] = {} - self.ifindex: int = 0 + self.ifaces: Dict[int, CoreInterface] = {} + self.iface_id: int = 0 self.canvas: Optional[int] = None self.icon: Optional[str] = None - self.opaque: Optional[str] = None self.position: Position = Position() self.up: bool = False - use_ovs = session.options.get_config("ovs") == "True" - self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd) + self.net_client: LinuxNetClient = get_net_client( + self.session.use_ovs(), self.host_cmd + ) + @abc.abstractmethod def startup(self) -> None: """ Each object implements its own startup method. @@ -89,6 +86,7 @@ def startup(self) -> None: """ raise NotImplementedError + @abc.abstractmethod def shutdown(self) -> None: """ Each object implements its own shutdown method. @@ -140,103 +138,61 @@ def getposition(self) -> Tuple[float, float, float]: """ return self.position.get() - def ifname(self, ifindex: int) -> str: - """ - Retrieve interface name for index. - - :param ifindex: interface index - :return: interface name - """ - return self._netif[ifindex].name - - def netifs(self, sort: bool = False) -> List[CoreInterface]: + def get_iface(self, iface_id: int) -> CoreInterface: """ - Retrieve network interfaces, sorted if desired. + Retrieve interface based on id. - :param sort: boolean used to determine if interfaces should be sorted - :return: network interfaces + :param iface_id: id of interface to retrieve + :return: interface + :raises CoreError: when interface does not exist """ - if sort: - return [self._netif[x] for x in sorted(self._netif)] - else: - return list(self._netif.values()) + if iface_id not in self.ifaces: + raise CoreError(f"node({self.name}) does not have interface({iface_id})") + return self.ifaces[iface_id] - def numnetif(self) -> int: + def get_ifaces(self, control: bool = True) -> List[CoreInterface]: """ - Return the attached interface count. + Retrieve sorted list of interfaces, optionally do not include control + interfaces. - :return: number of network interfaces + :param control: False to exclude control interfaces, included otherwise + :return: list of interfaces """ - return len(self._netif) + ifaces = [] + for iface_id in sorted(self.ifaces): + iface = self.ifaces[iface_id] + if not control and getattr(iface, "control", False): + continue + ifaces.append(iface) + return ifaces - def getifindex(self, netif: CoreInterface) -> int: + def get_iface_id(self, iface: CoreInterface) -> int: """ - Retrieve index for an interface. + Retrieve id for an interface. - :param netif: interface to get index for + :param iface: interface to get id for :return: interface index if found, -1 otherwise """ - for ifindex in self._netif: - if self._netif[ifindex] is netif: - return ifindex - return -1 + for iface_id, local_iface in self.ifaces.items(): + if local_iface is iface: + return iface_id + raise CoreError(f"node({self.name}) does not have interface({iface.name})") - def newifindex(self) -> int: + def next_iface_id(self) -> int: """ Create a new interface index. :return: interface index """ - while self.ifindex in self._netif: - self.ifindex += 1 - ifindex = self.ifindex - self.ifindex += 1 - return ifindex - - def data( - self, message_type: MessageFlags = MessageFlags.NONE, source: str = None - ) -> Optional[NodeData]: - """ - Build a data object for this node. - - :param message_type: purpose for the data object we are creating - :param source: source of node data - :return: node data object - """ - if self.apitype is None: - return None - - x, y, _ = self.getposition() - model = self.type - server = None - if self.server is not None: - server = self.server.name - services = [service.name for service in self.services] - return NodeData( - message_type=message_type, - id=self.id, - node_type=self.apitype, - name=self.name, - emulation_id=self.id, - canvas=self.canvas, - icon=self.icon, - opaque=self.opaque, - x_position=x, - y_position=y, - latitude=self.position.lat, - longitude=self.position.lon, - altitude=self.position.alt, - model=model, - server=server, - services=services, - source=source, - ) + while self.iface_id in self.ifaces: + self.iface_id += 1 + iface_id = self.iface_id + self.iface_id += 1 + return iface_id - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ - Build CORE Link data for this object. There is no default - method for PyCoreObjs as PyCoreNodes do not implement this but - PyCoreNets do. + Build link data for this node. :param flags: message flags :return: list of link data @@ -254,7 +210,6 @@ def __init__( session: "Session", _id: int = None, name: str = None, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ @@ -263,15 +218,82 @@ def __init__( :param session: CORE session object :param _id: object id :param name: object name - :param start: boolean for starting :param server: remote server node will run on, default is None for localhost """ - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) self.config_services: Dict[str, "ConfigService"] = {} self.nodedir: Optional[str] = None self.tmpnodedir: bool = False + @abc.abstractmethod + def startup(self) -> None: + raise NotImplementedError + + @abc.abstractmethod + def shutdown(self) -> None: + raise NotImplementedError + + @abc.abstractmethod + def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: + """ + Create a node file with a given mode. + + :param filename: name of file to create + :param contents: contents of file + :param mode: mode for file + :return: nothing + """ + raise NotImplementedError + + @abc.abstractmethod + def addfile(self, srcname: str, filename: str) -> None: + """ + Add a file. + + :param srcname: source file name + :param filename: file name to add + :return: nothing + :raises CoreCommandError: when a non-zero exit status occurs + """ + raise NotImplementedError + + @abc.abstractmethod + def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: + """ + Runs a command within a node container. + + :param args: command to run + :param wait: True to wait for status, False otherwise + :param shell: True to use shell, False otherwise + :return: combined stdout and stderr + :raises CoreCommandError: when a non-zero exit status occurs + """ + raise NotImplementedError + + @abc.abstractmethod + def termcmdstring(self, sh: str) -> str: + """ + Create a terminal command string. + + :param sh: shell to execute command in + :return: str + """ + raise NotImplementedError + + @abc.abstractmethod + def new_iface( + self, net: "CoreNetworkBase", iface_data: InterfaceData + ) -> CoreInterface: + """ + Create a new interface. + + :param net: network to associate with + :param iface_data: interface data for new interface + :return: interface index + """ + raise NotImplementedError + def add_config_service(self, service_class: "ConfigServiceType") -> None: """ Adds a configuration service to the node. @@ -334,66 +356,53 @@ def rmnodedir(self) -> None: if self.tmpnodedir: self.host_cmd(f"rm -rf {self.nodedir}") - def addnetif(self, netif: CoreInterface, ifindex: int) -> None: + def add_iface(self, iface: CoreInterface, iface_id: int) -> None: """ Add network interface to node and set the network interface index if successful. - :param netif: network interface to add - :param ifindex: interface index + :param iface: network interface to add + :param iface_id: interface id :return: nothing """ - if ifindex in self._netif: - raise ValueError(f"ifindex {ifindex} already exists") - self._netif[ifindex] = netif - netif.netindex = ifindex + if iface_id in self.ifaces: + raise CoreError(f"interface({iface_id}) already exists") + self.ifaces[iface_id] = iface + iface.node_id = iface_id - def delnetif(self, ifindex: int) -> None: + def delete_iface(self, iface_id: int) -> None: """ Delete a network interface - :param ifindex: interface index to delete + :param iface_id: interface index to delete :return: nothing """ - if ifindex not in self._netif: - raise ValueError(f"ifindex {ifindex} does not exist") - netif = self._netif.pop(ifindex) - netif.shutdown() - del netif + if iface_id not in self.ifaces: + raise CoreError(f"node({self.name}) interface({iface_id}) does not exist") + iface = self.ifaces.pop(iface_id) + logging.info("node(%s) removing interface(%s)", self.name, iface.name) + iface.detachnet() + iface.shutdown() - def netif(self, ifindex: int) -> Optional[CoreInterface]: - """ - Retrieve network interface. - - :param ifindex: index of interface to retrieve - :return: network interface, or None if not found - """ - if ifindex in self._netif: - return self._netif[ifindex] - else: - return None - - def attachnet(self, ifindex: int, net: "CoreNetworkBase") -> None: + def attachnet(self, iface_id: int, net: "CoreNetworkBase") -> None: """ Attach a network. - :param ifindex: interface of index to attach + :param iface_id: interface of index to attach :param net: network to attach :return: nothing """ - if ifindex not in self._netif: - raise ValueError(f"ifindex {ifindex} does not exist") - self._netif[ifindex].attachnet(net) + iface = self.get_iface(iface_id) + iface.attachnet(net) - def detachnet(self, ifindex: int) -> None: + def detachnet(self, iface_id: int) -> None: """ Detach network interface. - :param ifindex: interface index to detach + :param iface_id: interface id to detach :return: nothing """ - if ifindex not in self._netif: - raise ValueError(f"ifindex {ifindex} does not exist") - self._netif[ifindex].detachnet() + iface = self.get_iface(iface_id) + iface.detachnet() def setposition(self, x: float = None, y: float = None, z: float = None) -> None: """ @@ -406,8 +415,8 @@ def setposition(self, x: float = None, y: float = None, z: float = None) -> None """ changed = super().setposition(x, y, z) if changed: - for netif in self.netifs(sort=True): - netif.setposition() + for iface in self.get_ifaces(): + iface.setposition() def commonnets( self, node: "CoreNodeBase", want_ctrl: bool = False @@ -422,65 +431,19 @@ def commonnets( :return: tuples of common networks """ common = [] - for netif1 in self.netifs(): - if not want_ctrl and hasattr(netif1, "control"): - continue - for netif2 in node.netifs(): - if netif1.net == netif2.net: - common.append((netif1.net, netif1, netif2)) + for iface1 in self.get_ifaces(control=want_ctrl): + for iface2 in node.get_ifaces(): + if iface1.net == iface2.net: + common.append((iface1.net, iface1, iface2)) return common - def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: - """ - Create a node file with a given mode. - - :param filename: name of file to create - :param contents: contents of file - :param mode: mode for file - :return: nothing - """ - raise NotImplementedError - - def addfile(self, srcname: str, filename: str) -> None: - """ - Add a file. - - :param srcname: source file name - :param filename: file name to add - :return: nothing - :raises CoreCommandError: when a non-zero exit status occurs - """ - raise NotImplementedError - - def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: - """ - Runs a command within a node container. - - :param args: command to run - :param wait: True to wait for status, False otherwise - :param shell: True to use shell, False otherwise - :return: combined stdout and stderr - :raises CoreCommandError: when a non-zero exit status occurs - """ - raise NotImplementedError - - def termcmdstring(self, sh: str) -> str: - """ - Create a terminal command string. - - :param sh: shell to execute command in - :return: str - """ - raise NotImplementedError - class CoreNode(CoreNodeBase): """ Provides standard core node logic. """ - apitype = NodeTypes.DEFAULT - valid_address_types = {"inet", "inet6", "inet6link"} + apitype: NodeTypes = NodeTypes.DEFAULT def __init__( self, @@ -488,7 +451,6 @@ def __init__( _id: int = None, name: str = None, nodedir: str = None, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ @@ -498,11 +460,10 @@ def __init__( :param _id: object id :param name: object name :param nodedir: node directory - :param start: start flag :param server: remote server node will run on, default is None for localhost """ - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) self.nodedir: Optional[str] = nodedir self.ctrlchnlname: str = os.path.abspath( os.path.join(self.session.session_dir, self.name) @@ -511,10 +472,9 @@ def __init__( self.pid: Optional[int] = None self.lock: RLock = RLock() self._mounts: List[Tuple[str, str]] = [] - use_ovs = session.options.get_config("ovs") == "True" - self.node_net_client: LinuxNetClient = self.create_node_net_client(use_ovs) - if start: - self.startup() + self.node_net_client: LinuxNetClient = self.create_node_net_client( + self.session.use_ovs() + ) def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: """ @@ -553,7 +513,7 @@ def startup(self) -> None: # create a new namespace for this node using vnoded vnoded = ( - f"{VNODED_BIN} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log " + f"{VNODED} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log " f"-p {self.ctrlchnlname}.pid" ) if self.nodedir: @@ -601,8 +561,8 @@ def shutdown(self) -> None: self._mounts = [] # shutdown all interfaces - for netif in self.netifs(): - netif.shutdown() + for iface in self.get_ifaces(): + iface.shutdown() # kill node process if present try: @@ -617,7 +577,7 @@ def shutdown(self) -> None: logging.exception("error removing node directory") # clear interface data, close client, and mark self and not up - self._netif.clear() + self.ifaces.clear() self.client.close() self.up = False except OSError: @@ -682,39 +642,39 @@ def mount(self, source: str, target: str) -> None: source = os.path.abspath(source) logging.debug("node(%s) mounting: %s at %s", self.name, source, target) self.cmd(f"mkdir -p {target}") - self.cmd(f"{MOUNT_BIN} -n --bind {source} {target}") + self.cmd(f"{MOUNT} -n --bind {source} {target}") self._mounts.append((source, target)) - def newifindex(self) -> int: + def next_iface_id(self) -> int: """ Retrieve a new interface index. :return: new interface index """ with self.lock: - return super().newifindex() + return super().next_iface_id() - def newveth(self, ifindex: int = None, ifname: str = None) -> int: + def newveth(self, iface_id: int = None, ifname: str = None) -> int: """ Create a new interface. - :param ifindex: index for the new interface + :param iface_id: id for the new interface :param ifname: name for the new interface :return: nothing """ with self.lock: - if ifindex is None: - ifindex = self.newifindex() + if iface_id is None: + iface_id = self.next_iface_id() if ifname is None: - ifname = f"eth{ifindex}" + ifname = f"eth{iface_id}" sessionid = self.session.short_session_id() try: - suffix = f"{self.id:x}.{ifindex}.{sessionid}" + suffix = f"{self.id:x}.{iface_id}.{sessionid}" except TypeError: - suffix = f"{self.id}.{ifindex}.{sessionid}" + suffix = f"{self.id}.{iface_id}.{sessionid}" localname = f"veth{suffix}" if len(localname) >= 16: @@ -739,145 +699,131 @@ def newveth(self, ifindex: int = None, ifname: str = None) -> int: flow_id = self.node_net_client.get_ifindex(veth.name) veth.flow_id = int(flow_id) logging.debug("interface flow index: %s - %s", veth.name, veth.flow_id) - hwaddr = self.node_net_client.get_mac(veth.name) - logging.debug("interface mac: %s - %s", veth.name, hwaddr) - veth.sethwaddr(hwaddr) + mac = self.node_net_client.get_mac(veth.name) + logging.debug("interface mac: %s - %s", veth.name, mac) + veth.set_mac(mac) try: # add network interface to the node. If unsuccessful, destroy the # network interface and raise exception. - self.addnetif(veth, ifindex) + self.add_iface(veth, iface_id) except ValueError as e: veth.shutdown() del veth raise e - return ifindex + return iface_id - def newtuntap(self, ifindex: int = None, ifname: str = None) -> int: + def newtuntap(self, iface_id: int = None, ifname: str = None) -> int: """ Create a new tunnel tap. - :param ifindex: interface index + :param iface_id: interface id :param ifname: interface name :return: interface index """ with self.lock: - if ifindex is None: - ifindex = self.newifindex() + if iface_id is None: + iface_id = self.next_iface_id() if ifname is None: - ifname = f"eth{ifindex}" + ifname = f"eth{iface_id}" sessionid = self.session.short_session_id() - localname = f"tap{self.id}.{ifindex}.{sessionid}" + localname = f"tap{self.id}.{iface_id}.{sessionid}" name = ifname tuntap = TunTap(self.session, self, name, localname, start=self.up) try: - self.addnetif(tuntap, ifindex) + self.add_iface(tuntap, iface_id) except ValueError as e: tuntap.shutdown() del tuntap raise e - return ifindex + return iface_id - def sethwaddr(self, ifindex: int, addr: str) -> None: + def set_mac(self, iface_id: int, mac: str) -> None: """ - Set hardware addres for an interface. + Set hardware address for an interface. - :param ifindex: index of interface to set hardware address for - :param addr: hardware address to set + :param iface_id: id of interface to set hardware address for + :param mac: mac address to set :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - addr = utils.validate_mac(addr) - interface = self._netif[ifindex] - interface.sethwaddr(addr) + iface = self.get_iface(iface_id) + iface.set_mac(mac) if self.up: - self.node_net_client.device_mac(interface.name, addr) + self.node_net_client.device_mac(iface.name, str(iface.mac)) - def addaddr(self, ifindex: int, addr: str) -> None: + def add_ip(self, iface_id: int, ip: str) -> None: """ - Add interface address. + Add an ip address to an interface in the format "10.0.0.1/24". - :param ifindex: index of interface to add address to - :param addr: address to add to interface + :param iface_id: id of interface to add address to + :param ip: address to add to interface :return: nothing + :raises CoreError: when ip address provided is invalid + :raises CoreCommandError: when a non-zero exit status occurs """ - addr = utils.validate_ip(addr) - interface = self._netif[ifindex] - interface.addaddr(addr) + iface = self.get_iface(iface_id) + iface.add_ip(ip) if self.up: # ipv4 check broadcast = None - if netaddr.valid_ipv4(addr): + if netaddr.valid_ipv4(ip): broadcast = "+" - self.node_net_client.create_address(interface.name, addr, broadcast) + self.node_net_client.create_address(iface.name, ip, broadcast) - def deladdr(self, ifindex: int, addr: str) -> None: + def remove_ip(self, iface_id: int, ip: str) -> None: """ - Delete address from an interface. + Remove an ip address from an interface in the format "10.0.0.1/24". - :param ifindex: index of interface to delete address from - :param addr: address to delete from interface + :param iface_id: id of interface to delete address from + :param ip: ip address to remove from interface :return: nothing + :raises CoreError: when ip address provided is invalid :raises CoreCommandError: when a non-zero exit status occurs """ - interface = self._netif[ifindex] - - try: - interface.deladdr(addr) - except ValueError: - logging.exception("trying to delete unknown address: %s", addr) - + iface = self.get_iface(iface_id) + iface.remove_ip(ip) if self.up: - self.node_net_client.delete_address(interface.name, addr) + self.node_net_client.delete_address(iface.name, ip) - def ifup(self, ifindex: int) -> None: + def ifup(self, iface_id: int) -> None: """ Bring an interface up. - :param ifindex: index of interface to bring up + :param iface_id: index of interface to bring up :return: nothing """ if self.up: - interface_name = self.ifname(ifindex) - self.node_net_client.device_up(interface_name) + iface = self.get_iface(iface_id) + self.node_net_client.device_up(iface.name) - def newnetif(self, net: "CoreNetworkBase", interface: InterfaceData) -> int: + def new_iface( + self, net: "CoreNetworkBase", iface_data: InterfaceData + ) -> CoreInterface: """ Create a new network interface. :param net: network to associate with - :param interface: interface data for new interface + :param iface_data: interface data for new interface :return: interface index """ - addresses = interface.get_addresses() with self.lock: - # TODO: emane specific code - if net.is_emane is True: - ifindex = self.newtuntap(interface.id, interface.name) - # 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 - self.attachnet(ifindex, net) - netif = self.netif(ifindex) - netif.sethwaddr(interface.mac) - for address in addresses: - netif.addaddr(address) - return ifindex + if net.has_custom_iface: + return net.custom_iface(self, iface_data) else: - ifindex = self.newveth(interface.id, interface.name) - self.attachnet(ifindex, net) - if interface.mac: - self.sethwaddr(ifindex, interface.mac) - for address in addresses: - self.addaddr(ifindex, address) - self.ifup(ifindex) - return ifindex + iface_id = self.newveth(iface_data.id, iface_data.name) + self.attachnet(iface_id, net) + if iface_data.mac: + self.set_mac(iface_id, iface_data.mac) + for ip in iface_data.get_ips(): + self.add_ip(iface_id, ip) + self.ifup(iface_id) + return self.get_iface(iface_id) def addfile(self, srcname: str, filename: str) -> None: """ @@ -966,15 +912,14 @@ class CoreNetworkBase(NodeBase): Base class for networks """ - linktype = LinkTypes.WIRED - is_emane = False + linktype: LinkTypes = LinkTypes.WIRED + has_custom_iface: bool = False def __init__( self, session: "Session", _id: int, name: str, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ @@ -983,15 +928,15 @@ def __init__( :param session: CORE session object :param _id: object id :param name: object name - :param start: should object start :param server: remote server node will run on, default is None for localhost """ - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) self.brname = None self._linked = {} self._linked_lock = threading.Lock() + @abc.abstractmethod def startup(self) -> None: """ Each object implements its own startup method. @@ -1000,6 +945,7 @@ def startup(self) -> None: """ raise NotImplementedError + @abc.abstractmethod def shutdown(self) -> None: """ Each object implements its own shutdown method. @@ -1008,6 +954,7 @@ def shutdown(self) -> None: """ raise NotImplementedError + @abc.abstractmethod def linknet(self, net: "CoreNetworkBase") -> CoreInterface: """ Link network to another. @@ -1015,46 +962,63 @@ def linknet(self, net: "CoreNetworkBase") -> CoreInterface: :param net: network to link with :return: created interface """ - pass + raise NotImplementedError - def getlinknetif(self, net: "CoreNetworkBase") -> Optional[CoreInterface]: + @abc.abstractmethod + def linkconfig( + self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None + ) -> None: """ - Return the interface of that links this net with another net. + Configure link parameters by applying tc queuing disciplines on the interface. + + :param iface: interface one + :param options: options for configuring link + :param iface2: interface two + :return: nothing + """ + raise NotImplementedError + + def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface: + raise NotImplementedError + + def get_linked_iface(self, net: "CoreNetworkBase") -> Optional[CoreInterface]: + """ + Return the interface that links this net with another net. :param net: interface to get link for :return: interface the provided network is linked to """ - for netif in self.netifs(): - if getattr(netif, "othernet", None) == net: - return netif + for iface in self.get_ifaces(): + if iface.othernet == net: + return iface return None - def attach(self, netif: CoreInterface) -> None: + def attach(self, iface: CoreInterface) -> None: """ Attach network interface. - :param netif: network interface to attach + :param iface: network interface to attach :return: nothing """ - i = self.newifindex() - self._netif[i] = netif - netif.netifi = i + i = self.next_iface_id() + self.ifaces[i] = iface + iface.net_id = i with self._linked_lock: - self._linked[netif] = {} + self._linked[iface] = {} - def detach(self, netif: CoreInterface) -> None: + def detach(self, iface: CoreInterface) -> None: """ Detach network interface. - :param netif: network interface to detach + :param iface: network interface to detach :return: nothing """ - del self._netif[netif.netifi] - netif.netifi = None + del self.ifaces[iface.net_id] + iface.net_id = None with self._linked_lock: - del self._linked[netif] + del self._linked[iface] - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build link data objects for this network. Each link object describes a link between this network and a node. @@ -1066,99 +1030,65 @@ def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkDat # build a link message from this network node to each node having a # connected interface - for netif in self.netifs(sort=True): - if not hasattr(netif, "node"): - continue + for iface in self.get_ifaces(): uni = False - linked_node = netif.node + linked_node = iface.node if linked_node is None: # two layer-2 switches/hubs linked together via linknet() - if not netif.othernet: + if not iface.othernet: continue - linked_node = netif.othernet + linked_node = iface.othernet if linked_node.id == self.id: continue - netif.swapparams("_params_up") - upstream_params = netif.getparams() - netif.swapparams("_params_up") - if netif.getparams() != upstream_params: + iface.swapparams("_params_up") + upstream_params = iface.getparams() + iface.swapparams("_params_up") + if iface.getparams() != upstream_params: uni = True unidirectional = 0 if uni: unidirectional = 1 - interface2_ip4 = None - interface2_ip4_mask = None - interface2_ip6 = None - interface2_ip6_mask = None - for address in netif.addrlist: - ip, _sep, mask = address.partition("/") - mask = int(mask) - if netaddr.valid_ipv4(ip): - interface2_ip4 = ip - interface2_ip4_mask = mask - else: - interface2_ip6 = ip - interface2_ip6_mask = mask - + mac = str(iface.mac) if iface.mac else None + iface2_data = InterfaceData( + id=linked_node.get_iface_id(iface), name=iface.name, mac=mac + ) + ip4 = iface.get_ip4() + if ip4: + iface2_data.ip4 = str(ip4.ip) + iface2_data.ip4_mask = ip4.prefixlen + ip6 = iface.get_ip6() + if ip6: + iface2_data.ip6 = str(ip6.ip) + iface2_data.ip6_mask = ip6.prefixlen + + options_data = iface.get_link_options(unidirectional) link_data = LinkData( message_type=flags, + type=self.linktype, node1_id=self.id, node2_id=linked_node.id, - link_type=self.linktype, - unidirectional=unidirectional, - interface2_id=linked_node.getifindex(netif), - interface2_name=netif.name, - interface2_mac=netif.hwaddr, - interface2_ip4=interface2_ip4, - interface2_ip4_mask=interface2_ip4_mask, - interface2_ip6=interface2_ip6, - interface2_ip6_mask=interface2_ip6_mask, - delay=netif.getparam("delay"), - bandwidth=netif.getparam("bw"), - dup=netif.getparam("duplicate"), - jitter=netif.getparam("jitter"), - per=netif.getparam("loss"), + iface2=iface2_data, + options=options_data, ) - all_links.append(link_data) if not uni: continue - - netif.swapparams("_params_up") + iface.swapparams("_params_up") + options_data = iface.get_link_options(unidirectional) link_data = LinkData( message_type=MessageFlags.NONE, + type=self.linktype, node1_id=linked_node.id, node2_id=self.id, - link_type=self.linktype, - unidirectional=1, - delay=netif.getparam("delay"), - bandwidth=netif.getparam("bw"), - dup=netif.getparam("duplicate"), - jitter=netif.getparam("jitter"), - per=netif.getparam("loss"), + options=options_data, ) - netif.swapparams("_params_up") - + iface.swapparams("_params_up") all_links.append(link_data) - return all_links - def linkconfig( - self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None - ) -> None: - """ - Configure link parameters by applying tc queuing disciplines on the interface. - - :param netif: interface one - :param options: options for configuring link - :param netif2: interface two - :return: nothing - """ - raise NotImplementedError - class Position: """ diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index c004b814e..93e099cf8 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -5,7 +5,7 @@ """ from core import utils -from core.constants import VCMD_BIN +from core.executables import VCMD class VnodeClient: @@ -50,7 +50,7 @@ def close(self) -> None: pass def create_cmd(self, args: str) -> str: - return f"{VCMD_BIN} -c {self.ctrlchnlname} -- {args}" + return f"{VCMD} -c {self.ctrlchnlname} -- {args}" def check_cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: """ diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index fa4b8f8bc..ce34bd986 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -77,9 +77,8 @@ def __init__( _id: int = None, name: str = None, nodedir: str = None, - start: bool = True, server: DistributedServer = None, - image: str = None + image: str = None, ) -> None: """ Create a DockerNode instance. @@ -88,7 +87,6 @@ def __init__( :param _id: object id :param name: object name :param nodedir: node directory - :param start: start flag :param server: remote server node will run on, default is None for localhost :param image: image to start container with @@ -96,7 +94,7 @@ def __init__( if image is None: image = "ubuntu" self.image: str = image - super().__init__(session, _id, name, nodedir, start, server) + super().__init__(session, _id, name, nodedir, server) def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: """ @@ -143,7 +141,7 @@ def shutdown(self) -> None: return with self.lock: - self._netif.clear() + self.ifaces.clear() self.client.stop_container() self.up = False @@ -211,9 +209,7 @@ def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: if self.server is not None: self.host_cmd(f"rm -f {temp.name}") os.unlink(temp.name) - logging.debug( - "node(%s) added file: %s; mode: 0%o", self.name, filename, mode - ) + logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode) def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: """ diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index e73e29896..7f33973e3 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -6,9 +6,12 @@ import time from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple +import netaddr + from core import utils -from core.emulator.enumerations import MessageFlags, TransportType -from core.errors import CoreCommandError +from core.emulator.data import LinkOptions +from core.emulator.enumerations import TransportType +from core.errors import CoreCommandError, CoreError from core.nodes.netclient import LinuxNetClient, get_net_client if TYPE_CHECKING: @@ -51,21 +54,23 @@ def __init__( self.net: Optional[CoreNetworkBase] = None self.othernet: Optional[CoreNetworkBase] = None self._params: Dict[str, float] = {} - self.addrlist: List[str] = [] - self.hwaddr: Optional[str] = None + self.ip4s: List[netaddr.IPNetwork] = [] + self.ip6s: List[netaddr.IPNetwork] = [] + self.mac: Optional[netaddr.EUI] = None # placeholder position hook self.poshook: Callable[[CoreInterface], None] = lambda x: None # used with EMANE - self.transport_type: Optional[TransportType] = None - # node interface index - self.netindex: Optional[int] = None - # net interface index - self.netifi: Optional[int] = None - # index used to find flow data + self.transport_type: TransportType = TransportType.VIRTUAL + # id of interface for node + self.node_id: Optional[int] = None + # id of interface for network + self.net_id: Optional[int] = None + # id used to find flow data self.flow_id: Optional[int] = None self.server: Optional["DistributedServer"] = server - use_ovs = session.options.get_config("ovs") == "True" - self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd) + self.net_client: LinuxNetClient = get_net_client( + self.session.use_ovs(), self.host_cmd + ) def host_cmd( self, @@ -130,35 +135,81 @@ def detachnet(self) -> None: if self.net is not None: self.net.detach(self) - def addaddr(self, addr: str) -> None: + def add_ip(self, ip: str) -> None: """ - Add address. + Add ip address in the format "10.0.0.1/24". - :param addr: address to add + :param ip: ip address to add :return: nothing + :raises CoreError: when ip address provided is invalid """ - addr = utils.validate_ip(addr) - self.addrlist.append(addr) + try: + ip = netaddr.IPNetwork(ip) + address = str(ip.ip) + if netaddr.valid_ipv4(address): + self.ip4s.append(ip) + else: + self.ip6s.append(ip) + except netaddr.AddrFormatError as e: + raise CoreError(f"adding invalid address {ip}: {e}") - def deladdr(self, addr: str) -> None: + def remove_ip(self, ip: str) -> None: """ - Delete address. + Remove ip address in the format "10.0.0.1/24". - :param addr: address to delete + :param ip: ip address to delete :return: nothing + :raises CoreError: when ip address provided is invalid + """ + try: + ip = netaddr.IPNetwork(ip) + address = str(ip.ip) + if netaddr.valid_ipv4(address): + self.ip4s.remove(ip) + else: + self.ip6s.remove(ip) + except (netaddr.AddrFormatError, ValueError) as e: + raise CoreError(f"deleting invalid address {ip}: {e}") + + def get_ip4(self) -> Optional[netaddr.IPNetwork]: + """ + Looks for the first ip4 address. + + :return: ip4 address, None otherwise + """ + return next(iter(self.ip4s), None) + + def get_ip6(self) -> Optional[netaddr.IPNetwork]: + """ + Looks for the first ip6 address. + + :return: ip6 address, None otherwise + """ + return next(iter(self.ip6s), None) + + def ips(self) -> List[netaddr.IPNetwork]: """ - self.addrlist.remove(addr) + Retrieve a list of all ip4 and ip6 addresses combined. - def sethwaddr(self, addr: str) -> None: + :return: ip4 and ip6 addresses """ - Set hardware address. + return self.ip4s + self.ip6s - :param addr: hardware address to set to. + def set_mac(self, mac: Optional[str]) -> None: + """ + Set mac address. + + :param mac: mac address to set, None for random mac :return: nothing + :raises CoreError: when there is an invalid mac address """ - if addr is not None: - addr = utils.validate_mac(addr) - self.hwaddr = addr + if mac is None: + self.mac = mac + else: + try: + self.mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded) + except netaddr.AddrFormatError as e: + raise CoreError(f"invalid mac address({mac}): {e}") def getparam(self, key: str) -> float: """ @@ -169,6 +220,34 @@ def getparam(self, key: str) -> float: """ return self._params.get(key) + def get_link_options(self, unidirectional: int) -> LinkOptions: + """ + Get currently set params as link options. + + :param unidirectional: unidirectional setting + :return: link options + """ + delay = self.getparam("delay") + if delay is not None: + delay = int(delay) + bandwidth = self.getparam("bw") + if bandwidth is not None: + bandwidth = int(bandwidth) + dup = self.getparam("duplicate") + if dup is not None: + dup = int(dup) + jitter = self.getparam("jitter") + if jitter is not None: + jitter = int(jitter) + return LinkOptions( + delay=delay, + bandwidth=bandwidth, + dup=dup, + jitter=jitter, + loss=self.getparam("loss"), + unidirectional=unidirectional, + ) + def getparams(self) -> List[Tuple[str, float]]: """ Return (key, value) pairs for parameters. @@ -231,6 +310,22 @@ def __lt__(self, other: "CoreInterface") -> bool: """ return id(self) < id(other) + def is_raw(self) -> bool: + """ + Used to determine if this interface is considered a raw interface. + + :return: True if raw interface, False otherwise + """ + return self.transport_type == TransportType.RAW + + def is_virtual(self) -> bool: + """ + Used to determine if this interface is considered a virtual interface. + + :return: True if virtual interface, False otherwise + """ + return self.transport_type == TransportType.VIRTUAL + class Veth(CoreInterface): """ @@ -284,19 +379,16 @@ def shutdown(self) -> None: """ if not self.up: return - if self.node: try: self.node.node_net_client.device_flush(self.name) except CoreCommandError: logging.exception("error shutting down interface") - if self.localname: try: self.net_client.delete_device(self.localname) except CoreCommandError: logging.info("link already removed: %s", self.localname) - self.up = False @@ -328,7 +420,6 @@ def __init__( :param start: start flag """ super().__init__(session, node, name, localname, mtu, server) - self.transport_type = TransportType.VIRTUAL if start: self.startup() @@ -459,15 +550,15 @@ def install(self) -> None: self.node.node_net_client.device_name(self.localname, self.name) self.node.node_net_client.device_up(self.name) - def setaddrs(self) -> None: + def set_ips(self) -> None: """ - Set interface addresses based on self.addrlist. + Set interface ip addresses. :return: nothing """ self.waitfordevicenode() - for addr in self.addrlist: - self.node.node_net_client.create_address(self.name, str(addr)) + for ip in self.ips(): + self.node.node_net_client.create_address(self.name, str(ip)) class GreTap(CoreInterface): @@ -518,7 +609,7 @@ def __init__( if not start: return if remoteip is None: - raise ValueError("missing remote IP required for GRE TAP device") + raise CoreError("missing remote IP required for GRE TAP device") self.net_client.create_gretap(self.localname, remoteip, localip, ttl, key) self.net_client.device_up(self.localname) self.up = True @@ -535,23 +626,4 @@ def shutdown(self) -> None: self.net_client.delete_device(self.localname) except CoreCommandError: logging.exception("error during shutdown") - self.localname = None - - def data(self, message_type: int) -> None: - """ - Data for a gre tap. - - :param message_type: message type for data - :return: None - """ - return None - - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List: - """ - Retrieve link data. - - :param flags: link flags - :return: link data - """ - return [] diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index af906f012..9773cb959 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -74,7 +74,6 @@ def __init__( _id: int = None, name: str = None, nodedir: str = None, - start: bool = True, server: DistributedServer = None, image: str = None, ) -> None: @@ -85,7 +84,6 @@ def __init__( :param _id: object id :param name: object name :param nodedir: node directory - :param start: start flag :param server: remote server node will run on, default is None for localhost :param image: image to start container with @@ -93,7 +91,7 @@ def __init__( if image is None: image = "ubuntu" self.image: str = image - super().__init__(session, _id, name, nodedir, start, server) + super().__init__(session, _id, name, nodedir, server) def alive(self) -> bool: """ @@ -128,7 +126,7 @@ def shutdown(self) -> None: return with self.lock: - self._netif.clear() + self.ifaces.clear() self.client.stop_container() self.up = False @@ -217,7 +215,7 @@ def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> Non self.client.copy_file(source, filename) self.cmd(f"chmod {mode:o} {filename}") - def addnetif(self, netif: CoreInterface, ifindex: int) -> None: - super().addnetif(netif, ifindex) + def add_iface(self, iface: CoreInterface, iface_id: int) -> None: + super().add_iface(iface, iface_id) # adding small delay to allow time for adding addresses to work correctly time.sleep(0.5) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 25a10b99b..96a1f4beb 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -5,7 +5,7 @@ import netaddr -from core.constants import ETHTOOL_BIN, IP_BIN, OVS_BIN, SYSCTL_BIN, TC_BIN +from core.executables import ETHTOOL, IP, OVS_VSCTL, SYSCTL, TC class LinuxNetClient: @@ -38,7 +38,7 @@ def create_route(self, route: str, device: str) -> None: :param device: device to add route to :return: nothing """ - self.run(f"{IP_BIN} route add {route} dev {device}") + self.run(f"{IP} route add {route} dev {device}") def device_up(self, device: str) -> None: """ @@ -47,7 +47,7 @@ def device_up(self, device: str) -> None: :param device: device to bring up :return: nothing """ - self.run(f"{IP_BIN} link set {device} up") + self.run(f"{IP} link set {device} up") def device_down(self, device: str) -> None: """ @@ -56,7 +56,7 @@ def device_down(self, device: str) -> None: :param device: device to bring down :return: nothing """ - self.run(f"{IP_BIN} link set {device} down") + self.run(f"{IP} link set {device} down") def device_name(self, device: str, name: str) -> None: """ @@ -66,7 +66,7 @@ def device_name(self, device: str, name: str) -> None: :param name: name to set :return: nothing """ - self.run(f"{IP_BIN} link set {device} name {name}") + self.run(f"{IP} link set {device} name {name}") def device_show(self, device: str) -> str: """ @@ -75,7 +75,7 @@ def device_show(self, device: str) -> str: :param device: device to get information for :return: device information """ - return self.run(f"{IP_BIN} link show {device}") + return self.run(f"{IP} link show {device}") def address_show(self, device: str) -> str: """ @@ -84,7 +84,7 @@ def address_show(self, device: str) -> str: :param device: device name :return: address information """ - return self.run(f"{IP_BIN} address show {device}") + return self.run(f"{IP} address show {device}") def get_mac(self, device: str) -> str: """ @@ -112,7 +112,7 @@ def device_ns(self, device: str, namespace: str) -> None: :param namespace: namespace to set device to :return: nothing """ - self.run(f"{IP_BIN} link set {device} netns {namespace}") + self.run(f"{IP} link set {device} netns {namespace}") def device_flush(self, device: str) -> None: """ @@ -123,7 +123,7 @@ def device_flush(self, device: str) -> None: """ self.run( f"[ -e /sys/class/net/{device} ] && " - f"{IP_BIN} address flush dev {device} || true", + f"{IP} address flush dev {device} || true", shell=True, ) @@ -135,7 +135,7 @@ def device_mac(self, device: str, mac: str) -> None: :param mac: mac to set :return: nothing """ - self.run(f"{IP_BIN} link set dev {device} address {mac}") + self.run(f"{IP} link set dev {device} address {mac}") def delete_device(self, device: str) -> None: """ @@ -144,7 +144,7 @@ def delete_device(self, device: str) -> None: :param device: device to delete :return: nothing """ - self.run(f"{IP_BIN} link delete {device}") + self.run(f"{IP} link delete {device}") def delete_tc(self, device: str) -> None: """ @@ -153,16 +153,16 @@ def delete_tc(self, device: str) -> None: :param device: device to remove tc :return: nothing """ - self.run(f"{TC_BIN} qdisc delete dev {device} root") + self.run(f"{TC} qdisc delete dev {device} root") - def checksums_off(self, interface_name: str) -> None: + def checksums_off(self, iface_name: str) -> None: """ Turns interface checksums off. - :param interface_name: interface to update + :param iface_name: interface to update :return: nothing """ - self.run(f"{ETHTOOL_BIN} -K {interface_name} rx off tx off") + self.run(f"{ETHTOOL} -K {iface_name} rx off tx off") def create_address(self, device: str, address: str, broadcast: str = None) -> None: """ @@ -174,15 +174,13 @@ def create_address(self, device: str, address: str, broadcast: str = None) -> No :return: nothing """ if broadcast is not None: - self.run( - f"{IP_BIN} address add {address} broadcast {broadcast} dev {device}" - ) + self.run(f"{IP} address add {address} broadcast {broadcast} dev {device}") else: - self.run(f"{IP_BIN} address add {address} dev {device}") + self.run(f"{IP} address add {address} dev {device}") if netaddr.valid_ipv6(address.split("/")[0]): # IPv6 addresses are removed by default on interface down. # Make sure that the IPv6 address we add is not removed - self.run(f"{SYSCTL_BIN} -w net.ipv6.conf.{device}.keep_addr_on_down=1") + self.run(f"{SYSCTL} -w net.ipv6.conf.{device}.keep_addr_on_down=1") def delete_address(self, device: str, address: str) -> None: """ @@ -192,7 +190,7 @@ def delete_address(self, device: str, address: str) -> None: :param address: address to remove :return: nothing """ - self.run(f"{IP_BIN} address delete {address} dev {device}") + self.run(f"{IP} address delete {address} dev {device}") def create_veth(self, name: str, peer: str) -> None: """ @@ -202,7 +200,7 @@ def create_veth(self, name: str, peer: str) -> None: :param peer: peer name :return: nothing """ - self.run(f"{IP_BIN} link add name {name} type veth peer name {peer}") + self.run(f"{IP} link add name {name} type veth peer name {peer}") def create_gretap( self, device: str, address: str, local: str, ttl: int, key: int @@ -217,7 +215,7 @@ def create_gretap( :param key: key for tap :return: nothing """ - cmd = f"{IP_BIN} link add {device} type gretap remote {address}" + cmd = f"{IP} link add {device} type gretap remote {address}" if local is not None: cmd += f" local {local}" if ttl is not None: @@ -233,11 +231,11 @@ def create_bridge(self, name: str) -> None: :param name: bridge name :return: nothing """ - self.run(f"{IP_BIN} link add name {name} type bridge") - self.run(f"{IP_BIN} link set {name} type bridge stp_state 0") - self.run(f"{IP_BIN} link set {name} type bridge forward_delay 0") - self.run(f"{IP_BIN} link set {name} type bridge mcast_snooping 0") - self.run(f"{IP_BIN} link set {name} type bridge group_fwd_mask 65528") + self.run(f"{IP} link add name {name} type bridge") + self.run(f"{IP} link set {name} type bridge stp_state 0") + self.run(f"{IP} link set {name} type bridge forward_delay 0") + self.run(f"{IP} link set {name} type bridge mcast_snooping 0") + self.run(f"{IP} link set {name} type bridge group_fwd_mask 65528") self.device_up(name) def delete_bridge(self, name: str) -> None: @@ -248,28 +246,28 @@ def delete_bridge(self, name: str) -> None: :return: nothing """ self.device_down(name) - self.run(f"{IP_BIN} link delete {name} type bridge") + self.run(f"{IP} link delete {name} type bridge") - def set_interface_master(self, bridge_name: str, interface_name: str) -> None: + def set_iface_master(self, bridge_name: str, iface_name: str) -> None: """ Assign interface master to a Linux bridge. :param bridge_name: bridge name - :param interface_name: interface name + :param iface_name: interface name :return: nothing """ - self.run(f"{IP_BIN} link set dev {interface_name} master {bridge_name}") - self.device_up(interface_name) + self.run(f"{IP} link set dev {iface_name} master {bridge_name}") + self.device_up(iface_name) - def delete_interface(self, bridge_name: str, interface_name: str) -> None: + def delete_iface(self, bridge_name: str, iface_name: str) -> None: """ Delete an interface associated with a Linux bridge. :param bridge_name: bridge name - :param interface_name: interface name + :param iface_name: interface name :return: nothing """ - self.run(f"{IP_BIN} link set dev {interface_name} nomaster") + self.run(f"{IP} link set dev {iface_name} nomaster") def existing_bridges(self, _id: int) -> bool: """ @@ -278,7 +276,7 @@ def existing_bridges(self, _id: int) -> bool: :param _id: node id to check bridges for :return: True if there are existing bridges, False otherwise """ - output = self.run(f"{IP_BIN} -o link show type bridge") + output = self.run(f"{IP} -o link show type bridge") lines = output.split("\n") for line in lines: values = line.split(":") @@ -299,7 +297,7 @@ def disable_mac_learning(self, name: str) -> None: :param name: bridge name :return: nothing """ - self.run(f"{IP_BIN} link set {name} type bridge ageing_time 0") + self.run(f"{IP} link set {name} type bridge ageing_time 0") class OvsNetClient(LinuxNetClient): @@ -314,10 +312,10 @@ def create_bridge(self, name: str) -> None: :param name: bridge name :return: nothing """ - self.run(f"{OVS_BIN} add-br {name}") - self.run(f"{OVS_BIN} set bridge {name} stp_enable=false") - self.run(f"{OVS_BIN} set bridge {name} other_config:stp-max-age=6") - self.run(f"{OVS_BIN} set bridge {name} other_config:stp-forward-delay=4") + self.run(f"{OVS_VSCTL} add-br {name}") + self.run(f"{OVS_VSCTL} set bridge {name} stp_enable=false") + self.run(f"{OVS_VSCTL} set bridge {name} other_config:stp-max-age=6") + self.run(f"{OVS_VSCTL} set bridge {name} other_config:stp-forward-delay=4") self.device_up(name) def delete_bridge(self, name: str) -> None: @@ -328,28 +326,28 @@ def delete_bridge(self, name: str) -> None: :return: nothing """ self.device_down(name) - self.run(f"{OVS_BIN} del-br {name}") + self.run(f"{OVS_VSCTL} del-br {name}") - def set_interface_master(self, bridge_name: str, interface_name: str) -> None: + def set_iface_master(self, bridge_name: str, iface_name: str) -> None: """ Create an interface associated with a network bridge. :param bridge_name: bridge name - :param interface_name: interface name + :param iface_name: interface name :return: nothing """ - self.run(f"{OVS_BIN} add-port {bridge_name} {interface_name}") - self.device_up(interface_name) + self.run(f"{OVS_VSCTL} add-port {bridge_name} {iface_name}") + self.device_up(iface_name) - def delete_interface(self, bridge_name: str, interface_name: str) -> None: + def delete_iface(self, bridge_name: str, iface_name: str) -> None: """ Delete an interface associated with a OVS bridge. :param bridge_name: bridge name - :param interface_name: interface name + :param iface_name: interface name :return: nothing """ - self.run(f"{OVS_BIN} del-port {bridge_name} {interface_name}") + self.run(f"{OVS_VSCTL} del-port {bridge_name} {iface_name}") def existing_bridges(self, _id: int) -> bool: """ @@ -358,7 +356,7 @@ def existing_bridges(self, _id: int) -> bool: :param _id: node id to check bridges for :return: True if there are existing bridges, False otherwise """ - output = self.run(f"{OVS_BIN} list-br") + output = self.run(f"{OVS_VSCTL} list-br") if output: for line in output.split("\n"): fields = line.split(".") @@ -373,7 +371,7 @@ def disable_mac_learning(self, name: str) -> None: :param name: bridge name :return: nothing """ - self.run(f"{OVS_BIN} set bridge {name} other_config:mac-aging-time=0") + self.run(f"{OVS_VSCTL} set bridge {name} other_config:mac-aging-time=0") def get_net_client(use_ovs: bool, run: Callable[..., str]) -> LinuxNetClient: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 095fbe9b7..58c1e195b 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -10,9 +10,7 @@ import netaddr from core import utils -from core.constants import EBTABLES_BIN, TC_BIN -from core.emulator.data import LinkData, NodeData -from core.emulator.emudata import LinkOptions +from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.enumerations import ( LinkTypes, MessageFlags, @@ -21,6 +19,7 @@ RegisterTlvs, ) from core.errors import CoreCommandError, CoreError +from core.executables import EBTABLES, TC from core.nodes.base import CoreNetworkBase from core.nodes.interface import CoreInterface, GreTap, Veth from core.nodes.netclient import get_net_client @@ -105,7 +104,7 @@ def ebatomiccmd(self, cmd: str) -> str: :param cmd: ebtable command :return: ebtable atomic command """ - return f"{EBTABLES_BIN} --atomic-file {self.atomic_file} {cmd}" + return f"{EBTABLES} --atomic-file {self.atomic_file} {cmd}" def lastupdate(self, wlan: "CoreNetwork") -> float: """ @@ -216,20 +215,20 @@ def buildcmds(self, wlan: "CoreNetwork") -> None: ] ) # rebuild the chain - for netif1, v in wlan._linked.items(): - for netif2, linked in v.items(): + for iface1, v in wlan._linked.items(): + for oface2, linked in v.items(): if wlan.policy == NetworkPolicy.DROP and linked: self.cmds.extend( [ - f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j ACCEPT", - f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j ACCEPT", + f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j ACCEPT", + f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j ACCEPT", ] ) elif wlan.policy == NetworkPolicy.ACCEPT and not linked: self.cmds.extend( [ - f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j DROP", - f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j DROP", + f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j DROP", + f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j DROP", ] ) @@ -264,7 +263,6 @@ def __init__( session: "Session", _id: int = None, name: str = None, - start: bool = True, server: "DistributedServer" = None, policy: NetworkPolicy = None, ) -> None: @@ -274,12 +272,11 @@ def __init__( :param session: core session instance :param _id: object id :param name: object name - :param start: start flag :param server: remote server node will run on, default is None for localhost :param policy: network policy """ - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) if name is None: name = str(self.id) if policy is not None: @@ -288,9 +285,6 @@ def __init__( sessionid = self.session.short_session_id() self.brname: str = f"b.{self.id}.{sessionid}" self.has_ebtables_chain: bool = False - if start: - self.startup() - ebq.startupdateloop(self) def host_cmd( self, @@ -327,6 +321,7 @@ def startup(self) -> None: self.net_client.create_bridge(self.brname) self.has_ebtables_chain = False self.up = True + ebq.startupdateloop(self) def shutdown(self) -> None: """ @@ -343,61 +338,61 @@ def shutdown(self) -> None: self.net_client.delete_bridge(self.brname) if self.has_ebtables_chain: cmds = [ - f"{EBTABLES_BIN} -D FORWARD --logical-in {self.brname} -j {self.brname}", - f"{EBTABLES_BIN} -X {self.brname}", + f"{EBTABLES} -D FORWARD --logical-in {self.brname} -j {self.brname}", + f"{EBTABLES} -X {self.brname}", ] ebtablescmds(self.host_cmd, cmds) except CoreCommandError: logging.exception("error during shutdown") # removes veth pairs used for bridge-to-bridge connections - for netif in self.netifs(): - netif.shutdown() + for iface in self.get_ifaces(): + iface.shutdown() - self._netif.clear() + self.ifaces.clear() self._linked.clear() del self.session self.up = False - def attach(self, netif: CoreInterface) -> None: + def attach(self, iface: CoreInterface) -> None: """ Attach a network interface. - :param netif: network interface to attach + :param iface: network interface to attach :return: nothing """ if self.up: - netif.net_client.set_interface_master(self.brname, netif.localname) - super().attach(netif) + iface.net_client.set_iface_master(self.brname, iface.localname) + super().attach(iface) - def detach(self, netif: CoreInterface) -> None: + def detach(self, iface: CoreInterface) -> None: """ Detach a network interface. - :param netif: network interface to detach + :param iface: network interface to detach :return: nothing """ if self.up: - netif.net_client.delete_interface(self.brname, netif.localname) - super().detach(netif) + iface.net_client.delete_iface(self.brname, iface.localname) + super().detach(iface) - def linked(self, netif1: CoreInterface, netif2: CoreInterface) -> bool: + def linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool: """ Determine if the provided network interfaces are linked. - :param netif1: interface one - :param netif2: interface two + :param iface1: interface one + :param iface2: interface two :return: True if interfaces are linked, False otherwise """ # check if the network interfaces are attached to this network - if self._netif[netif1.netifi] != netif1: - raise ValueError(f"inconsistency for netif {netif1.name}") + if self.ifaces[iface1.net_id] != iface1: + raise ValueError(f"inconsistency for interface {iface1.name}") - if self._netif[netif2.netifi] != netif2: - raise ValueError(f"inconsistency for netif {netif2.name}") + if self.ifaces[iface2.net_id] != iface2: + raise ValueError(f"inconsistency for interface {iface2.name}") try: - linked = self._linked[netif1][netif2] + linked = self._linked[iface1][iface2] except KeyError: if self.policy == NetworkPolicy.ACCEPT: linked = True @@ -405,93 +400,93 @@ def linked(self, netif1: CoreInterface, netif2: CoreInterface) -> bool: linked = False else: raise Exception(f"unknown policy: {self.policy.value}") - self._linked[netif1][netif2] = linked + self._linked[iface1][iface2] = linked return linked - def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None: + def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: """ Unlink two interfaces, resulting in adding or removing ebtables filtering rules. - :param netif1: interface one - :param netif2: interface two + :param iface1: interface one + :param iface2: interface two :return: nothing """ with self._linked_lock: - if not self.linked(netif1, netif2): + if not self.linked(iface1, iface2): return - self._linked[netif1][netif2] = False + self._linked[iface1][iface2] = False ebq.ebchange(self) - def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None: + def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: """ Link two interfaces together, resulting in adding or removing ebtables filtering rules. - :param netif1: interface one - :param netif2: interface two + :param iface1: interface one + :param iface2: interface two :return: nothing """ with self._linked_lock: - if self.linked(netif1, netif2): + if self.linked(iface1, iface2): return - self._linked[netif1][netif2] = True + self._linked[iface1][iface2] = True ebq.ebchange(self) def linkconfig( - self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None + self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None ) -> None: """ Configure link parameters by applying tc queuing disciplines on the interface. - :param netif: interface one + :param iface: interface one :param options: options for configuring link - :param netif2: interface two + :param iface2: interface two :return: nothing """ - devname = netif.localname - tc = f"{TC_BIN} qdisc replace dev {devname}" + devname = iface.localname + tc = f"{TC} qdisc replace dev {devname}" parent = "root" changed = False bw = options.bandwidth - if netif.setparam("bw", bw): + if iface.setparam("bw", bw): # from tc-tbf(8): minimum value for burst is rate / kernel_hz - burst = max(2 * netif.mtu, int(bw / 1000)) + burst = max(2 * iface.mtu, int(bw / 1000)) # max IP payload limit = 0xFFFF tbf = f"tbf rate {bw} burst {burst} limit {limit}" if bw > 0: if self.up: cmd = f"{tc} {parent} handle 1: {tbf}" - netif.host_cmd(cmd) - netif.setparam("has_tbf", True) + iface.host_cmd(cmd) + iface.setparam("has_tbf", True) changed = True - elif netif.getparam("has_tbf") and bw <= 0: + elif iface.getparam("has_tbf") and bw <= 0: if self.up: - cmd = f"{TC_BIN} qdisc delete dev {devname} {parent}" - netif.host_cmd(cmd) - netif.setparam("has_tbf", False) + cmd = f"{TC} qdisc delete dev {devname} {parent}" + iface.host_cmd(cmd) + iface.setparam("has_tbf", False) # removing the parent removes the child - netif.setparam("has_netem", False) + iface.setparam("has_netem", False) changed = True - if netif.getparam("has_tbf"): + if iface.getparam("has_tbf"): parent = "parent 1:1" netem = "netem" delay = options.delay - changed = max(changed, netif.setparam("delay", delay)) - loss = options.per + changed = max(changed, iface.setparam("delay", delay)) + loss = options.loss if loss is not None: loss = float(loss) - changed = max(changed, netif.setparam("loss", loss)) + changed = max(changed, iface.setparam("loss", loss)) duplicate = options.dup if duplicate is not None: duplicate = int(duplicate) - changed = max(changed, netif.setparam("duplicate", duplicate)) + changed = max(changed, iface.setparam("duplicate", duplicate)) jitter = options.jitter - changed = max(changed, netif.setparam("jitter", jitter)) + changed = max(changed, iface.setparam("jitter", jitter)) if not changed: return # jitter and delay use the same delay statement @@ -514,19 +509,17 @@ def linkconfig( duplicate_check = duplicate is None or duplicate <= 0 if all([delay_check, jitter_check, loss_check, duplicate_check]): # possibly remove netem if it exists and parent queue wasn't removed - if not netif.getparam("has_netem"): + if not iface.getparam("has_netem"): return if self.up: - cmd = f"{TC_BIN} qdisc delete dev {devname} {parent} handle 10:" - netif.host_cmd(cmd) - netif.setparam("has_netem", False) + cmd = f"{TC} qdisc delete dev {devname} {parent} handle 10:" + iface.host_cmd(cmd) + iface.setparam("has_netem", False) elif len(netem) > 1: if self.up: - cmd = ( - f"{TC_BIN} qdisc replace dev {devname} {parent} handle 10: {netem}" - ) - netif.host_cmd(cmd) - netif.setparam("has_netem", True) + cmd = f"{TC} qdisc replace dev {devname} {parent} handle 10: {netem}" + iface.host_cmd(cmd) + iface.setparam("has_netem", True) def linknet(self, net: CoreNetworkBase) -> CoreInterface: """ @@ -555,19 +548,19 @@ def linknet(self, net: CoreNetworkBase) -> CoreInterface: if len(name) >= 16: raise ValueError(f"interface name {name} too long") - netif = Veth(self.session, None, name, localname, start=self.up) - self.attach(netif) + iface = Veth(self.session, None, name, localname, start=self.up) + self.attach(iface) if net.up and net.brname: - netif.net_client.set_interface_master(net.brname, netif.name) - i = net.newifindex() - net._netif[i] = netif + iface.net_client.set_iface_master(net.brname, iface.name) + i = net.next_iface_id() + net.ifaces[i] = iface with net._linked_lock: - net._linked[netif] = {} - netif.net = self - netif.othernet = net - return netif + net._linked[iface] = {} + iface.net = self + iface.othernet = net + return iface - def getlinknetif(self, net: CoreNetworkBase) -> Optional[CoreInterface]: + def get_linked_iface(self, net: CoreNetworkBase) -> Optional[CoreInterface]: """ Return the interface of that links this net with another net (that were linked using linknet()). @@ -575,23 +568,22 @@ def getlinknetif(self, net: CoreNetworkBase) -> Optional[CoreInterface]: :param net: interface to get link for :return: interface the provided network is linked to """ - for netif in self.netifs(): - if netif.othernet == net: - return netif + for iface in self.get_ifaces(): + if iface.othernet == net: + return iface return None - def addrconfig(self, addrlist: List[str]) -> None: + def add_ips(self, ips: List[str]) -> None: """ - Set addresses on the bridge. + Add ip addresses on the bridge in the format "10.0.0.1/24". - :param addrlist: address list + :param ips: ip address to add :return: nothing """ if not self.up: return - - for addr in addrlist: - self.net_client.create_address(self.brname, str(addr)) + for ip in ips: + self.net_client.create_address(self.brname, ip) class GreTapBridge(CoreNetwork): @@ -610,7 +602,6 @@ def __init__( localip: str = None, ttl: int = 255, key: int = None, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ @@ -624,11 +615,10 @@ def __init__( :param localip: local address :param ttl: ttl value :param key: gre tap key - :param start: start flag :param server: remote server node will run on, default is None for localhost """ - CoreNetwork.__init__(self, session, _id, name, False, server, policy) + CoreNetwork.__init__(self, session, _id, name, server, policy) if key is None: key = self.session.id ^ self.id self.grekey: int = key @@ -647,8 +637,6 @@ def __init__( ttl=ttl, key=self.grekey, ) - if start: - self.startup() def startup(self) -> None: """ @@ -672,22 +660,22 @@ def shutdown(self) -> None: self.gretap = None super().shutdown() - def addrconfig(self, addrlist: List[str]) -> None: + def add_ips(self, ips: List[str]) -> None: """ Set the remote tunnel endpoint. This is a one-time method for creating the GreTap device, which requires the remoteip at startup. The 1st address in the provided list is remoteip, 2nd optionally specifies localip. - :param addrlist: address list + :param ips: address list :return: nothing """ if self.gretap: raise ValueError(f"gretap already exists for {self.name}") - remoteip = addrlist[0].split("/")[0] + remoteip = ips[0].split("/")[0] localip = None - if len(addrlist) > 1: - localip = addrlist[1].split("/")[0] + if len(ips) > 1: + localip = ips[1].split("/")[0] self.gretap = GreTap( session=self.session, remoteip=remoteip, @@ -697,15 +685,19 @@ def addrconfig(self, addrlist: List[str]) -> None: ) self.attach(self.gretap) - def setkey(self, key: int) -> None: + def setkey(self, key: int, iface_data: InterfaceData) -> None: """ Set the GRE key used for the GreTap device. This needs to be set prior to instantiating the GreTap device (before addrconfig). :param key: gre key + :param iface_data: interface data for setting up tunnel key :return: nothing """ self.grekey = key + ips = iface_data.get_ips() + if ips: + self.add_ips(ips) class CtrlNet(CoreNetwork): @@ -730,7 +722,6 @@ def __init__( _id: int = None, name: str = None, hostid: int = None, - start: bool = True, server: "DistributedServer" = None, assign_address: bool = True, updown_script: str = None, @@ -744,7 +735,6 @@ def __init__( :param name: node namee :param prefix: control network ipv4 prefix :param hostid: host id - :param start: start flag :param server: remote server node will run on, default is None for localhost :param assign_address: assigned address @@ -757,7 +747,7 @@ def __init__( self.assign_address: bool = assign_address self.updown_script: Optional[str] = updown_script self.serverintf: Optional[str] = serverintf - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) def add_addresses(self, index: int) -> None: """ @@ -766,7 +756,7 @@ def add_addresses(self, index: int) -> None: :param index: starting address index :return: nothing """ - use_ovs = self.session.options.get_config("ovs") == "True" + use_ovs = self.session.use_ovs() address = self.prefix[index] current = f"{address}/{self.prefix.prefixlen}" net_client = get_net_client(use_ovs, utils.cmd) @@ -807,7 +797,7 @@ def startup(self) -> None: self.host_cmd(f"{self.updown_script} {self.brname} startup") if self.serverintf: - self.net_client.set_interface_master(self.brname, self.serverintf) + self.net_client.set_iface_master(self.brname, self.serverintf) def shutdown(self) -> None: """ @@ -817,7 +807,7 @@ def shutdown(self) -> None: """ if self.serverintf is not None: try: - self.net_client.delete_interface(self.brname, self.serverintf) + self.net_client.delete_iface(self.brname, self.serverintf) except CoreCommandError: logging.exception( "error deleting server interface %s from bridge %s", @@ -838,7 +828,7 @@ def shutdown(self) -> None: super().shutdown() - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Do not include CtrlNet in link messages describing this session. @@ -855,33 +845,18 @@ class PtpNet(CoreNetwork): policy: NetworkPolicy = NetworkPolicy.ACCEPT - def attach(self, netif: CoreInterface) -> None: + def attach(self, iface: CoreInterface) -> None: """ Attach a network interface, but limit attachment to two interfaces. - :param netif: network interface + :param iface: network interface :return: nothing """ - if len(self._netif) >= 2: - raise ValueError( - "Point-to-point links support at most 2 network interfaces" - ) - super().attach(netif) - - def data( - self, message_type: MessageFlags = MessageFlags.NONE, source: str = None - ) -> Optional[NodeData]: - """ - Do not generate a Node Message for point-to-point links. They are - built using a link message instead. + if len(self.ifaces) >= 2: + raise CoreError("ptp links support at most 2 network interfaces") + super().attach(iface) - :param message_type: purpose for the data object we are creating - :param source: source of node data - :return: node data object - """ - return None - - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build CORE API TLVs for a point-to-point link. One Link message describes this network. @@ -890,91 +865,70 @@ def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkDat :return: list of link data """ all_links = [] - - if len(self._netif) != 2: + if len(self.ifaces) != 2: return all_links - if1, if2 = self._netif.values() + ifaces = self.get_ifaces() + iface1 = ifaces[0] + iface2 = ifaces[1] unidirectional = 0 - if if1.getparams() != if2.getparams(): + if iface1.getparams() != iface2.getparams(): unidirectional = 1 - interface1_ip4 = None - interface1_ip4_mask = None - interface1_ip6 = None - interface1_ip6_mask = None - for address in if1.addrlist: - ip, _sep, mask = address.partition("/") - mask = int(mask) - if netaddr.valid_ipv4(ip): - interface1_ip4 = ip - interface1_ip4_mask = mask - else: - interface1_ip6 = ip - interface1_ip6_mask = mask - - interface2_ip4 = None - interface2_ip4_mask = None - interface2_ip6 = None - interface2_ip6_mask = None - for address in if2.addrlist: - ip, _sep, mask = address.partition("/") - mask = int(mask) - if netaddr.valid_ipv4(ip): - interface2_ip4 = ip - interface2_ip4_mask = mask - else: - interface2_ip6 = ip - interface2_ip6_mask = mask - + mac = str(iface1.mac) if iface1.mac else None + iface1_data = InterfaceData( + id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=mac + ) + ip4 = iface1.get_ip4() + if ip4: + iface1_data.ip4 = str(ip4.ip) + iface1_data.ip4_mask = ip4.prefixlen + ip6 = iface1.get_ip6() + if ip6: + iface1_data.ip6 = str(ip6.ip) + iface1_data.ip6_mask = ip6.prefixlen + + mac = str(iface2.mac) if iface2.mac else None + iface2_data = InterfaceData( + id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=mac + ) + ip4 = iface2.get_ip4() + if ip4: + iface2_data.ip4 = str(ip4.ip) + iface2_data.ip4_mask = ip4.prefixlen + ip6 = iface2.get_ip6() + if ip6: + iface2_data.ip6 = str(ip6.ip) + iface2_data.ip6_mask = ip6.prefixlen + + options_data = iface1.get_link_options(unidirectional) link_data = LinkData( message_type=flags, - node1_id=if1.node.id, - node2_id=if2.node.id, - link_type=self.linktype, - unidirectional=unidirectional, - delay=if1.getparam("delay"), - bandwidth=if1.getparam("bw"), - per=if1.getparam("loss"), - dup=if1.getparam("duplicate"), - jitter=if1.getparam("jitter"), - interface1_id=if1.node.getifindex(if1), - interface1_name=if1.name, - interface1_mac=if1.hwaddr, - interface1_ip4=interface1_ip4, - interface1_ip4_mask=interface1_ip4_mask, - interface1_ip6=interface1_ip6, - interface1_ip6_mask=interface1_ip6_mask, - interface2_id=if2.node.getifindex(if2), - interface2_name=if2.name, - interface2_mac=if2.hwaddr, - interface2_ip4=interface2_ip4, - interface2_ip4_mask=interface2_ip4_mask, - interface2_ip6=interface2_ip6, - interface2_ip6_mask=interface2_ip6_mask, + type=self.linktype, + node1_id=iface1.node.id, + node2_id=iface2.node.id, + iface1=iface1_data, + iface2=iface2_data, + options=options_data, ) - all_links.append(link_data) # build a 2nd link message for the upstream link parameters # (swap if1 and if2) if unidirectional: + iface1_data = InterfaceData(id=iface2.node.get_iface_id(iface2)) + iface2_data = InterfaceData(id=iface1.node.get_iface_id(iface1)) + options_data = iface2.get_link_options(unidirectional) link_data = LinkData( message_type=MessageFlags.NONE, - link_type=self.linktype, - node1_id=if2.node.id, - node2_id=if1.node.id, - delay=if2.getparam("delay"), - bandwidth=if2.getparam("bw"), - per=if2.getparam("loss"), - dup=if2.getparam("duplicate"), - jitter=if2.getparam("jitter"), - unidirectional=1, - interface1_id=if2.node.getifindex(if2), - interface2_id=if1.node.getifindex(if1), + type=self.linktype, + node1_id=iface2.node.id, + node2_id=iface1.node.id, + iface1=iface1_data, + iface2=iface2_data, + options=options_data, ) all_links.append(link_data) - return all_links @@ -1023,7 +977,6 @@ def __init__( session: "Session", _id: int = None, name: str = None, - start: bool = True, server: "DistributedServer" = None, policy: NetworkPolicy = None, ) -> None: @@ -1033,12 +986,11 @@ def __init__( :param session: core session instance :param _id: node id :param name: node name - :param start: start flag :param server: remote server node will run on, default is None for localhost :param policy: wlan policy """ - super().__init__(session, _id, name, start, server, policy) + super().__init__(session, _id, name, server, policy) # wireless and mobility models (BasicRangeModel, Ns2WaypointMobility) self.model: Optional[WirelessModel] = None self.mobility: Optional[WayPointMobility] = None @@ -1053,17 +1005,17 @@ def startup(self) -> None: self.net_client.disable_mac_learning(self.brname) ebq.ebchange(self) - def attach(self, netif: CoreInterface) -> None: + def attach(self, iface: CoreInterface) -> None: """ Attach a network interface. - :param netif: network interface + :param iface: network interface :return: nothing """ - super().attach(netif) + super().attach(iface) if self.model: - netif.poshook = self.model.position_callback - netif.setposition() + iface.poshook = self.model.position_callback + iface.setposition() def setmodel(self, model: "WirelessModelType", config: Dict[str, str]): """ @@ -1076,9 +1028,9 @@ def setmodel(self, model: "WirelessModelType", config: Dict[str, str]): logging.debug("node(%s) setting model: %s", self.name, model.name) if model.config_type == RegisterTlvs.WIRELESS: self.model = model(session=self.session, _id=self.id) - for netif in self.netifs(): - netif.poshook = self.model.position_callback - netif.setposition() + for iface in self.get_ifaces(): + iface.poshook = self.model.position_callback + iface.setposition() self.updatemodel(config) elif model.config_type == RegisterTlvs.MOBILITY: self.mobility = model(session=self.session, _id=self.id) @@ -1086,30 +1038,30 @@ def setmodel(self, model: "WirelessModelType", config: Dict[str, str]): def update_mobility(self, config: Dict[str, str]) -> None: if not self.mobility: - raise ValueError(f"no mobility set to update for node({self.id})") + raise CoreError(f"no mobility set to update for node({self.name})") self.mobility.update_config(config) def updatemodel(self, config: Dict[str, str]) -> None: if not self.model: - raise ValueError(f"no model set to update for node({self.id})") + raise CoreError(f"no model set to update for node({self.name})") logging.debug( "node(%s) updating model(%s): %s", self.id, self.model.name, config ) self.model.update_config(config) - for netif in self.netifs(): - netif.setposition() + for iface in self.get_ifaces(): + iface.setposition() - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Retrieve all link data. :param flags: message flags :return: list of link data """ - all_links = super().all_link_data(flags) + links = super().links(flags) if self.model: - all_links.extend(self.model.all_link_data(flags)) - return all_links + links.extend(self.model.links(flags)) + return links class TunnelNode(GreTapBridge): diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index ee00c7053..f48a0d10d 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -7,12 +7,11 @@ import threading from typing import IO, TYPE_CHECKING, List, Optional, Tuple -from core import utils -from core.constants import MOUNT_BIN, UMOUNT_BIN +from core.emulator.data import InterfaceData, LinkOptions from core.emulator.distributed import DistributedServer -from core.emulator.emudata import InterfaceData, LinkOptions from core.emulator.enumerations import NodeTypes, TransportType from core.errors import CoreCommandError, CoreError +from core.executables import MOUNT, UMOUNT from core.nodes.base import CoreNetworkBase, CoreNodeBase from core.nodes.interface import CoreInterface from core.nodes.network import CoreNetwork, GreTap @@ -28,22 +27,19 @@ def __init__( _id: int = None, name: str = None, nodedir: str = None, - start: bool = True, server: DistributedServer = None, ) -> None: - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) if not self.server: raise CoreError("physical nodes must be assigned to a remote server") self.nodedir: Optional[str] = nodedir - self.up: bool = start self.lock: threading.RLock = threading.RLock() self._mounts: List[Tuple[str, str]] = [] - if start: - self.startup() def startup(self) -> None: with self.lock: self.makenodedir() + self.up = True def shutdown(self) -> None: if not self.up: @@ -54,8 +50,8 @@ def shutdown(self) -> None: _source, target = self._mounts.pop(-1) self.umount(target) - for netif in self.netifs(): - netif.shutdown() + for iface in self.get_ifaces(): + iface.shutdown() self.rmnodedir() @@ -68,115 +64,114 @@ def termcmdstring(self, sh: str = "/bin/sh") -> str: """ return sh - def sethwaddr(self, ifindex: int, addr: str) -> None: + def set_mac(self, iface_id: int, mac: str) -> None: """ - Set hardware address for an interface. + Set mac address for an interface. - :param ifindex: index of interface to set hardware address for - :param addr: hardware address to set + :param iface_id: index of interface to set hardware address for + :param mac: mac address to set :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - addr = utils.validate_mac(addr) - interface = self._netif[ifindex] - interface.sethwaddr(addr) + iface = self.get_iface(iface_id) + iface.set_mac(mac) if self.up: - self.net_client.device_mac(interface.name, addr) + self.net_client.device_mac(iface.name, str(iface.mac)) - def addaddr(self, ifindex: int, addr: str) -> None: + def add_ip(self, iface_id: int, ip: str) -> None: """ - Add an address to an interface. + Add an ip address to an interface in the format "10.0.0.1/24". - :param ifindex: index of interface to add address to - :param addr: address to add + :param iface_id: id of interface to add address to + :param ip: address to add to interface :return: nothing + :raises CoreError: when ip address provided is invalid + :raises CoreCommandError: when a non-zero exit status occurs """ - addr = utils.validate_ip(addr) - interface = self._netif[ifindex] + iface = self.get_iface(iface_id) + iface.add_ip(ip) if self.up: - self.net_client.create_address(interface.name, addr) - interface.addaddr(addr) + self.net_client.create_address(iface.name, ip) - def deladdr(self, ifindex: int, addr: str) -> None: + def remove_ip(self, iface_id: int, ip: str) -> None: """ - Delete an address from an interface. + Remove an ip address from an interface in the format "10.0.0.1/24". - :param ifindex: index of interface to delete - :param addr: address to delete + :param iface_id: id of interface to delete address from + :param ip: ip address to remove from interface :return: nothing + :raises CoreError: when ip address provided is invalid + :raises CoreCommandError: when a non-zero exit status occurs """ - interface = self._netif[ifindex] - - try: - interface.deladdr(addr) - except ValueError: - logging.exception("trying to delete unknown address: %s", addr) - + iface = self.get_iface(iface_id) + iface.remove_ip(ip) if self.up: - self.net_client.delete_address(interface.name, addr) + self.net_client.delete_address(iface.name, ip) - def adoptnetif( - self, netif: CoreInterface, ifindex: int, hwaddr: str, addrlist: List[str] + def adopt_iface( + self, iface: CoreInterface, iface_id: int, mac: str, ips: List[str] ) -> None: """ When a link message is received linking this node to another part of the emulation, no new interface is created; instead, adopt the - GreTap netif as the node interface. + GreTap interface as the node interface. """ - netif.name = f"gt{ifindex}" - netif.node = self - self.addnetif(netif, ifindex) + iface.name = f"gt{iface_id}" + iface.node = self + self.add_iface(iface, iface_id) # use a more reasonable name, e.g. "gt0" instead of "gt.56286.150" if self.up: - self.net_client.device_down(netif.localname) - self.net_client.device_name(netif.localname, netif.name) - netif.localname = netif.name - if hwaddr: - self.sethwaddr(ifindex, hwaddr) - for addr in addrlist: - self.addaddr(ifindex, addr) + self.net_client.device_down(iface.localname) + self.net_client.device_name(iface.localname, iface.name) + iface.localname = iface.name + if mac: + self.set_mac(iface_id, mac) + for ip in ips: + self.add_ip(iface_id, ip) if self.up: - self.net_client.device_up(netif.localname) + self.net_client.device_up(iface.localname) def linkconfig( - self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None + self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None ) -> None: """ Apply tc queing disciplines using linkconfig. """ - linux_bridge = CoreNetwork(session=self.session, start=False) + linux_bridge = CoreNetwork(self.session) linux_bridge.up = True - linux_bridge.linkconfig(netif, options, netif2) + linux_bridge.linkconfig(iface, options, iface2) del linux_bridge - def newifindex(self) -> int: + def next_iface_id(self) -> int: with self.lock: - while self.ifindex in self._netif: - self.ifindex += 1 - ifindex = self.ifindex - self.ifindex += 1 - return ifindex - - def newnetif(self, net: CoreNetworkBase, interface: InterfaceData) -> int: + while self.iface_id in self.ifaces: + self.iface_id += 1 + iface_id = self.iface_id + self.iface_id += 1 + return iface_id + + def new_iface( + self, net: CoreNetworkBase, iface_data: InterfaceData + ) -> CoreInterface: logging.info("creating interface") - addresses = interface.get_addresses() - ifindex = interface.id - if ifindex is None: - ifindex = self.newifindex() - name = interface.name + ips = iface_data.get_ips() + iface_id = iface_data.id + if iface_id is None: + iface_id = self.next_iface_id() + name = iface_data.name if name is None: - name = f"gt{ifindex}" + name = f"gt{iface_id}" if self.up: # this is reached when this node is linked to a network node # tunnel to net not built yet, so build it now and adopt it _, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server) - self.adoptnetif(remote_tap, ifindex, interface.mac, addresses) - return ifindex + self.adopt_iface(remote_tap, iface_id, iface_data.mac, ips) + return remote_tap else: # this is reached when configuring services (self.up=False) - netif = GreTap(node=self, name=name, session=self.session, start=False) - self.adoptnetif(netif, ifindex, interface.mac, addresses) - return ifindex + iface = GreTap(node=self, name=name, session=self.session, start=False) + self.adopt_iface(iface, iface_id, iface_data.mac, ips) + return iface def privatedir(self, path: str) -> None: if path[0] != "/": @@ -191,13 +186,13 @@ def mount(self, source: str, target: str) -> None: source = os.path.abspath(source) logging.info("mounting %s at %s", source, target) os.makedirs(target) - self.host_cmd(f"{MOUNT_BIN} --bind {source} {target}", cwd=self.nodedir) + self.host_cmd(f"{MOUNT} --bind {source} {target}", cwd=self.nodedir) self._mounts.append((source, target)) def umount(self, target: str) -> None: logging.info("unmounting '%s'", target) try: - self.host_cmd(f"{UMOUNT_BIN} -l {target}", cwd=self.nodedir) + self.host_cmd(f"{UMOUNT} -l {target}", cwd=self.nodedir) except CoreCommandError: logging.exception("unmounting failed for %s", target) @@ -218,14 +213,17 @@ def opennodefile(self, filename: str, mode: str = "w") -> IO: return open(hostfilename, mode) def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: - with self.opennodefile(filename, "w") as node_file: - node_file.write(contents) - os.chmod(node_file.name, mode) - logging.info("created nodefile: '%s'; mode: 0%o", node_file.name, mode) + with self.opennodefile(filename, "w") as f: + f.write(contents) + os.chmod(f.name, mode) + logging.info("created nodefile: '%s'; mode: 0%o", f.name, mode) def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: return self.host_cmd(args, wait=wait) + def addfile(self, srcname: str, filename: str) -> None: + raise CoreError("physical node does not support addfile") + class Rj45Node(CoreNodeBase): """ @@ -242,7 +240,6 @@ def __init__( _id: int = None, name: str = None, mtu: int = 1500, - start: bool = True, server: DistributedServer = None, ) -> None: """ @@ -252,19 +249,16 @@ def __init__( :param _id: node id :param name: node name :param mtu: rj45 mtu - :param start: start flag :param server: remote server node will run on, default is None for localhost """ - super().__init__(session, _id, name, start, server) - self.interface = CoreInterface(session, self, name, name, mtu, server) - self.interface.transport_type = TransportType.RAW + super().__init__(session, _id, name, server) + self.iface = CoreInterface(session, self, name, name, mtu, server) + self.iface.transport_type = TransportType.RAW self.lock: threading.RLock = threading.RLock() - self.ifindex: Optional[int] = None + self.iface_id: Optional[int] = None self.old_up: bool = False self.old_addrs: List[Tuple[str, Optional[str]]] = [] - if start: - self.startup() def startup(self) -> None: """ @@ -275,7 +269,7 @@ def startup(self) -> None: """ # interface will also be marked up during net.attach() self.savestate() - self.net_client.device_up(self.interface.localname) + self.net_client.device_up(self.iface.localname) self.up = True def shutdown(self) -> None: @@ -287,7 +281,7 @@ def shutdown(self) -> None: """ if not self.up: return - localname = self.interface.localname + localname = self.iface.localname self.net_client.device_down(localname) self.net_client.device_flush(localname) try: @@ -297,102 +291,86 @@ def shutdown(self) -> None: self.up = False self.restorestate() - def newnetif(self, net: CoreNetworkBase, interface: InterfaceData) -> int: + def new_iface( + self, net: CoreNetworkBase, iface_data: InterfaceData + ) -> CoreInterface: """ This is called when linking with another node. Since this node represents an interface, we do not create another object here, but attach ourselves to the given network. :param net: new network instance - :param interface: interface data for new interface + :param iface_data: interface data for new interface :return: interface index :raises ValueError: when an interface has already been created, one max """ with self.lock: - ifindex = interface.id - if ifindex is None: - ifindex = 0 - if self.interface.net is not None: - raise ValueError("RJ45 nodes support at most 1 network interface") - self._netif[ifindex] = self.interface - self.ifindex = ifindex + iface_id = iface_data.id + if iface_id is None: + iface_id = 0 + if self.iface.net is not None: + raise CoreError("RJ45 nodes support at most 1 network interface") + self.ifaces[iface_id] = self.iface + self.iface_id = iface_id if net is not None: - self.interface.attachnet(net) - for addr in interface.get_addresses(): - self.addaddr(addr) - return ifindex + self.iface.attachnet(net) + for ip in iface_data.get_ips(): + self.add_ip(ip) + return self.iface - def delnetif(self, ifindex: int) -> None: + def delete_iface(self, iface_id: int) -> None: """ Delete a network interface. - :param ifindex: interface index to delete + :param iface_id: interface index to delete :return: nothing """ - if ifindex is None: - ifindex = 0 - self._netif.pop(ifindex) - if ifindex == self.ifindex: - self.shutdown() - else: - raise ValueError(f"ifindex {ifindex} does not exist") - - def netif( - self, ifindex: int, net: CoreNetworkBase = None - ) -> Optional[CoreInterface]: - """ - This object is considered the network interface, so we only - return self here. This keeps the RJ45Node compatible with - real nodes. + self.get_iface(iface_id) + self.ifaces.pop(iface_id) + self.shutdown() - :param ifindex: interface index to retrieve - :param net: network to retrieve - :return: a network interface - """ - if net is not None and net == self.interface.net: - return self.interface - if ifindex is None: - ifindex = 0 - if ifindex == self.ifindex: - return self.interface - return None + def get_iface(self, iface_id: int) -> CoreInterface: + if iface_id != self.iface_id or iface_id not in self.ifaces: + raise CoreError(f"node({self.name}) interface({iface_id}) does not exist") + return self.iface - def getifindex(self, netif: CoreInterface) -> Optional[int]: + def get_iface_id(self, iface: CoreInterface) -> Optional[int]: """ Retrieve network interface index. - :param netif: network interface to retrieve + :param iface: network interface to retrieve index for :return: interface index, None otherwise """ - if netif != self.interface: - return None - return self.ifindex + if iface is not self.iface: + raise CoreError(f"node({self.name}) does not have interface({iface.name})") + return self.iface_id - def addaddr(self, addr: str) -> None: + def add_ip(self, ip: str) -> None: """ - Add address to to network interface. + Add an ip address to an interface in the format "10.0.0.1/24". - :param addr: address to add + :param ip: address to add to interface :return: nothing - :raises CoreCommandError: when there is a command exception + :raises CoreError: when ip address provided is invalid + :raises CoreCommandError: when a non-zero exit status occurs """ - addr = utils.validate_ip(addr) + self.iface.add_ip(ip) if self.up: - self.net_client.create_address(self.name, addr) - self.interface.addaddr(addr) + self.net_client.create_address(self.name, ip) - def deladdr(self, addr: str) -> None: + def remove_ip(self, ip: str) -> None: """ - Delete address from network interface. + Remove an ip address from an interface in the format "10.0.0.1/24". - :param addr: address to delete + :param ip: ip address to remove from interface :return: nothing - :raises CoreCommandError: when there is a command exception + :raises CoreError: when ip address provided is invalid + :raises CoreCommandError: when a non-zero exit status occurs """ + self.iface.remove_ip(ip) if self.up: - self.net_client.delete_address(self.name, addr) - self.interface.deladdr(addr) + self.net_client.delete_address(self.name, ip) def savestate(self) -> None: """ @@ -404,7 +382,7 @@ def savestate(self) -> None: """ self.old_up = False self.old_addrs: List[Tuple[str, Optional[str]]] = [] - localname = self.interface.localname + localname = self.iface.localname output = self.net_client.address_show(localname) for line in output.split("\n"): items = line.split() @@ -429,7 +407,7 @@ def restorestate(self) -> None: :return: nothing :raises CoreCommandError: when there is a command exception """ - localname = self.interface.localname + localname = self.iface.localname logging.info("restoring rj45 state: %s", localname) for addr in self.old_addrs: self.net_client.create_address(localname, addr[0], addr[1]) @@ -446,13 +424,16 @@ def setposition(self, x: float = None, y: float = None, z: float = None) -> None :return: True if position changed, False otherwise """ super().setposition(x, y, z) - self.interface.setposition() + self.iface.setposition() def termcmdstring(self, sh: str) -> str: - """ - Create a terminal command string. + raise CoreError("rj45 does not support terminal commands") - :param sh: shell to execute command in - :return: str - """ - raise NotImplementedError + def addfile(self, srcname: str, filename: str) -> None: + raise CoreError("rj45 does not support addfile") + + def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: + raise CoreError("rj45 does not support nodefile") + + def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: + raise CoreError("rj45 does not support cmds") diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 8b4ec39fe..27e54ff33 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -8,8 +8,7 @@ from typing import IO, TYPE_CHECKING, Dict, Optional, Set, Tuple from urllib.parse import urlparse -from core import constants -from core.constants import CORE_DATA_DIR +from core.constants import CORE_CONF_DIR, CORE_DATA_DIR from core.emane.nodes import EmaneNet from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import EventTypes, MessageFlags @@ -21,8 +20,8 @@ from core.emulator.session import Session -def get_link_id(node_one: int, node_two: int, network_id: int) -> str: - link_id = f"{node_one}-{node_two}" +def get_link_id(node1_id: int, node2_id: int, network_id: int) -> str: + link_id = f"{node1_id}-{node2_id}" if network_id is not None: link_id = f"{link_id}-{network_id}" return link_id @@ -215,7 +214,7 @@ def sendobjs(self) -> None: for layer in CORE_LAYERS: self.cmd(f"layer {layer}") - 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, CoreNetworkBase): @@ -225,7 +224,7 @@ def sendobjs(self) -> None: self.add_node(node) for net in nets: - all_links = net.all_link_data(flags=MessageFlags.ADD) + all_links = net.links(flags=MessageFlags.ADD) for link_data in all_links: is_wireless = isinstance(net, (WlanNode, EmaneNet)) if is_wireless and link_data.node1_id == net.id: @@ -264,8 +263,8 @@ def add_node(self, node: NodeBase) -> None: icon = node.icon if icon: node_type = node.name - icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR) - icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR) + icon = icon.replace("$CORE_DATA_DIR", CORE_DATA_DIR) + icon = icon.replace("$CORE_CONF_DIR", CORE_CONF_DIR) self.cmd(f"sprite {node_type} image {icon}") self.cmd( f'node {node.id} nodeLayer "{NODE_LAYER}" ' @@ -314,26 +313,22 @@ def handle_node_update(self, node_data: NodeData) -> None: :param node_data: node data being updated :return: nothing """ - logging.debug("sdt handle node update: %s - %s", node_data.id, node_data.name) if not self.connect(): return - - # delete node + node = node_data.node + logging.debug("sdt handle node update: %s - %s", node.id, node.name) if node_data.message_type == MessageFlags.DELETE: - self.cmd(f"delete node,{node_data.id}") + self.cmd(f"delete node,{node.id}") else: - x = node_data.x_position - y = node_data.y_position - lat = node_data.latitude - lon = node_data.longitude - alt = node_data.altitude + x, y, _ = node.position.get() + lon, lat, alt = node.position.get_geo() if all([lat is not None, lon is not None, alt is not None]): pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" - self.cmd(f"node {node_data.id} {pos}") + self.cmd(f"node {node.id} {pos}") elif node_data.message_type == 0: lat, lon, alt = self.session.location.getgeo(x, y, 0) pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" - self.cmd(f"node {node_data.id} {pos}") + self.cmd(f"node {node.id} {pos}") def wireless_net_check(self, node_id: int) -> bool: """ @@ -351,27 +346,27 @@ def wireless_net_check(self, node_id: int) -> bool: return result def add_link( - self, node_one: int, node_two: int, network_id: int = None, label: str = None + self, node1_id: int, node2_id: int, network_id: int = None, label: str = None ) -> None: """ Handle adding a link in SDT. - :param node_one: node one id - :param node_two: node two id + :param node1_id: node one id + :param node2_id: node two id :param network_id: network link is associated with, None otherwise :param label: label for link :return: nothing """ - logging.debug("sdt add link: %s, %s, %s", node_one, node_two, network_id) + logging.debug("sdt add link: %s, %s, %s", node1_id, node2_id, network_id) if not self.connect(): return - if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): + if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): return color = DEFAULT_LINK_COLOR if network_id: color = self.session.get_link_color(network_id) line = f"{color},2" - link_id = get_link_id(node_one, node_two, network_id) + link_id = get_link_id(node1_id, node2_id, network_id) layer = LINK_LAYER if network_id: node = self.session.nodes.get(network_id) @@ -383,47 +378,47 @@ def add_link( if label: link_label = f'linklabel on,"{label}"' self.cmd( - f"link {node_one},{node_two},{link_id} linkLayer {layer} line {line} " + f"link {node1_id},{node2_id},{link_id} linkLayer {layer} line {line} " f"{link_label}" ) - def delete_link(self, node_one: int, node_two: int, network_id: int = None) -> None: + def delete_link(self, node1_id: int, node2_id: int, network_id: int = None) -> None: """ Handle deleting a link in SDT. - :param node_one: node one id - :param node_two: node two id + :param node1_id: node one id + :param node2_id: node two id :param network_id: network link is associated with, None otherwise :return: nothing """ - logging.debug("sdt delete link: %s, %s, %s", node_one, node_two, network_id) + logging.debug("sdt delete link: %s, %s, %s", node1_id, node2_id, network_id) if not self.connect(): return - if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): + if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): return - link_id = get_link_id(node_one, node_two, network_id) - self.cmd(f"delete link,{node_one},{node_two},{link_id}") + link_id = get_link_id(node1_id, node2_id, network_id) + self.cmd(f"delete link,{node1_id},{node2_id},{link_id}") def edit_link( - self, node_one: int, node_two: int, network_id: int, label: str + self, node1_id: int, node2_id: int, network_id: int, label: str ) -> None: """ Handle editing a link in SDT. - :param node_one: node one id - :param node_two: node two id + :param node1_id: node one id + :param node2_id: node two id :param network_id: network link is associated with, None otherwise :param label: label to update :return: nothing """ - logging.debug("sdt edit link: %s, %s, %s", node_one, node_two, network_id) + logging.debug("sdt edit link: %s, %s, %s", node1_id, node2_id, network_id) if not self.connect(): return - if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): + if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): return - link_id = get_link_id(node_one, node_two, network_id) + link_id = get_link_id(node1_id, node2_id, network_id) link_label = f'linklabel on,"{label}"' - self.cmd(f"link {node_one},{node_two},{link_id} {link_label}") + self.cmd(f"link {node1_id},{node2_id},{link_id} {link_label}") def handle_link_update(self, link_data: LinkData) -> None: """ @@ -432,13 +427,13 @@ def handle_link_update(self, link_data: LinkData) -> None: :param link_data: link data to handle :return: nothing """ - node_one = link_data.node1_id - node_two = link_data.node2_id + node1_id = link_data.node1_id + node2_id = link_data.node2_id network_id = link_data.network_id label = link_data.label if link_data.message_type == MessageFlags.ADD: - self.add_link(node_one, node_two, network_id, label) + self.add_link(node1_id, node2_id, network_id, label) elif link_data.message_type == MessageFlags.DELETE: - self.delete_link(node_one, node_two, network_id) + self.delete_link(node1_id, node2_id, network_id) elif link_data.message_type == MessageFlags.NONE and label: - self.edit_link(node_one, node_two, network_id, label) + self.edit_link(node1_id, node2_id, network_id, label) diff --git a/daemon/core/services/bird.py b/daemon/core/services/bird.py index 4901ea56e..ffb177f3e 100644 --- a/daemon/core/services/bird.py +++ b/daemon/core/services/bird.py @@ -1,8 +1,9 @@ """ bird.py: defines routing services provided by the BIRD Internet Routing Daemon. """ -import netaddr +from typing import Optional, Tuple +from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -11,46 +12,41 @@ class Bird(CoreService): Bird router support """ - name = "bird" - executables = ("bird",) - group = "BIRD" - dirs = ("/etc/bird",) - configs = ("/etc/bird/bird.conf",) - startup = ("bird -c %s" % (configs[0]),) - shutdown = ("killall bird",) - validate = ("pidof bird",) + name: str = "bird" + group: str = "BIRD" + executables: Tuple[str, ...] = ("bird",) + dirs: Tuple[str, ...] = ("/etc/bird",) + configs: Tuple[str, ...] = ("/etc/bird/bird.conf",) + startup: Tuple[str, ...] = ("bird -c %s" % (configs[0]),) + shutdown: Tuple[str, ...] = ("killall bird",) + validate: Tuple[str, ...] = ("pidof bird",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the bird.conf file contents. """ if filename == cls.configs[0]: - return cls.generateBirdConf(node) + return cls.generate_bird_config(node) else: raise ValueError @staticmethod - def routerid(node): + def router_id(node: CoreNode) -> str: """ Helper to return the first IPv4 address of a node as its router ID. """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return a - # raise ValueError, "no IPv4 address found for router ID" + for iface in node.get_ifaces(control=False): + ip4 = iface.get_ip4() + if ip4: + return str(ip4.ip) return "0.0.0.0" @classmethod - def generateBirdConf(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: """ Returns configuration file text. Other services that depend on bird - will have generatebirdifcconfig() and generatebirdconfig() - hooks that are invoked here. + will have hooks that are invoked here. """ cfg = """\ /* Main configuration file for BIRD. This is ony a template, @@ -77,15 +73,16 @@ def generateBirdConf(cls, node): """ % ( cls.name, - cls.routerid(node), + cls.router_id(node), ) - # Generate protocol specific configurations + # generate protocol specific configurations for s in node.services: if cls.name not in s.dependencies: continue - cfg += s.generatebirdconfig(node) - + if not (isinstance(s, BirdService) or issubclass(s, BirdService)): + continue + cfg += s.generate_bird_config(node) return cfg @@ -95,34 +92,26 @@ class BirdService(CoreService): common to Bird's routing daemons. """ - name = None - executables = ("bird",) - group = "BIRD" - dependencies = ("bird",) - dirs = () - configs = () - startup = () - shutdown = () - meta = "The config file for this service can be found in the bird service." + name: Optional[str] = None + group: str = "BIRD" + executables: Tuple[str, ...] = ("bird",) + dependencies: Tuple[str, ...] = ("bird",) + meta: str = "The config file for this service can be found in the bird service." @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: return "" @classmethod - def generatebirdifcconfig(cls, node): + def generate_bird_iface_config(cls, node: CoreNode) -> str: """ Use only bare interfaces descriptions in generated protocol configurations. This has the slight advantage of being the same everywhere. """ cfg = "" - - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += ' interface "%s";\n' % ifc.name - + for iface in node.get_ifaces(control=False): + cfg += ' interface "%s";\n' % iface.name return cfg @@ -131,11 +120,11 @@ class BirdBgp(BirdService): BGP BIRD Service (configuration generation) """ - name = "BIRD_BGP" - custom_needed = True + name: str = "BIRD_BGP" + custom_needed: bool = True @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: return """ /* This is a sample config that should be customized with appropriate AS numbers * and peers; add one section like this for each neighbor */ @@ -162,10 +151,10 @@ class BirdOspf(BirdService): OSPF BIRD Service (configuration generation) """ - name = "BIRD_OSPFv2" + name: str = "BIRD_OSPFv2" @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: cfg = "protocol ospf {\n" cfg += " export filter {\n" cfg += " if source = RTS_BGP then {\n" @@ -175,10 +164,9 @@ def generatebirdconfig(cls, node): cfg += " accept;\n" cfg += " };\n" cfg += " area 0.0.0.0 {\n" - cfg += cls.generatebirdifcconfig(node) + cfg += cls.generate_bird_iface_config(node) cfg += " };\n" cfg += "}\n\n" - return cfg @@ -187,15 +175,14 @@ class BirdRadv(BirdService): RADV BIRD Service (configuration generation) """ - name = "BIRD_RADV" + name: str = "BIRD_RADV" @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: cfg = "/* This is a sample config that must be customized */\n" - cfg += "protocol radv {\n" cfg += " # auto configuration on all interfaces\n" - cfg += cls.generatebirdifcconfig(node) + cfg += cls.generate_bird_iface_config(node) cfg += " # Advertise DNS\n" cfg += " rdnss {\n" cfg += "# lifetime mult 10;\n" @@ -206,7 +193,6 @@ def generatebirdconfig(cls, node): cfg += "# ns 2001:0DB8:1234::12;\n" cfg += " };\n" cfg += "}\n\n" - return cfg @@ -215,20 +201,19 @@ class BirdRip(BirdService): RIP BIRD Service (configuration generation) """ - name = "BIRD_RIP" + name: str = "BIRD_RIP" @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: cfg = "protocol rip {\n" cfg += " period 10;\n" cfg += " garbage time 60;\n" - cfg += cls.generatebirdifcconfig(node) + cfg += cls.generate_bird_iface_config(node) cfg += " honor neighbor;\n" cfg += " authentication none;\n" cfg += " import all;\n" cfg += " export all;\n" cfg += "}\n\n" - return cfg @@ -237,11 +222,11 @@ class BirdStatic(BirdService): Static Bird Service (configuration generation) """ - name = "BIRD_static" - custom_needed = True + name: str = "BIRD_static" + custom_needed: bool = True @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: cfg = "/* This is a sample config that must be customized */\n" cfg += "protocol static {\n" cfg += "# route 0.0.0.0/0 via 198.51.100.130; # Default route. Do NOT advertise on BGP !\n" diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 491113ffd..8c41c57dc 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -10,13 +10,12 @@ import enum import logging import time -from typing import TYPE_CHECKING, Iterable, List, Tuple, Type +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple, Type from core import utils -from core.constants import which from core.emulator.data import FileData from core.emulator.enumerations import ExceptionLevels, MessageFlags, RegisterTlvs -from core.errors import CoreCommandError +from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode if TYPE_CHECKING: @@ -36,14 +35,15 @@ class ServiceMode(enum.Enum): class ServiceDependencies: """ Can generate boot paths for services, based on their dependencies. Will validate - that all services will be booted and that all dependencies exist within the services provided. + that all services will be booted and that all dependencies exist within the services + provided. """ - def __init__(self, services: List[Type["CoreService"]]) -> None: + def __init__(self, services: List["CoreService"]) -> None: # helpers to check validity - self.dependents = {} - self.booted = set() - self.node_services = {} + self.dependents: Dict[str, Set[str]] = {} + self.booted: Set[str] = set() + self.node_services: Dict[str, "CoreService"] = {} for service in services: self.node_services[service.name] = service for dependency in service.dependencies: @@ -51,9 +51,9 @@ def __init__(self, services: List[Type["CoreService"]]) -> None: dependents.add(service.name) # used to find paths - self.path = [] - self.visited = set() - self.visiting = set() + self.path: List["CoreService"] = [] + self.visited: Set[str] = set() + self.visiting: Set[str] = set() def boot_paths(self) -> List[List["CoreService"]]: """ @@ -131,7 +131,7 @@ def _visit(self, current_service: "CoreService") -> List["CoreService"]: class ServiceShim: - keys = [ + keys: List[str] = [ "dirs", "files", "startidx", @@ -241,10 +241,10 @@ class ServiceManager: Manages services available for CORE nodes to use. """ - services = {} + services: Dict[str, Type["CoreService"]] = {} @classmethod - def add(cls, service: "CoreService") -> None: + def add(cls, service: Type["CoreService"]) -> None: """ Add a service to manager. @@ -261,7 +261,10 @@ def add(cls, service: "CoreService") -> None: # validate dependent executables are present for executable in service.executables: - which(executable, required=True) + try: + utils.which(executable, required=True) + except CoreError as e: + raise CoreError(f"service({name}): {e}") # validate service on load succeeds try: @@ -299,7 +302,7 @@ def add_services(cls, path: str) -> List[str]: try: cls.add(service) - except ValueError as e: + except (CoreError, ValueError) as e: service_errors.append(service.name) logging.debug("not loading service(%s): %s", service.name, e) return service_errors @@ -314,8 +317,8 @@ class CoreServices: custom service configuration. A CoreService is not a Configurable. """ - name = "services" - config_type = RegisterTlvs.UTILITY + name: str = "services" + config_type: RegisterTlvs = RegisterTlvs.UTILITY def __init__(self, session: "Session") -> None: """ @@ -323,11 +326,17 @@ def __init__(self, session: "Session") -> None: :param session: session this manager is tied to """ - self.session = session + self.session: "Session" = session # dict of default services tuples, key is node type - self.default_services = {} + self.default_services: Dict[str, List[str]] = { + "mdr": ["zebra", "OSPFv3MDR", "IPForward"], + "PC": ["DefaultRoute"], + "prouter": [], + "router": ["zebra", "OSPFv2", "OSPFv3", "IPForward"], + "host": ["DefaultRoute", "SSH"], + } # dict of node ids to dict of custom services by name - self.custom_services = {} + self.custom_services: Dict[int, Dict[str, "CoreService"]] = {} def reset(self) -> None: """ @@ -419,7 +428,7 @@ def add_services( continue node.services.append(service) - def all_configs(self) -> List[Tuple[int, Type["CoreService"]]]: + def all_configs(self) -> List[Tuple[int, "CoreService"]]: """ Return (node_id, service) tuples for all stored configs. Used when reconnecting to a session or opening XML. @@ -802,50 +811,50 @@ class CoreService: """ # service name should not include spaces - name = None + name: Optional[str] = None # executables that must exist for service to run - executables = () + executables: Tuple[str, ...] = () # sets service requirements that must be started prior to this service starting - dependencies = () + dependencies: Tuple[str, ...] = () # group string allows grouping services together - group = None + group: Optional[str] = None # private, per-node directories required by this service - dirs = () + dirs: Tuple[str, ...] = () # config files written by this service - configs = () + configs: Tuple[str, ...] = () # config file data - config_data = {} + config_data: Dict[str, str] = {} # list of startup commands - startup = () + startup: Tuple[str, ...] = () # list of shutdown commands - shutdown = () + shutdown: Tuple[str, ...] = () # list of validate commands - validate = () + validate: Tuple[str, ...] = () # validation mode, used to determine startup success - validation_mode = ServiceMode.NON_BLOCKING + validation_mode: ServiceMode = ServiceMode.NON_BLOCKING # time to wait in seconds for determining if service started successfully - validation_timer = 5 + validation_timer: int = 5 # validation period in seconds, how frequent validation is attempted - validation_period = 0.5 + validation_period: float = 0.5 # metadata associated with this service - meta = None + meta: Optional[str] = None # custom configuration text - custom = False - custom_needed = False + custom: bool = False + custom_needed: bool = False def __init__(self) -> None: """ @@ -853,8 +862,8 @@ def __init__(self) -> None: against their config. Services are instantiated when a custom configuration is used to override their default parameters. """ - self.custom = True - self.config_data = self.__class__.config_data.copy() + self.custom: bool = True + self.config_data: Dict[str, str] = self.__class__.config_data.copy() @classmethod def on_load(cls) -> None: diff --git a/daemon/core/services/emaneservices.py b/daemon/core/services/emaneservices.py index 9d09516e5..d694317a2 100644 --- a/daemon/core/services/emaneservices.py +++ b/daemon/core/services/emaneservices.py @@ -1,47 +1,34 @@ +from typing import Tuple + from core.emane.nodes import EmaneNet -from core.errors import CoreError +from core.nodes.base import CoreNode from core.services.coreservices import CoreService from core.xml import emanexml class EmaneTransportService(CoreService): - name = "transportd" - executables = ("emanetransportd", "emanegentransportxml") - group = "EMANE" - dependencies = () - dirs = () - configs = ("emanetransport.sh",) - startup = ("sh %s" % configs[0],) - validate = ("pidof %s" % executables[0],) - validation_timer = 0.5 - shutdown = ("killall %s" % executables[0],) + name: str = "transportd" + group: str = "EMANE" + executables: Tuple[str, ...] = ("emanetransportd", "emanegentransportxml") + dependencies: Tuple[str, ...] = () + dirs: Tuple[str, ...] = () + configs: Tuple[str, ...] = ("emanetransport.sh",) + startup: Tuple[str, ...] = (f"sh {configs[0]}",) + validate: Tuple[str, ...] = (f"pidof {executables[0]}",) + validation_timer: float = 0.5 + shutdown: Tuple[str, ...] = (f"killall {executables[0]}",) @classmethod - def generate_config(cls, node, filename): - if filename == cls.configs[0]: - transport_commands = [] - for interface in node.netifs(sort=True): - try: - network_node = node.session.get_node(interface.net.id, EmaneNet) - config = node.session.emane.get_configs( - network_node.id, network_node.model.name - ) - if config and emanexml.is_external(config): - nem_id = network_node.getnemid(interface) - command = ( - "emanetransportd -r -l 0 -d ../transportdaemon%s.xml" - % nem_id - ) - transport_commands.append(command) - except CoreError: - pass - transport_commands = "\n".join(transport_commands) - return """ -emanegentransportxml -o ../ ../platform%s.xml -%s -""" % ( - node.id, - transport_commands, - ) - else: - raise ValueError + def generate_config(cls, node: CoreNode, filename: str) -> str: + emane_manager = node.session.emane + cfg = "" + for iface in node.get_ifaces(): + if not isinstance(iface.net, EmaneNet): + continue + emane_net = iface.net + config = emane_manager.get_iface_config(emane_net, iface) + if emanexml.is_external(config): + nem_id = emane_manager.get_nem_id(iface) + cfg += f"emanegentransportxml {iface.name}-platform.xml\n" + cfg += f"emanetransportd -r -l 0 -d transportdaemon{nem_id}.xml\n" + return cfg diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 9a3443394..b130fd8c5 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -2,69 +2,73 @@ frr.py: defines routing services provided by FRRouting. Assumes installation of FRR via https://deb.frrouting.org/ """ +from typing import Optional, Tuple + import netaddr -from core import constants from core.emane.nodes import EmaneNet +from core.nodes.base import CoreNode +from core.nodes.interface import CoreInterface from core.nodes.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node from core.services.coreservices import CoreService +FRR_STATE_DIR: str = "/var/run/frr" + class FRRZebra(CoreService): - name = "FRRzebra" - group = "FRR" - dirs = ("/usr/local/etc/frr", "/var/run/frr", "/var/log/frr") - configs = ( + name: str = "FRRzebra" + group: str = "FRR" + dirs: Tuple[str, ...] = ("/usr/local/etc/frr", "/var/run/frr", "/var/log/frr") + configs: Tuple[str, ...] = ( "/usr/local/etc/frr/frr.conf", "frrboot.sh", "/usr/local/etc/frr/vtysh.conf", "/usr/local/etc/frr/daemons", ) - startup = ("sh frrboot.sh zebra",) - shutdown = ("killall zebra",) - validate = ("pidof zebra",) + startup: Tuple[str, ...] = ("sh frrboot.sh zebra",) + shutdown: Tuple[str, ...] = ("killall zebra",) + validate: Tuple[str, ...] = ("pidof zebra",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the frr.conf or frrboot.sh file contents. """ if filename == cls.configs[0]: - return cls.generateFrrConf(node) + return cls.generate_frr_conf(node) elif filename == cls.configs[1]: - return cls.generateFrrBoot(node) + return cls.generate_frr_boot(node) elif filename == cls.configs[2]: - return cls.generateVtyshConf(node) + return cls.generate_vtysh_conf(node) elif filename == cls.configs[3]: - return cls.generateFrrDaemons(node) + return cls.generate_frr_daemons(node) else: raise ValueError( "file name (%s) is not a known configuration: %s", filename, cls.configs ) @classmethod - def generateVtyshConf(cls, node): + def generate_vtysh_conf(cls, node: CoreNode) -> str: """ Returns configuration file text. """ return "service integrated-vtysh-config\n" @classmethod - def generateFrrConf(cls, node): + def generate_frr_conf(cls, node: CoreNode) -> str: """ Returns configuration file text. Other services that depend on zebra - will have generatefrrifcconfig() and generatefrrconfig() - hooks that are invoked here. + will have hooks that are invoked here. """ # we could verify here that filename == frr.conf cfg = "" - for ifc in node.netifs(): - cfg += "interface %s\n" % ifc.name + for iface in node.get_ifaces(): + cfg += "interface %s\n" % iface.name # include control interfaces in addressing but not routing daemons - if hasattr(ifc, "control") and ifc.control is True: + if hasattr(iface, "control") and iface.control is True: cfg += " " - cfg += "\n ".join(map(cls.addrstr, ifc.addrlist)) + cfg += "\n ".join(map(cls.addrstr, iface.ips())) cfg += "\n" continue cfgv4 = "" @@ -74,29 +78,25 @@ def generateFrrConf(cls, node): for s in node.services: if cls.name not in s.dependencies: continue - ifccfg = s.generatefrrifcconfig(node, ifc) + if not (isinstance(s, FrrService) or issubclass(s, FrrService)): + continue + iface_config = s.generate_frr_iface_config(node, iface) if s.ipv4_routing: want_ipv4 = True if s.ipv6_routing: want_ipv6 = True - cfgv6 += ifccfg + cfgv6 += iface_config else: - cfgv4 += ifccfg + cfgv4 += iface_config if want_ipv4: - ipv4list = filter( - lambda x: netaddr.valid_ipv4(x.split("/")[0]), ifc.addrlist - ) cfg += " " - cfg += "\n ".join(map(cls.addrstr, ipv4list)) + cfg += "\n ".join(map(cls.addrstr, iface.ip4s)) cfg += "\n" cfg += cfgv4 if want_ipv6: - ipv6list = filter( - lambda x: netaddr.valid_ipv6(x.split("/")[0]), ifc.addrlist - ) cfg += " " - cfg += "\n ".join(map(cls.addrstr, ipv6list)) + cfg += "\n ".join(map(cls.addrstr, iface.ip6s)) cfg += "\n" cfg += cfgv6 cfg += "!\n" @@ -104,24 +104,26 @@ def generateFrrConf(cls, node): for s in node.services: if cls.name not in s.dependencies: continue - cfg += s.generatefrrconfig(node) + if not (isinstance(s, FrrService) or issubclass(s, FrrService)): + continue + cfg += s.generate_frr_config(node) return cfg @staticmethod - def addrstr(x): + def addrstr(ip: netaddr.IPNetwork) -> str: """ helper for mapping IP addresses to zebra config statements """ - addr = x.split("/")[0] - if netaddr.valid_ipv4(addr): - return "ip address %s" % x - elif netaddr.valid_ipv6(addr): - return "ipv6 address %s" % x + address = str(ip.ip) + if netaddr.valid_ipv4(address): + return "ip address %s" % ip + elif netaddr.valid_ipv6(address): + return "ipv6 address %s" % ip else: - raise ValueError("invalid address: %s", x) + raise ValueError("invalid address: %s", ip) @classmethod - def generateFrrBoot(cls, node): + def generate_frr_boot(cls, node: CoreNode) -> str: """ Generate a shell script used to boot the FRR daemons. """ @@ -235,16 +237,16 @@ def generateFrrBoot(cls, node): cls.configs[0], frr_sbin_search, frr_bin_search, - constants.FRR_STATE_DIR, + FRR_STATE_DIR, ) - for ifc in node.netifs(): - cfg += f"ip link set dev {ifc.name} down\n" + for iface in node.get_ifaces(): + cfg += f"ip link set dev {iface.name} down\n" cfg += "sleep 1\n" - cfg += f"ip link set dev {ifc.name} up\n" + cfg += f"ip link set dev {iface.name} up\n" return cfg @classmethod - def generateFrrDaemons(cls, node): + def generate_frr_daemons(cls, node: CoreNode) -> str: """ Returns configuration file text. """ @@ -271,6 +273,7 @@ def generateFrrDaemons(cls, node): eigrpd=yes babeld=yes sharpd=yes +staticd=yes pbrd=yes bfdd=yes fabricd=yes @@ -317,57 +320,48 @@ class FrrService(CoreService): common to FRR's routing daemons. """ - name = None - group = "FRR" - dependencies = ("FRRzebra",) - dirs = () - configs = () - startup = () - shutdown = () - meta = "The config file for this service can be found in the Zebra service." - - ipv4_routing = False - ipv6_routing = False + name: Optional[str] = None + group: str = "FRR" + dependencies: Tuple[str, ...] = ("FRRzebra",) + meta: str = "The config file for this service can be found in the Zebra service." + ipv4_routing: bool = False + ipv6_routing: bool = False @staticmethod - def routerid(node): + def router_id(node: CoreNode) -> str: """ Helper to return the first IPv4 address of a node as its router ID. """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return a - # raise ValueError, "no IPv4 address found for router ID" + for iface in node.get_ifaces(control=False): + ip4 = iface.get_ip4() + if ip4: + return str(ip4.ip) return "0.0.0.0" @staticmethod - def rj45check(ifc): + def rj45check(iface: CoreInterface) -> bool: """ Helper to detect whether interface is connected an external RJ45 link. """ - if ifc.net: - for peerifc in ifc.net.netifs(): - if peerifc == ifc: + if iface.net: + for peer_iface in iface.net.get_ifaces(): + if peer_iface == iface: continue - if isinstance(peerifc.node, Rj45Node): + if isinstance(peer_iface.node, Rj45Node): return True return False @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" @classmethod - def generatefrrifcconfig(cls, node, ifc): + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: return "" @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: return "" @@ -378,60 +372,54 @@ class FRROspfv2(FrrService): unified frr.conf file. """ - name = "FRROSPFv2" - startup = () - shutdown = ("killall ospfd",) - validate = ("pidof ospfd",) - ipv4_routing = True + name: str = "FRROSPFv2" + shutdown: Tuple[str, ...] = ("killall ospfd",) + validate: Tuple[str, ...] = ("pidof ospfd",) + ipv4_routing: bool = True @staticmethod - def mtucheck(ifc): + def mtu_check(iface: CoreInterface) -> str: """ 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: # a workaround for PhysicalNode GreTap, which has no knowledge of # the other nodes/nets return " ip ospf mtu-ignore\n" - if not ifc.net: + if not iface.net: return "" - for i in ifc.net.netifs(): - if i.mtu != ifc.mtu: + for iface in iface.net.get_ifaces(): + if iface.mtu != iface.mtu: return " ip ospf mtu-ignore\n" return "" @staticmethod - def ptpcheck(ifc): + def ptp_check(iface: CoreInterface) -> str: """ Helper to detect whether interface is connected to a notional point-to-point link. """ - if isinstance(ifc.net, PtpNet): + if isinstance(iface.net, PtpNet): return " ip ospf network point-to-point\n" return "" @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router ospf\n" - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " router-id %s\n" % rtrid # network 10.0.0.0/24 area 0 - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: - addr = a.split("/")[0] - if not netaddr.valid_ipv4(addr): - continue - cfg += " network %s area 0\n" % a + for iface in node.get_ifaces(control=False): + for ip4 in iface.ip4s: + cfg += f" network {ip4} area 0\n" cfg += "!\n" return cfg @classmethod - def generatefrrifcconfig(cls, node, ifc): - return cls.mtucheck(ifc) + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + return cls.mtu_check(iface) class FRROspfv3(FrrService): @@ -441,71 +429,67 @@ class FRROspfv3(FrrService): unified frr.conf file. """ - name = "FRROSPFv3" - startup = () - shutdown = ("killall ospf6d",) - validate = ("pidof ospf6d",) - ipv4_routing = True - ipv6_routing = True + name: str = "FRROSPFv3" + shutdown: Tuple[str, ...] = ("killall ospf6d",) + validate: Tuple[str, ...] = ("pidof ospf6d",) + ipv4_routing: bool = True + ipv6_routing: bool = True @staticmethod - def minmtu(ifc): + def 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 @classmethod - def mtucheck(cls, ifc): + def mtu_check(cls, iface: CoreInterface) -> str: """ Helper to detect MTU mismatch and add the appropriate OSPFv3 ifmtu command. This is needed when e.g. a node is linked via a GreTap device. """ - minmtu = cls.minmtu(ifc) - if minmtu < ifc.mtu: + minmtu = cls.min_mtu(iface) + if minmtu < iface.mtu: return " ipv6 ospf6 ifmtu %d\n" % minmtu else: return "" @staticmethod - def ptpcheck(ifc): + def ptp_check(iface: CoreInterface) -> str: """ Helper to detect whether interface is connected to a notional point-to-point link. """ - if isinstance(ifc.net, PtpNet): + if isinstance(iface.net, PtpNet): return " ipv6 ospf6 network point-to-point\n" return "" @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router ospf6\n" - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " router-id %s\n" % rtrid - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += " interface %s area 0.0.0.0\n" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += " interface %s area 0.0.0.0\n" % iface.name cfg += "!\n" return cfg @classmethod - def generatefrrifcconfig(cls, node, ifc): - return cls.mtucheck(ifc) + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + return cls.mtu_check(iface) # cfg = cls.mtucheck(ifc) # external RJ45 connections will use default OSPF timers # if cls.rj45check(ifc): # return cfg # cfg += cls.ptpcheck(ifc) - # return cfg + """\ @@ -522,21 +506,20 @@ class FRRBgp(FrrService): having the same AS number. """ - name = "FRRBGP" - startup = () - shutdown = ("killall bgpd",) - validate = ("pidof bgpd",) - custom_needed = True - ipv4_routing = True - ipv6_routing = True + name: str = "FRRBGP" + shutdown: Tuple[str, ...] = ("killall bgpd",) + validate: Tuple[str, ...] = ("pidof bgpd",) + custom_needed: bool = True + ipv4_routing: bool = True + ipv6_routing: bool = True @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = "!\n! BGP configuration\n!\n" cfg += "! You should configure the AS number below,\n" cfg += "! along with this router's peers.\n!\n" cfg += "router bgp %s\n" % node.id - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " bgp router-id %s\n" % rtrid cfg += " redistribute connected\n" cfg += "! neighbor 1.2.3.4 remote-as 555\n!\n" @@ -548,14 +531,13 @@ class FRRRip(FrrService): The RIP service provides IPv4 routing for wired networks. """ - name = "FRRRIP" - startup = () - shutdown = ("killall ripd",) - validate = ("pidof ripd",) - ipv4_routing = True + name: str = "FRRRIP" + shutdown: Tuple[str, ...] = ("killall ripd",) + validate: Tuple[str, ...] = ("pidof ripd",) + ipv4_routing: bool = True @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = """\ router rip redistribute static @@ -572,14 +554,13 @@ class FRRRipng(FrrService): The RIP NG service provides IPv6 routing for wired networks. """ - name = "FRRRIPNG" - startup = () - shutdown = ("killall ripngd",) - validate = ("pidof ripngd",) - ipv6_routing = True + name: str = "FRRRIPNG" + shutdown: Tuple[str, ...] = ("killall ripngd",) + validate: Tuple[str, ...] = ("pidof ripngd",) + ipv6_routing: bool = True @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = """\ router ripng redistribute static @@ -597,25 +578,22 @@ class FRRBabel(FrrService): protocol for IPv6 and IPv4 with fast convergence properties. """ - name = "FRRBabel" - startup = () - shutdown = ("killall babeld",) - validate = ("pidof babeld",) - ipv6_routing = True + name: str = "FRRBabel" + shutdown: Tuple[str, ...] = ("killall babeld",) + validate: Tuple[str, ...] = ("pidof babeld",) + ipv6_routing: bool = True @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router babel\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += " network %s\n" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += " network %s\n" % iface.name cfg += " redistribute static\n redistribute ipv4 connected\n" return cfg @classmethod - def generatefrrifcconfig(cls, node, ifc): - if ifc.net and isinstance(ifc.net, (EmaneNet, WlanNode)): + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + if iface.net and isinstance(iface.net, (EmaneNet, WlanNode)): return " babel wireless\n no babel split-horizon\n" else: return " babel wired\n babel split-horizon\n" @@ -626,18 +604,17 @@ class FRRpimd(FrrService): PIM multicast routing based on XORP. """ - name = "FRRpimd" - startup = () - shutdown = ("killall pimd",) - validate = ("pidof pimd",) - ipv4_routing = True + name: str = "FRRpimd" + shutdown: Tuple[str, ...] = ("killall pimd",) + validate: Tuple[str, ...] = ("pidof pimd",) + ipv4_routing: bool = True @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: ifname = "eth0" - for ifc in node.netifs(): - if ifc.name != "lo": - ifname = ifc.name + for iface in node.get_ifaces(): + if iface.name != "lo": + ifname = iface.name break cfg = "router mfea\n!\n" cfg += "router igmp\n!\n" @@ -649,7 +626,7 @@ def generatefrrconfig(cls, node): return cfg @classmethod - def generatefrrifcconfig(cls, node, ifc): + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: return " ip mfea\n ip igmp\n ip pim\n" @@ -660,25 +637,24 @@ class FRRIsis(FrrService): unified frr.conf file. """ - name = "FRRISIS" - startup = () - shutdown = ("killall isisd",) - validate = ("pidof isisd",) - ipv4_routing = True - ipv6_routing = True + name: str = "FRRISIS" + shutdown: Tuple[str, ...] = ("killall isisd",) + validate: Tuple[str, ...] = ("pidof isisd",) + ipv4_routing: bool = True + ipv6_routing: bool = True @staticmethod - def ptpcheck(ifc): + def ptp_check(iface: CoreInterface) -> str: """ Helper to detect whether interface is connected to a notional point-to-point link. """ - if isinstance(ifc.net, PtpNet): + if isinstance(iface.net, PtpNet): return " isis network point-to-point\n" return "" @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router isis DEFAULT\n" cfg += " net 47.0001.0000.1900.%04x.00\n" % node.id cfg += " metric-style wide\n" @@ -687,9 +663,9 @@ def generatefrrconfig(cls, node): return cfg @classmethod - def generatefrrifcconfig(cls, node, ifc): + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: cfg = " ip router isis DEFAULT\n" cfg += " ipv6 router isis DEFAULT\n" cfg += " isis circuit-type level-2-only\n" - cfg += cls.ptpcheck(ifc) + cfg += cls.ptp_check(iface) return cfg diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py index 3c9f262d2..697f4eeef 100644 --- a/daemon/core/services/nrl.py +++ b/daemon/core/services/nrl.py @@ -2,9 +2,10 @@ nrl.py: defines services provided by NRL protolib tools hosted here: http://www.nrl.navy.mil/itd/ncs/products """ -import netaddr +from typing import Optional, Tuple from core import utils +from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -14,53 +15,45 @@ class NrlService(CoreService): common to NRL's routing daemons. """ - name = None - group = "ProtoSvc" - dirs = () - configs = () - startup = () - shutdown = () + name: Optional[str] = None + group: str = "ProtoSvc" @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" @staticmethod - def firstipv4prefix(node, prefixlen=24): + def firstipv4prefix(node: CoreNode, prefixlen: int = 24) -> str: """ Similar to QuaggaService.routerid(). Helper to return the first IPv4 prefix of a node, using the supplied prefix length. This ignores the interface's prefix length, so e.g. '/32' can turn into '/24'. """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return f"{a}/{prefixlen}" - # raise ValueError, "no IPv4 address found" + for iface in node.get_ifaces(control=False): + ip4 = iface.get_ip4() + if ip4: + return f"{ip4.ip}/{prefixlen}" return "0.0.0.0/%s" % prefixlen class MgenSinkService(NrlService): - name = "MGEN_Sink" - executables = ("mgen",) - configs = ("sink.mgen",) - startup = ("mgen input sink.mgen",) - validate = ("pidof mgen",) - shutdown = ("killall mgen",) + name: str = "MGEN_Sink" + executables: Tuple[str, ...] = ("mgen",) + configs: Tuple[str, ...] = ("sink.mgen",) + startup: Tuple[str, ...] = ("mgen input sink.mgen",) + validate: Tuple[str, ...] = ("pidof mgen",) + shutdown: Tuple[str, ...] = ("killall mgen",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: cfg = "0.0 LISTEN UDP 5000\n" - for ifc in node.netifs(): - name = utils.sysctl_devname(ifc.name) + for iface in node.get_ifaces(): + name = utils.sysctl_devname(iface.name) cfg += "0.0 Join 224.225.1.2 INTERFACE %s\n" % name return cfg @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: cmd = cls.startup[0] cmd += " output /tmp/mgen_%s.log" % node.name return (cmd,) @@ -71,32 +64,29 @@ class NrlNhdp(NrlService): NeighborHood Discovery Protocol for MANET networks. """ - name = "NHDP" - executables = ("nrlnhdp",) - startup = ("nrlnhdp",) - shutdown = ("killall nrlnhdp",) - validate = ("pidof nrlnhdp",) + name: str = "NHDP" + executables: Tuple[str, ...] = ("nrlnhdp",) + startup: Tuple[str, ...] = ("nrlnhdp",) + shutdown: Tuple[str, ...] = ("killall nrlnhdp",) + validate: Tuple[str, ...] = ("pidof nrlnhdp",) @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ cmd = cls.startup[0] cmd += " -l /var/log/nrlnhdp.log" cmd += " -rpipe %s_nhdp" % node.name - servicenames = map(lambda x: x.name, node.services) if "SMF" in servicenames: cmd += " -flooding ecds" cmd += " -smfClient %s_smf" % node.name - - netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) - if len(netifs) > 0: - interfacenames = map(lambda x: x.name, netifs) + ifaces = node.get_ifaces(control=False) + if len(ifaces) > 0: + iface_names = map(lambda x: x.name, ifaces) cmd += " -i " - cmd += " -i ".join(interfacenames) - + cmd += " -i ".join(iface_names) return (cmd,) @@ -105,15 +95,15 @@ class NrlSmf(NrlService): Simplified Multicast Forwarding for MANET networks. """ - name = "SMF" - executables = ("nrlsmf",) - startup = ("sh startsmf.sh",) - shutdown = ("killall nrlsmf",) - validate = ("pidof nrlsmf",) - configs = ("startsmf.sh",) + name: str = "SMF" + executables: Tuple[str, ...] = ("nrlsmf",) + startup: Tuple[str, ...] = ("sh startsmf.sh",) + shutdown: Tuple[str, ...] = ("killall nrlsmf",) + validate: Tuple[str, ...] = ("pidof nrlsmf",) + configs: Tuple[str, ...] = ("startsmf.sh",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a startup script for SMF. Because nrlsmf does not daemonize, it can cause problems in some situations when launched @@ -125,16 +115,16 @@ def generate_config(cls, node, filename): cmd = "nrlsmf instance %s_smf" % node.name servicenames = map(lambda x: x.name, node.services) - netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) - if len(netifs) == 0: + ifaces = node.get_ifaces(control=False) + if len(ifaces) == 0: return "" if "arouted" in servicenames: comments += "# arouted service is enabled\n" cmd += " tap %s_tap" % (node.name,) cmd += " unicast %s" % cls.firstipv4prefix(node, 24) - cmd += " push lo,%s resequence on" % netifs[0].name - if len(netifs) > 0: + cmd += " push lo,%s resequence on" % ifaces[0].name + if len(ifaces) > 0: if "NHDP" in servicenames: comments += "# NHDP service is enabled\n" cmd += " ecds " @@ -143,12 +133,11 @@ def generate_config(cls, node, filename): cmd += " smpr " else: cmd += " cf " - interfacenames = map(lambda x: x.name, netifs) - cmd += ",".join(interfacenames) + iface_names = map(lambda x: x.name, ifaces) + cmd += ",".join(iface_names) cmd += " hash MD5" cmd += " log /var/log/nrlsmf.log" - cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n" return cfg @@ -158,33 +147,31 @@ class NrlOlsr(NrlService): Optimized Link State Routing protocol for MANET networks. """ - name = "OLSR" - executables = ("nrlolsrd",) - startup = ("nrlolsrd",) - shutdown = ("killall nrlolsrd",) - validate = ("pidof nrlolsrd",) + name: str = "OLSR" + executables: Tuple[str, ...] = ("nrlolsrd",) + startup: Tuple[str, ...] = ("nrlolsrd",) + shutdown: Tuple[str, ...] = ("killall nrlolsrd",) + validate: Tuple[str, ...] = ("pidof nrlolsrd",) @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ cmd = cls.startup[0] # are multiple interfaces supported? No. - netifs = list(node.netifs()) - if len(netifs) > 0: - ifc = netifs[0] - cmd += " -i %s" % ifc.name + ifaces = node.get_ifaces() + if len(ifaces) > 0: + iface = ifaces[0] + cmd += " -i %s" % iface.name cmd += " -l /var/log/nrlolsrd.log" cmd += " -rpipe %s_olsr" % node.name - servicenames = map(lambda x: x.name, node.services) if "SMF" in servicenames and "NHDP" not in servicenames: cmd += " -flooding s-mpr" cmd += " -smfClient %s_smf" % node.name if "zebra" in servicenames: cmd += " -z" - return (cmd,) @@ -193,34 +180,30 @@ class NrlOlsrv2(NrlService): Optimized Link State Routing protocol version 2 for MANET networks. """ - name = "OLSRv2" - executables = ("nrlolsrv2",) - startup = ("nrlolsrv2",) - shutdown = ("killall nrlolsrv2",) - validate = ("pidof nrlolsrv2",) + name: str = "OLSRv2" + executables: Tuple[str, ...] = ("nrlolsrv2",) + startup: Tuple[str, ...] = ("nrlolsrv2",) + shutdown: Tuple[str, ...] = ("killall nrlolsrv2",) + validate: Tuple[str, ...] = ("pidof nrlolsrv2",) @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ cmd = cls.startup[0] cmd += " -l /var/log/nrlolsrv2.log" cmd += " -rpipe %s_olsrv2" % node.name - servicenames = map(lambda x: x.name, node.services) if "SMF" in servicenames: cmd += " -flooding ecds" cmd += " -smfClient %s_smf" % node.name - cmd += " -p olsr" - - netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) - if len(netifs) > 0: - interfacenames = map(lambda x: x.name, netifs) + ifaces = node.get_ifaces(control=False) + if len(ifaces) > 0: + iface_names = map(lambda x: x.name, ifaces) cmd += " -i " - cmd += " -i ".join(interfacenames) - + cmd += " -i ".join(iface_names) return (cmd,) @@ -229,32 +212,32 @@ class OlsrOrg(NrlService): Optimized Link State Routing protocol from olsr.org for MANET networks. """ - name = "OLSRORG" - executables = ("olsrd",) - configs = ("/etc/olsrd/olsrd.conf",) - dirs = ("/etc/olsrd",) - startup = ("olsrd",) - shutdown = ("killall olsrd",) - validate = ("pidof olsrd",) + name: str = "OLSRORG" + executables: Tuple[str, ...] = ("olsrd",) + configs: Tuple[str, ...] = ("/etc/olsrd/olsrd.conf",) + dirs: Tuple[str, ...] = ("/etc/olsrd",) + startup: Tuple[str, ...] = ("olsrd",) + shutdown: Tuple[str, ...] = ("killall olsrd",) + validate: Tuple[str, ...] = ("pidof olsrd",) @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ cmd = cls.startup[0] - netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) - if len(netifs) > 0: - interfacenames = map(lambda x: x.name, netifs) + ifaces = node.get_ifaces(control=False) + if len(ifaces) > 0: + iface_names = map(lambda x: x.name, ifaces) cmd += " -i " - cmd += " -i ".join(interfacenames) - + cmd += " -i ".join(iface_names) return (cmd,) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ - Generate a default olsrd config file to use the broadcast address of 255.255.255.255. + Generate a default olsrd config file to use the broadcast address of + 255.255.255.255. """ cfg = """\ # @@ -579,24 +562,16 @@ class MgenActor(NrlService): """ # a unique name is required, without spaces - name = "MgenActor" - executables = ("mgen",) - # you can create your own group here - group = "ProtoSvc" - # per-node directories - dirs = () - # generated files (without a full path this file goes in the node's dir, - # e.g. /tmp/pycore.12345/n1.conf/) - configs = ("start_mgen_actor.sh",) - # list of startup commands, also may be generated during startup - startup = ("sh start_mgen_actor.sh",) - # list of validation commands - validate = ("pidof mgen",) - # list of shutdown commands - shutdown = ("killall mgen",) + name: str = "MgenActor" + group: str = "ProtoSvc" + executables: Tuple[str, ...] = ("mgen",) + configs: Tuple[str, ...] = ("start_mgen_actor.sh",) + startup: Tuple[str, ...] = ("sh start_mgen_actor.sh",) + validate: Tuple[str, ...] = ("pidof mgen",) + shutdown: Tuple[str, ...] = ("killall mgen",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a startup script for MgenActor. Because mgenActor does not daemonize, it can cause problems in some situations when launched @@ -606,11 +581,9 @@ def generate_config(cls, node, filename): cfg += "# auto-generated by nrl.py:MgenActor.generateconfig()\n" comments = "" cmd = "mgenBasicActor.py -n %s -a 0.0.0.0" % node.name - - netifs = [x for x in node.netifs() if not getattr(x, "control", False)] - if len(netifs) == 0: + ifaces = node.get_ifaces(control=False) + if len(ifaces) == 0: return "" - cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n" return cfg @@ -620,15 +593,15 @@ class Arouted(NrlService): Adaptive Routing """ - name = "arouted" - executables = ("arouted",) - configs = ("startarouted.sh",) - startup = ("sh startarouted.sh",) - shutdown = ("pkill arouted",) - validate = ("pidof arouted",) + name: str = "arouted" + executables: Tuple[str, ...] = ("arouted",) + configs: Tuple[str, ...] = ("startarouted.sh",) + startup: Tuple[str, ...] = ("sh startarouted.sh",) + shutdown: Tuple[str, ...] = ("pkill arouted",) + validate: Tuple[str, ...] = ("pidof arouted",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the Quagga.conf or quaggaboot.sh file contents. """ diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index a62cbc5ca..9e2c7cc00 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -1,67 +1,71 @@ """ quagga.py: defines routing services provided by Quagga. """ +from typing import Optional, Tuple + import netaddr -from core import constants from core.emane.nodes import EmaneNet from core.emulator.enumerations import LinkTypes +from core.nodes.base import CoreNode +from core.nodes.interface import CoreInterface from core.nodes.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node from core.services.coreservices import CoreService +QUAGGA_STATE_DIR: str = "/var/run/quagga" + class Zebra(CoreService): - name = "zebra" - group = "Quagga" - dirs = ("/usr/local/etc/quagga", "/var/run/quagga") - configs = ( + name: str = "zebra" + group: str = "Quagga" + dirs: Tuple[str, ...] = ("/usr/local/etc/quagga", "/var/run/quagga") + configs: Tuple[str, ...] = ( "/usr/local/etc/quagga/Quagga.conf", "quaggaboot.sh", "/usr/local/etc/quagga/vtysh.conf", ) - startup = ("sh quaggaboot.sh zebra",) - shutdown = ("killall zebra",) - validate = ("pidof zebra",) + startup: Tuple[str, ...] = ("sh quaggaboot.sh zebra",) + shutdown: Tuple[str, ...] = ("killall zebra",) + validate: Tuple[str, ...] = ("pidof zebra",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the Quagga.conf or quaggaboot.sh file contents. """ if filename == cls.configs[0]: - return cls.generateQuaggaConf(node) + return cls.generate_quagga_conf(node) elif filename == cls.configs[1]: - return cls.generateQuaggaBoot(node) + return cls.generate_quagga_boot(node) elif filename == cls.configs[2]: - return cls.generateVtyshConf(node) + return cls.generate_vtysh_conf(node) else: raise ValueError( "file name (%s) is not a known configuration: %s", filename, cls.configs ) @classmethod - def generateVtyshConf(cls, node): + def generate_vtysh_conf(cls, node: CoreNode) -> str: """ Returns configuration file text. """ return "service integrated-vtysh-config\n" @classmethod - def generateQuaggaConf(cls, node): + def generate_quagga_conf(cls, node: CoreNode) -> str: """ Returns configuration file text. Other services that depend on zebra - will have generatequaggaifcconfig() and generatequaggaconfig() - hooks that are invoked here. + will have hooks that are invoked here. """ # we could verify here that filename == Quagga.conf cfg = "" - for ifc in node.netifs(): - cfg += "interface %s\n" % ifc.name + for iface in node.get_ifaces(): + cfg += "interface %s\n" % iface.name # include control interfaces in addressing but not routing daemons - if hasattr(ifc, "control") and ifc.control is True: + if getattr(iface, "control", False): cfg += " " - cfg += "\n ".join(map(cls.addrstr, ifc.addrlist)) + cfg += "\n ".join(map(cls.addrstr, iface.ips())) cfg += "\n" continue cfgv4 = "" @@ -71,29 +75,25 @@ def generateQuaggaConf(cls, node): for s in node.services: if cls.name not in s.dependencies: continue - ifccfg = s.generatequaggaifcconfig(node, ifc) + if not (isinstance(s, QuaggaService) or issubclass(s, QuaggaService)): + continue + iface_config = s.generate_quagga_iface_config(node, iface) if s.ipv4_routing: want_ipv4 = True if s.ipv6_routing: want_ipv6 = True - cfgv6 += ifccfg + cfgv6 += iface_config else: - cfgv4 += ifccfg + cfgv4 += iface_config if want_ipv4: - ipv4list = filter( - lambda x: netaddr.valid_ipv4(x.split("/")[0]), ifc.addrlist - ) cfg += " " - cfg += "\n ".join(map(cls.addrstr, ipv4list)) + cfg += "\n ".join(map(cls.addrstr, iface.ip4s)) cfg += "\n" cfg += cfgv4 if want_ipv6: - ipv6list = filter( - lambda x: netaddr.valid_ipv6(x.split("/")[0]), ifc.addrlist - ) cfg += " " - cfg += "\n ".join(map(cls.addrstr, ipv6list)) + cfg += "\n ".join(map(cls.addrstr, iface.ip6s)) cfg += "\n" cfg += cfgv6 cfg += "!\n" @@ -101,24 +101,26 @@ def generateQuaggaConf(cls, node): for s in node.services: if cls.name not in s.dependencies: continue - cfg += s.generatequaggaconfig(node) + if not (isinstance(s, QuaggaService) or issubclass(s, QuaggaService)): + continue + cfg += s.generate_quagga_config(node) return cfg @staticmethod - def addrstr(x): + def addrstr(ip: netaddr.IPNetwork) -> str: """ helper for mapping IP addresses to zebra config statements """ - addr = x.split("/")[0] - if netaddr.valid_ipv4(addr): - return "ip address %s" % x - elif netaddr.valid_ipv6(addr): - return "ipv6 address %s" % x + address = str(ip.ip) + if netaddr.valid_ipv4(address): + return "ip address %s" % ip + elif netaddr.valid_ipv6(address): + return "ipv6 address %s" % ip else: - raise ValueError("invalid address: %s", x) + raise ValueError("invalid address: %s", ip) @classmethod - def generateQuaggaBoot(cls, node): + def generate_quagga_boot(cls, node: CoreNode) -> str: """ Generate a shell script used to boot the Quagga daemons. """ @@ -225,7 +227,7 @@ def generateQuaggaBoot(cls, node): cls.configs[0], quagga_sbin_search, quagga_bin_search, - constants.QUAGGA_STATE_DIR, + QUAGGA_STATE_DIR, ) @@ -235,57 +237,48 @@ class QuaggaService(CoreService): common to Quagga's routing daemons. """ - name = None - group = "Quagga" - dependencies = ("zebra",) - dirs = () - configs = () - startup = () - shutdown = () - meta = "The config file for this service can be found in the Zebra service." - - ipv4_routing = False - ipv6_routing = False + name: Optional[str] = None + group: str = "Quagga" + dependencies: Tuple[str, ...] = (Zebra.name,) + meta: str = "The config file for this service can be found in the Zebra service." + ipv4_routing: bool = False + ipv6_routing: bool = False @staticmethod - def routerid(node): + def router_id(node: CoreNode) -> str: """ Helper to return the first IPv4 address of a node as its router ID. """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return a - # raise ValueError, "no IPv4 address found for router ID" - return "0.0.0.%d" % node.id + for iface in node.get_ifaces(control=False): + ip4 = iface.get_ip4() + if ip4: + return str(ip4.ip) + return f"0.0.0.{node.id:d}" @staticmethod - def rj45check(ifc): + def rj45check(iface: CoreInterface) -> bool: """ Helper to detect whether interface is connected an external RJ45 link. """ - if ifc.net: - for peerifc in ifc.net.netifs(): - if peerifc == ifc: + if iface.net: + for peer_iface in iface.net.get_ifaces(): + if peer_iface == iface: continue - if isinstance(peerifc.node, Rj45Node): + if isinstance(peer_iface.node, Rj45Node): return True return False @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" @classmethod - def generatequaggaifcconfig(cls, node, ifc): + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: return "" @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: return "" @@ -296,63 +289,58 @@ class Ospfv2(QuaggaService): unified Quagga.conf file. """ - name = "OSPFv2" - startup = () - shutdown = ("killall ospfd",) - validate = ("pidof ospfd",) - ipv4_routing = True + name: str = "OSPFv2" + shutdown: Tuple[str, ...] = ("killall ospfd",) + validate: Tuple[str, ...] = ("pidof ospfd",) + ipv4_routing: bool = True @staticmethod - def mtucheck(ifc): + def mtu_check(iface: CoreInterface) -> str: """ 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: # a workaround for PhysicalNode GreTap, which has no knowledge of # the other nodes/nets return " ip ospf mtu-ignore\n" - if not ifc.net: + if not iface.net: return "" - for i in ifc.net.netifs(): - if i.mtu != ifc.mtu: + for iface in iface.net.get_ifaces(): + if iface.mtu != iface.mtu: return " ip ospf mtu-ignore\n" return "" @staticmethod - def ptpcheck(ifc): + def ptp_check(iface: CoreInterface) -> str: """ Helper to detect whether interface is connected to a notional point-to-point link. """ - if isinstance(ifc.net, PtpNet): + if isinstance(iface.net, PtpNet): return " ip ospf network point-to-point\n" return "" @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = "router ospf\n" - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " router-id %s\n" % rtrid # network 10.0.0.0/24 area 0 - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: - addr = a.split("/")[0] - if netaddr.valid_ipv4(addr): - cfg += " network %s area 0\n" % a + for iface in node.get_ifaces(control=False): + for ip4 in iface.ip4s: + cfg += f" network {ip4} area 0\n" cfg += "!\n" return cfg @classmethod - def generatequaggaifcconfig(cls, node, ifc): - cfg = cls.mtucheck(ifc) + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + cfg = cls.mtu_check(iface) # external RJ45 connections will use default OSPF timers - if cls.rj45check(ifc): + if cls.rj45check(iface): return cfg - cfg += cls.ptpcheck(ifc) + cfg += cls.ptp_check(iface) return ( cfg + """\ @@ -370,66 +358,63 @@ class Ospfv3(QuaggaService): unified Quagga.conf file. """ - name = "OSPFv3" - startup = () - shutdown = ("killall ospf6d",) - validate = ("pidof ospf6d",) - ipv4_routing = True - ipv6_routing = True + name: str = "OSPFv3" + shutdown: Tuple[str, ...] = ("killall ospf6d",) + validate: Tuple[str, ...] = ("pidof ospf6d",) + ipv4_routing: bool = True + ipv6_routing: bool = True @staticmethod - def minmtu(ifc): + def 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 @classmethod - def mtucheck(cls, ifc): + def mtu_check(cls, iface: CoreInterface) -> str: """ Helper to detect MTU mismatch and add the appropriate OSPFv3 ifmtu command. This is needed when e.g. a node is linked via a GreTap device. """ - minmtu = cls.minmtu(ifc) - if minmtu < ifc.mtu: + minmtu = cls.min_mtu(iface) + if minmtu < iface.mtu: return " ipv6 ospf6 ifmtu %d\n" % minmtu else: return "" @staticmethod - def ptpcheck(ifc): + def ptp_check(iface: CoreInterface) -> str: """ Helper to detect whether interface is connected to a notional point-to-point link. """ - if isinstance(ifc.net, PtpNet): + if isinstance(iface.net, PtpNet): return " ipv6 ospf6 network point-to-point\n" return "" @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = "router ospf6\n" - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " instance-id 65\n" cfg += " router-id %s\n" % rtrid - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += " interface %s area 0.0.0.0\n" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += " interface %s area 0.0.0.0\n" % iface.name cfg += "!\n" return cfg @classmethod - def generatequaggaifcconfig(cls, node, ifc): - return cls.mtucheck(ifc) + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + return cls.mtu_check(iface) class Ospfv3mdr(Ospfv3): @@ -440,13 +425,13 @@ class Ospfv3mdr(Ospfv3): unified Quagga.conf file. """ - name = "OSPFv3MDR" - ipv4_routing = True + name: str = "OSPFv3MDR" + ipv4_routing: bool = True @classmethod - def generatequaggaifcconfig(cls, node, ifc): - cfg = cls.mtucheck(ifc) - if ifc.net is not None and isinstance(ifc.net, (WlanNode, EmaneNet)): + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + cfg = cls.mtu_check(iface) + if iface.net is not None and isinstance(iface.net, (WlanNode, EmaneNet)): return ( cfg + """\ @@ -470,21 +455,20 @@ class Bgp(QuaggaService): having the same AS number. """ - name = "BGP" - startup = () - shutdown = ("killall bgpd",) - validate = ("pidof bgpd",) - custom_needed = True - ipv4_routing = True - ipv6_routing = True + name: str = "BGP" + shutdown: Tuple[str, ...] = ("killall bgpd",) + validate: Tuple[str, ...] = ("pidof bgpd",) + custom_needed: bool = True + ipv4_routing: bool = True + ipv6_routing: bool = True @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = "!\n! BGP configuration\n!\n" cfg += "! You should configure the AS number below,\n" cfg += "! along with this router's peers.\n!\n" cfg += "router bgp %s\n" % node.id - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " bgp router-id %s\n" % rtrid cfg += " redistribute connected\n" cfg += "! neighbor 1.2.3.4 remote-as 555\n!\n" @@ -496,14 +480,13 @@ class Rip(QuaggaService): The RIP service provides IPv4 routing for wired networks. """ - name = "RIP" - startup = () - shutdown = ("killall ripd",) - validate = ("pidof ripd",) - ipv4_routing = True + name: str = "RIP" + shutdown: Tuple[str, ...] = ("killall ripd",) + validate: Tuple[str, ...] = ("pidof ripd",) + ipv4_routing: bool = True @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = """\ router rip redistribute static @@ -520,14 +503,13 @@ class Ripng(QuaggaService): The RIP NG service provides IPv6 routing for wired networks. """ - name = "RIPNG" - startup = () - shutdown = ("killall ripngd",) - validate = ("pidof ripngd",) - ipv6_routing = True + name: str = "RIPNG" + shutdown: Tuple[str, ...] = ("killall ripngd",) + validate: Tuple[str, ...] = ("pidof ripngd",) + ipv6_routing: bool = True @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = """\ router ripng redistribute static @@ -545,25 +527,22 @@ class Babel(QuaggaService): protocol for IPv6 and IPv4 with fast convergence properties. """ - name = "Babel" - startup = () - shutdown = ("killall babeld",) - validate = ("pidof babeld",) - ipv6_routing = True + name: str = "Babel" + shutdown: Tuple[str, ...] = ("killall babeld",) + validate: Tuple[str, ...] = ("pidof babeld",) + ipv6_routing: bool = True @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = "router babel\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += " network %s\n" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += " network %s\n" % iface.name cfg += " redistribute static\n redistribute connected\n" return cfg @classmethod - def generatequaggaifcconfig(cls, node, ifc): - if ifc.net and ifc.net.linktype == LinkTypes.WIRELESS: + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + if iface.net and iface.net.linktype == LinkTypes.WIRELESS: return " babel wireless\n no babel split-horizon\n" else: return " babel wired\n babel split-horizon\n" @@ -574,18 +553,17 @@ class Xpimd(QuaggaService): PIM multicast routing based on XORP. """ - name = "Xpimd" - startup = () - shutdown = ("killall xpimd",) - validate = ("pidof xpimd",) - ipv4_routing = True + name: str = "Xpimd" + shutdown: Tuple[str, ...] = ("killall xpimd",) + validate: Tuple[str, ...] = ("pidof xpimd",) + ipv4_routing: bool = True @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: ifname = "eth0" - for ifc in node.netifs(): - if ifc.name != "lo": - ifname = ifc.name + for iface in node.get_ifaces(): + if iface.name != "lo": + ifname = iface.name break cfg = "router mfea\n!\n" cfg += "router igmp\n!\n" @@ -597,5 +575,5 @@ def generatequaggaconfig(cls, node): return cfg @classmethod - def generatequaggaifcconfig(cls, node, ifc): + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: return " ip mfea\n ip igmp\n ip pim\n" diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index ab46f551d..ef077662a 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -3,9 +3,9 @@ """ import re +from typing import Tuple -import netaddr - +from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -14,24 +14,28 @@ class SdnService(CoreService): Parent class for SDN services. """ - group = "SDN" + group: str = "SDN" @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" class OvsService(SdnService): - name = "OvsService" - executables = ("ovs-ofctl", "ovs-vsctl") - group = "SDN" - dirs = ("/etc/openvswitch", "/var/run/openvswitch", "/var/log/openvswitch") - configs = ("OvsService.sh",) - startup = ("sh OvsService.sh",) - shutdown = ("killall ovs-vswitchd", "killall ovsdb-server") + name: str = "OvsService" + group: str = "SDN" + executables: Tuple[str, ...] = ("ovs-ofctl", "ovs-vsctl") + dirs: Tuple[str, ...] = ( + "/etc/openvswitch", + "/var/run/openvswitch", + "/var/log/openvswitch", + ) + configs: Tuple[str, ...] = ("OvsService.sh",) + startup: Tuple[str, ...] = ("sh OvsService.sh",) + shutdown: Tuple[str, ...] = ("killall ovs-vswitchd", "killall ovsdb-server") @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: # Check whether the node is running zebra has_zebra = 0 for s in node.services: @@ -46,13 +50,11 @@ def generate_config(cls, node, filename): cfg += "## this stops it from routing traffic without defined flows.\n" cfg += "## remove the -- and everything after if you want it to act as a regular switch\n" cfg += "ovs-vsctl add-br ovsbr0 -- set Bridge ovsbr0 fail-mode=secure\n" - cfg += "\n## Now add all our interfaces as ports to the switch\n" + portnum = 1 - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - ifnumstr = re.findall(r"\d+", ifc.name) + for iface in node.get_ifaces(control=False): + ifnumstr = re.findall(r"\d+", iface.name) ifnum = ifnumstr[0] # create virtual interfaces @@ -61,18 +63,14 @@ def generate_config(cls, node, filename): # remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces # or assign them manually to rtr interfaces if zebra is not running - for ifcaddr in ifc.addrlist: - addr = ifcaddr.split("/")[0] - if netaddr.valid_ipv4(addr): - cfg += "ip addr del %s dev %s\n" % (ifcaddr, ifc.name) - if has_zebra == 0: - cfg += "ip addr add %s dev rtr%s\n" % (ifcaddr, ifnum) - elif netaddr.valid_ipv6(addr): - cfg += "ip -6 addr del %s dev %s\n" % (ifcaddr, ifc.name) - if has_zebra == 0: - cfg += "ip -6 addr add %s dev rtr%s\n" % (ifcaddr, ifnum) - else: - raise ValueError("invalid address: %s" % ifcaddr) + for ip4 in iface.ip4s: + cfg += "ip addr del %s dev %s\n" % (ip4.ip, iface.name) + if has_zebra == 0: + cfg += "ip addr add %s dev rtr%s\n" % (ip4.ip, ifnum) + for ip6 in iface.ip6s: + cfg += "ip -6 addr del %s dev %s\n" % (ip6.ip, iface.name) + if has_zebra == 0: + cfg += "ip -6 addr add %s dev rtr%s\n" % (ip6.ip, ifnum) # add interfaces to bridge # Make port numbers explicit so they're easier to follow in reading the script @@ -102,9 +100,7 @@ def generate_config(cls, node, filename): cfg += "## if the above controller will be present then you probably want to delete them\n" # Setup default flows portnum = 1 - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue + for iface in node.get_ifaces(control=False): cfg += "## Take the data from the CORE interface and put it on the veth and vice versa\n" cfg += ( "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" @@ -115,21 +111,19 @@ def generate_config(cls, node, filename): % (portnum + 1, portnum) ) portnum += 2 - return cfg class RyuService(SdnService): - name = "ryuService" - executables = ("ryu-manager",) - group = "SDN" - dirs = () - configs = ("ryuService.sh",) - startup = ("sh ryuService.sh",) - shutdown = ("killall ryu-manager",) + name: str = "ryuService" + group: str = "SDN" + executables: Tuple[str, ...] = ("ryu-manager",) + configs: Tuple[str, ...] = ("ryuService.sh",) + startup: Tuple[str, ...] = ("sh ryuService.sh",) + shutdown: Tuple[str, ...] = ("killall ryu-manager",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return a string that will be written to filename, or sent to the GUI for user customization. diff --git a/daemon/core/services/security.py b/daemon/core/services/security.py index eb6545b28..b813579e7 100644 --- a/daemon/core/services/security.py +++ b/daemon/core/services/security.py @@ -4,78 +4,79 @@ """ import logging +from typing import Tuple from core import constants +from core.nodes.base import CoreNode +from core.nodes.interface import CoreInterface from core.services.coreservices import CoreService class VPNClient(CoreService): - name = "VPNClient" - group = "Security" - configs = ("vpnclient.sh",) - startup = ("sh vpnclient.sh",) - shutdown = ("killall openvpn",) - validate = ("pidof openvpn",) - custom_needed = True + name: str = "VPNClient" + group: str = "Security" + configs: Tuple[str, ...] = ("vpnclient.sh",) + startup: Tuple[str, ...] = ("sh vpnclient.sh",) + shutdown: Tuple[str, ...] = ("killall openvpn",) + validate: Tuple[str, ...] = ("pidof openvpn",) + custom_needed: bool = True @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the client.conf and vpnclient.sh file contents to """ cfg = "#!/bin/sh\n" cfg += "# custom VPN Client configuration for service (security.py)\n" - fname = "%s/examples/services/sampleVPNClient" % constants.CORE_DATA_DIR - + fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleVPNClient" try: - cfg += open(fname, "rb").read() + with open(fname, "r") as f: + cfg += f.read() except IOError: logging.exception( - "Error opening VPN client configuration template (%s)", fname + "error opening VPN client configuration template (%s)", fname ) - return cfg class VPNServer(CoreService): - name = "VPNServer" - group = "Security" - configs = ("vpnserver.sh",) - startup = ("sh vpnserver.sh",) - shutdown = ("killall openvpn",) - validate = ("pidof openvpn",) - custom_needed = True + name: str = "VPNServer" + group: str = "Security" + configs: Tuple[str, ...] = ("vpnserver.sh",) + startup: Tuple[str, ...] = ("sh vpnserver.sh",) + shutdown: Tuple[str, ...] = ("killall openvpn",) + validate: Tuple[str, ...] = ("pidof openvpn",) + custom_needed: bool = True @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the sample server.conf and vpnserver.sh file contents to GUI for user customization. """ cfg = "#!/bin/sh\n" cfg += "# custom VPN Server Configuration for service (security.py)\n" - fname = "%s/examples/services/sampleVPNServer" % constants.CORE_DATA_DIR - + fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleVPNServer" try: - cfg += open(fname, "rb").read() + with open(fname, "r") as f: + cfg += f.read() except IOError: logging.exception( "Error opening VPN server configuration template (%s)", fname ) - return cfg class IPsec(CoreService): - name = "IPsec" - group = "Security" - configs = ("ipsec.sh",) - startup = ("sh ipsec.sh",) - shutdown = ("killall racoon",) - custom_needed = True + name: str = "IPsec" + group: str = "Security" + configs: Tuple[str, ...] = ("ipsec.sh",) + startup: Tuple[str, ...] = ("sh ipsec.sh",) + shutdown: Tuple[str, ...] = ("killall racoon",) + custom_needed: bool = True @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the ipsec.conf and racoon.conf file contents to GUI for user customization. @@ -83,7 +84,7 @@ def generate_config(cls, node, filename): cfg = "#!/bin/sh\n" cfg += "# set up static tunnel mode security assocation for service " cfg += "(security.py)\n" - fname = "%s/examples/services/sampleIPsec" % constants.CORE_DATA_DIR + fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleIPsec" try: with open(fname, "r") as f: cfg += f.read() @@ -93,28 +94,27 @@ def generate_config(cls, node, filename): class Firewall(CoreService): - name = "Firewall" - group = "Security" - configs = ("firewall.sh",) - startup = ("sh firewall.sh",) - custom_needed = True + name: str = "Firewall" + group: str = "Security" + configs: Tuple[str, ...] = ("firewall.sh",) + startup: Tuple[str, ...] = ("sh firewall.sh",) + custom_needed: bool = True @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the firewall rule examples to GUI for user customization. """ cfg = "#!/bin/sh\n" cfg += "# custom node firewall rules for service (security.py)\n" - fname = "%s/examples/services/sampleFirewall" % constants.CORE_DATA_DIR - + fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleFirewall" try: - cfg += open(fname, "rb").read() + with open(fname, "r") as f: + cfg += f.read() except IOError: logging.exception( "Error opening Firewall configuration template (%s)", fname ) - return cfg @@ -123,30 +123,28 @@ class Nat(CoreService): IPv4 source NAT service. """ - name = "NAT" - executables = ("iptables",) - group = "Security" - configs = ("nat.sh",) - startup = ("sh nat.sh",) - custom_needed = False + name: str = "NAT" + group: str = "Security" + executables: Tuple[str, ...] = ("iptables",) + configs: Tuple[str, ...] = ("nat.sh",) + startup: Tuple[str, ...] = ("sh nat.sh",) + custom_needed: bool = False @classmethod - def generateifcnatrule(cls, ifc, line_prefix=""): + def generate_iface_nat_rule(cls, iface: CoreInterface, prefix: str = "") -> str: """ Generate a NAT line for one interface. """ - cfg = line_prefix + "iptables -t nat -A POSTROUTING -o " - cfg += ifc.name + " -j MASQUERADE\n" - - cfg += line_prefix + "iptables -A FORWARD -i " + ifc.name + cfg = prefix + "iptables -t nat -A POSTROUTING -o " + cfg += iface.name + " -j MASQUERADE\n" + cfg += prefix + "iptables -A FORWARD -i " + iface.name cfg += " -m state --state RELATED,ESTABLISHED -j ACCEPT\n" - - cfg += line_prefix + "iptables -A FORWARD -i " - cfg += ifc.name + " -j DROP\n" + cfg += prefix + "iptables -A FORWARD -i " + cfg += iface.name + " -j DROP\n" return cfg @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ NAT out the first interface """ @@ -154,14 +152,12 @@ def generate_config(cls, node, filename): cfg += "# generated by security.py\n" cfg += "# NAT out the first interface by default\n" have_nat = False - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue + for iface in node.get_ifaces(control=False): if have_nat: - cfg += cls.generateifcnatrule(ifc, line_prefix="#") + cfg += cls.generate_iface_nat_rule(iface, prefix="#") else: have_nat = True - cfg += "# NAT out the " + ifc.name + " interface\n" - cfg += cls.generateifcnatrule(ifc) + cfg += "# NAT out the " + iface.name + " interface\n" + cfg += cls.generate_iface_nat_rule(iface) cfg += "\n" return cfg diff --git a/daemon/core/services/ucarp.py b/daemon/core/services/ucarp.py index 1eb801797..8ac92dd34 100644 --- a/daemon/core/services/ucarp.py +++ b/daemon/core/services/ucarp.py @@ -1,52 +1,52 @@ """ ucarp.py: defines high-availability IP address controlled by ucarp """ +from typing import Tuple +from core.nodes.base import CoreNode from core.services.coreservices import CoreService UCARP_ETC = "/usr/local/etc/ucarp" class Ucarp(CoreService): - name = "ucarp" - group = "Utility" - dirs = (UCARP_ETC,) - configs = ( + name: str = "ucarp" + group: str = "Utility" + dirs: Tuple[str, ...] = (UCARP_ETC,) + configs: Tuple[str, ...] = ( UCARP_ETC + "/default.sh", UCARP_ETC + "/default-up.sh", UCARP_ETC + "/default-down.sh", "ucarpboot.sh", ) - startup = ("sh ucarpboot.sh",) - shutdown = ("killall ucarp",) - validate = ("pidof ucarp",) + startup: Tuple[str, ...] = ("sh ucarpboot.sh",) + shutdown: Tuple[str, ...] = ("killall ucarp",) + validate: Tuple[str, ...] = ("pidof ucarp",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the default file contents """ if filename == cls.configs[0]: - return cls.generateUcarpConf(node) + return cls.generate_ucarp_conf(node) elif filename == cls.configs[1]: - return cls.generateVipUp(node) + return cls.generate_vip_up(node) elif filename == cls.configs[2]: - return cls.generateVipDown(node) + return cls.generate_vip_down(node) elif filename == cls.configs[3]: - return cls.generateUcarpBoot(node) + return cls.generate_ucarp_boot(node) else: raise ValueError @classmethod - def generateUcarpConf(cls, node): + def generate_ucarp_conf(cls, node: CoreNode) -> str: """ Returns configuration file text. """ - try: - ucarp_bin = node.session.cfg["ucarp_bin"] - except KeyError: - ucarp_bin = "/usr/sbin/ucarp" - + ucarp_bin = node.session.options.get_config( + "ucarp_bin", default="/usr/sbin/ucarp" + ) return """\ #!/bin/sh # Location of UCARP executable @@ -110,7 +110,7 @@ def generateUcarpConf(cls, node): ) @classmethod - def generateUcarpBoot(cls, node): + def generate_ucarp_boot(cls, node: CoreNode) -> str: """ Generate a shell script used to boot the Ucarp daemons. """ @@ -130,7 +130,7 @@ def generateUcarpBoot(cls, node): ) @classmethod - def generateVipUp(cls, node): + def generate_vip_up(cls, node: CoreNode) -> str: """ Generate a shell script used to start the virtual ip """ @@ -152,7 +152,7 @@ def generateVipUp(cls, node): """ @classmethod - def generateVipDown(cls, node): + def generate_vip_down(cls, node: CoreNode) -> str: """ Generate a shell script used to stop the virtual ip """ diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 8a6e828b8..774c41045 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -1,12 +1,14 @@ """ utility.py: defines miscellaneous utility services. """ -import os +from typing import Optional, Tuple import netaddr -from core import constants, utils +from core import utils from core.errors import CoreCommandError +from core.executables import SYSCTL +from core.nodes.base import CoreNode from core.services.coreservices import CoreService, ServiceMode @@ -15,32 +17,25 @@ class UtilService(CoreService): Parent class for utility services. """ - name = None - group = "Utility" - dirs = () - configs = () - startup = () - shutdown = () + name: Optional[str] = None + group: str = "Utility" @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" class IPForwardService(UtilService): - name = "IPForward" - configs = ("ipforward.sh",) - startup = ("sh ipforward.sh",) + name: str = "IPForward" + configs: Tuple[str, ...] = ("ipforward.sh",) + startup: Tuple[str, ...] = ("sh ipforward.sh",) @classmethod - def generate_config(cls, node, filename): - if os.uname()[0] == "Linux": - return cls.generateconfiglinux(node, filename) - else: - raise Exception("unknown platform") + def generate_config(cls, node: CoreNode, filename: str) -> str: + return cls.generateconfiglinux(node, filename) @classmethod - def generateconfiglinux(cls, node, filename): + def generateconfiglinux(cls, node: CoreNode, filename: str) -> str: cfg = """\ #!/bin/sh # auto-generated by IPForward service (utility.py) @@ -53,35 +48,29 @@ def generateconfiglinux(cls, node, filename): %(sysctl)s -w net.ipv4.conf.all.rp_filter=0 %(sysctl)s -w net.ipv4.conf.default.rp_filter=0 """ % { - "sysctl": constants.SYSCTL_BIN + "sysctl": SYSCTL } - for ifc in node.netifs(): - name = utils.sysctl_devname(ifc.name) - cfg += "%s -w net.ipv4.conf.%s.forwarding=1\n" % ( - constants.SYSCTL_BIN, - name, - ) - cfg += "%s -w net.ipv4.conf.%s.send_redirects=0\n" % ( - constants.SYSCTL_BIN, - name, - ) - cfg += "%s -w net.ipv4.conf.%s.rp_filter=0\n" % (constants.SYSCTL_BIN, name) + for iface in node.get_ifaces(): + name = utils.sysctl_devname(iface.name) + cfg += "%s -w net.ipv4.conf.%s.forwarding=1\n" % (SYSCTL, name) + cfg += "%s -w net.ipv4.conf.%s.send_redirects=0\n" % (SYSCTL, name) + cfg += "%s -w net.ipv4.conf.%s.rp_filter=0\n" % (SYSCTL, name) return cfg class DefaultRouteService(UtilService): - name = "DefaultRoute" - configs = ("defaultroute.sh",) - startup = ("sh defaultroute.sh",) + name: str = "DefaultRoute" + configs: Tuple[str, ...] = ("defaultroute.sh",) + startup: Tuple[str, ...] = ("sh defaultroute.sh",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: routes = [] - netifs = node.netifs(sort=True) - if netifs: - netif = netifs[0] - for x in netif.addrlist: - net = netaddr.IPNetwork(x).cidr + ifaces = 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)) @@ -93,78 +82,65 @@ def generate_config(cls, node, filename): class DefaultMulticastRouteService(UtilService): - name = "DefaultMulticastRoute" - configs = ("defaultmroute.sh",) - startup = ("sh defaultmroute.sh",) + name: str = "DefaultMulticastRoute" + configs: Tuple[str, ...] = ("defaultmroute.sh",) + startup: Tuple[str, ...] = ("sh defaultmroute.sh",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: cfg = "#!/bin/sh\n" cfg += "# auto-generated by DefaultMulticastRoute service (utility.py)\n" cfg += "# the first interface is chosen below; please change it " cfg += "as needed\n" - - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - if os.uname()[0] == "Linux": - rtcmd = "ip route add 224.0.0.0/4 dev" - else: - raise Exception("unknown platform") - cfg += "%s %s\n" % (rtcmd, ifc.name) + for iface in node.get_ifaces(control=False): + rtcmd = "ip route add 224.0.0.0/4 dev" + cfg += "%s %s\n" % (rtcmd, iface.name) cfg += "\n" break return cfg class StaticRouteService(UtilService): - name = "StaticRoute" - configs = ("staticroute.sh",) - startup = ("sh staticroute.sh",) - custom_needed = True + name: str = "StaticRoute" + configs: Tuple[str, ...] = ("staticroute.sh",) + startup: Tuple[str, ...] = ("sh staticroute.sh",) + custom_needed: bool = True @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: cfg = "#!/bin/sh\n" cfg += "# auto-generated by StaticRoute service (utility.py)\n#\n" cfg += "# NOTE: this service must be customized to be of any use\n" cfg += "# Below are samples that you can uncomment and edit.\n#\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\n".join(map(cls.routestr, ifc.addrlist)) + for iface in node.get_ifaces(control=False): + cfg += "\n".join(map(cls.routestr, iface.ips())) cfg += "\n" return cfg @staticmethod - def routestr(x): - addr = x.split("/")[0] - if netaddr.valid_ipv6(addr): + def routestr(ip: netaddr.IPNetwork) -> str: + 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]: + if ip[-2] == ip[1]: return "" else: - if os.uname()[0] == "Linux": - rtcmd = "#/sbin/ip route add %s via" % dst - else: - raise Exception("unknown platform") - return "%s %s" % (rtcmd, net[1]) + rtcmd = "#/sbin/ip route add %s via" % dst + return "%s %s" % (rtcmd, ip[1]) class SshService(UtilService): - name = "SSH" - configs = ("startsshd.sh", "/etc/ssh/sshd_config") - dirs = ("/etc/ssh", "/var/run/sshd") - startup = ("sh startsshd.sh",) - shutdown = ("killall sshd",) - validate = () - validation_mode = ServiceMode.BLOCKING + name: str = "SSH" + configs: Tuple[str, ...] = ("startsshd.sh", "/etc/ssh/sshd_config") + dirs: Tuple[str, ...] = ("/etc/ssh", "/var/run/sshd") + startup: Tuple[str, ...] = ("sh startsshd.sh",) + shutdown: Tuple[str, ...] = ("killall sshd",) + validation_mode: ServiceMode = ServiceMode.BLOCKING @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Use a startup script for launching sshd in order to wait for host key generation. @@ -232,15 +208,15 @@ def generate_config(cls, node, filename): class DhcpService(UtilService): - name = "DHCP" - configs = ("/etc/dhcp/dhcpd.conf",) - dirs = ("/etc/dhcp", "/var/lib/dhcp") - startup = ("touch /var/lib/dhcp/dhcpd.leases", "dhcpd") - shutdown = ("killall dhcpd",) - validate = ("pidof dhcpd",) + name: str = "DHCP" + configs: Tuple[str, ...] = ("/etc/dhcp/dhcpd.conf",) + dirs: Tuple[str, ...] = ("/etc/dhcp", "/var/lib/dhcp") + startup: Tuple[str, ...] = ("touch /var/lib/dhcp/dhcpd.leases", "dhcpd") + shutdown: Tuple[str, ...] = ("killall dhcpd",) + validate: Tuple[str, ...] = ("pidof dhcpd",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a dhcpd config file using the network address of each interface. @@ -259,28 +235,25 @@ def generate_config(cls, node, filename): ddns-update-style none; """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\n".join(map(cls.subnetentry, ifc.addrlist)) + for iface in node.get_ifaces(control=False): + cfg += "\n".join(map(cls.subnetentry, iface.ips())) cfg += "\n" return cfg @staticmethod - def subnetentry(x): + def subnetentry(ip: netaddr.IPNetwork) -> str: """ Generate a subnet declaration block given an IPv4 prefix string for inclusion in the dhcpd3 config file. """ - addr = x.split("/")[0] - if netaddr.valid_ipv6(addr): + address = str(ip.ip) + if netaddr.valid_ipv6(address): return "" else: - net = netaddr.IPNetwork(x) # divide the address space in half - index = (net.size - 2) / 2 - rangelow = net[index] - rangehigh = net[-2] + index = (ip.size - 2) / 2 + rangelow = ip[index] + rangehigh = ip[-2] return """ subnet %s netmask %s { pool { @@ -290,11 +263,11 @@ def subnetentry(x): } } """ % ( - net.ip, - net.netmask, + ip.ip, + ip.netmask, rangelow, rangehigh, - addr, + address, ) @@ -303,14 +276,14 @@ class DhcpClientService(UtilService): Use a DHCP client for all interfaces for addressing. """ - name = "DHCPClient" - configs = ("startdhcpclient.sh",) - startup = ("sh startdhcpclient.sh",) - shutdown = ("killall dhclient",) - validate = ("pidof dhclient",) + name: str = "DHCPClient" + configs: Tuple[str, ...] = ("startdhcpclient.sh",) + startup: Tuple[str, ...] = ("sh startdhcpclient.sh",) + shutdown: Tuple[str, ...] = ("killall dhclient",) + validate: Tuple[str, ...] = ("pidof dhclient",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a script to invoke dhclient on all interfaces. """ @@ -319,14 +292,11 @@ def generate_config(cls, node, filename): cfg += "# uncomment this mkdir line and symlink line to enable client-" cfg += "side DNS\n# resolution based on the DHCP server response.\n" cfg += "#mkdir -p /var/run/resolvconf/interface\n" - - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "#ln -s /var/run/resolvconf/interface/%s.dhclient" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += "#ln -s /var/run/resolvconf/interface/%s.dhclient" % iface.name cfg += " /var/run/resolvconf/resolv.conf\n" - cfg += "/sbin/dhclient -nw -pf /var/run/dhclient-%s.pid" % ifc.name - cfg += " -lf /var/run/dhclient-%s.lease %s\n" % (ifc.name, ifc.name) + cfg += "/sbin/dhclient -nw -pf /var/run/dhclient-%s.pid" % iface.name + cfg += " -lf /var/run/dhclient-%s.lease %s\n" % (iface.name, iface.name) return cfg @@ -335,15 +305,15 @@ class FtpService(UtilService): Start a vsftpd server. """ - name = "FTP" - configs = ("vsftpd.conf",) - dirs = ("/var/run/vsftpd/empty", "/var/ftp") - startup = ("vsftpd ./vsftpd.conf",) - shutdown = ("killall vsftpd",) - validate = ("pidof vsftpd",) + name: str = "FTP" + configs: Tuple[str, ...] = ("vsftpd.conf",) + dirs: Tuple[str, ...] = ("/var/run/vsftpd/empty", "/var/ftp") + startup: Tuple[str, ...] = ("vsftpd ./vsftpd.conf",) + shutdown: Tuple[str, ...] = ("killall vsftpd",) + validate: Tuple[str, ...] = ("pidof vsftpd",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a vsftpd.conf configuration file. """ @@ -368,13 +338,13 @@ class HttpService(UtilService): Start an apache server. """ - name = "HTTP" - configs = ( + name: str = "HTTP" + configs: Tuple[str, ...] = ( "/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html", ) - dirs = ( + dirs: Tuple[str, ...] = ( "/etc/apache2", "/var/run/apache2", "/var/log/apache2", @@ -382,14 +352,14 @@ class HttpService(UtilService): "/var/lock/apache2", "/var/www", ) - startup = ("chown www-data /var/lock/apache2", "apache2ctl start") - shutdown = ("apache2ctl stop",) - validate = ("pidof apache2",) - - APACHEVER22, APACHEVER24 = (22, 24) + startup: Tuple[str, ...] = ("chown www-data /var/lock/apache2", "apache2ctl start") + shutdown: Tuple[str, ...] = ("apache2ctl stop",) + validate: Tuple[str, ...] = ("pidof apache2",) + APACHEVER22: int = 22 + APACHEVER24: int = 24 @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate an apache2.conf configuration file. """ @@ -403,7 +373,7 @@ def generate_config(cls, node, filename): return "" @classmethod - def detectversionfromcmd(cls): + def detectversionfromcmd(cls) -> int: """ Detect the apache2 version using the 'a2query' command. """ @@ -413,14 +383,12 @@ def detectversionfromcmd(cls): except CoreCommandError as e: status = e.returncode result = e.stderr - if status == 0 and result[:3] == "2.4": return cls.APACHEVER24 - return cls.APACHEVER22 @classmethod - def generateapache2conf(cls, node, filename): + def generateapache2conf(cls, node: CoreNode, filename: str) -> str: lockstr = { cls.APACHEVER22: "LockFile ${APACHE_LOCK_DIR}/accept.lock\n", cls.APACHEVER24: "Mutex file:${APACHE_LOCK_DIR} default\n", @@ -429,22 +397,18 @@ def generateapache2conf(cls, node, filename): cls.APACHEVER22: "", cls.APACHEVER24: "LoadModule mpm_worker_module /usr/lib/apache2/modules/mod_mpm_worker.so\n", } - permstr = { cls.APACHEVER22: " Order allow,deny\n Deny from all\n Satisfy all\n", cls.APACHEVER24: " Require all denied\n", } - authstr = { cls.APACHEVER22: "LoadModule authz_default_module /usr/lib/apache2/modules/mod_authz_default.so\n", cls.APACHEVER24: "LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so\n", } - permstr2 = { cls.APACHEVER22: "\t\tOrder allow,deny\n\t\tallow from all\n", cls.APACHEVER24: "\t\tRequire all granted\n", } - version = cls.detectversionfromcmd() cfg = "# apache2.conf generated by utility.py:HttpService\n" cfg += lockstr[version] @@ -560,7 +524,7 @@ def generateapache2conf(cls, node, filename): return cfg @classmethod - def generateenvvars(cls, node, filename): + def generateenvvars(cls, node: CoreNode, filename: str) -> str: return """\ # this file is used by apache2ctl - generated by utility.py:HttpService # these settings come from a default Ubuntu apache2 installation @@ -575,7 +539,7 @@ def generateenvvars(cls, node, filename): """ @classmethod - def generatehtml(cls, node, filename): + def generatehtml(cls, node: CoreNode, filename: str) -> str: body = ( """\ @@ -585,10 +549,8 @@ def generatehtml(cls, node, filename): """ % node.name ) - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - body += "
  • %s - %s
  • \n" % (ifc.name, ifc.addrlist) + for iface in node.get_ifaces(control=False): + body += "
  • %s - %s
  • \n" % (iface.name, [str(x) for x in iface.ips()]) return "%s" % body @@ -597,16 +559,15 @@ class PcapService(UtilService): Pcap service for logging packets. """ - name = "pcap" - configs = ("pcap.sh",) - dirs = () - startup = ("sh pcap.sh start",) - shutdown = ("sh pcap.sh stop",) - validate = ("pidof tcpdump",) - meta = "logs network traffic to pcap packet capture files" + name: str = "pcap" + configs: Tuple[str, ...] = ("pcap.sh",) + startup: Tuple[str, ...] = ("sh pcap.sh start",) + shutdown: Tuple[str, ...] = ("sh pcap.sh stop",) + validate: Tuple[str, ...] = ("pidof tcpdump",) + meta: str = "logs network traffic to pcap packet capture files" @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a startpcap.sh traffic logging script. """ @@ -619,14 +580,14 @@ def generate_config(cls, node, filename): if [ "x$1" = "xstart" ]; then """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: + for iface in node.get_ifaces(): + if hasattr(iface, "control") and iface.control is True: cfg += "# " redir = "< /dev/null" cfg += "tcpdump ${DUMPOPTS} -w %s.%s.pcap -i %s %s &\n" % ( node.name, - ifc.name, - ifc.name, + iface.name, + iface.name, redir, ) cfg += """ @@ -640,24 +601,24 @@ def generate_config(cls, node, filename): class RadvdService(UtilService): - name = "radvd" - configs = ("/etc/radvd/radvd.conf",) - dirs = ("/etc/radvd",) - startup = ("radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log",) - shutdown = ("pkill radvd",) - validate = ("pidof radvd",) + name: str = "radvd" + configs: Tuple[str, ...] = ("/etc/radvd/radvd.conf",) + dirs: Tuple[str, ...] = ("/etc/radvd",) + startup: Tuple[str, ...] = ( + "radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log", + ) + shutdown: Tuple[str, ...] = ("pkill radvd",) + validate: Tuple[str, ...] = ("pidof radvd",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a RADVD router advertisement daemon config file using the network address of each interface. """ cfg = "# auto-generated by RADVD service (utility.py)\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - prefixes = list(map(cls.subnetentry, ifc.addrlist)) + for iface in node.get_ifaces(control=False): + prefixes = list(map(cls.subnetentry, iface.ips())) if len(prefixes) < 1: continue cfg += ( @@ -670,7 +631,7 @@ def generate_config(cls, node, filename): AdvDefaultPreference low; AdvHomeAgentFlag off; """ - % ifc.name + % iface.name ) for prefix in prefixes: if prefix == "": @@ -690,14 +651,14 @@ def generate_config(cls, node, filename): return cfg @staticmethod - def subnetentry(x): + def subnetentry(ip: netaddr.IPNetwork) -> str: """ Generate a subnet declaration block given an IPv6 prefix string for inclusion in the RADVD config file. """ - addr = x.split("/")[0] - if netaddr.valid_ipv6(addr): - return x + address = str(ip.ip) + if netaddr.valid_ipv6(address): + return str(ip) else: return "" @@ -707,14 +668,14 @@ class AtdService(UtilService): Atd service for scheduling at jobs """ - name = "atd" - configs = ("startatd.sh",) - dirs = ("/var/spool/cron/atjobs", "/var/spool/cron/atspool") - startup = ("sh startatd.sh",) - shutdown = ("pkill atd",) + name: str = "atd" + configs: Tuple[str, ...] = ("startatd.sh",) + dirs: Tuple[str, ...] = ("/var/spool/cron/atjobs", "/var/spool/cron/atspool") + startup: Tuple[str, ...] = ("sh startatd.sh",) + shutdown: Tuple[str, ...] = ("pkill atd",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return """ #!/bin/sh echo 00001 > /var/spool/cron/atjobs/.SEQ @@ -729,5 +690,5 @@ class UserDefinedService(UtilService): Dummy service allowing customization of anything. """ - name = "UserDefined" - meta = "Customize this service to do anything upon startup." + name: str = "UserDefined" + meta: str = "Customize this service to do anything upon startup." diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 2312e6d43..485fe159f 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -2,10 +2,12 @@ xorp.py: defines routing services provided by the XORP routing suite. """ -import logging +from typing import Optional, Tuple import netaddr +from core.nodes.base import CoreNode +from core.nodes.interface import CoreInterface from core.services.coreservices import CoreService @@ -15,61 +17,59 @@ class XorpRtrmgr(CoreService): enabled XORP services, and launches necessary daemons upon startup. """ - name = "xorp_rtrmgr" - executables = ("xorp_rtrmgr",) - group = "XORP" - dirs = ("/etc/xorp",) - configs = ("/etc/xorp/config.boot",) - startup = ( + name: str = "xorp_rtrmgr" + group: str = "XORP" + executables: Tuple[str, ...] = ("xorp_rtrmgr",) + dirs: Tuple[str, ...] = ("/etc/xorp",) + configs: Tuple[str, ...] = ("/etc/xorp/config.boot",) + startup: Tuple[str, ...] = ( "xorp_rtrmgr -d -b %s -l /var/log/%s.log -P /var/run/%s.pid" % (configs[0], name, name), ) - shutdown = ("killall xorp_rtrmgr",) - validate = ("pidof xorp_rtrmgr",) + shutdown: Tuple[str, ...] = ("killall xorp_rtrmgr",) + validate: Tuple[str, ...] = ("pidof xorp_rtrmgr",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Returns config.boot configuration file text. Other services that depend on this will have generatexorpconfig() hooks that are invoked here. Filename currently ignored. """ cfg = "interfaces {\n" - for ifc in node.netifs(): - cfg += " interface %s {\n" % ifc.name - cfg += "\tvif %s {\n" % ifc.name - cfg += "".join(map(cls.addrstr, ifc.addrlist)) - cfg += cls.lladdrstr(ifc) + for iface in node.get_ifaces(): + cfg += " interface %s {\n" % iface.name + cfg += "\tvif %s {\n" % iface.name + cfg += "".join(map(cls.addrstr, iface.ips())) + cfg += cls.lladdrstr(iface) cfg += "\t}\n" cfg += " }\n" cfg += "}\n\n" for s in node.services: - try: - s.dependencies.index(cls.name) - cfg += s.generatexorpconfig(node) - except ValueError: - logging.exception("error getting value from service: %s", cls.name) - + if cls.name not in s.dependencies: + continue + if not (isinstance(s, XorpService) or issubclass(s, XorpService)): + continue + cfg += s.generate_xorp_config(node) return cfg @staticmethod - def addrstr(x): + def addrstr(ip: netaddr.IPNetwork) -> str: """ helper for mapping IP addresses to XORP config statements """ - addr, plen = x.split("/") - cfg = "\t address %s {\n" % addr - cfg += "\t\tprefix-length: %s\n" % plen + cfg = "\t address %s {\n" % ip.ip + cfg += "\t\tprefix-length: %s\n" % ip.prefixlen cfg += "\t }\n" return cfg @staticmethod - def lladdrstr(ifc): + def lladdrstr(iface: CoreInterface) -> str: """ helper for adding link-local address entries (required by OSPFv3) """ - cfg = "\t address %s {\n" % ifc.hwaddr.tolinklocal() + cfg = "\t address %s {\n" % iface.mac.eui64() cfg += "\t\tprefix-length: 64\n" cfg += "\t }\n" return cfg @@ -81,18 +81,16 @@ class XorpService(CoreService): common to XORP's routing daemons. """ - name = None - executables = ("xorp_rtrmgr",) - group = "XORP" - dependencies = ("xorp_rtrmgr",) - dirs = () - configs = () - startup = () - shutdown = () - meta = "The config file for this service can be found in the xorp_rtrmgr service." + name: Optional[str] = None + group: str = "XORP" + executables: Tuple[str, ...] = ("xorp_rtrmgr",) + dependencies: Tuple[str, ...] = ("xorp_rtrmgr",) + meta: str = ( + "The config file for this service can be found in the xorp_rtrmgr service." + ) @staticmethod - def fea(forwarding): + def fea(forwarding: str) -> str: """ Helper to add a forwarding engine entry to the config file. """ @@ -104,17 +102,14 @@ def fea(forwarding): return cfg @staticmethod - def mfea(forwarding, ifcs): + def mfea(forwarding, node: CoreNode) -> str: """ Helper to add a multicast forwarding engine entry to the config file. """ names = [] - for ifc in ifcs: - if hasattr(ifc, "control") and ifc.control is True: - continue - names.append(ifc.name) + for iface in node.get_ifaces(control=False): + names.append(iface.name) names.append("register_vif") - cfg = "plumbing {\n" cfg += " %s {\n" % forwarding for name in names: @@ -128,7 +123,7 @@ def mfea(forwarding, ifcs): return cfg @staticmethod - def policyexportconnected(): + def policyexportconnected() -> str: """ Helper to add a policy statement for exporting connected routes. """ @@ -144,26 +139,22 @@ def policyexportconnected(): return cfg @staticmethod - def routerid(node): + def router_id(node: CoreNode) -> str: """ Helper to return the first IPv4 address of a node as its router ID. """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return a - # raise ValueError, "no IPv4 address found for router ID" + for iface in node.get_ifaces(control=False): + ip4 = iface.get_ip4() + if ip4: + return str(ip4.ip) return "0.0.0.0" @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: return "" @@ -174,26 +165,21 @@ class XorpOspfv2(XorpService): unified XORP configuration file. """ - name = "XORP_OSPFv2" + name: str = "XORP_OSPFv2" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = cls.fea("unicast-forwarding4") - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " ospf4 {\n" cfg += "\trouter-id: %s\n" % rtrid cfg += "\tarea 0.0.0.0 {\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\t interface %s {\n" % ifc.name - cfg += "\t\tvif %s {\n" % ifc.name - for a in ifc.addrlist: - addr = a.split("/")[0] - if not netaddr.valid_ipv4(addr): - continue - cfg += "\t\t address %s {\n" % addr + for iface in node.get_ifaces(control=False): + cfg += "\t interface %s {\n" % iface.name + cfg += "\t\tvif %s {\n" % iface.name + for ip4 in iface.ip4s: + cfg += "\t\t address %s {\n" % ip4.ip cfg += "\t\t }\n" cfg += "\t\t}\n" cfg += "\t }\n" @@ -210,21 +196,19 @@ class XorpOspfv3(XorpService): unified XORP configuration file. """ - name = "XORP_OSPFv3" + name: str = "XORP_OSPFv3" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = cls.fea("unicast-forwarding6") - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " ospf6 0 { /* Instance ID 0 */\n" cfg += "\trouter-id: %s\n" % rtrid cfg += "\tarea 0.0.0.0 {\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\t interface %s {\n" % ifc.name - cfg += "\t\tvif %s {\n" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += "\t interface %s {\n" % iface.name + cfg += "\t\tvif %s {\n" % iface.name cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" @@ -238,16 +222,16 @@ class XorpBgp(XorpService): IPv4 inter-domain routing. AS numbers and peers must be customized. """ - name = "XORP_BGP" - custom_needed = True + name: str = "XORP_BGP" + custom_needed: bool = True @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = "/* This is a sample config that should be customized with\n" cfg += " appropriate AS numbers and peers */\n" cfg += cls.fea("unicast-forwarding4") cfg += cls.policyexportconnected() - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " bgp {\n" cfg += "\tbgp-id: %s\n" % rtrid @@ -268,25 +252,20 @@ class XorpRip(XorpService): RIP IPv4 unicast routing. """ - name = "XORP_RIP" + name: str = "XORP_RIP" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = cls.fea("unicast-forwarding4") cfg += cls.policyexportconnected() cfg += "\nprotocols {\n" cfg += " rip {\n" cfg += '\texport: "export-connected"\n' - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\tinterface %s {\n" % ifc.name - cfg += "\t vif %s {\n" % ifc.name - for a in ifc.addrlist: - addr = a.split("/")[0] - if not netaddr.valid_ipv4(addr): - continue - cfg += "\t\taddress %s {\n" % addr + for iface in node.get_ifaces(control=False): + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name + for ip4 in iface.ip4s: + cfg += "\t\taddress %s {\n" % ip4.ip cfg += "\t\t disable: false\n" cfg += "\t\t}\n" cfg += "\t }\n" @@ -301,21 +280,19 @@ class XorpRipng(XorpService): RIP NG IPv6 unicast routing. """ - name = "XORP_RIPNG" + name: str = "XORP_RIPNG" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = cls.fea("unicast-forwarding6") cfg += cls.policyexportconnected() cfg += "\nprotocols {\n" cfg += " ripng {\n" cfg += '\texport: "export-connected"\n' - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\tinterface %s {\n" % ifc.name - cfg += "\t vif %s {\n" % ifc.name - cfg += "\t\taddress %s {\n" % ifc.hwaddr.tolinklocal() + for iface in node.get_ifaces(control=False): + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name + cfg += "\t\taddress %s {\n" % iface.mac.eui64() cfg += "\t\t disable: false\n" cfg += "\t\t}\n" cfg += "\t }\n" @@ -330,27 +307,23 @@ class XorpPimSm4(XorpService): PIM Sparse Mode IPv4 multicast routing. """ - name = "XORP_PIMSM4" + name: str = "XORP_PIMSM4" @classmethod - def generatexorpconfig(cls, node): - cfg = cls.mfea("mfea4", node.netifs()) - + def generate_xorp_config(cls, node: CoreNode) -> str: + cfg = cls.mfea("mfea4", node) cfg += "\nprotocols {\n" cfg += " igmp {\n" names = [] - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - names.append(ifc.name) - cfg += "\tinterface %s {\n" % ifc.name - cfg += "\t vif %s {\n" % ifc.name + for iface in node.get_ifaces(control=False): + names.append(iface.name) + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name cfg += "\t\tdisable: false\n" cfg += "\t }\n" cfg += "\t}\n" cfg += " }\n" cfg += "}\n" - cfg += "\nprotocols {\n" cfg += " pimsm4 {\n" @@ -373,10 +346,8 @@ def generatexorpconfig(cls, node): cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" - cfg += " }\n" cfg += "}\n" - cfg += "\nprotocols {\n" cfg += " fib2mrib {\n" cfg += "\tdisable: false\n" @@ -390,27 +361,23 @@ class XorpPimSm6(XorpService): PIM Sparse Mode IPv6 multicast routing. """ - name = "XORP_PIMSM6" + name: str = "XORP_PIMSM6" @classmethod - def generatexorpconfig(cls, node): - cfg = cls.mfea("mfea6", node.netifs()) - + def generate_xorp_config(cls, node: CoreNode) -> str: + cfg = cls.mfea("mfea6", node) cfg += "\nprotocols {\n" cfg += " mld {\n" names = [] - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - names.append(ifc.name) - cfg += "\tinterface %s {\n" % ifc.name - cfg += "\t vif %s {\n" % ifc.name + for iface in node.get_ifaces(control=False): + names.append(iface.name) + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name cfg += "\t\tdisable: false\n" cfg += "\t }\n" cfg += "\t}\n" cfg += " }\n" cfg += "}\n" - cfg += "\nprotocols {\n" cfg += " pimsm6 {\n" @@ -433,10 +400,8 @@ def generatexorpconfig(cls, node): cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" - cfg += " }\n" cfg += "}\n" - cfg += "\nprotocols {\n" cfg += " fib2mrib {\n" cfg += "\tdisable: false\n" @@ -450,25 +415,20 @@ class XorpOlsr(XorpService): OLSR IPv4 unicast MANET routing. """ - name = "XORP_OLSR" + name: str = "XORP_OLSR" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = cls.fea("unicast-forwarding4") - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " olsr4 {\n" cfg += "\tmain-address: %s\n" % rtrid - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\tinterface %s {\n" % ifc.name - cfg += "\t vif %s {\n" % ifc.name - for a in ifc.addrlist: - addr = a.split("/")[0] - if not netaddr.valid_ipv4(addr): - continue - cfg += "\t\taddress %s {\n" % addr + for iface in node.get_ifaces(control=False): + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name + for ip4 in iface.ip4s: + cfg += "\t\taddress %s {\n" % ip4.ip cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 3b1ea46a3..459b7d568 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -154,7 +154,7 @@ def which(command: str, required: bool) -> str: """ found_path = shutil.which(command) if found_path is None and required: - raise ValueError(f"failed to find required executable({command}) in path") + raise CoreError(f"failed to find required executable({command}) in path") return found_path @@ -430,31 +430,3 @@ def random_mac() -> str: value |= 0x00163E << 24 mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded) return str(mac) - - -def validate_mac(value: str) -> str: - """ - Validate mac and return unix formatted version. - - :param value: address to validate - :return: unix formatted mac - """ - try: - mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded) - return str(mac) - except netaddr.AddrFormatError as e: - raise CoreError(f"invalid mac address {value}: {e}") - - -def validate_ip(value: str) -> str: - """ - Validate ip address with prefix and return formatted version. - - :param value: address to validate - :return: formatted ip address - """ - try: - ip = netaddr.IPNetwork(value) - return str(ip) - except (ValueError, netaddr.AddrFormatError) as e: - raise CoreError(f"invalid ip address {value}: {e}") diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index cb25e7176..7e3b35a21 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -6,8 +6,7 @@ import core.nodes.base import core.nodes.physical 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 EventTypes, NodeTypes from core.errors import CoreXmlError from core.nodes.base import CoreNodeBase, NodeBase @@ -45,11 +44,11 @@ def get_type(element: etree.Element, name: str, _type: Generic[T]) -> Optional[T return value -def get_float(element: etree.Element, name: str) -> float: +def get_float(element: etree.Element, name: str) -> Optional[float]: return get_type(element, name, float) -def get_int(element: etree.Element, name: str) -> int: +def get_int(element: etree.Element, name: str) -> Optional[int]: return get_type(element, name, int) @@ -58,16 +57,16 @@ def add_attribute(element: etree.Element, name: str, value: Any) -> None: element.set(name, str(value)) -def create_interface_data(interface_element: etree.Element) -> InterfaceData: - interface_id = int(interface_element.get("id")) - name = interface_element.get("name") - mac = interface_element.get("mac") - ip4 = interface_element.get("ip4") - ip4_mask = get_int(interface_element, "ip4_mask") - ip6 = interface_element.get("ip6") - ip6_mask = get_int(interface_element, "ip6_mask") +def create_iface_data(iface_element: etree.Element) -> InterfaceData: + iface_id = int(iface_element.get("id")) + name = iface_element.get("name") + mac = iface_element.get("mac") + ip4 = iface_element.get("ip4") + ip4_mask = get_int(iface_element, "ip4_mask") + ip6 = iface_element.get("ip6") + ip6_mask = get_int(iface_element, "ip6_mask") return InterfaceData( - id=interface_id, + id=iface_id, name=name, mac=mac, ip4=ip4, @@ -124,11 +123,13 @@ def add_configuration(parent: etree.Element, name: str, value: str) -> None: class NodeElement: def __init__(self, session: "Session", node: NodeBase, element_name: str) -> None: - self.session = session - self.node = node - self.element = etree.Element(element_name) + self.session: "Session" = session + self.node: NodeBase = node + self.element: etree.Element = etree.Element(element_name) add_attribute(self.element, "id", node.id) add_attribute(self.element, "name", node.name) + server = self.node.server.name if self.node.server else None + add_attribute(self.element, "server", server) add_attribute(self.element, "icon", node.icon) add_attribute(self.element, "canvas", node.canvas) self.add_position() @@ -151,8 +152,8 @@ def add_position(self) -> None: class ServiceElement: def __init__(self, service: Type[CoreService]) -> None: - self.service = service - self.element = etree.Element("service") + self.service: Type[CoreService] = service + self.element: etree.Element = etree.Element("service") add_attribute(self.element, "name", service.name) self.add_directories() self.add_startup() @@ -268,10 +269,10 @@ def add_type(self) -> None: class CoreXmlWriter: def __init__(self, session: "Session") -> None: - self.session = session - self.scenario = etree.Element("scenario") - self.networks = None - self.devices = None + self.session: "Session" = session + self.scenario: etree.Element = etree.Element("scenario") + self.networks: etree.SubElement = etree.SubElement(self.scenario, "networks") + self.devices: etree.SubElement = etree.SubElement(self.scenario, "devices") self.write_session() def write_session(self) -> None: @@ -283,6 +284,7 @@ def write_session(self) -> None: self.write_service_configs() self.write_configservice_configs() self.write_session_origin() + self.write_servers() self.write_session_hooks() self.write_session_options() self.write_session_metadata() @@ -317,11 +319,20 @@ def write_session_origin(self) -> None: add_attribute(origin, "y", y) add_attribute(origin, "z", z) + def write_servers(self) -> None: + servers = etree.Element("servers") + for server in self.session.distributed.servers.values(): + server_element = etree.SubElement(servers, "server") + add_attribute(server_element, "name", server.name) + add_attribute(server_element, "address", server.host) + if servers.getchildren(): + self.scenario.append(servers) + def write_session_hooks(self) -> None: # hook scripts hooks = etree.Element("session_hooks") - for state in sorted(self.session._hooks, key=lambda x: x.value): - for file_name, data in self.session._hooks[state]: + for state in sorted(self.session.hooks, key=lambda x: x.value): + for file_name, data in self.session.hooks[state]: hook = etree.SubElement(hooks, "hook") add_attribute(hook, "name", file_name) add_attribute(hook, "state", state.value) @@ -362,13 +373,11 @@ def write_session_metadata(self) -> None: def write_emane_configs(self) -> None: emane_global_configuration = create_emane_config(self.session) self.scenario.append(emane_global_configuration) - emane_configurations = etree.Element("emane_configurations") for node_id in self.session.emane.nodes(): all_configs = self.session.emane.get_all_configs(node_id) if not all_configs: continue - for model_name in all_configs: config = all_configs[model_name] logging.debug( @@ -453,9 +462,6 @@ def write_default_services(self) -> None: self.scenario.append(node_types) def write_nodes(self) -> List[LinkData]: - self.networks = etree.SubElement(self.scenario, "networks") - self.devices = etree.SubElement(self.scenario, "devices") - links = [] for node_id in self.session.nodes: node = self.session.nodes[node_id] @@ -471,8 +477,7 @@ def write_nodes(self) -> List[LinkData]: self.write_device(node) # add known links - links.extend(node.all_link_data()) - + links.extend(node.links()) return links def write_network(self, node: NodeBase) -> None: @@ -488,12 +493,10 @@ def write_links(self, links: List[LinkData]) -> None: # add link data for link_data in links: # skip basic range links - if link_data.interface1_id is None and link_data.interface2_id is None: + if link_data.iface1 is None and link_data.iface2 is None: continue - link_element = self.create_link_element(link_data) link_elements.append(link_element) - if link_elements.getchildren(): self.scenario.append(link_elements) @@ -501,94 +504,64 @@ def write_device(self, node: NodeBase) -> None: device = DeviceElement(self.session, node) self.devices.append(device.element) - def create_interface_element( - self, - element_name: str, - node_id: int, - interface_id: int, - mac: str, - ip4: str, - ip4_mask: int, - ip6: str, - ip6_mask: int, + def create_iface_element( + self, element_name: str, node_id: int, iface_data: InterfaceData ) -> etree.Element: - interface = etree.Element(element_name) + iface_element = etree.Element(element_name) node = self.session.get_node(node_id, NodeBase) - interface_name = None if isinstance(node, CoreNodeBase): - node_interface = node.netif(interface_id) - interface_name = node_interface.name - + iface = node.get_iface(iface_data.id) # check if emane interface - if isinstance(node_interface.net, EmaneNet): - nem = node_interface.net.getnemid(node_interface) - add_attribute(interface, "nem", nem) - - add_attribute(interface, "id", interface_id) - add_attribute(interface, "name", interface_name) - add_attribute(interface, "mac", mac) - add_attribute(interface, "ip4", ip4) - add_attribute(interface, "ip4_mask", ip4_mask) - add_attribute(interface, "ip6", ip6) - add_attribute(interface, "ip6_mask", ip6_mask) - return interface + if isinstance(iface.net, EmaneNet): + nem_id = self.session.emane.get_nem_id(iface) + add_attribute(iface_element, "nem", nem_id) + add_attribute(iface_element, "id", iface_data.id) + add_attribute(iface_element, "name", iface_data.name) + add_attribute(iface_element, "mac", iface_data.mac) + add_attribute(iface_element, "ip4", iface_data.ip4) + add_attribute(iface_element, "ip4_mask", iface_data.ip4_mask) + add_attribute(iface_element, "ip6", iface_data.ip6) + add_attribute(iface_element, "ip6_mask", iface_data.ip6_mask) + return iface_element def create_link_element(self, link_data: LinkData) -> etree.Element: link_element = etree.Element("link") - add_attribute(link_element, "node_one", link_data.node1_id) - add_attribute(link_element, "node_two", link_data.node2_id) + add_attribute(link_element, "node1", link_data.node1_id) + add_attribute(link_element, "node2", link_data.node2_id) # check for interface one - if link_data.interface1_id is not None: - interface_one = self.create_interface_element( - "interface_one", - link_data.node1_id, - link_data.interface1_id, - link_data.interface1_mac, - link_data.interface1_ip4, - link_data.interface1_ip4_mask, - link_data.interface1_ip6, - link_data.interface1_ip6_mask, + if link_data.iface1 is not None: + iface1 = self.create_iface_element( + "iface1", link_data.node1_id, link_data.iface1 ) - link_element.append(interface_one) + link_element.append(iface1) # check for interface two - if link_data.interface2_id is not None: - interface_two = self.create_interface_element( - "interface_two", - link_data.node2_id, - link_data.interface2_id, - link_data.interface2_mac, - link_data.interface2_ip4, - link_data.interface2_ip4_mask, - link_data.interface2_ip6, - link_data.interface2_ip6_mask, + if link_data.iface2 is not None: + iface2 = self.create_iface_element( + "iface2", link_data.node2_id, link_data.iface2 ) - link_element.append(interface_two) + link_element.append(iface2) # check for options, don't write for emane/wlan links - node_one = self.session.get_node(link_data.node1_id, NodeBase) - node_two = self.session.get_node(link_data.node2_id, NodeBase) - is_node_one_wireless = isinstance(node_one, (WlanNode, EmaneNet)) - is_node_two_wireless = isinstance(node_two, (WlanNode, EmaneNet)) - if not any([is_node_one_wireless, is_node_two_wireless]): + node1 = self.session.get_node(link_data.node1_id, NodeBase) + node2 = self.session.get_node(link_data.node2_id, NodeBase) + is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet)) + is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet)) + if not any([is_node1_wireless, is_node2_wireless]): + options_data = link_data.options options = etree.Element("options") - add_attribute(options, "delay", link_data.delay) - add_attribute(options, "bandwidth", link_data.bandwidth) - add_attribute(options, "per", link_data.per) - add_attribute(options, "dup", link_data.dup) - add_attribute(options, "jitter", link_data.jitter) - add_attribute(options, "mer", link_data.mer) - add_attribute(options, "burst", link_data.burst) - add_attribute(options, "mburst", link_data.mburst) - add_attribute(options, "type", link_data.link_type) - add_attribute(options, "gui_attributes", link_data.gui_attributes) - add_attribute(options, "unidirectional", link_data.unidirectional) - add_attribute(options, "emulation_id", link_data.emulation_id) + add_attribute(options, "delay", options_data.delay) + add_attribute(options, "bandwidth", options_data.bandwidth) + add_attribute(options, "loss", options_data.loss) + add_attribute(options, "dup", options_data.dup) + add_attribute(options, "jitter", options_data.jitter) + add_attribute(options, "mer", options_data.mer) + add_attribute(options, "burst", options_data.burst) + add_attribute(options, "mburst", options_data.mburst) + add_attribute(options, "unidirectional", options_data.unidirectional) add_attribute(options, "network_id", link_data.network_id) - add_attribute(options, "key", link_data.key) - add_attribute(options, "opaque", link_data.opaque) - add_attribute(options, "session", link_data.session) + add_attribute(options, "key", options_data.key) if options.items(): link_element.append(options) @@ -597,8 +570,8 @@ def create_link_element(self, link_data: LinkData) -> etree.Element: class CoreXmlReader: def __init__(self, session: "Session") -> None: - self.session = session - self.scenario = None + self.session: "Session" = session + self.scenario: Optional[etree.ElementTree] = None def read(self, file_name: str) -> None: xml_tree = etree.parse(file_name) @@ -609,6 +582,7 @@ def read(self, file_name: str) -> None: self.read_session_metadata() self.read_session_options() self.read_session_hooks() + self.read_servers() self.read_session_origin() self.read_service_configs() self.read_mobility_configs() @@ -672,6 +646,16 @@ def read_session_hooks(self) -> None: logging.info("reading hook: state(%s) name(%s)", state, name) self.session.add_hook(state, name, data) + def read_servers(self) -> None: + servers = self.scenario.find("servers") + if servers is None: + return + for server in servers.iterchildren(): + name = server.get("name") + address = server.get("address") + logging.info("reading server: name(%s) address(%s)", name, address) + self.session.distributed.add_server(name, address) + def read_session_origin(self) -> None: session_origin = self.scenario.find("session_origin") if session_origin is None: @@ -840,8 +824,10 @@ def read_device(self, device_element: etree.Element) -> None: icon = device_element.get("icon") clazz = device_element.get("class") image = device_element.get("image") - options = NodeOptions(name=name, model=model, image=image, icon=icon) - + server = device_element.get("server") + options = NodeOptions( + name=name, model=model, image=image, icon=icon, server=server + ) node_type = NodeTypes.DEFAULT if clazz == "docker": node_type = NodeTypes.DOCKER @@ -881,7 +867,8 @@ def read_network(self, network_element: etree.Element) -> None: node_type = NodeTypes[network_element.get("type")] _class = self.session.get_node_class(node_type) icon = network_element.get("icon") - options = NodeOptions(name=name, icon=icon) + server = network_element.get("server") + options = NodeOptions(name=name, icon=icon, server=server) position_element = network_element.find("position") if position_element is not None: @@ -938,52 +925,53 @@ def read_links(self) -> None: node_sets = set() for link_element in link_elements.iterchildren(): - node_one = get_int(link_element, "node_one") - node_two = get_int(link_element, "node_two") - node_set = frozenset((node_one, node_two)) - - interface_one_element = link_element.find("interface_one") - interface_one = None - if interface_one_element is not None: - interface_one = create_interface_data(interface_one_element) - - interface_two_element = link_element.find("interface_two") - interface_two = None - if interface_two_element is not None: - interface_two = create_interface_data(interface_two_element) + node1_id = get_int(link_element, "node1") + if node1_id is None: + node1_id = get_int(link_element, "node_one") + node2_id = get_int(link_element, "node2") + if node2_id is None: + node2_id = get_int(link_element, "node_two") + node_set = frozenset((node1_id, node2_id)) + + iface1_element = link_element.find("iface1") + if iface1_element is None: + iface1_element = link_element.find("interface_one") + iface1_data = None + if iface1_element is not None: + iface1_data = create_iface_data(iface1_element) + + iface2_element = link_element.find("iface2") + if iface2_element is None: + iface2_element = link_element.find("interface_two") + iface2_data = None + if iface2_element is not None: + iface2_data = create_iface_data(iface2_element) options_element = link_element.find("options") - link_options = LinkOptions() + options = LinkOptions() if options_element is not None: - link_options.bandwidth = get_int(options_element, "bandwidth") - link_options.burst = get_int(options_element, "burst") - link_options.delay = get_int(options_element, "delay") - link_options.dup = get_int(options_element, "dup") - link_options.mer = get_int(options_element, "mer") - link_options.mburst = get_int(options_element, "mburst") - link_options.jitter = get_int(options_element, "jitter") - link_options.key = get_int(options_element, "key") - link_options.per = get_float(options_element, "per") - link_options.unidirectional = get_int(options_element, "unidirectional") - link_options.session = options_element.get("session") - link_options.emulation_id = get_int(options_element, "emulation_id") - link_options.network_id = get_int(options_element, "network_id") - link_options.opaque = options_element.get("opaque") - link_options.gui_attributes = options_element.get("gui_attributes") - - if link_options.unidirectional == 1 and node_set in node_sets: - logging.info( - "updating link node_one(%s) node_two(%s)", node_one, node_two - ) + options.bandwidth = get_int(options_element, "bandwidth") + options.burst = get_int(options_element, "burst") + options.delay = get_int(options_element, "delay") + options.dup = get_int(options_element, "dup") + options.mer = get_int(options_element, "mer") + options.mburst = get_int(options_element, "mburst") + options.jitter = get_int(options_element, "jitter") + options.key = get_int(options_element, "key") + options.loss = get_float(options_element, "loss") + if options.loss is None: + options.loss = get_float(options_element, "per") + options.unidirectional = get_int(options_element, "unidirectional") + + if options.unidirectional == 1 and node_set in node_sets: + logging.info("updating link node1(%s) node2(%s)", node1_id, node2_id) self.session.update_link( - node_one, node_two, interface_one.id, interface_two.id, link_options + node1_id, node2_id, iface1_data.id, iface2_data.id, options ) else: - logging.info( - "adding link node_one(%s) node_two(%s)", node_one, node_two - ) + logging.info("adding link node1(%s) node2(%s)", node1_id, node2_id) self.session.add_link( - node_one, node_two, interface_one, interface_two, link_options + node1_id, node2_id, iface1_data, iface2_data, options ) node_sets.add(node_set) diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 5f340b694..c062a1d2a 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -6,10 +6,9 @@ from lxml import etree from core import utils -from core.constants import IP_BIN from core.emane.nodes import EmaneNet +from core.executables import IP from core.nodes.base import CoreNodeBase, NodeBase -from core.nodes.interface import CoreInterface if TYPE_CHECKING: from core.emulator.session import Session @@ -24,25 +23,24 @@ def add_address( parent_element: etree.Element, address_type: str, address: str, - interface_name: str = None, + iface_name: str = None, ) -> None: address_element = etree.SubElement(parent_element, "address", type=address_type) address_element.text = address - if interface_name is not None: - address_element.set("iface", interface_name) + if iface_name is not None: + address_element.set("iface", iface_name) def add_mapping(parent_element: etree.Element, maptype: str, mapref: str) -> None: etree.SubElement(parent_element, "mapping", type=maptype, ref=mapref) -def add_emane_interface( +def add_emane_iface( host_element: etree.Element, - netif: CoreInterface, + nem_id: int, platform_name: str = "p1", transport_name: str = "t1", ) -> etree.Element: - nem_id = netif.net.nemidmap[netif] host_id = host_element.get("id") # platform data @@ -83,16 +81,16 @@ def get_address_type(address: str) -> str: def get_ipv4_addresses(hostname: str) -> List[Tuple[str, str]]: if hostname == "localhost": addresses = [] - args = f"{IP_BIN} -o -f inet address show" + args = f"{IP} -o -f inet address show" output = utils.cmd(args) for line in output.split(os.linesep): split = line.split() if not split: continue - interface_name = split[1] + iface_name = split[1] address = split[3] if not address.startswith("127."): - addresses.append((interface_name, address)) + addresses.append((iface_name, address)) return addresses else: # TODO: handle other hosts @@ -101,9 +99,9 @@ def get_ipv4_addresses(hostname: str) -> List[Tuple[str, str]]: class CoreXmlDeployment: def __init__(self, session: "Session", scenario: etree.Element) -> None: - self.session = session - self.scenario = scenario - self.root = etree.SubElement( + self.session: "Session" = session + self.scenario: etree.Element = scenario + self.root: etree.SubElement = etree.SubElement( scenario, "container", id="TestBed", name="TestBed" ) self.add_deployment() @@ -112,11 +110,11 @@ def find_device(self, name: str) -> etree.Element: device = self.scenario.find(f"devices/device[@name='{name}']") return device - def find_interface(self, device: NodeBase, name: str) -> etree.Element: - interface = self.scenario.find( + def find_iface(self, device: NodeBase, name: str) -> etree.Element: + iface = self.scenario.find( f"devices/device[@name='{device.name}']/interfaces/interface[@name='{name}']" ) - return interface + return iface def add_deployment(self) -> None: physical_host = self.add_physical_host(socket.gethostname()) @@ -136,8 +134,8 @@ def add_physical_host(self, name: str) -> etree.Element: add_type(host_element, "physical") # add ipv4 addresses - for interface_name, address in get_ipv4_addresses("localhost"): - add_address(host_element, "IPv4", address, interface_name) + for iface_name, address in get_ipv4_addresses("localhost"): + add_address(host_element, "IPv4", address, iface_name) return host_element @@ -155,15 +153,17 @@ def add_virtual_host(self, physical_host: etree.Element, node: NodeBase) -> None # add host type add_type(host_element, "virtual") - for netif in node.netifs(): + for iface in node.get_ifaces(): emane_element = None - if isinstance(netif.net, EmaneNet): - emane_element = add_emane_interface(host_element, netif) + if isinstance(iface.net, EmaneNet): + nem_id = self.session.emane.get_nem_id(iface) + emane_element = add_emane_iface(host_element, nem_id) parent_element = host_element if emane_element is not None: parent_element = emane_element - for address in netif.addrlist: + for ip in iface.ips(): + address = str(ip.ip) address_type = get_address_type(address) - add_address(parent_element, address_type, address, netif.name) + add_address(parent_element, address_type, address, iface.name) diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 2589edd9a..88aeaa97b 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -9,7 +9,7 @@ from core.config import Configuration from core.emane.nodes import EmaneNet from core.emulator.distributed import DistributedServer -from core.emulator.enumerations import TransportType +from core.errors import CoreError from core.nodes.interface import CoreInterface from core.nodes.network import CtrlNet from core.xml import corexml @@ -18,7 +18,7 @@ from core.emane.emanemanager import EmaneManager from core.emane.emanemodel import EmaneModel -_hwaddr_prefix = "02:02" +_MAC_PREFIX = "02:02" def is_external(config: Dict[str, str]) -> bool: @@ -40,15 +40,11 @@ def _value_to_params(value: str) -> Optional[Tuple[str]]: """ try: values = utils.make_tuple_fromstr(value, str) - if not hasattr(values, "__iter__"): return None - if len(values) < 2: return None - return values - except SyntaxError: logging.exception("error in value string to param list") return None @@ -66,16 +62,15 @@ def create_file( :param xml_element: root element to write to file :param doc_name: name to use in the emane doctype :param file_path: file path to write xml file to - :param server: remote server node - will run on, default is None for localhost + :param server: remote server to create file on :return: nothing """ doctype = ( f'' ) - if server is not None: + if server: temp = NamedTemporaryFile(delete=False) - create_file(xml_element, doc_name, temp.name) + corexml.write_xml_file(xml_element, temp.name, doctype=doctype) temp.close() server.remote_put(temp.name, file_path) os.unlink(temp.name) @@ -83,6 +78,26 @@ def create_file( corexml.write_xml_file(xml_element, file_path, doctype=doctype) +def create_iface_file( + iface: CoreInterface, xml_element: etree.Element, doc_name: str, file_name: str +) -> None: + """ + Create emane xml for an interface. + + :param iface: interface running emane + :param xml_element: root element to write to file + :param doc_name: name to use in the emane doctype + :param file_name: name of xml file + :return: + """ + node = iface.node + if iface.is_raw(): + file_path = os.path.join(node.session.session_dir, file_name) + else: + file_path = os.path.join(node.nodedir, file_name) + create_file(xml_element, doc_name, file_path, node.server) + + def add_param(xml_element: etree.Element, name: str, value: str) -> None: """ Add emane configuration parameter to xml element. @@ -127,13 +142,13 @@ def add_configurations( add_param(xml_element, name, value) -def build_node_platform_xml( +def build_platform_xml( emane_manager: "EmaneManager", control_net: CtrlNet, - node: EmaneNet, + emane_net: EmaneNet, + iface: CoreInterface, nem_id: int, - platform_xmls: Dict[str, etree.Element], -) -> int: +) -> None: """ Create platform xml for a specific node. @@ -141,313 +156,149 @@ def build_node_platform_xml( configurations :param control_net: control net node for this emane network - :param node: node to write platform xml for - :param nem_id: nem id to use for interfaces for this node - :param platform_xmls: stores platform xml elements to append nem entries to + :param emane_net: emane network associated with interface + :param iface: interface running emane + :param nem_id: nem id to use for this interface :return: the next nem id that can be used for creating platform xml files """ - logging.debug( - "building emane platform xml for node(%s) nem_id(%s): %s", - node, - nem_id, - node.name, + # build nem xml + nem_definition = nem_file_name(iface) + nem_element = etree.Element( + "nem", id=str(nem_id), name=iface.localname, definition=nem_definition ) - nem_entries = {} - - if node.model is None: - logging.warning("warning: EMANE network %s has no associated model", node.name) - return nem_id - for netif in node.netifs(): - logging.debug( - "building platform xml for interface(%s) nem_id(%s)", netif.name, nem_id - ) - # build nem xml - nem_definition = nem_file_name(node.model, netif) - nem_element = etree.Element( - "nem", id=str(nem_id), name=netif.localname, definition=nem_definition + # check if this is an external transport, get default config if an interface + # specific one does not exist + config = emane_manager.get_iface_config(emane_net, iface) + if is_external(config): + nem_element.set("transport", "external") + platform_endpoint = "platformendpoint" + add_param(nem_element, platform_endpoint, config[platform_endpoint]) + transport_endpoint = "transportendpoint" + add_param(nem_element, transport_endpoint, config[transport_endpoint]) + else: + transport_name = transport_file_name(iface) + transport_element = etree.SubElement( + nem_element, "transport", definition=transport_name ) + add_param(transport_element, "device", iface.name) - # check if this is an external transport, get default config if an interface - # specific one does not exist - config = emane_manager.getifcconfig(node.model.id, netif, node.model.name) - - if is_external(config): - nem_element.set("transport", "external") - platform_endpoint = "platformendpoint" - add_param(nem_element, platform_endpoint, config[platform_endpoint]) - transport_endpoint = "transportendpoint" - add_param(nem_element, transport_endpoint, config[transport_endpoint]) - else: - # build transport xml - transport_type = netif.transport_type - if not transport_type: - logging.info("warning: %s interface type unsupported!", netif.name) - transport_type = TransportType.RAW - transport_file = transport_file_name(node.id, transport_type) - transport_element = etree.SubElement( - nem_element, "transport", definition=transport_file - ) - - # add transport parameter - add_param(transport_element, "device", netif.name) - - # add nem entry - nem_entries[netif] = nem_element - - # merging code - key = netif.node.id - if netif.transport_type == TransportType.RAW: - key = "host" - otadev = control_net.brname - eventdev = control_net.brname + transport_configs = {"otamanagerdevice", "eventservicedevice"} + platform_element = etree.Element("platform") + for configuration in emane_manager.emane_config.emulator_config: + name = configuration.id + if iface.is_raw() and name in transport_configs: + value = control_net.brname else: - otadev = None - eventdev = None - - platform_element = platform_xmls.get(key) - if platform_element is None: - platform_element = etree.Element("platform") - - if otadev: - emane_manager.set_config("otamanagerdevice", otadev) - - if eventdev: - emane_manager.set_config("eventservicedevice", eventdev) - - # append all platform options (except starting id) to doc - for configuration in emane_manager.emane_config.emulator_config: - name = configuration.id - if name == "platform_id_start": - continue - - value = emane_manager.get_config(name) - add_param(platform_element, name, value) - - # add platform xml - platform_xmls[key] = platform_element - - platform_element.append(nem_element) - - node.setnemid(netif, nem_id) - macstr = _hwaddr_prefix + ":00:00:" - macstr += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" - netif.sethwaddr(macstr) - - # increment nem id - nem_id += 1 + value = emane_manager.get_config(name) + add_param(platform_element, name, value) + platform_element.append(nem_element) + mac = _MAC_PREFIX + ":00:00:" + mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" + iface.set_mac(mac) doc_name = "platform" - for key in sorted(platform_xmls.keys()): - platform_element = platform_xmls[key] - if key == "host": - file_name = "platform.xml" - file_path = os.path.join(emane_manager.session.session_dir, file_name) - create_file(platform_element, doc_name, file_path) - else: - file_name = f"platform{key}.xml" - file_path = os.path.join(emane_manager.session.session_dir, file_name) - linked_node = emane_manager.session.nodes[key] - create_file(platform_element, doc_name, file_path, linked_node.server) - - return nem_id - - -def build_xml_files(emane_manager: "EmaneManager", node: EmaneNet) -> None: - """ - Generate emane xml files required for node. - - :param emane_manager: emane manager with emane - configurations - :param node: node to write platform xml for - :return: nothing - """ - logging.debug("building all emane xml for node(%s): %s", node, node.name) - if node.model is None: - return - - # get model configurations - config = emane_manager.get_configs(node.model.id, node.model.name) - if not config: - return - - # build XML for overall network EMANE configs - node.model.build_xml_files(config) - - # build XML for specific interface (NEM) configs - need_virtual = False - need_raw = False - vtype = TransportType.VIRTUAL - rtype = TransportType.RAW - - for netif in node.netifs(): - # check for interface specific emane configuration and write xml files - config = emane_manager.getifcconfig(node.model.id, netif, node.model.name) - if config: - node.model.build_xml_files(config, netif) - - # check transport type needed for interface - if netif.transport_type == TransportType.VIRTUAL: - need_virtual = True - vtype = netif.transport_type - else: - need_raw = True - rtype = netif.transport_type + file_name = f"{iface.name}-platform.xml" + create_iface_file(iface, platform_element, doc_name, file_name) - if need_virtual: - build_transport_xml(emane_manager, node, vtype) - if need_raw: - build_transport_xml(emane_manager, node, rtype) - - -def build_transport_xml( - emane_manager: "EmaneManager", node: EmaneNet, transport_type: TransportType -) -> None: +def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None: """ Build transport xml file for node and transport type. - :param emane_manager: emane manager with emane - configurations - :param node: node to write platform xml for - :param transport_type: transport type to build xml for + :param iface: interface to build transport xml for + :param config: all current configuration values :return: nothing """ + transport_type = iface.transport_type transport_element = etree.Element( "transport", name=f"{transport_type.value.capitalize()} Transport", library=f"trans{transport_type.value.lower()}", ) - - # add bitrate add_param(transport_element, "bitrate", "0") # get emane model cnfiguration - config = emane_manager.get_configs(node.id, node.model.name) flowcontrol = config.get("flowcontrolenable", "0") == "1" - - if transport_type == TransportType.VIRTUAL: + if iface.is_virtual(): device_path = "/dev/net/tun_flowctl" if not os.path.exists(device_path): device_path = "/dev/net/tun" add_param(transport_element, "devicepath", device_path) - if flowcontrol: add_param(transport_element, "flowcontrolenable", "on") - doc_name = "transport" - file_name = transport_file_name(node.id, transport_type) - file_path = os.path.join(emane_manager.session.session_dir, file_name) - create_file(transport_element, doc_name, file_path) - emane_manager.session.distributed.execute( - lambda x: create_file(transport_element, doc_name, file_path, x) - ) + transport_name = transport_file_name(iface) + create_iface_file(iface, transport_element, doc_name, transport_name) def create_phy_xml( - emane_model: "EmaneModel", - config: Dict[str, str], - file_path: str, - server: DistributedServer, + emane_model: "EmaneModel", iface: CoreInterface, config: Dict[str, str] ) -> None: """ Create the phy xml document. :param emane_model: emane model to create xml + :param iface: interface to create xml for :param config: all current configuration values - :param file_path: path to write file to - :param server: remote server node - will run on, default is None for localhost :return: nothing """ phy_element = etree.Element("phy", name=f"{emane_model.name} PHY") if emane_model.phy_library: phy_element.set("library", emane_model.phy_library) - add_configurations( phy_element, emane_model.phy_config, config, emane_model.config_ignore ) - create_file(phy_element, "phy", file_path) - if server is not None: - create_file(phy_element, "phy", file_path, server) - else: - create_file(phy_element, "phy", file_path) - emane_model.session.distributed.execute( - lambda x: create_file(phy_element, "phy", file_path, x) - ) + file_name = phy_file_name(iface) + create_iface_file(iface, phy_element, "phy", file_name) def create_mac_xml( - emane_model: "EmaneModel", - config: Dict[str, str], - file_path: str, - server: DistributedServer, + emane_model: "EmaneModel", iface: CoreInterface, config: Dict[str, str] ) -> None: """ Create the mac xml document. :param emane_model: emane model to create xml + :param iface: interface to create xml for :param config: all current configuration values - :param file_path: path to write file to - :param server: remote server node - will run on, default is None for localhost :return: nothing """ if not emane_model.mac_library: - raise ValueError("must define emane model library") - + raise CoreError("must define emane model library") mac_element = etree.Element( "mac", name=f"{emane_model.name} MAC", library=emane_model.mac_library ) add_configurations( mac_element, emane_model.mac_config, config, emane_model.config_ignore ) - create_file(mac_element, "mac", file_path) - if server is not None: - create_file(mac_element, "mac", file_path, server) - else: - create_file(mac_element, "mac", file_path) - emane_model.session.distributed.execute( - lambda x: create_file(mac_element, "mac", file_path, x) - ) + file_name = mac_file_name(iface) + create_iface_file(iface, mac_element, "mac", file_name) def create_nem_xml( - emane_model: "EmaneModel", - config: Dict[str, str], - nem_file: str, - transport_definition: str, - mac_definition: str, - phy_definition: str, - server: DistributedServer, + emane_model: "EmaneModel", iface: CoreInterface, config: Dict[str, str] ) -> None: """ Create the nem xml document. :param emane_model: emane model to create xml + :param iface: interface to create xml for :param config: all current configuration values - :param nem_file: nem file path to write - :param transport_definition: transport file definition path - :param mac_definition: mac file definition path - :param phy_definition: phy file definition path - :param server: remote server node - will run on, default is None for localhost :return: nothing """ nem_element = etree.Element("nem", name=f"{emane_model.name} NEM") if is_external(config): nem_element.set("type", "unstructured") else: - etree.SubElement(nem_element, "transport", definition=transport_definition) - etree.SubElement(nem_element, "mac", definition=mac_definition) - etree.SubElement(nem_element, "phy", definition=phy_definition) - if server is not None: - create_file(nem_element, "nem", nem_file, server) - else: - create_file(nem_element, "nem", nem_file) - emane_model.session.distributed.execute( - lambda x: create_file(nem_element, "nem", nem_file, x) - ) + transport_name = transport_file_name(iface) + etree.SubElement(nem_element, "transport", definition=transport_name) + mac_name = mac_file_name(iface) + etree.SubElement(nem_element, "mac", definition=mac_name) + phy_name = phy_file_name(iface) + etree.SubElement(nem_element, "phy", definition=phy_name) + nem_name = nem_file_name(iface) + create_iface_file(iface, nem_element, "nem", nem_name) def create_event_service_xml( @@ -483,81 +334,52 @@ def create_event_service_xml( create_file(event_element, "emaneeventmsgsvc", file_path, server) -def transport_file_name(node_id: int, transport_type: TransportType) -> str: +def transport_file_name(iface: CoreInterface) -> str: """ Create name for a transport xml file. - :param node_id: node id to generate transport file name for - :param transport_type: transport type to generate transport file - :return: - """ - return f"n{node_id}trans{transport_type.value}.xml" - - -def _basename(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: - """ - Create name that is leveraged for configuration file creation. - - :param emane_model: emane model to create name for - :param interface: interface for this model - :return: basename used for file creation + :param iface: interface running emane + :return: transport xml file name """ - name = f"n{emane_model.id}" - - if interface: - node_id = interface.node.id - if emane_model.session.emane.getifcconfig(node_id, interface, emane_model.name): - name = interface.localname.replace(".", "_") - - return f"{name}{emane_model.name}" + return f"{iface.name}-trans-{iface.transport_type.value}.xml" -def nem_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: +def nem_file_name(iface: CoreInterface) -> str: """ - Return the string name for the NEM XML file, e.g. "n3rfpipenem.xml" + Return the string name for the NEM XML file, e.g. "eth0-nem.xml" - :param emane_model: emane model to create file - :param interface: interface for this model - :return: nem xml filename + :param iface: interface running emane + :return: nem xm file name """ - basename = _basename(emane_model, interface) - append = "" - if interface and interface.transport_type == TransportType.RAW: - append = "_raw" - return f"{basename}nem{append}.xml" + append = "-raw" if iface.is_raw() else "" + return f"{iface.name}-nem{append}.xml" -def shim_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: +def shim_file_name(iface: CoreInterface = None) -> str: """ - Return the string name for the SHIM XML file, e.g. "commeffectshim.xml" + Return the string name for the SHIM XML file, e.g. "eth0-shim.xml" - :param emane_model: emane model to create file - :param interface: interface for this model - :return: shim xml filename + :param iface: interface running emane + :return: shim xml file name """ - name = _basename(emane_model, interface) - return f"{name}shim.xml" + return f"{iface.name}-shim.xml" -def mac_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: +def mac_file_name(iface: CoreInterface) -> str: """ - Return the string name for the MAC XML file, e.g. "n3rfpipemac.xml" + Return the string name for the MAC XML file, e.g. "eth0-mac.xml" - :param emane_model: emane model to create file - :param interface: interface for this model - :return: mac xml filename + :param iface: interface running emane + :return: mac xml file name """ - name = _basename(emane_model, interface) - return f"{name}mac.xml" + return f"{iface.name}-mac.xml" -def phy_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: +def phy_file_name(iface: CoreInterface) -> str: """ - Return the string name for the PHY XML file, e.g. "n3rfpipephy.xml" + Return the string name for the PHY XML file, e.g. "eth0-phy.xml" - :param emane_model: emane model to create file - :param interface: interface for this model - :return: phy xml filename + :param iface: interface running emane + :return: phy xml file name """ - name = _basename(emane_model, interface) - return f"{name}phy.xml" + return f"{iface.name}-phy.xml" diff --git a/daemon/data/core.conf b/daemon/data/core.conf index 5ff0be7ff..20ee5d1f1 100644 --- a/daemon/data/core.conf +++ b/daemon/data/core.conf @@ -13,7 +13,7 @@ frr_sbin_search = "/usr/local/sbin /usr/sbin /usr/lib/frr" # this may be a comma-separated list, and directory names should be unique # and not named 'services' #custom_services_dir = /home/username/.core/myservices -#custom_config_services_dir = /home/username/.coretk/custom_services +#custom_config_services_dir = /home/username/.coregui/custom_services # uncomment to establish a standalone control backchannel for accessing nodes # (overriden by the session option of the same name) diff --git a/daemon/examples/configservices/testing.py b/daemon/examples/configservices/testing.py index bc67ff462..9706f2c93 100644 --- a/daemon/examples/configservices/testing.py +++ b/daemon/examples/configservices/testing.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.network import SwitchNode @@ -11,7 +11,7 @@ # setup basic network prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") - options = NodeOptions(model="nothing") + options = NodeOptions(model=None) coreemu = CoreEmu() session = coreemu.create_session() session.set_state(EventTypes.CONFIGURATION_STATE) @@ -19,14 +19,14 @@ # node one options.config_services = ["DefaultRoute", "IPForward"] - node_one = session.add_node(CoreNode, options=options) - interface = prefixes.create_interface(node_one) - session.add_link(node_one.id, switch.id, interface_one=interface) + node1 = session.add_node(CoreNode, options=options) + interface = prefixes.create_iface(node1) + session.add_link(node1.id, switch.id, iface1_data=interface) # node two - node_two = session.add_node(CoreNode, options=options) - interface = prefixes.create_interface(node_two) - session.add_link(node_two.id, switch.id, interface_one=interface) + node2 = session.add_node(CoreNode, options=options) + interface = prefixes.create_iface(node2) + session.add_link(node2.id, switch.id, iface1_data=interface) # start session and run services session.instantiate() diff --git a/daemon/examples/docker/README.md b/daemon/examples/docker/README.md index 3c2b13722..17c6cb90d 100644 --- a/daemon/examples/docker/README.md +++ b/daemon/examples/docker/README.md @@ -44,3 +44,18 @@ newgrp docker This directory provides a few small examples creating Docker nodes and linking them to themselves or with standard CORE nodes. + +Images used by nodes need to have networking tools installed for CORE to automate +setup and configuration of the container. + +Example Dockerfile: +``` +FROM ubuntu:latest +RUN apt-get update +RUN apt-get install -y iproute2 ethtool +``` + +Build image: +```shell +sudo docker build -t . +``` diff --git a/daemon/examples/docker/docker2core.py b/daemon/examples/docker/docker2core.py index 1211a16f9..ae7dae797 100644 --- a/daemon/examples/docker/docker2core.py +++ b/daemon/examples/docker/docker2core.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.docker import DockerNode @@ -17,15 +17,15 @@ options = NodeOptions(model=None, image="ubuntu") # create node one - node_one = session.add_node(DockerNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(DockerNode, options=options) + interface1_data = prefixes.create_iface(node1) # create node two - node_two = session.add_node(CoreNode) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(CoreNode) + interface2_data = prefixes.create_iface(node2) # add link - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session.instantiate() diff --git a/daemon/examples/docker/docker2docker.py b/daemon/examples/docker/docker2docker.py index 9e1ae11ff..308fd00f3 100644 --- a/daemon/examples/docker/docker2docker.py +++ b/daemon/examples/docker/docker2docker.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.docker import DockerNode @@ -18,15 +18,15 @@ options = NodeOptions(model=None, image="ubuntu") # create node one - node_one = session.add_node(DockerNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(DockerNode, options=options) + interface1_data = prefixes.create_iface(node1) # create node two - node_two = session.add_node(DockerNode, options=options) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(DockerNode, options=options) + interface2_data = prefixes.create_iface(node2) # add link - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session.instantiate() diff --git a/daemon/examples/docker/switch.py b/daemon/examples/docker/switch.py index 74d58fe02..fa9e4e40e 100644 --- a/daemon/examples/docker/switch.py +++ b/daemon/examples/docker/switch.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.docker import DockerNode @@ -22,20 +22,20 @@ switch = session.add_node(SwitchNode) # node one - node_one = session.add_node(DockerNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(DockerNode, options=options) + interface1_data = prefixes.create_iface(node1) # node two - node_two = session.add_node(DockerNode, options=options) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(DockerNode, options=options) + interface2_data = prefixes.create_iface(node2) # node three node_three = session.add_node(CoreNode) - interface_three = prefixes.create_interface(node_three) + interface_three = prefixes.create_iface(node_three) # add links - session.add_link(node_one.id, switch.id, interface_one) - session.add_link(node_two.id, switch.id, interface_two) + session.add_link(node1.id, switch.id, interface1_data) + session.add_link(node2.id, switch.id, interface2_data) session.add_link(node_three.id, switch.id, interface_three) # instantiate diff --git a/daemon/examples/grpc/distributed_switch.py b/daemon/examples/grpc/distributed_switch.py index 0477efdd0..0d781c19f 100644 --- a/daemon/examples/grpc/distributed_switch.py +++ b/daemon/examples/grpc/distributed_switch.py @@ -44,11 +44,11 @@ def main(args): node = Node(position=position) response = core.add_node(session_id, node) logging.info("created node one: %s", response) - node_one_id = response.node_id + node1_id = response.node_id # create link - interface_one = interface_helper.create_interface(node_one_id, 0) - response = core.add_link(session_id, node_one_id, switch_id, interface_one) + interface1 = interface_helper.create_iface(node1_id, 0) + response = core.add_link(session_id, node1_id, switch_id, interface1) logging.info("created link from node one to switch: %s", response) # create node two @@ -56,11 +56,11 @@ def main(args): node = Node(position=position, server=server_name) response = core.add_node(session_id, node) logging.info("created node two: %s", response) - node_two_id = response.node_id + node2_id = response.node_id # create link - interface_one = interface_helper.create_interface(node_two_id, 0) - response = core.add_link(session_id, node_two_id, switch_id, interface_one) + interface1 = interface_helper.create_iface(node2_id, 0) + response = core.add_link(session_id, node2_id, switch_id, interface1) logging.info("created link from node two to switch: %s", response) # change session state diff --git a/daemon/examples/grpc/emane80211.py b/daemon/examples/grpc/emane80211.py index 5656268c2..b8036db07 100644 --- a/daemon/examples/grpc/emane80211.py +++ b/daemon/examples/grpc/emane80211.py @@ -57,11 +57,11 @@ def main(): node2_id = response.node_id # links nodes to switch - interface_one = interface_helper.create_interface(node1_id, 0) - response = core.add_link(session_id, node1_id, emane_id, interface_one) + interface1 = interface_helper.create_iface(node1_id, 0) + response = core.add_link(session_id, node1_id, emane_id, interface1) logging.info("created link: %s", response) - interface_one = interface_helper.create_interface(node2_id, 0) - response = core.add_link(session_id, node2_id, emane_id, interface_one) + interface1 = interface_helper.create_iface(node2_id, 0) + response = core.add_link(session_id, node2_id, emane_id, interface1) logging.info("created link: %s", response) # change session state diff --git a/daemon/examples/grpc/switch.py b/daemon/examples/grpc/switch.py index 3ab0e0ba4..79a4e6216 100644 --- a/daemon/examples/grpc/switch.py +++ b/daemon/examples/grpc/switch.py @@ -40,24 +40,24 @@ def main(): # create node one position = Position(x=100, y=100) - node1 = Node(type=NodeType.DEFAULT, position=position) + node1 = Node(type=NodeType.DEFAULT, position=position, model="PC") response = core.add_node(session_id, node1) logging.info("created node: %s", response) node1_id = response.node_id # create node two position = Position(x=300, y=100) - node2 = Node(type=NodeType.DEFAULT, position=position) + node2 = Node(type=NodeType.DEFAULT, position=position, model="PC") response = core.add_node(session_id, node2) logging.info("created node: %s", response) node2_id = response.node_id # links nodes to switch - interface_one = interface_helper.create_interface(node1_id, 0) - response = core.add_link(session_id, node1_id, switch_id, interface_one) + interface1 = interface_helper.create_iface(node1_id, 0) + response = core.add_link(session_id, node1_id, switch_id, interface1) logging.info("created link: %s", response) - interface_one = interface_helper.create_interface(node2_id, 0) - response = core.add_link(session_id, node2_id, switch_id, interface_one) + interface1 = interface_helper.create_iface(node2_id, 0) + response = core.add_link(session_id, node2_id, switch_id, interface1) logging.info("created link: %s", response) # change session state diff --git a/daemon/examples/grpc/wlan.py b/daemon/examples/grpc/wlan.py index 6118ae4ce..715d47066 100644 --- a/daemon/examples/grpc/wlan.py +++ b/daemon/examples/grpc/wlan.py @@ -65,11 +65,11 @@ def main(): node2_id = response.node_id # links nodes to switch - interface_one = interface_helper.create_interface(node1_id, 0) - response = core.add_link(session_id, node1_id, wlan_id, interface_one) + interface1 = interface_helper.create_iface(node1_id, 0) + response = core.add_link(session_id, node1_id, wlan_id, interface1) logging.info("created link: %s", response) - interface_one = interface_helper.create_interface(node2_id, 0) - response = core.add_link(session_id, node2_id, wlan_id, interface_one) + interface1 = interface_helper.create_iface(node2_id, 0) + response = core.add_link(session_id, node2_id, wlan_id, interface1) logging.info("created link: %s", response) # change session state diff --git a/daemon/examples/lxd/lxd2core.py b/daemon/examples/lxd/lxd2core.py index 1365bd4c8..b41520d89 100644 --- a/daemon/examples/lxd/lxd2core.py +++ b/daemon/examples/lxd/lxd2core.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.lxd import LxcNode @@ -17,15 +17,15 @@ options = NodeOptions(image="ubuntu") # create node one - node_one = session.add_node(LxcNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(LxcNode, options=options) + interface1_data = prefixes.create_iface(node1) # create node two - node_two = session.add_node(CoreNode) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(CoreNode) + interface2_data = prefixes.create_iface(node2) # add link - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session.instantiate() diff --git a/daemon/examples/lxd/lxd2lxd.py b/daemon/examples/lxd/lxd2lxd.py index 53a360e8f..3a55e2e1d 100644 --- a/daemon/examples/lxd/lxd2lxd.py +++ b/daemon/examples/lxd/lxd2lxd.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.lxd import LxcNode @@ -18,15 +18,15 @@ options = NodeOptions(image="ubuntu:18.04") # create node one - node_one = session.add_node(LxcNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(LxcNode, options=options) + interface1_data = prefixes.create_iface(node1) # create node two - node_two = session.add_node(LxcNode, options=options) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(LxcNode, options=options) + interface2_data = prefixes.create_iface(node2) # add link - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session.instantiate() diff --git a/daemon/examples/lxd/switch.py b/daemon/examples/lxd/switch.py index 3b6226e44..12767e71d 100644 --- a/daemon/examples/lxd/switch.py +++ b/daemon/examples/lxd/switch.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.lxd import LxcNode @@ -22,21 +22,21 @@ switch = session.add_node(SwitchNode) # node one - node_one = session.add_node(LxcNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(LxcNode, options=options) + interface1_data = prefixes.create_iface(node1) # node two - node_two = session.add_node(LxcNode, options=options) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(LxcNode, options=options) + interface2_data = prefixes.create_iface(node2) # node three - node_three = session.add_node(CoreNode) - interface_three = prefixes.create_interface(node_three) + node3 = session.add_node(CoreNode) + interface3_data = prefixes.create_iface(node3) # add links - session.add_link(node_one.id, switch.id, interface_one) - session.add_link(node_two.id, switch.id, interface_two) - session.add_link(node_three.id, switch.id, interface_three) + session.add_link(node1.id, switch.id, interface1_data) + session.add_link(node2.id, switch.id, interface2_data) + session.add_link(node3.id, switch.id, interface3_data) # instantiate session.instantiate() diff --git a/daemon/examples/myservices/sample.py b/daemon/examples/myservices/sample.py index 8c6dbe061..e0c9a232f 100644 --- a/daemon/examples/myservices/sample.py +++ b/daemon/examples/myservices/sample.py @@ -80,8 +80,8 @@ def generate_config(cls, node, filename): if filename == cls.configs[0]: cfg += "# auto-generated by MyService (sample.py)\n" - for ifc in node.netifs(): - cfg += f'echo "Node {node.name} has interface {ifc.name}"\n' + for iface in node.get_ifaces(): + cfg += f'echo "Node {node.name} has interface {iface.name}"\n' elif filename == cls.configs[1]: cfg += "echo hello" diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index 3248a8e3e..4421283fd 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -9,7 +9,7 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode @@ -52,17 +52,17 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) emane_net = session.add_node(EmaneNet) session.emane.set_model(emane_net, EmaneIeee80211abgModel) options.server = server_name - node_two = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # create node interfaces and link - interface_one = prefixes.create_interface(node_one) - interface_two = prefixes.create_interface(node_two) - session.add_link(node_one.id, emane_net.id, interface_one=interface_one) - session.add_link(node_two.id, emane_net.id, interface_one=interface_two) + interface1_data = prefixes.create_iface(node1) + interface2_data = prefixes.create_iface(node2) + session.add_link(node1.id, emane_net.id, iface1_data=interface1_data) + session.add_link(node2.id, emane_net.id, iface1_data=interface2_data) # instantiate session session.instantiate() diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py index de919012a..26f7caa6c 100644 --- a/daemon/examples/python/distributed_lxd.py +++ b/daemon/examples/python/distributed_lxd.py @@ -7,7 +7,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.lxd import LxcNode @@ -43,14 +43,14 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions(image="ubuntu:18.04") - node_one = session.add_node(LxcNode, options=options) + node1 = session.add_node(LxcNode, options=options) options.server = server_name - node_two = session.add_node(LxcNode, options=options) + node2 = session.add_node(LxcNode, options=options) # create node interfaces and link - interface_one = prefixes.create_interface(node_one) - interface_two = prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + interface1_data = prefixes.create_iface(node1) + interface2_data = prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session session.instantiate() diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 26531399d..fe714e1d3 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -7,7 +7,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode @@ -43,14 +43,14 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions() - node_one = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) options.server = server_name - node_two = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # create node interfaces and link - interface_one = prefixes.create_interface(node_one) - interface_two = prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + interface1_data = prefixes.create_iface(node1) + interface2_data = prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session session.instantiate() diff --git a/daemon/examples/python/distributed_switch.py b/daemon/examples/python/distributed_switch.py index c52c1cc1a..35de1cada 100644 --- a/daemon/examples/python/distributed_switch.py +++ b/daemon/examples/python/distributed_switch.py @@ -7,7 +7,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.network import SwitchNode @@ -45,17 +45,17 @@ def main(args): session.set_state(EventTypes.CONFIGURATION_STATE) # create local node, switch, and remote nodes - node_one = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) switch = session.add_node(SwitchNode) options = NodeOptions() options.server = server_name - node_two = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # create node interfaces and link - interface_one = prefixes.create_interface(node_one) - interface_two = prefixes.create_interface(node_two) - session.add_link(node_one.id, switch.id, interface_one=interface_one) - session.add_link(node_two.id, switch.id, interface_one=interface_two) + interface1_data = prefixes.create_iface(node1) + interface2_data = prefixes.create_iface(node2) + session.add_link(node1.id, switch.id, iface1_data=interface1_data) + session.add_link(node2.id, switch.id, iface1_data=interface2_data) # instantiate session session.instantiate() diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index da93026bc..48133ce0b 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -10,7 +10,7 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode @@ -42,8 +42,8 @@ def main(): for i in range(NODES): node = session.add_node(CoreNode, options=options) node.setposition(x=150 * (i + 1), y=150) - interface = prefixes.create_interface(node) - session.add_link(node.id, emane_network.id, interface_one=interface) + interface = prefixes.create_iface(node) + session.add_link(node.id, emane_network.id, iface1_data=interface) # instantiate session session.instantiate() @@ -55,7 +55,7 @@ def main(): # get nodes to run example first_node = session.get_node(1, CoreNode) last_node = session.get_node(NODES, CoreNode) - address = prefixes.ip4_address(first_node) + address = prefixes.ip4_address(first_node.id) logging.info("node %s pinging %s", last_node.name, address) output = last_node.cmd(f"ping -c 3 {address}") logging.info(output) diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index 9475fc471..c5e62e4ae 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -6,7 +6,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes +from core.emulator.data import IpPrefixes from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.network import SwitchNode @@ -31,8 +31,8 @@ def main(): # create nodes for _ in range(NODES): node = session.add_node(CoreNode) - interface = prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface_one=interface) + interface = prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface1_data=interface) # instantiate session session.instantiate() @@ -40,7 +40,7 @@ def main(): # get nodes to run example first_node = session.get_node(1, CoreNode) last_node = session.get_node(NODES, CoreNode) - address = prefixes.ip4_address(first_node) + address = prefixes.ip4_address(first_node.id) logging.info("node %s pinging %s", last_node.name, address) output = last_node.cmd(f"ping -c 3 {address}") logging.info(output) diff --git a/daemon/examples/python/switch_inject.py b/daemon/examples/python/switch_inject.py index 8c929e917..18a75a497 100644 --- a/daemon/examples/python/switch_inject.py +++ b/daemon/examples/python/switch_inject.py @@ -8,7 +8,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes +from core.emulator.data import IpPrefixes from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.network import SwitchNode @@ -33,8 +33,8 @@ def main(): # create nodes for _ in range(NODES): node = session.add_node(CoreNode) - interface = prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface_one=interface) + interface = prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface1_data=interface) # instantiate session session.instantiate() diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index b09ae5ce0..7c16bad84 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -6,7 +6,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.location.mobility import BasicRangeModel from core.nodes.base import CoreNode @@ -35,8 +35,8 @@ def main(): options.set_position(0, 0) for _ in range(NODES): node = session.add_node(CoreNode, options=options) - interface = prefixes.create_interface(node) - session.add_link(node.id, wlan.id, interface_one=interface) + interface = prefixes.create_iface(node) + session.add_link(node.id, wlan.id, iface1_data=interface) # instantiate session session.instantiate() @@ -44,7 +44,7 @@ def main(): # get nodes for example run first_node = session.get_node(1, CoreNode) last_node = session.get_node(NODES, CoreNode) - address = prefixes.ip4_address(first_node) + address = prefixes.ip4_address(first_node.id) logging.info("node %s pinging %s", last_node.name, address) output = last_node.cmd(f"ping -c 3 {address}") logging.info(output) diff --git a/daemon/poetry.lock b/daemon/poetry.lock new file mode 100644 index 000000000..9de19d13a --- /dev/null +++ b/daemon/poetry.lock @@ -0,0 +1,1085 @@ +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.4" + +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "main" +description = "Modern password hashing for your software and your servers" +name = "bcrypt" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.1.7" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)"] + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "19.3b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +toml = ">=0.9.4" + +[package.extras] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "main" +description = "Foreign Function Interface for Python calling C code." +name = "cffi" +optional = false +python-versions = "*" +version = "1.14.0" + +[package.dependencies] +pycparser = "*" + +[[package]] +category = "dev" +description = "Validate configuration and produce human readable error messages." +name = "cfgv" +optional = false +python-versions = ">=3.6" +version = "3.0.0" + +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "main" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +name = "cryptography" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "2.9.2" + +[package.dependencies] +cffi = ">=1.8,<1.11.3 || >1.11.3" +six = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +idna = ["idna (>=2.1)"] +pep8test = ["flake8", "flake8-import-order", "pep8-naming"] +test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] + +[[package]] +category = "main" +description = "A backport of the dataclasses module for Python 3.6" +marker = "python_version == \"3.6\"" +name = "dataclasses" +optional = false +python-versions = ">=3.6, <3.7" +version = "0.7" + +[[package]] +category = "dev" +description = "Distribution utilities" +name = "distlib" +optional = false +python-versions = "*" +version = "0.3.0" + +[[package]] +category = "main" +description = "High level SSH command execution" +name = "fabric" +optional = false +python-versions = "*" +version = "2.5.0" + +[package.dependencies] +invoke = ">=1.3,<2.0" +paramiko = ">=2.4" + +[package.extras] +pytest = ["mock (>=2.0.0,<3.0)", "pytest (>=3.2.5,<4.0)"] +testing = ["mock (>=2.0.0,<3.0)"] + +[[package]] +category = "dev" +description = "A platform independent file lock." +name = "filelock" +optional = false +python-versions = "*" +version = "3.0.12" + +[[package]] +category = "dev" +description = "the modular source code checker: pep8 pyflakes and co" +name = "flake8" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "3.8.2" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[[package]] +category = "main" +description = "HTTP/2-based RPC framework" +name = "grpcio" +optional = false +python-versions = "*" +version = "1.27.2" + +[package.dependencies] +six = ">=1.5.2" + +[[package]] +category = "dev" +description = "Protobuf code generator for gRPC" +name = "grpcio-tools" +optional = false +python-versions = "*" +version = "1.27.2" + +[package.dependencies] +grpcio = ">=1.27.2" +protobuf = ">=3.5.0.post1" + +[[package]] +category = "dev" +description = "File identification library for Python" +name = "identify" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "1.4.18" + +[package.extras] +license = ["editdistance"] + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.6.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + +[[package]] +category = "dev" +description = "Read resources from Python packages" +marker = "python_version < \"3.7\"" +name = "importlib-resources" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.5.0" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[package.dependencies.zipp] +python = "<3.8" +version = ">=0.4" + +[package.extras] +docs = ["sphinx", "rst.linker", "jaraco.packaging"] + +[[package]] +category = "main" +description = "Pythonic task execution" +name = "invoke" +optional = false +python-versions = "*" +version = "1.4.1" + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.3.21" + +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] + +[[package]] +category = "main" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +name = "lxml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +version = "4.5.1" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +category = "main" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +name = "mako" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.3" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "Rolling backport of unittest.mock for all Pythons" +name = "mock" +optional = false +python-versions = ">=3.6" +version = "4.0.2" + +[package.extras] +build = ["twine", "wheel", "blurb"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.3.0" + +[[package]] +category = "main" +description = "A network address manipulation library for Python" +name = "netaddr" +optional = false +python-versions = "*" +version = "0.7.19" + +[[package]] +category = "dev" +description = "Node.js virtual environment builder" +name = "nodeenv" +optional = false +python-versions = "*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "main" +description = "SSH2 protocol library" +name = "paramiko" +optional = false +python-versions = "*" +version = "2.7.1" + +[package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" + +[package.extras] +all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] + +[[package]] +category = "main" +description = "Python Imaging Library (Fork)" +name = "pillow" +optional = false +python-versions = ">=3.5" +version = "7.1.2" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +name = "pre-commit" +optional = false +python-versions = ">=3.6" +version = "2.1.1" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=15.2" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[package.dependencies.importlib-resources] +python = "<3.7" +version = "*" + +[[package]] +category = "main" +description = "Protocol Buffers" +name = "protobuf" +optional = false +python-versions = "*" +version = "3.12.2" + +[package.dependencies] +setuptools = "*" +six = ">=1.9" + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.1" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.6.0" + +[[package]] +category = "main" +description = "C parser in Python" +name = "pycparser" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.20" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.2.0" + +[[package]] +category = "main" +description = "Python binding to the Networking and Cryptography (NaCl) library" +name = "pynacl" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + +[package.dependencies] +cffi = ">=1.4.1" +six = "*" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)", "hypothesis (>=3.27.0)"] + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "main" +description = "Python interface to PROJ (cartographic projections and coordinate transformations library)" +name = "pyproj" +optional = false +python-versions = ">=3.5" +version = "2.6.1.post1" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.4.3" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +checkqa-mypy = ["mypy (v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "main" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = "*" +version = "5.3.1" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.1" + +[[package]] +category = "dev" +description = "Virtual Python Environment builder" +name = "virtualenv" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "20.0.21" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.0,<1" +filelock = ">=3.0.0,<4" +six = ">=1.9.0,<2" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12,<2" + +[package.dependencies.importlib-resources] +python = "<3.7" +version = ">=1.0,<2" + +[package.extras] +docs = ["sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2)"] +testing = ["pytest (>=4)", "coverage (>=5)", "coverage-enable-subprocess (>=1)", "pytest-xdist (>=1.31.0)", "pytest-mock (>=2)", "pytest-env (>=0.6.2)", "pytest-randomly (>=1)", "pytest-timeout", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + +[[package]] +category = "dev" +description = "Measures the displayed width of unicode strings in a terminal" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.2.3" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=3.6" +version = "3.1.0" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["jaraco.itertools", "func-timeout"] + +[metadata] +content-hash = "94df87a12a92ccb6512e4c30965e7ba1fe54b4fa3ff75827ca55b3de8472b30e" +python-versions = "^3.6" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +bcrypt = [ + {file = "bcrypt-3.1.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7"}, + {file = "bcrypt-3.1.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31"}, + {file = "bcrypt-3.1.7-cp27-cp27m-win32.whl", hash = "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161"}, + {file = "bcrypt-3.1.7-cp27-cp27m-win_amd64.whl", hash = "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e"}, + {file = "bcrypt-3.1.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0"}, + {file = "bcrypt-3.1.7-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052"}, + {file = "bcrypt-3.1.7-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105"}, + {file = "bcrypt-3.1.7-cp34-cp34m-win32.whl", hash = "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de"}, + {file = "bcrypt-3.1.7-cp34-cp34m-win_amd64.whl", hash = "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133"}, + {file = "bcrypt-3.1.7-cp35-cp35m-win32.whl", hash = "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5"}, + {file = "bcrypt-3.1.7-cp35-cp35m-win_amd64.whl", hash = "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09"}, + {file = "bcrypt-3.1.7-cp36-cp36m-win32.whl", hash = "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c"}, + {file = "bcrypt-3.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89"}, + {file = "bcrypt-3.1.7-cp37-cp37m-win32.whl", hash = "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294"}, + {file = "bcrypt-3.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"}, + {file = "bcrypt-3.1.7-cp38-cp38-win32.whl", hash = "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1"}, + {file = "bcrypt-3.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752"}, + {file = "bcrypt-3.1.7.tar.gz", hash = "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42"}, +] +black = [ + {file = "black-19.3b0-py36-none-any.whl", hash = "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf"}, + {file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"}, +] +cffi = [ + {file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"}, + {file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"}, + {file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"}, + {file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"}, + {file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"}, + {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"}, + {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"}, + {file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"}, + {file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"}, + {file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"}, + {file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"}, + {file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"}, + {file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"}, + {file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"}, + {file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"}, + {file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"}, + {file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"}, + {file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"}, + {file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"}, + {file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"}, + {file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"}, + {file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"}, + {file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"}, + {file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"}, + {file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"}, + {file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"}, + {file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"}, + {file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"}, +] +cfgv = [ + {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, + {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +cryptography = [ + {file = "cryptography-2.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e"}, + {file = "cryptography-2.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b"}, + {file = "cryptography-2.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365"}, + {file = "cryptography-2.9.2-cp27-cp27m-win32.whl", hash = "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0"}, + {file = "cryptography-2.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55"}, + {file = "cryptography-2.9.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270"}, + {file = "cryptography-2.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf"}, + {file = "cryptography-2.9.2-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d"}, + {file = "cryptography-2.9.2-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785"}, + {file = "cryptography-2.9.2-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b"}, + {file = "cryptography-2.9.2-cp35-cp35m-win32.whl", hash = "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae"}, + {file = "cryptography-2.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b"}, + {file = "cryptography-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6"}, + {file = "cryptography-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3"}, + {file = "cryptography-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b"}, + {file = "cryptography-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e"}, + {file = "cryptography-2.9.2-cp38-cp38-win32.whl", hash = "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0"}, + {file = "cryptography-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5"}, + {file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"}, +] +dataclasses = [ + {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, + {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, +] +distlib = [ + {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, +] +fabric = [ + {file = "fabric-2.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"}, + {file = "fabric-2.5.0.tar.gz", hash = "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +flake8 = [ + {file = "flake8-3.8.2-py2.py3-none-any.whl", hash = "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5"}, + {file = "flake8-3.8.2.tar.gz", hash = "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634"}, +] +grpcio = [ + {file = "grpcio-1.27.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca"}, + {file = "grpcio-1.27.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942"}, + {file = "grpcio-1.27.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5"}, + {file = "grpcio-1.27.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed"}, + {file = "grpcio-1.27.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47"}, + {file = "grpcio-1.27.2-cp27-cp27m-win32.whl", hash = "sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045"}, + {file = "grpcio-1.27.2-cp27-cp27m-win_amd64.whl", hash = "sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c"}, + {file = "grpcio-1.27.2-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7"}, + {file = "grpcio-1.27.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7"}, + {file = "grpcio-1.27.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3"}, + {file = "grpcio-1.27.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a"}, + {file = "grpcio-1.27.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866"}, + {file = "grpcio-1.27.2-cp35-cp35m-linux_armv7l.whl", hash = "sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345"}, + {file = "grpcio-1.27.2-cp35-cp35m-macosx_10_7_intel.whl", hash = "sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3"}, + {file = "grpcio-1.27.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d"}, + {file = "grpcio-1.27.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6"}, + {file = "grpcio-1.27.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844"}, + {file = "grpcio-1.27.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00"}, + {file = "grpcio-1.27.2-cp35-cp35m-win32.whl", hash = "sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6"}, + {file = "grpcio-1.27.2-cp35-cp35m-win_amd64.whl", hash = "sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8"}, + {file = "grpcio-1.27.2-cp36-cp36m-linux_armv7l.whl", hash = "sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6"}, + {file = "grpcio-1.27.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571"}, + {file = "grpcio-1.27.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8"}, + {file = "grpcio-1.27.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371"}, + {file = "grpcio-1.27.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6"}, + {file = "grpcio-1.27.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c"}, + {file = "grpcio-1.27.2-cp36-cp36m-win32.whl", hash = "sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54"}, + {file = "grpcio-1.27.2-cp36-cp36m-win_amd64.whl", hash = "sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49"}, + {file = "grpcio-1.27.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb"}, + {file = "grpcio-1.27.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6"}, + {file = "grpcio-1.27.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1"}, + {file = "grpcio-1.27.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386"}, + {file = "grpcio-1.27.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb"}, + {file = "grpcio-1.27.2-cp37-cp37m-win32.whl", hash = "sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8"}, + {file = "grpcio-1.27.2-cp37-cp37m-win_amd64.whl", hash = "sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e"}, + {file = "grpcio-1.27.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85"}, + {file = "grpcio-1.27.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b"}, + {file = "grpcio-1.27.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4"}, + {file = "grpcio-1.27.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a"}, + {file = "grpcio-1.27.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454"}, + {file = "grpcio-1.27.2-cp38-cp38-win32.whl", hash = "sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961"}, + {file = "grpcio-1.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1"}, + {file = "grpcio-1.27.2.tar.gz", hash = "sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e"}, +] +grpcio-tools = [ + {file = "grpcio-tools-1.27.2.tar.gz", hash = "sha256:845a51305af9fc7f9e2078edaec9a759153195f6cf1fbb12b1fa6f077e56b260"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:7a2d5fb558ac153a326e742ebfd7020eb781c43d3ffd920abd42b2e6c6fdfb37"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:99961156a36aae4a402d6b14c1e7efde642794b3ddbf32c51db0cb3a199e8b11"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:069826dd02ce1886444cf4519c4fe1b05ac9ef41491f26e97400640531db47f6"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:fae91f30dc050a8d0b32d20dc700e6092f0bd2138d83e9570fff3f0372c1b27e"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a14dc7a36c845991d908a7179502ca47bcba5ae1817c4426ce68cf2c97b20ad9"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-win32.whl", hash = "sha256:d1a5e5fa47ba9557a7d3b31605631805adc66cdba9d95b5d10dfc52cca1fed53"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-win_amd64.whl", hash = "sha256:7b54b283ec83190680903a9037376dc915e1f03852a2d574ba4d981b7a1fd3d0"}, + {file = "grpcio_tools-1.27.2-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:4698c6b6a57f73b14d91a542c69ff33a2da8729691b7060a5d7f6383624d045e"}, + {file = "grpcio_tools-1.27.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:87e8ca2c2d2d3e09b2a2bed5d740d7b3e64028dafb7d6be543b77eec85590736"}, + {file = "grpcio_tools-1.27.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bd7f59ff1252a3db8a143b13ea1c1e93d4b8cf4b852eb48b22ef1e6942f62a84"}, + {file = "grpcio_tools-1.27.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:a8f892378b0b02526635b806f59141abbb429d19bec56e869e04f396502c9651"}, + {file = "grpcio_tools-1.27.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:69c4a63919b9007e845d9f8980becd2f89d808a4a431ca32b9723ee37b521cb1"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-linux_armv7l.whl", hash = "sha256:dcbc06556f3713a9348c4fce02d05d91e678fc320fb2bcf0ddf8e4bb11d17867"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:16dc3fad04fe18d50777c56af7b2d9b9984cd1cfc71184646eb431196d1645c6"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1de5a273eaffeb3d126a63345e9e848ea7db740762f700eb8b5d84c5e3e7687d"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6016c07d6566e3109a3c032cf3861902d66501ecc08a5a84c47e43027302f367"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:915a695bc112517af48126ee0ecdb6aff05ed33f3eeef28f0d076f1f6b52ef5e"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:ea4b3ad696d976d5eac74ec8df9a2c692113e455446ee38d5b3bd87f8e034fa6"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-win32.whl", hash = "sha256:a140bf853edb2b5e8692fe94869e3e34077d7599170c113d07a58286c604f4fe"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-win_amd64.whl", hash = "sha256:77e25c241e33b75612f2aa62985f746c6f6803ec4e452da508bb7f8d90a69db4"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-linux_armv7l.whl", hash = "sha256:5fd7efc2fd3370bd2c72dc58f31a407a5dff5498befa145da211b2e8c6a52c63"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9ba88c2d99bcaf7b9cb720925e3290d73b2367d238c5779363fd5598b2dc98c7"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b56caecc16307b088a431a4038c3b3bb7d0e7f9988cbd0e9fa04ac937455ea38"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f8514453411d72cc3cf7d481f2b6057e5b7436736d0cd39ee2b2f72088bbf497"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c1bb8f47d58e9f7c4825abfe01e6b85eda53c8b31d2267ca4cddf3c4d0829b80"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e17b2e0936b04ced99769e26111e1e86ba81619d1b2691b1364f795e45560953"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-win32.whl", hash = "sha256:520b7dafddd0f82cb7e4f6e9c6ba1049aa804d0e207870def9fe7f94d1e14090"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ee50b0cf0d28748ef9f941894eb50fc464bd61b8e96aaf80c5056bea9b80d580"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:627c91923df75091d8c4d244af38d5ab7ed8d786d480751d6c2b9267fbb92fe0"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ef624b6134aef737b3daa4fb7e806cb8c5749efecd0b1fa9ce4f7e060c7a0221"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e6932518db389ede8bf06b4119bbd3e17f42d4626e72dec2b8955b20ec732cb6"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:43a1573400527a23e4174d88604fde7a9d9a69bf9473c21936b7f409858f8ebb"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:57f8b9e2c7f55cd45f6dd930d6de61deb42d3eb7f9788137fbc7155cf724132a"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-win32.whl", hash = "sha256:2ca280af2cae1a014a238057bd3c0a254527569a6a9169a01c07f0590081d530"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-win_amd64.whl", hash = "sha256:59fbeb5bb9a7b94eb61642ac2cee1db5233b8094ca76fc56d4e0c6c20b5dd85f"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:00c5080cfb197ed20ecf0d0ff2d07f1fc9c42c724cad21c40ff2d048de5712b1"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f5450aa904e720f9c6407b59e96a8951ed6a95463f49444b6d2594b067d39588"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:aaa5ae26883c3d58d1a4323981f96b941fa09bb8f0f368d97c6225585280cf04"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1266b577abe7c720fd16a83d0a4999a192e87c4a98fc9f97e0b99b106b3e155f"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a3d2aec4b09c8e59fee8b0d1ed668d09e8c48b738f03f5d8401d7eb409111c47"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-win32.whl", hash = "sha256:8e7738a4b93842bca1158cde81a3587c9b7111823e40a1ddf73292ca9d58e08b"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88"}, +] +identify = [ + {file = "identify-1.4.18-py2.py3-none-any.whl", hash = "sha256:9f53e80371f2ac7c969eefda8efaabd4f77c6300f5f8fc4b634744a0db8fe5cc"}, + {file = "identify-1.4.18.tar.gz", hash = "sha256:de4e1de6c23f52b71c8a54ff558219f3783ff011b432f29360d84a8a31ba561c"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, + {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, +] +importlib-resources = [ + {file = "importlib_resources-1.5.0-py2.py3-none-any.whl", hash = "sha256:85dc0b9b325ff78c8bef2e4ff42616094e16b98ebd5e3b50fe7e2f0bbcdcde49"}, + {file = "importlib_resources-1.5.0.tar.gz", hash = "sha256:6f87df66833e1942667108628ec48900e02a4ab4ad850e25fbf07cb17cf734ca"}, +] +invoke = [ + {file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"}, + {file = "invoke-1.4.1-py3-none-any.whl", hash = "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132"}, + {file = "invoke-1.4.1.tar.gz", hash = "sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +lxml = [ + {file = "lxml-4.5.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ee2be8b8f72a2772e72ab926a3bccebf47bb727bda41ae070dc91d1fb759b726"}, + {file = "lxml-4.5.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:fadd2a63a2bfd7fb604508e553d1cf68eca250b2fbdbd81213b5f6f2fbf23529"}, + {file = "lxml-4.5.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4f282737d187ae723b2633856085c31ae5d4d432968b7f3f478a48a54835f5c4"}, + {file = "lxml-4.5.1-cp27-cp27m-win32.whl", hash = "sha256:7fd88cb91a470b383aafad554c3fe1ccf6dfb2456ff0e84b95335d582a799804"}, + {file = "lxml-4.5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:0790ddca3f825dd914978c94c2545dbea5f56f008b050e835403714babe62a5f"}, + {file = "lxml-4.5.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9144ce36ca0824b29ebc2e02ca186e54040ebb224292072250467190fb613b96"}, + {file = "lxml-4.5.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a636346c6c0e1092ffc202d97ec1843a75937d8c98aaf6771348ad6422e44bb0"}, + {file = "lxml-4.5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:f95d28193c3863132b1f55c1056036bf580b5a488d908f7d22a04ace8935a3a9"}, + {file = "lxml-4.5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b26719890c79a1dae7d53acac5f089d66fd8cc68a81f4e4bd355e45470dc25e1"}, + {file = "lxml-4.5.1-cp35-cp35m-win32.whl", hash = "sha256:a9e3b8011388e7e373565daa5e92f6c9cb844790dc18e43073212bb3e76f7007"}, + {file = "lxml-4.5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:2754d4406438c83144f9ffd3628bbe2dcc6d62b20dbc5c1ec4bc4385e5d44b42"}, + {file = "lxml-4.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:925baf6ff1ef2c45169f548cc85204433e061360bfa7d01e1be7ae38bef73194"}, + {file = "lxml-4.5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a87dbee7ad9dce3aaefada2081843caf08a44a8f52e03e0a4cc5819f8398f2f4"}, + {file = "lxml-4.5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:51bb4edeb36d24ec97eb3e6a6007be128b720114f9a875d6b370317d62ac80b9"}, + {file = "lxml-4.5.1-cp36-cp36m-win32.whl", hash = "sha256:c79e5debbe092e3c93ca4aee44c9a7631bdd407b2871cb541b979fd350bbbc29"}, + {file = "lxml-4.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b7462cdab6fffcda853338e1741ce99706cdf880d921b5a769202ea7b94e8528"}, + {file = "lxml-4.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06748c7192eab0f48e3d35a7adae609a329c6257495d5e53878003660dc0fec6"}, + {file = "lxml-4.5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1aa7a6197c1cdd65d974f3e4953764eee3d9c7b67e3966616b41fab7f8f516b7"}, + {file = "lxml-4.5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:afb53edf1046599991fb4a7d03e601ab5f5422a5435c47ee6ba91ec3b61416a6"}, + {file = "lxml-4.5.1-cp37-cp37m-win32.whl", hash = "sha256:2d1ddce96cf15f1254a68dba6935e6e0f1fe39247de631c115e84dd404a6f031"}, + {file = "lxml-4.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:22c6d34fdb0e65d5f782a4d1a1edb52e0a8365858dafb1c08cb1d16546cf0786"}, + {file = "lxml-4.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c47a8a5d00060122ca5908909478abce7bbf62d812e3fc35c6c802df8fb01fe7"}, + {file = "lxml-4.5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b77975465234ff49fdad871c08aa747aae06f5e5be62866595057c43f8d2f62c"}, + {file = "lxml-4.5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2b02c106709466a93ed424454ce4c970791c486d5fcdf52b0d822a7e29789626"}, + {file = "lxml-4.5.1-cp38-cp38-win32.whl", hash = "sha256:7eee37c1b9815e6505847aa5e68f192e8a1b730c5c7ead39ff317fde9ce29448"}, + {file = "lxml-4.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:d8d40e0121ca1606aa9e78c28a3a7d88a05c06b3ca61630242cded87d8ce55fa"}, + {file = "lxml-4.5.1.tar.gz", hash = "sha256:27ee0faf8077c7c1a589573b1450743011117f1aa1a91d5ae776bbc5ca6070f2"}, +] +mako = [ + {file = "Mako-1.1.3-py2.py3-none-any.whl", hash = "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"}, + {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mock = [ + {file = "mock-4.0.2-py3-none-any.whl", hash = "sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0"}, + {file = "mock-4.0.2.tar.gz", hash = "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72"}, +] +more-itertools = [ + {file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"}, + {file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"}, +] +netaddr = [ + {file = "netaddr-0.7.19-py2.py3-none-any.whl", hash = "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"}, + {file = "netaddr-0.7.19.tar.gz", hash = "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd"}, +] +nodeenv = [ + {file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +paramiko = [ + {file = "paramiko-2.7.1-py2.py3-none-any.whl", hash = "sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f"}, + {file = "paramiko-2.7.1.tar.gz", hash = "sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f"}, +] +pillow = [ + {file = "Pillow-7.1.2-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:ae2b270f9a0b8822b98655cb3a59cdb1bd54a34807c6c56b76dd2e786c3b7db3"}, + {file = "Pillow-7.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:d23e2aa9b969cf9c26edfb4b56307792b8b374202810bd949effd1c6e11ebd6d"}, + {file = "Pillow-7.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b532bcc2f008e96fd9241177ec580829dee817b090532f43e54074ecffdcd97f"}, + {file = "Pillow-7.1.2-cp35-cp35m-win32.whl", hash = "sha256:12e4bad6bddd8546a2f9771485c7e3d2b546b458ae8ff79621214119ac244523"}, + {file = "Pillow-7.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9744350687459234867cbebfe9df8f35ef9e1538f3e729adbd8fde0761adb705"}, + {file = "Pillow-7.1.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:f54be399340aa602066adb63a86a6a5d4f395adfdd9da2b9a0162ea808c7b276"}, + {file = "Pillow-7.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1f694e28c169655c50bb89a3fa07f3b854d71eb47f50783621de813979ba87f3"}, + {file = "Pillow-7.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f784aad988f12c80aacfa5b381ec21fd3f38f851720f652b9f33facc5101cf4d"}, + {file = "Pillow-7.1.2-cp36-cp36m-win32.whl", hash = "sha256:b37bb3bd35edf53125b0ff257822afa6962649995cbdfde2791ddb62b239f891"}, + {file = "Pillow-7.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b67a6c47ed963c709ed24566daa3f95a18f07d3831334da570c71da53d97d088"}, + {file = "Pillow-7.1.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:eaa83729eab9c60884f362ada982d3a06beaa6cc8b084cf9f76cae7739481dfa"}, + {file = "Pillow-7.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f46e0e024346e1474083c729d50de909974237c72daca05393ee32389dabe457"}, + {file = "Pillow-7.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0e2a3bceb0fd4e0cb17192ae506d5f082b309ffe5fc370a5667959c9b2f85fa3"}, + {file = "Pillow-7.1.2-cp37-cp37m-win32.whl", hash = "sha256:ccc9ad2460eb5bee5642eaf75a0438d7f8887d484490d5117b98edd7f33118b7"}, + {file = "Pillow-7.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b943e71c2065ade6fef223358e56c167fc6ce31c50bc7a02dd5c17ee4338e8ac"}, + {file = "Pillow-7.1.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:04766c4930c174b46fd72d450674612ab44cca977ebbcc2dde722c6933290107"}, + {file = "Pillow-7.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f455efb7a98557412dc6f8e463c1faf1f1911ec2432059fa3e582b6000fc90e2"}, + {file = "Pillow-7.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ee94fce8d003ac9fd206496f2707efe9eadcb278d94c271f129ab36aa7181344"}, + {file = "Pillow-7.1.2-cp38-cp38-win32.whl", hash = "sha256:4b02b9c27fad2054932e89f39703646d0c543f21d3cc5b8e05434215121c28cd"}, + {file = "Pillow-7.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:3d25dd8d688f7318dca6d8cd4f962a360ee40346c15893ae3b95c061cdbc4079"}, + {file = "Pillow-7.1.2-pp373-pypy36_pp73-win32.whl", hash = "sha256:0f01e63c34f0e1e2580cc0b24e86a5ccbbfa8830909a52ee17624c4193224cd9"}, + {file = "Pillow-7.1.2-py3.8-macosx-10.9-x86_64.egg", hash = "sha256:70e3e0d99a0dcda66283a185f80697a9b08806963c6149c8e6c5f452b2aa59c0"}, + {file = "Pillow-7.1.2.tar.gz", hash = "sha256:a0b49960110bc6ff5fead46013bcb8825d101026d466f3a4de3476defe0fb0dd"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +pre-commit = [ + {file = "pre_commit-2.1.1-py2.py3-none-any.whl", hash = "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6"}, + {file = "pre_commit-2.1.1.tar.gz", hash = "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"}, +] +protobuf = [ + {file = "protobuf-3.12.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e1464a4a2cf12f58f662c8e6421772c07947266293fb701cb39cd9c1e183f63c"}, + {file = "protobuf-3.12.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6f349adabf1c004aba53f7b4633459f8ca8a09654bf7e69b509c95a454755776"}, + {file = "protobuf-3.12.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:be04fe14ceed7f8641e30f36077c1a654ff6f17d0c7a5283b699d057d150d82a"}, + {file = "protobuf-3.12.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f4b73736108a416c76c17a8a09bc73af3d91edaa26c682aaa460ef91a47168d3"}, + {file = "protobuf-3.12.2-cp35-cp35m-win32.whl", hash = "sha256:5524c7020eb1fb7319472cb75c4c3206ef18b34d6034d2ee420a60f99cddeb07"}, + {file = "protobuf-3.12.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bff02030bab8b969f4de597543e55bd05e968567acb25c0a87495a31eb09e925"}, + {file = "protobuf-3.12.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c9ca9f76805e5a637605f171f6c4772fc4a81eced4e2f708f79c75166a2c99ea"}, + {file = "protobuf-3.12.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:304e08440c4a41a0f3592d2a38934aad6919d692bb0edfb355548786728f9a5e"}, + {file = "protobuf-3.12.2-cp36-cp36m-win32.whl", hash = "sha256:b5a114ea9b7fc90c2cc4867a866512672a47f66b154c6d7ee7e48ddb68b68122"}, + {file = "protobuf-3.12.2-cp36-cp36m-win_amd64.whl", hash = "sha256:85b94d2653b0fdf6d879e39d51018bf5ccd86c81c04e18a98e9888694b98226f"}, + {file = "protobuf-3.12.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7ab28a8f1f043c58d157bceb64f80e4d2f7f1b934bc7ff5e7f7a55a337ea8b0"}, + {file = "protobuf-3.12.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eafe9fa19fcefef424ee089fb01ac7177ff3691af7cc2ae8791ae523eb6ca907"}, + {file = "protobuf-3.12.2-cp37-cp37m-win32.whl", hash = "sha256:612bc97e42b22af10ba25e4140963fbaa4c5181487d163f4eb55b0b15b3dfcd2"}, + {file = "protobuf-3.12.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e72736dd822748b0721f41f9aaaf6a5b6d5cfc78f6c8690263aef8bba4457f0e"}, + {file = "protobuf-3.12.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:87535dc2d2ef007b9d44e309d2b8ea27a03d2fa09556a72364d706fcb7090828"}, + {file = "protobuf-3.12.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:50b5fee674878b14baea73b4568dc478c46a31dd50157a5b5d2f71138243b1a9"}, + {file = "protobuf-3.12.2-py2.py3-none-any.whl", hash = "sha256:a96f8fc625e9ff568838e556f6f6ae8eca8b4837cdfb3f90efcb7c00e342a2eb"}, + {file = "protobuf-3.12.2.tar.gz", hash = "sha256:49ef8ab4c27812a89a76fa894fe7a08f42f2147078392c0dee51d4a444ef6df5"}, +] +py = [ + {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, + {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pynacl = [ + {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, + {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, + {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, + {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, + {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pyproj = [ + {file = "pyproj-2.6.1.post1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:457ad3856014ac26af1d86def6dc8cf69c1fa377b6e2fd6e97912d51cf66bdbe"}, + {file = "pyproj-2.6.1.post1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6f3f36440ea61f5f6da4e6beb365dddcbe159815450001d9fb753545affa45ff"}, + {file = "pyproj-2.6.1.post1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6a212d0e5c7efa33d039f0c8b0a489e2204fcd28b56206567852ad7f5f2a653e"}, + {file = "pyproj-2.6.1.post1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:451a3d1c563b672458029ebc04acbb3266cd8b3025268eb871a9176dc3638911"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e015f900b4b84e908f8035ab16ebf02d67389c1c216c17a2196fc2e515c00762"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a13e5731b3a360ee7fbd1e9199ec9203fafcece8ebd0b1351f16d0a90cad6828"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:33c1c2968a4f4f87d517c4275a18b557e5c13907cf2609371fadea8463c3ba05"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3fef83a01c1e86dd9fa99d8214f749837cfafc34d9d6230b4b0a998fa7a68a1a"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-win32.whl", hash = "sha256:a6ac4861979cd05a0f5400fefa41d26c0269a5fb8237618aef7c998907db39e1"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-win_amd64.whl", hash = "sha256:cbf6ccf990860b06c5262ff97c4b78e1d07883981635cd53a6aa438a68d92945"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adacb67a9f71fb54ca1b887a6ab20f32dd536fcdf2acec84a19e25ad768f7965"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e50d5d20b87758acf8f13f39a3b3eb21d5ef32339d2bc8cdeb8092416e0051df"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2518d1606e2229b82318e704b40290e02a2a52d77b40cdcb2978973d6fc27b20"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:33a5d1cfbb40a019422eb80709a0e270704390ecde7278fdc0b88f3647c56a39"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-win32.whl", hash = "sha256:daf2998e3f5bcdd579a18faf009f37f53538e9b7d0a252581a610297d31e8536"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-win_amd64.whl", hash = "sha256:a8b7c8accdc61dac8e91acab7c1f7b4590d1e102f2ee9b1f1e6399fad225958e"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f097e8f341a162438918e908be86d105a28194ff6224633b2e9616c5031153f"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d90a5d1fdd066b0e9b22409b0f5e81933469918fa04c2cf7f9a76ce84cb29dad"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f5a8015c74ec8f6508aebf493b58ba20ccb4da8168bf05f0c2a37faccb518da9"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d87836be6b720fb4d9c112136aa47621b6ca09a554e645c1081561eb8e2fa1f4"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-win32.whl", hash = "sha256:bc2f3a15d065e206d63edd2cc4739aa0a35c05338ee276ab1dc72f56f1944bda"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-win_amd64.whl", hash = "sha256:93cbad7b699e8e80def7de80c350617f35e6a0b82862f8ce3c014657c25fdb3c"}, + {file = "pyproj-2.6.1.post1.tar.gz", hash = "sha256:4f5b02b4abbd41610397c635b275a8ee4a2b5bc72a75572b98ac6ae7befa471e"}, +] +pytest = [ + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] +virtualenv = [ + {file = "virtualenv-20.0.21-py2.py3-none-any.whl", hash = "sha256:a730548b27366c5e6cbdf6f97406d861cccece2e22275e8e1a757aeff5e00c70"}, + {file = "virtualenv-20.0.21.tar.gz", hash = "sha256:a116629d4e7f4d03433b8afa27f43deba09d48bc48f5ecefa4f015a178efb6cf"}, +] +wcwidth = [ + {file = "wcwidth-0.2.3-py2.py3-none-any.whl", hash = "sha256:980fbf4f3c196c0f329cdcd1e84c554d6a211f18e252e525a0cf4223154a41d6"}, + {file = "wcwidth-0.2.3.tar.gz", hash = "sha256:edbc2b718b4db6cdf393eefe3a420183947d6aa312505ce6754516f458ff8830"}, +] +zipp = [ + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, +] diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index d602f9d32..9214ad1b8 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -39,14 +39,20 @@ service CoreApi { } rpc SetSessionState (SetSessionStateRequest) returns (SetSessionStateResponse) { } + rpc SetSessionUser (SetSessionUserRequest) returns (SetSessionUserResponse) { + } rpc AddSessionServer (AddSessionServerRequest) returns (AddSessionServerResponse) { } + rpc SessionAlert (SessionAlertRequest) returns (SessionAlertResponse) { + } // streams rpc Events (EventsRequest) returns (stream Event) { } rpc Throughputs (ThroughputsRequest) returns (stream ThroughputsEvent) { } + rpc CpuUsage (CpuUsageRequest) returns (stream CpuUsageEvent) { + } // node rpc rpc AddNode (AddNodeRequest) returns (AddNodeResponse) { @@ -297,6 +303,15 @@ message SetSessionStateResponse { bool result = 1; } +message SetSessionUserRequest { + int32 session_id = 1; + string user = 2; +} + +message SetSessionUserResponse { + bool result = 1; +} + message AddSessionServerRequest { int32 session_id = 1; string name = 2; @@ -307,6 +322,18 @@ message AddSessionServerResponse { bool result = 1; } +message SessionAlertRequest { + int32 session_id = 1; + ExceptionLevel.Enum level = 2; + string source = 3; + string text = 4; + int32 node_id = 5; +} + +message SessionAlertResponse { + bool result = 1; +} + message EventsRequest { int32 session_id = 1; repeated EventType.Enum events = 2; @@ -319,12 +346,20 @@ message ThroughputsRequest { message ThroughputsEvent { int32 session_id = 1; repeated BridgeThroughput bridge_throughputs = 2; - repeated InterfaceThroughput interface_throughputs = 3; + repeated InterfaceThroughput iface_throughputs = 3; +} + +message CpuUsageRequest { + int32 delay = 1; +} + +message CpuUsageEvent { + double usage = 1; } message InterfaceThroughput { int32 node_id = 1; - int32 interface_id = 2; + int32 iface_id = 2; double throughput = 3; } @@ -343,11 +378,12 @@ message Event { FileEvent file_event = 6; } int32 session_id = 7; + string source = 8; } message NodeEvent { Node node = 1; - string source = 2; + MessageType.Enum message_type = 2; } message LinkEvent { @@ -374,7 +410,7 @@ message ConfigEvent { string bitmap = 8; string possible_values = 9; string groups = 10; - int32 interface = 11; + int32 iface_id = 11; int32 network_id = 12; string opaque = 13; } @@ -403,6 +439,7 @@ message FileEvent { message AddNodeRequest { int32 session_id = 1; Node node = 2; + string source = 3; } message AddNodeResponse { @@ -416,7 +453,7 @@ message GetNodeRequest { message GetNodeResponse { Node node = 1; - repeated Interface interfaces = 2; + repeated Interface ifaces = 2; } message EditNodeRequest { @@ -435,6 +472,7 @@ message EditNodeResponse { message DeleteNodeRequest { int32 session_id = 1; int32 node_id = 2; + string source = 3; } message DeleteNodeResponse { @@ -488,21 +526,23 @@ message GetNodeLinksResponse { message AddLinkRequest { int32 session_id = 1; Link link = 2; + string source = 3; } message AddLinkResponse { bool result = 1; - Interface interface_one = 2; - Interface interface_two = 3; + Interface iface1 = 2; + Interface iface2 = 3; } message EditLinkRequest { int32 session_id = 1; - int32 node_one_id = 2; - int32 node_two_id = 3; - int32 interface_one_id = 4; - int32 interface_two_id = 5; + int32 node1_id = 2; + int32 node2_id = 3; + int32 iface1_id = 4; + int32 iface2_id = 5; LinkOptions options = 6; + string source = 7; } message EditLinkResponse { @@ -511,10 +551,11 @@ message EditLinkResponse { message DeleteLinkRequest { int32 session_id = 1; - int32 node_one_id = 2; - int32 node_two_id = 3; - int32 interface_one_id = 4; - int32 interface_two_id = 5; + int32 node1_id = 2; + int32 node2_id = 3; + int32 iface1_id = 4; + int32 iface2_id = 5; + string source = 6; } message DeleteLinkResponse { @@ -561,7 +602,7 @@ message GetInterfacesRequest { } message GetInterfacesResponse { - repeated string interfaces = 1; + repeated string ifaces = 1; } message ExecuteScriptRequest { @@ -692,21 +733,20 @@ message Node { repeated string services = 6; string emane = 7; string icon = 8; - string opaque = 9; - string image = 10; - string server = 11; - repeated string config_services = 12; - Geo geo = 13; - string dir = 14; - string channel = 15; + string image = 9; + string server = 10; + repeated string config_services = 11; + Geo geo = 12; + string dir = 13; + string channel = 14; } message Link { - int32 node_one_id = 1; - int32 node_two_id = 2; + int32 node1_id = 1; + int32 node2_id = 2; LinkType.Enum type = 3; - Interface interface_one = 4; - Interface interface_two = 5; + Interface iface1 = 4; + Interface iface2 = 5; LinkOptions options = 6; int32 network_id = 7; string label = 8; @@ -714,17 +754,16 @@ message Link { } message LinkOptions { - string opaque = 1; - int64 jitter = 2; - int32 key = 3; - int32 mburst = 4; - int32 mer = 5; - float per = 6; - int64 bandwidth = 7; - int32 burst = 8; - int64 delay = 9; - int32 dup = 10; - bool unidirectional = 11; + int64 jitter = 1; + int32 key = 2; + int32 mburst = 3; + int32 mer = 4; + float loss = 5; + int64 bandwidth = 6; + int32 burst = 7; + int64 delay = 8; + int32 dup = 9; + bool unidirectional = 10; } message Interface { @@ -732,12 +771,14 @@ message Interface { string name = 2; string mac = 3; string ip4 = 4; - int32 ip4mask = 5; + int32 ip4_mask = 5; string ip6 = 6; - int32 ip6mask = 7; - int32 netid = 8; - int32 flowid = 9; + int32 ip6_mask = 7; + int32 net_id = 8; + int32 flow_id = 9; int32 mtu = 10; + int32 node_id = 11; + int32 net2_id = 12; } message SessionLocation { diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto index 8c3ee4cae..ac5456fdc 100644 --- a/daemon/proto/core/api/grpc/emane.proto +++ b/daemon/proto/core/api/grpc/emane.proto @@ -32,7 +32,7 @@ message GetEmaneModelsResponse { message GetEmaneModelConfigRequest { int32 session_id = 1; int32 node_id = 2; - int32 interface = 3; + int32 iface_id = 3; string model = 4; } @@ -57,7 +57,7 @@ message GetEmaneModelConfigsResponse { message ModelConfig { int32 node_id = 1; string model = 2; - int32 interface = 3; + int32 iface_id = 3; map config = 4; } repeated ModelConfig configs = 1; @@ -75,8 +75,8 @@ message GetEmaneEventChannelResponse { message EmaneLinkRequest { int32 session_id = 1; - int32 nem_one = 2; - int32 nem_two = 3; + int32 nem1 = 2; + int32 nem2 = 3; bool linked = 4; } @@ -86,19 +86,19 @@ message EmaneLinkResponse { message EmaneModelConfig { int32 node_id = 1; - int32 interface_id = 2; + int32 iface_id = 2; string model = 3; map config = 4; } message EmanePathlossesRequest { int32 session_id = 1; - int32 node_one = 2; - float rx_one = 3; - int32 interface_one_id = 4; - int32 node_two = 5; - float rx_two = 6; - int32 interface_two_id = 7; + int32 node1_id = 2; + float rx1 = 3; + int32 iface1_id = 4; + int32 node2_id = 5; + float rx2 = 6; + int32 iface2_id = 7; } message EmanePathlossesResponse { diff --git a/daemon/proto/core/api/grpc/wlan.proto b/daemon/proto/core/api/grpc/wlan.proto index bbb9757f2..9605d633d 100644 --- a/daemon/proto/core/api/grpc/wlan.proto +++ b/daemon/proto/core/api/grpc/wlan.proto @@ -38,8 +38,8 @@ message SetWlanConfigResponse { message WlanLinkRequest { int32 session_id = 1; int32 wlan = 2; - int32 node_one = 3; - int32 node_two = 4; + int32 node1_id = 3; + int32 node2_id = 4; bool linked = 5; } diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml new file mode 100644 index 000000000..b75f1ee3b --- /dev/null +++ b/daemon/pyproject.toml @@ -0,0 +1,49 @@ +[tool.poetry] +name = "core" +version = "7.0.0" +description = "CORE Common Open Research Emulator" +authors = ["Boeing Research and Technology"] +license = "BSD-2-Clause" +repository = "https://github.com/coreemu/core" +documentation = "https://coreemu.github.io/core/" +include = ["core/gui/data/**/*", "core/configservices/*/templates"] + +[tool.poetry.dependencies] +python = "^3.6" +dataclasses = { version = "*", python = "3.6" } +fabric = "*" +grpcio = "1.27.2" +invoke = "*" +lxml = "*" +mako = "*" +netaddr = "*" +pillow = "*" +protobuf = "*" +pyproj = "*" +pyyaml = "*" + +[tool.poetry.dev-dependencies] +black = "==19.3b0" +flake8 = "*" +grpcio-tools = "1.27.2" +isort = "*" +mock = "*" +pre-commit = "*" +pytest = "*" + +[tool.isort] +skip_glob = "*_pb2*.py,doc,build" +multi_line_output = 3 +include_trailing_comma = "True" +force_grid_wrap = 0 +use_parentheses = "True" +line_length = 88 + +[tool.black] +line_length = 88 +exclude = ".+_pb2.*.py|doc/|build/|__pycache__/" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" + diff --git a/daemon/requirements.txt b/daemon/requirements.txt deleted file mode 100644 index 19d155e59..000000000 --- a/daemon/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -bcrypt==3.1.7 -cffi==1.14.0 -cryptography==2.8 -dataclasses==0.7; python_version == "3.6" -fabric==2.5.0 -grpcio==1.27.2 -invoke==1.4.1 -lxml==4.5.0 -Mako==1.1.1 -MarkupSafe==1.1.1 -netaddr==0.7.19 -paramiko==2.7.1 -Pillow==7.0.0 -protobuf==3.11.3 -pycparser==2.19 -PyNaCl==1.3.0 -pyproj==2.5.0 -PyYAML==5.3 -six==1.14.0 diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli new file mode 100755 index 000000000..a75714712 --- /dev/null +++ b/daemon/scripts/core-cli @@ -0,0 +1,541 @@ +#!/usr/bin/env python3 +import sys +from argparse import ( + ArgumentDefaultsHelpFormatter, + ArgumentParser, + ArgumentTypeError, + Namespace, + _SubParsersAction, +) +from functools import wraps +from pathlib import Path +from typing import Any, Optional, Tuple + +import grpc +import netaddr +from google.protobuf.json_format import MessageToJson +from netaddr import EUI, AddrFormatError, IPNetwork + +from core.api.grpc.client import CoreGrpcClient +from core.api.grpc.core_pb2 import ( + Geo, + Interface, + LinkOptions, + Node, + NodeType, + Position, + SessionState, +) + +NODE_TYPES = [k for k, v in NodeType.Enum.items() if v != NodeType.PEER_TO_PEER] + + +def coreclient(func): + @wraps(func) + def wrapper(*args, **kwargs): + core = CoreGrpcClient() + try: + with core.context_connect(): + return func(core, *args, **kwargs) + except grpc.RpcError as e: + print(f"grpc error: {e.details()}") + + return wrapper + + +def mac_type(value: str) -> str: + try: + mac = EUI(value, dialect=netaddr.mac_unix_expanded) + return str(mac) + except AddrFormatError: + raise ArgumentTypeError(f"invalid mac address: {value}") + + +def ip4_type(value: str) -> IPNetwork: + try: + ip = IPNetwork(value) + if not netaddr.valid_ipv4(str(ip.ip)): + raise ArgumentTypeError(f"invalid ip4 address: {value}") + return ip + except AddrFormatError: + raise ArgumentTypeError(f"invalid ip4 address: {value}") + + +def ip6_type(value: str) -> IPNetwork: + try: + ip = IPNetwork(value) + if not netaddr.valid_ipv6(str(ip.ip)): + raise ArgumentTypeError(f"invalid ip6 address: {value}") + return ip + except AddrFormatError: + raise ArgumentTypeError(f"invalid ip6 address: {value}") + + +def position_type(value: str) -> Tuple[float, float]: + error = "invalid position, must be in the format: float,float" + try: + values = [float(x) for x in value.split(",")] + except ValueError: + raise ArgumentTypeError(error) + if len(values) != 2: + raise ArgumentTypeError(error) + x, y = values + return x, y + + +def geo_type(value: str) -> Tuple[float, float, float]: + error = "invalid geo, must be in the format: float,float,float" + try: + values = [float(x) for x in value.split(",")] + except ValueError: + raise ArgumentTypeError(error) + if len(values) != 3: + raise ArgumentTypeError(error) + lon, lat, alt = values + return lon, lat, alt + + +def file_type(value: str) -> str: + path = Path(value) + if not path.is_file(): + raise ArgumentTypeError(f"invalid file: {value}") + return str(path.absolute()) + + +def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int: + if session_id: + return session_id + response = core.get_sessions() + if not response.sessions: + print("no current session to interact with") + sys.exit(1) + return response.sessions[0].id + + +def create_iface(iface_id: int, mac: str, ip4_net: IPNetwork, ip6_net: IPNetwork) -> Interface: + ip4 = str(ip4_net.ip) if ip4_net else None + ip4_mask = ip4_net.prefixlen if ip4_net else None + ip6 = str(ip6_net.ip) if ip6_net else None + ip6_mask = ip6_net.prefixlen if ip6_net else None + return Interface( + id=iface_id, + mac=mac, + ip4=ip4, + ip4_mask=ip4_mask, + ip6=ip6, + ip6_mask=ip6_mask, + ) + + +def print_iface_header() -> None: + print("ID | MAC Address | IP4 Address | IP6 Address") + + +def print_iface(iface: Interface) -> None: + iface_ip4 = f"{iface.ip4}/{iface.ip4_mask}" if iface.ip4 else "" + iface_ip6 = f"{iface.ip6}/{iface.ip6_mask}" if iface.ip6 else "" + print(f"{iface.id:<3} | {iface.mac:<17} | {iface_ip4:<18} | {iface_ip6}") + + +def print_json(message: Any) -> None: + json = MessageToJson(message, preserving_proto_field_name=True) + print(json) + + +@coreclient +def get_wlan_config(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + response = core.get_wlan_config(session_id, args.node) + if args.json: + print_json(response) + else: + size = 0 + for option in response.config.values(): + size = max(size, len(option.name)) + print(f"{'Name':<{size}.{size}} | Value") + for option in response.config.values(): + print(f"{option.name:<{size}.{size}} | {option.value}") + + +@coreclient +def set_wlan_config(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + config = {} + if args.bandwidth: + config["bandwidth"] = str(args.bandwidth) + if args.delay: + config["delay"] = str(args.delay) + if args.loss: + config["error"] = str(args.loss) + if args.jitter: + config["jitter"] = str(args.jitter) + if args.range: + config["range"] = str(args.range) + response = core.set_wlan_config(session_id, args.node, config) + if args.json: + print_json(response) + else: + print(f"set wlan config: {response.result}") + + +@coreclient +def open_xml(core: CoreGrpcClient, args: Namespace) -> None: + response = core.open_xml(args.file, args.start) + if args.json: + print_json(response) + else: + print(f"opened xml: {response.result}") + + +@coreclient +def query_sessions(core: CoreGrpcClient, args: Namespace) -> None: + response = core.get_sessions() + if args.json: + print_json(response) + else: + print("Session ID | Session State | Nodes") + for s in response.sessions: + state = SessionState.Enum.Name(s.state) + print(f"{s.id:<10} | {state:<13} | {s.nodes}") + + +@coreclient +def query_session(core: CoreGrpcClient, args: Namespace) -> None: + response = core.get_session(args.id) + if args.json: + print_json(response) + else: + print("Nodes") + print("Node ID | Node Name | Node Type") + names = {} + for node in response.session.nodes: + names[node.id] = node.name + node_type = NodeType.Enum.Name(node.type) + print(f"{node.id:<7} | {node.name:<9} | {node_type}") + + print("\nLinks") + for link in response.session.links: + n1 = names[link.node1_id] + n2 = names[link.node2_id] + print(f"Node | ", end="") + print_iface_header() + print(f"{n1:<6} | ", end="") + if link.HasField("iface1"): + print_iface(link.iface1) + else: + print() + print(f"{n2:<6} | ", end="") + if link.HasField("iface2"): + print_iface(link.iface2) + else: + print() + print() + + +@coreclient +def query_node(core: CoreGrpcClient, args: Namespace) -> None: + names = {} + response = core.get_session(args.id) + for node in response.session.nodes: + names[node.id] = node.name + + response = core.get_node(args.id, args.node) + if args.json: + print_json(response) + else: + node = response.node + node_type = NodeType.Enum.Name(node.type) + print("ID | Name | Type") + print(f"{node.id:<4} | {node.name:<7} | {node_type}") + print("Interfaces") + print("Connected To | ", end="") + print_iface_header() + for iface in response.ifaces: + if iface.net_id == node.id: + if iface.node_id: + name = names[iface.node_id] + else: + name = names[iface.net2_id] + else: + name = names.get(iface.net_id, "") + print(f"{name:<12} | ", end="") + print_iface(iface) + + +@coreclient +def add_node(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + node_type = NodeType.Enum.Value(args.type) + pos = None + if args.pos: + x, y = args.pos + pos = Position(x=x, y=y) + geo = None + if args.geo: + lon, lat, alt = args.geo + geo = Geo(lon=lon, lat=lat, alt=alt) + node = Node( + id=args.id, + name=args.name, + type=node_type, + model=args.model, + emane=args.emane, + icon=args.icon, + image=args.image, + position=pos, + geo=geo, + ) + response = core.add_node(session_id, node) + if args.json: + print_json(response) + else: + print(f"created node: {response.node_id}") + + +@coreclient +def edit_node(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + pos = None + if args.pos: + x, y = args.pos + pos = Position(x=x, y=y) + geo = None + if args.geo: + lon, lat, alt = args.geo + geo = Geo(lon=lon, lat=lat, alt=alt) + response = core.edit_node(session_id, args.id, pos, args.icon, geo) + if args.json: + print_json(response) + else: + print(f"edit node: {response.result}") + + +@coreclient +def delete_node(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + response = core.delete_node(session_id, args.id) + if args.json: + print_json(response) + else: + print(f"deleted node: {response.result}") + + +@coreclient +def add_link(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + iface1 = None + if args.iface1_id is not None: + iface1 = create_iface(args.iface1_id, args.iface1_mac, args.iface1_ip4, args.iface1_ip6) + iface2 = None + if args.iface2_id is not None: + iface2 = create_iface(args.iface2_id, args.iface2_mac, args.iface2_ip4, args.iface2_ip6) + options = LinkOptions( + bandwidth=args.bandwidth, + loss=args.loss, + jitter=args.jitter, + delay=args.delay, + dup=args.duplicate, + unidirectional=args.uni, + ) + response = core.add_link(session_id, args.node1, args.node2, iface1, iface2, options) + if args.json: + print_json(response) + else: + print(f"add link: {response.result}") + + +@coreclient +def edit_link(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + options = LinkOptions( + bandwidth=args.bandwidth, + loss=args.loss, + jitter=args.jitter, + delay=args.delay, + dup=args.duplicate, + unidirectional=args.uni, + ) + response = core.edit_link( + session_id, args.node1, args.node2, options, args.iface1, args.iface2 + ) + if args.json: + print_json(response) + else: + print(f"edit link: {response.result}") + + +@coreclient +def delete_link(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + response = core.delete_link(session_id, args.node1, args.node2, args.iface1, args.iface2) + if args.json: + print_json(response) + else: + print(f"delete link: {response.result}") + + +def setup_node_parser(parent: _SubParsersAction) -> None: + parser = parent.add_parser("node", help="node interactions") + parser.formatter_class = ArgumentDefaultsHelpFormatter + parser.add_argument("-s", "--session", type=int, help="session to interact with") + subparsers = parser.add_subparsers(help="node commands") + subparsers.required = True + subparsers.dest = "command" + + add_parser = subparsers.add_parser("add", help="add a node") + add_parser.formatter_class = ArgumentDefaultsHelpFormatter + add_parser.add_argument("-i", "--id", type=int, help="id to use, optional") + add_parser.add_argument("-n", "--name", help="name to use, optional") + add_parser.add_argument( + "-t", "--type", choices=NODE_TYPES, default="DEFAULT", help="type of node" + ) + add_parser.add_argument("-m", "--model", help="used to determine services, optional") + group = add_parser.add_mutually_exclusive_group(required=True) + group.add_argument("-p", "--pos", type=position_type, help="x,y position") + group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position") + add_parser.add_argument("-ic", "--icon", help="icon to use, optional") + add_parser.add_argument("-im", "--image", help="container image, optional") + add_parser.add_argument("-e", "--emane", help="emane model, only required for emane nodes") + add_parser.set_defaults(func=add_node) + + edit_parser = subparsers.add_parser("edit", help="edit a node") + edit_parser.formatter_class = ArgumentDefaultsHelpFormatter + edit_parser.add_argument("-i", "--id", type=int, help="id to use, optional") + group = edit_parser.add_mutually_exclusive_group(required=True) + group.add_argument("-p", "--pos", type=position_type, help="x,y position") + group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position") + edit_parser.add_argument("-ic", "--icon", help="icon to use, optional") + edit_parser.set_defaults(func=edit_node) + + delete_parser = subparsers.add_parser("delete", help="delete a node") + delete_parser.formatter_class = ArgumentDefaultsHelpFormatter + delete_parser.add_argument("-i", "--id", type=int, help="node id", required=True) + delete_parser.set_defaults(func=delete_node) + + +def setup_link_parser(parent: _SubParsersAction) -> None: + parser = parent.add_parser("link", help="link interactions") + parser.formatter_class = ArgumentDefaultsHelpFormatter + parser.add_argument("-s", "--session", type=int, help="session to interact with") + subparsers = parser.add_subparsers(help="link commands") + subparsers.required = True + subparsers.dest = "command" + + add_parser = subparsers.add_parser("add", help="add a node") + add_parser.formatter_class = ArgumentDefaultsHelpFormatter + add_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True) + add_parser.add_argument("-n2", "--node2", type=int, help="node2 id", required=True) + add_parser.add_argument("-i1-i", "--iface1-id", type=int, help="node1 interface id") + add_parser.add_argument("-i1-m", "--iface1-mac", type=mac_type, help="node1 interface mac") + add_parser.add_argument("-i1-4", "--iface1-ip4", type=ip4_type, help="node1 interface ip4") + add_parser.add_argument("-i1-6", "--iface1-ip6", type=ip6_type, help="node1 interface ip6") + add_parser.add_argument("-i2-i", "--iface2-id", type=int, help="node2 interface id") + add_parser.add_argument("-i2-m", "--iface2-mac", type=mac_type, help="node2 interface mac") + add_parser.add_argument("-i2-4", "--iface2-ip4", type=ip4_type, help="node2 interface ip4") + add_parser.add_argument("-i2-6", "--iface2-ip6", type=ip6_type, help="node2 interface ip6") + add_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)") + add_parser.add_argument("-l", "--loss", type=float, help="loss (%%)") + add_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)") + add_parser.add_argument("-de", "--delay", type=int, help="delay (us)") + add_parser.add_argument("-du", "--duplicate", type=int, help="duplicate (%%)") + add_parser.add_argument("-u", "--uni", action="store_true", help="is link unidirectional?") + add_parser.set_defaults(func=add_link) + + edit_parser = subparsers.add_parser("edit", help="edit a link") + edit_parser.formatter_class = ArgumentDefaultsHelpFormatter + edit_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True) + edit_parser.add_argument("-n2", "--node2", type=int, help="node2 id", required=True) + edit_parser.add_argument("-i1", "--iface1", type=int, help="node1 interface id") + edit_parser.add_argument("-i2", "--iface2", type=int, help="node2 interface id") + edit_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)") + edit_parser.add_argument("-l", "--loss", type=float, help="loss (%%)") + edit_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)") + edit_parser.add_argument("-de", "--delay", type=int, help="delay (us)") + edit_parser.add_argument("-du", "--duplicate", type=int, help="duplicate (%%)") + edit_parser.add_argument( + "-u", "--uni", action="store_true", help="is link unidirectional?" + ) + edit_parser.set_defaults(func=edit_link) + + delete_parser = subparsers.add_parser("delete", help="delete a link") + delete_parser.formatter_class = ArgumentDefaultsHelpFormatter + delete_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True) + delete_parser.add_argument("-n2", "--node2", type=int, help="node1 id", required=True) + delete_parser.add_argument("-i1", "--iface1", type=int, help="node1 interface id") + delete_parser.add_argument("-i2", "--iface2", type=int, help="node2 interface id") + delete_parser.set_defaults(func=delete_link) + + +def setup_query_parser(parent: _SubParsersAction) -> None: + parser = parent.add_parser("query", help="query interactions") + subparsers = parser.add_subparsers(help="query commands") + subparsers.required = True + subparsers.dest = "command" + + sessions_parser = subparsers.add_parser("sessions", help="query current sessions") + sessions_parser.formatter_class = ArgumentDefaultsHelpFormatter + sessions_parser.set_defaults(func=query_sessions) + + session_parser = subparsers.add_parser("session", help="query session") + session_parser.formatter_class = ArgumentDefaultsHelpFormatter + session_parser.add_argument("-i", "--id", type=int, help="session to query", required=True) + session_parser.set_defaults(func=query_session) + + node_parser = subparsers.add_parser("node", help="query node") + node_parser.formatter_class = ArgumentDefaultsHelpFormatter + node_parser.add_argument("-i", "--id", type=int, help="session to query", required=True) + node_parser.add_argument("-n", "--node", type=int, help="node to query", required=True) + node_parser.set_defaults(func=query_node) + + +def setup_xml_parser(parent: _SubParsersAction) -> None: + parser = parent.add_parser("xml", help="open session xml") + parser.formatter_class = ArgumentDefaultsHelpFormatter + parser.add_argument("-f", "--file", type=file_type, help="xml file to open", required=True) + parser.add_argument("-s", "--start", action="store_true", help="start the session?") + parser.set_defaults(func=open_xml) + + +def setup_wlan_parser(parent: _SubParsersAction) -> None: + parser = parent.add_parser("wlan", help="wlan specific interactions") + parser.formatter_class = ArgumentDefaultsHelpFormatter + parser.add_argument("-s", "--session", type=int, help="session to interact with") + subparsers = parser.add_subparsers(help="link commands") + subparsers.required = True + subparsers.dest = "command" + + get_parser = subparsers.add_parser("get", help="get wlan configuration") + get_parser.formatter_class = ArgumentDefaultsHelpFormatter + get_parser.add_argument("-n", "--node", type=int, help="wlan node", required=True) + get_parser.set_defaults(func=get_wlan_config) + + set_parser = subparsers.add_parser("set", help="set wlan configuration") + set_parser.formatter_class = ArgumentDefaultsHelpFormatter + set_parser.add_argument("-n", "--node", type=int, help="wlan node", required=True) + set_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)") + set_parser.add_argument("-d", "--delay", type=int, help="delay (us)") + set_parser.add_argument("-l", "--loss", type=float, help="loss (%%)") + set_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)") + set_parser.add_argument("-r", "--range", type=int, help="range (pixels)") + set_parser.set_defaults(func=set_wlan_config) + + +def main() -> None: + parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) + parser.add_argument( + "-js", "--json", action="store_true", help="print responses to terminal as json" + ) + subparsers = parser.add_subparsers(help="supported commands") + subparsers.required = True + subparsers.dest = "command" + setup_node_parser(subparsers) + setup_link_parser(subparsers) + setup_query_parser(subparsers) + setup_xml_parser(subparsers) + setup_wlan_parser(subparsers) + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/daemon/scripts/core-daemon b/daemon/scripts/core-daemon index a95e59fad..16b0ac59c 100755 --- a/daemon/scripts/core-daemon +++ b/daemon/scripts/core-daemon @@ -118,6 +118,9 @@ def get_merged_config(filename): # parse command line options args = parser.parse_args() + # convert ovs to internal format + args.ovs = "1" if args.ovs else "0" + # read the config file if args.configfile is not None: filename = args.configfile diff --git a/daemon/scripts/core-pygui b/daemon/scripts/core-pygui index 46860ce93..888f4171e 100755 --- a/daemon/scripts/core-pygui +++ b/daemon/scripts/core-pygui @@ -13,6 +13,7 @@ if __name__ == "__main__": parser.add_argument("-l", "--level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], default="INFO", help="logging level") parser.add_argument("-p", "--proxy", action="store_true", help="enable proxy") + parser.add_argument("-s", "--session", type=int, help="session id to join") args = parser.parse_args() # check home directory exists and create if necessary @@ -28,5 +29,5 @@ if __name__ == "__main__": # start app Images.load_all() - app = Application(args.proxy) + app = Application(args.proxy, args.session) app.mainloop() diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor index b12e6205b..d644ae1be 100755 --- a/daemon/scripts/core-route-monitor +++ b/daemon/scripts/core-route-monitor @@ -101,8 +101,8 @@ class RouterMonitor: node_map[node.id] = node.channel if self.src_id is None: response = self.core.get_node(self.session, node.id) - for netif in response.interfaces: - if self.src == netif.ip4: + for iface in response.ifaces: + if self.src == iface.ip4: self.src_id = node.id break except grpc.RpcError: diff --git a/daemon/setup.cfg b/daemon/setup.cfg index a3084b8be..89c968b90 100644 --- a/daemon/setup.cfg +++ b/daemon/setup.cfg @@ -1,20 +1,9 @@ -[aliases] -test=pytest - -[isort] -skip_glob=*_pb2*.py,utm.py,doc,build -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 - [flake8] ignore=E501,W503,E203 max-line-length=88 max-complexity=26 select=B,C,E,F,W,T4 -exclude=*_pb2*.py,utm.py,doc,build +exclude=*_pb2*.py,doc,build [tool:pytest] norecursedirs=distributed emane diff --git a/daemon/setup.py.in b/daemon/setup.py.in deleted file mode 100644 index e8c99e679..000000000 --- a/daemon/setup.py.in +++ /dev/null @@ -1,60 +0,0 @@ -""" -Defines how CORE will be built for installation. -""" - -import glob -import os - -from setuptools import find_packages, setup - -_CORE_DIR = "/etc/core" -_MAN_DIR = "share/man/man1" -_EXAMPLES_DIR = "share/core" - - -def recursive_files(data_path, files_path): - all_files = [] - for path, _directories, filenames in os.walk(files_path): - directory = os.path.join(data_path, path) - files = [] - for filename in filenames: - files.append(os.path.join(path, filename)) - all_files.append((directory, files)) - return all_files - - -data_files = [ - (_CORE_DIR, glob.glob("data/*")), - (_MAN_DIR, glob.glob("../man/**.1")), -] -data_files.extend(recursive_files(_EXAMPLES_DIR, "examples")) - -setup( - name="core", - version="@PACKAGE_VERSION@", - packages=find_packages(), - install_requires=[ - 'dataclasses;python_version=="3.6"', - "fabric", - "grpcio", - "invoke", - "lxml", - "mako", - "netaddr", - "pillow", - "protobuf", - "pyproj", - "pyyaml", - ], - tests_require=[ - "pytest", - ], - data_files=data_files, - scripts=glob.glob("scripts/*"), - include_package_data=True, - description="Python components of CORE", - url="https://github.com/coreemu/core", - author="Boeing Research & Technology", - license="BSD", - long_description="Python scripts and modules for building virtual emulated networks.", -) diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index 9d54d9c20..0e25dee97 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -12,10 +12,9 @@ from core.api.grpc.client import InterfaceHelper from core.api.grpc.server import CoreGrpcServer from core.api.tlv.corehandlers import CoreHandler -from core.emane.emanemanager import EmaneManager from core.emulator.coreemu import CoreEmu +from core.emulator.data import IpPrefixes from core.emulator.distributed import DistributedServer -from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import EventTypes from core.emulator.session import Session from core.nodes.base import CoreNode @@ -56,6 +55,7 @@ def patcher(request): if request.config.getoption("mock"): patch_manager.patch("os.mkdir") patch_manager.patch("core.utils.cmd") + patch_manager.patch("core.utils.which") patch_manager.patch("core.nodes.netclient.get_net_client") patch_manager.patch_obj( LinuxNetClient, "get_mac", return_value="00:00:00:00:00:00" @@ -63,7 +63,6 @@ def patcher(request): patch_manager.patch_obj(CoreNode, "nodefile") patch_manager.patch_obj(Session, "write_state") patch_manager.patch_obj(Session, "write_nodes") - patch_manager.patch_obj(EmaneManager, "buildxml") yield patch_manager patch_manager.shutdown() @@ -89,7 +88,7 @@ def ip_prefixes(): @pytest.fixture(scope="session") -def interface_helper(): +def iface_helper(): return InterfaceHelper(ip4_prefix="10.83.0.0/16") diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 2d90ebccb..f51e30b91 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -3,6 +3,7 @@ """ import os from tempfile import TemporaryFile +from typing import Type from xml.etree import ElementTree import pytest @@ -14,7 +15,7 @@ from core.emane.nodes import EmaneNet from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.session import Session from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode @@ -43,7 +44,9 @@ def ping( class TestEmane: @pytest.mark.parametrize("model", _EMANE_MODELS) - def test_models(self, session: Session, model: EmaneModel, ip_prefixes: IpPrefixes): + def test_models( + self, session: Session, model: Type[EmaneModel], ip_prefixes: IpPrefixes + ): """ Test emane models within a basic network. @@ -70,20 +73,20 @@ def test_models(self, session: Session, model: EmaneModel, ip_prefixes: IpPrefix # create nodes options = NodeOptions(model="mdr") options.set_position(150, 150) - node_one = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) options.set_position(300, 150) - node_two = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) - for i, node in enumerate([node_one, node_two]): + for i, node in enumerate([node1, node2]): node.setposition(x=150 * (i + 1), y=150) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, emane_network.id, interface_one=interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, emane_network.id, iface1_data=iface_data) # instantiate session session.instantiate() - # ping n2 from n1 and assert success - status = ping(node_one, node_two, ip_prefixes, count=5) + # ping node2 from node1 and assert success + status = ping(node1, node2, ip_prefixes, count=5) assert not status def test_xml_emane( @@ -110,22 +113,22 @@ def test_xml_emane( # create nodes options = NodeOptions(model="mdr") options.set_position(150, 150) - node_one = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) options.set_position(300, 150) - node_two = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) - for i, node in enumerate([node_one, node_two]): + for i, node in enumerate([node1, node2]): node.setposition(x=150 * (i + 1), y=150) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, emane_network.id, interface_one=interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, emane_network.id, iface1_data=iface_data) # instantiate session session.instantiate() # get ids for nodes emane_id = emane_network.id - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -141,9 +144,9 @@ def test_xml_emane( # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) @@ -154,7 +157,7 @@ def test_xml_emane( ) # verify nodes and configuration were restored - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) assert session.get_node(emane_id, EmaneNet) assert value == config_value diff --git a/daemon/tests/test_conf.py b/daemon/tests/test_conf.py index 1973dceea..e90acfbd6 100644 --- a/daemon/tests/test_conf.py +++ b/daemon/tests/test_conf.py @@ -14,11 +14,11 @@ class TestConfigurableOptions(ConfigurableOptions): - name_one = "value1" - name_two = "value2" + name1 = "value1" + name2 = "value2" options = [ - Configuration(_id=name_one, _type=ConfigDataTypes.STRING, label=name_one), - Configuration(_id=name_two, _type=ConfigDataTypes.STRING, label=name_two), + Configuration(_id=name1, _type=ConfigDataTypes.STRING, label=name1), + Configuration(_id=name2, _type=ConfigDataTypes.STRING, label=name2), ] @@ -33,11 +33,11 @@ def test_configurable_options_default(self): # then assert len(default_values) == 2 - assert TestConfigurableOptions.name_one in default_values - assert TestConfigurableOptions.name_two in default_values + assert TestConfigurableOptions.name1 in default_values + assert TestConfigurableOptions.name2 in default_values assert len(instance_default_values) == 2 - assert TestConfigurableOptions.name_one in instance_default_values - assert TestConfigurableOptions.name_two in instance_default_values + assert TestConfigurableOptions.name1 in instance_default_values + assert TestConfigurableOptions.name2 in instance_default_values def test_nodes(self): # given diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 68515a41c..c44658631 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -8,7 +8,7 @@ import pytest -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import MessageFlags from core.emulator.session import Session from core.errors import CoreCommandError @@ -48,19 +48,19 @@ def test_wired_ping( net_node = session.add_node(net_type) # create nodes - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) # link nodes to net node - for node in [node_one, node_two]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, net_node.id, interface_one=interface) + for node in [node1, node2]: + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, net_node.id, iface1_data=iface_data) # instantiate session session.instantiate() - # ping n2 from n1 and assert success - status = ping(node_one, node_two, ip_prefixes) + # ping node2 from node1 and assert success + status = ping(node1, node2, ip_prefixes) assert not status def test_vnode_client(self, request, session: Session, ip_prefixes: IpPrefixes): @@ -75,16 +75,16 @@ def test_vnode_client(self, request, session: Session, ip_prefixes: IpPrefixes): ptp_node = session.add_node(PtpNet) # create nodes - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) # link nodes to ptp net - for node in [node_one, node_two]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface_one=interface) + for node in [node1, node2]: + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, ptp_node.id, iface1_data=iface_data) # get node client for testing - client = node_one.client + client = node1.client # instantiate session session.instantiate() @@ -96,9 +96,9 @@ def test_vnode_client(self, request, session: Session, ip_prefixes: IpPrefixes): if not request.config.getoption("mock"): assert client.check_cmd("echo hello") == "hello" - def test_netif(self, session: Session, ip_prefixes: IpPrefixes): + def test_iface(self, session: Session, ip_prefixes: IpPrefixes): """ - Test netif methods. + Test interface methods. :param session: session for test :param ip_prefixes: generates ip addresses for nodes @@ -108,37 +108,37 @@ def test_netif(self, session: Session, ip_prefixes: IpPrefixes): ptp_node = session.add_node(PtpNet) # create nodes - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) # link nodes to ptp net - for node in [node_one, node_two]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface_one=interface) + for node in [node1, node2]: + iface = ip_prefixes.create_iface(node) + session.add_link(node.id, ptp_node.id, iface1_data=iface) # instantiate session session.instantiate() # check link data gets generated - assert ptp_node.all_link_data(MessageFlags.ADD) + assert ptp_node.links(MessageFlags.ADD) # check common nets exist between linked nodes - assert node_one.commonnets(node_two) - assert node_two.commonnets(node_one) + assert node1.commonnets(node2) + assert node2.commonnets(node1) - # check we can retrieve netif index - assert node_one.ifname(0) - assert node_two.ifname(0) + # check we can retrieve interface id + assert 0 in node1.ifaces + assert 0 in node2.ifaces # check interface parameters - interface = node_one.netif(0) - interface.setparam("test", 1) - assert interface.getparam("test") == 1 - assert interface.getparams() + iface = node1.get_iface(0) + iface.setparam("test", 1) + assert iface.getparam("test") == 1 + assert iface.getparams() - # delete netif and test that if no longer exists - node_one.delnetif(0) - assert not node_one.netif(0) + # delete interface and test that if no longer exists + node1.delete_iface(0) + assert 0 not in node1.ifaces def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes): """ @@ -155,19 +155,19 @@ def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes): # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(CoreNode, options=options) - node_two = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # link nodes - for node in [node_one, node_two]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan_node.id, interface_one=interface) + for node in [node1, node2]: + iface_id = ip_prefixes.create_iface(node) + session.add_link(node.id, wlan_node.id, iface1_data=iface_id) # instantiate session session.instantiate() - # ping n2 from n1 and assert success - status = ping(node_one, node_two, ip_prefixes) + # ping node2 from node1 and assert success + status = ping(node1, node2, ip_prefixes) assert not status def test_mobility(self, session: Session, ip_prefixes: IpPrefixes): @@ -185,13 +185,13 @@ def test_mobility(self, session: Session, ip_prefixes: IpPrefixes): # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(CoreNode, options=options) - node_two = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # link nodes - for node in [node_one, node_two]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan_node.id, interface_one=interface) + for node in [node1, node2]: + iface_id = ip_prefixes.create_iface(node) + session.add_link(node.id, wlan_node.id, iface1_data=iface_id) # configure mobility script for session config = { diff --git a/daemon/tests/test_distributed.py b/daemon/tests/test_distributed.py index 0f4b1731d..01362cae9 100644 --- a/daemon/tests/test_distributed.py +++ b/daemon/tests/test_distributed.py @@ -1,4 +1,4 @@ -from core.emulator.emudata import NodeOptions +from core.emulator.data import NodeOptions from core.emulator.session import Session from core.nodes.base import CoreNode from core.nodes.network import HubNode diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index c0686d71f..a4efd6d9c 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -18,8 +18,7 @@ from core.api.tlv.enumerations import ConfigFlags from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet -from core.emulator.data import EventData, NodeData -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import EventData, IpPrefixes, NodeData, NodeOptions from core.emulator.enumerations import EventTypes, ExceptionLevels, NodeTypes from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility @@ -34,23 +33,25 @@ def test_start_session(self, grpc_server: CoreGrpcServer): client = CoreGrpcClient() session = grpc_server.coreemu.create_session() position = core_pb2.Position(x=50, y=100) - node_one = core_pb2.Node(id=1, position=position, model="PC") + node1 = core_pb2.Node(id=1, position=position, model="PC") position = core_pb2.Position(x=100, y=100) - node_two = core_pb2.Node(id=2, position=position, model="PC") + node2 = core_pb2.Node(id=2, position=position, model="PC") position = core_pb2.Position(x=200, y=200) wlan_node = core_pb2.Node( id=3, type=NodeTypes.WIRELESS_LAN.value, position=position ) - nodes = [node_one, node_two, wlan_node] - interface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") - interface_one = interface_helper.create_interface(node_one.id, 0) - interface_two = interface_helper.create_interface(node_two.id, 0) + nodes = [node1, node2, wlan_node] + iface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") + iface1_id = 0 + iface1 = iface_helper.create_iface(node1.id, iface1_id) + iface2_id = 0 + iface2 = iface_helper.create_iface(node2.id, iface2_id) link = core_pb2.Link( type=core_pb2.LinkType.WIRED, - node_one_id=node_one.id, - node_two_id=node_two.id, - interface_one=interface_one, - interface_two=interface_two, + node1_id=node1.id, + node2_id=node2.id, + iface1=iface1, + iface2=iface2, ) links = [link] hook = core_pb2.Hook( @@ -81,7 +82,7 @@ def test_start_session(self, grpc_server: CoreGrpcServer): model_config_value = "500000" model_config = EmaneModelConfig( node_id=model_node_id, - interface_id=-1, + iface_id=-1, model=EmaneIeee80211abgModel.name, config={model_config_key: model_config_value}, ) @@ -99,11 +100,11 @@ def test_start_session(self, grpc_server: CoreGrpcServer): ) mobility_configs = [mobility_config] service_config = ServiceConfig( - node_id=node_one.id, service="DefaultRoute", validate=["echo hello"] + node_id=node1.id, service="DefaultRoute", validate=["echo hello"] ) service_configs = [service_config] service_file_config = ServiceFileConfig( - node_id=node_one.id, + node_id=node1.id, service="DefaultRoute", file="defaultroute.sh", data="echo hello", @@ -128,12 +129,12 @@ def test_start_session(self, grpc_server: CoreGrpcServer): ) # then - assert node_one.id in session.nodes - assert node_two.id in session.nodes + assert node1.id in session.nodes + assert node2.id in session.nodes assert wlan_node.id in session.nodes - assert session.nodes[node_one.id].netif(0) is not None - assert session.nodes[node_two.id].netif(0) is not None - hook_file, hook_data = session._hooks[EventTypes.RUNTIME_STATE][0] + assert iface1_id in session.nodes[node1.id].ifaces + assert iface2_id in session.nodes[node2.id].ifaces + hook_file, hook_data = session.hooks[EventTypes.RUNTIME_STATE][0] assert hook_file == hook.file assert hook_data == hook.data assert session.location.refxyz == (location_x, location_y, location_z) @@ -153,11 +154,11 @@ def test_start_session(self, grpc_server: CoreGrpcServer): ) assert set_model_config[model_config_key] == model_config_value service = session.services.get_service( - node_one.id, service_config.service, default_service=True + node1.id, service_config.service, default_service=True ) assert service.validate == tuple(service_config.validate) service_file = session.services.get_service_file( - node_one, service_file_config.service, service_file_config.file + node1, service_file_config.service, service_file_config.file ) assert service_file.data == service_file_config.data @@ -522,8 +523,8 @@ def test_get_node_links(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefix session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface_data) # then with client.context_connect(): @@ -540,35 +541,33 @@ def test_get_node_links_exception( session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface_data) # then with pytest.raises(grpc.RpcError): with client.context_connect(): client.get_node_links(session.id, 3) - def test_add_link( - self, grpc_server: CoreGrpcServer, interface_helper: InterfaceHelper - ): + def test_add_link(self, grpc_server: CoreGrpcServer, iface_helper: InterfaceHelper): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) - assert len(switch.all_link_data()) == 0 + assert len(switch.links()) == 0 # then - interface = interface_helper.create_interface(node.id, 0) + iface = iface_helper.create_iface(node.id, 0) with client.context_connect(): - response = client.add_link(session.id, node.id, switch.id, interface) + response = client.add_link(session.id, node.id, switch.id, iface) # then assert response.result is True - assert len(switch.all_link_data()) == 1 + assert len(switch.links()) == 1 def test_add_link_exception( - self, grpc_server: CoreGrpcServer, interface_helper: InterfaceHelper + self, grpc_server: CoreGrpcServer, iface_helper: InterfaceHelper ): # given client = CoreGrpcClient() @@ -576,10 +575,10 @@ def test_add_link_exception( node = session.add_node(CoreNode) # then - interface = interface_helper.create_interface(node.id, 0) + iface = iface_helper.create_iface(node.id, 0) with pytest.raises(grpc.RpcError): with client.context_connect(): - client.add_link(session.id, 1, 3, interface) + client.add_link(session.id, 1, 3, iface) def test_edit_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): # given @@ -587,49 +586,49 @@ def test_edit_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface) + iface = ip_prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface) options = core_pb2.LinkOptions(bandwidth=30000) - link = switch.all_link_data()[0] - assert options.bandwidth != link.bandwidth + link = switch.links()[0] + assert options.bandwidth != link.options.bandwidth # then with client.context_connect(): response = client.edit_link( - session.id, node.id, switch.id, options, interface_one_id=interface.id + session.id, node.id, switch.id, options, iface1_id=iface.id ) # then assert response.result is True - link = switch.all_link_data()[0] - assert options.bandwidth == link.bandwidth + link = switch.links()[0] + assert options.bandwidth == link.options.bandwidth def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node_one = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) - node_two = session.add_node(CoreNode) - interface_two = ip_prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + node1 = session.add_node(CoreNode) + iface1 = ip_prefixes.create_iface(node1) + node2 = session.add_node(CoreNode) + iface2 = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface1, iface2) link_node = None for node_id in session.nodes: node = session.nodes[node_id] - if node.id not in {node_one.id, node_two.id}: + if node.id not in {node1.id, node2.id}: link_node = node break - assert len(link_node.all_link_data(0)) == 1 + assert len(link_node.links()) == 1 # then with client.context_connect(): response = client.delete_link( - session.id, node_one.id, node_two.id, interface_one.id, interface_two.id + session.id, node1.id, node2.id, iface1.id, iface2.id ) # then assert response.result is True - assert len(link_node.all_link_data(0)) == 0 + assert len(link_node.links()) == 0 def test_get_wlan_config(self, grpc_server: CoreGrpcServer): # given @@ -729,7 +728,7 @@ def test_get_emane_model_configs(self, grpc_server: CoreGrpcServer): assert emane_network.id == model_config.node_id assert model_config.model == EmaneIeee80211abgModel.name assert len(model_config.config) > 0 - assert model_config.interface == -1 + assert model_config.iface_id == -1 def test_set_emane_model_config(self, grpc_server: CoreGrpcServer): # given @@ -1028,9 +1027,9 @@ def test_link_events(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes) session = grpc_server.coreemu.create_session() wlan = session.add_node(WlanNode) node = session.add_node(CoreNode) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan.id, interface) - link_data = wlan.all_link_data()[0] + iface = ip_prefixes.create_iface(node) + session.add_link(node.id, wlan.id, iface) + link_data = wlan.links()[0] queue = Queue() def handle_event(event_data): @@ -1199,9 +1198,10 @@ def test_move_nodes_geo(self, grpc_server: CoreGrpcServer): queue = Queue() def node_handler(node_data: NodeData): - assert node_data.longitude == lon - assert node_data.latitude == lat - assert node_data.altitude == alt + n = node_data.node + assert n.position.lon == lon + assert n.position.lat == lat + assert n.position.alt == alt queue.put(node_data) session.node_handlers.append(node_handler) diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 800a8e620..a0b3bd8ab 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -50,12 +50,13 @@ def test_node_add( self, coretlv: CoreHandler, node_type: NodeTypes, model: Optional[str] ): node_id = 1 + name = "node1" message = coreapi.CoreNodeMessage.create( MessageFlags.ADD.value, [ (NodeTlvs.NUMBER, node_id), (NodeTlvs.TYPE, node_type.value), - (NodeTlvs.NAME, "n1"), + (NodeTlvs.NAME, name), (NodeTlvs.X_POSITION, 0), (NodeTlvs.Y_POSITION, 0), (NodeTlvs.MODEL, model), @@ -63,7 +64,9 @@ def test_node_add( ) coretlv.handle_message(message) - assert coretlv.session.get_node(node_id, NodeBase) is not None + node = coretlv.session.get_node(node_id, NodeBase) + assert node + assert node.name == name def test_node_update(self, coretlv: CoreHandler): node_id = 1 @@ -99,72 +102,72 @@ def test_node_delete(self, coretlv: CoreHandler): coretlv.session.get_node(node_id, NodeBase) def test_link_add_node_to_net(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - switch = 2 - coretlv.session.add_node(SwitchNode, _id=switch) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + switch_id = 2 + coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) + iface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) - all_links = switch_node.all_link_data() + switch_node = coretlv.session.get_node(switch_id, SwitchNode) + all_links = switch_node.links() assert len(all_links) == 1 def test_link_add_net_to_node(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - switch = 2 - coretlv.session.add_node(SwitchNode, _id=switch) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + switch_id = 2 + coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) + iface2_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, switch), - (LinkTlvs.N2_NUMBER, node_one), - (LinkTlvs.INTERFACE2_NUMBER, 0), - (LinkTlvs.INTERFACE2_IP4, interface_one), - (LinkTlvs.INTERFACE2_IP4_MASK, 24), + (LinkTlvs.N1_NUMBER, switch_id), + (LinkTlvs.N2_NUMBER, node1_id), + (LinkTlvs.IFACE2_NUMBER, 0), + (LinkTlvs.IFACE2_IP4, iface2_ip4), + (LinkTlvs.IFACE2_IP4_MASK, 24), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) - all_links = switch_node.all_link_data() + switch_node = coretlv.session.get_node(switch_id, SwitchNode) + all_links = switch_node.links() assert len(all_links) == 1 def test_link_add_node_to_node(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - node_two = 2 - coretlv.session.add_node(CoreNode, _id=node_two) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + node2_id = 2 + coretlv.session.add_node(CoreNode, _id=node2_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) - interface_two = str(ip_prefix[node_two]) + iface1_ip4 = str(ip_prefix[node1_id]) + iface2_ip4 = str(ip_prefix[node2_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, node_two), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), - (LinkTlvs.INTERFACE2_NUMBER, 0), - (LinkTlvs.INTERFACE2_IP4, interface_two), - (LinkTlvs.INTERFACE2_IP4_MASK, 24), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, node2_id), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), + (LinkTlvs.IFACE2_NUMBER, 0), + (LinkTlvs.IFACE2_IP4, iface2_ip4), + (LinkTlvs.IFACE2_IP4_MASK, 24), ], ) @@ -173,85 +176,85 @@ def test_link_add_node_to_node(self, coretlv: CoreHandler): all_links = [] for node_id in coretlv.session.nodes: node = coretlv.session.nodes[node_id] - all_links += node.all_link_data() + all_links += node.links() assert len(all_links) == 1 def test_link_update(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - switch = 2 - coretlv.session.add_node(SwitchNode, _id=switch) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + switch_id = 2 + coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) + iface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) - all_links = switch_node.all_link_data() + switch_node = coretlv.session.get_node(switch_id, SwitchNode) + all_links = switch_node.links() assert len(all_links) == 1 link = all_links[0] - assert link.bandwidth is None + assert link.options.bandwidth is None bandwidth = 50000 message = coreapi.CoreLinkMessage.create( 0, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), - (LinkTlvs.INTERFACE1_NUMBER, 0), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), + (LinkTlvs.IFACE1_NUMBER, 0), (LinkTlvs.BANDWIDTH, bandwidth), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) - all_links = switch_node.all_link_data() + switch_node = coretlv.session.get_node(switch_id, SwitchNode) + all_links = switch_node.links() assert len(all_links) == 1 link = all_links[0] - assert link.bandwidth == bandwidth + assert link.options.bandwidth == bandwidth def test_link_delete_node_to_node(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - node_two = 2 - coretlv.session.add_node(CoreNode, _id=node_two) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + node2_id = 2 + coretlv.session.add_node(CoreNode, _id=node2_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) - interface_two = str(ip_prefix[node_two]) + iface1_ip4 = str(ip_prefix[node1_id]) + iface2_ip4 = str(ip_prefix[node2_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, node_two), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), - (LinkTlvs.INTERFACE2_IP4, interface_two), - (LinkTlvs.INTERFACE2_IP4_MASK, 24), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, node2_id), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), + (LinkTlvs.IFACE2_IP4, iface2_ip4), + (LinkTlvs.IFACE2_IP4_MASK, 24), ], ) coretlv.handle_message(message) all_links = [] for node_id in coretlv.session.nodes: node = coretlv.session.nodes[node_id] - all_links += node.all_link_data() + all_links += node.links() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( MessageFlags.DELETE.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, node_two), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE2_NUMBER, 0), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, node2_id), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE2_NUMBER, 0), ], ) coretlv.handle_message(message) @@ -259,79 +262,79 @@ def test_link_delete_node_to_node(self, coretlv: CoreHandler): all_links = [] for node_id in coretlv.session.nodes: node = coretlv.session.nodes[node_id] - all_links += node.all_link_data() + all_links += node.links() assert len(all_links) == 0 def test_link_delete_node_to_net(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - switch = 2 - coretlv.session.add_node(SwitchNode, _id=switch) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + switch_id = 2 + coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) + iface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) - all_links = switch_node.all_link_data() + switch_node = coretlv.session.get_node(switch_id, SwitchNode) + all_links = switch_node.links() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( MessageFlags.DELETE.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), - (LinkTlvs.INTERFACE1_NUMBER, 0), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), + (LinkTlvs.IFACE1_NUMBER, 0), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) - all_links = switch_node.all_link_data() + switch_node = coretlv.session.get_node(switch_id, SwitchNode) + all_links = switch_node.links() assert len(all_links) == 0 def test_link_delete_net_to_node(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - switch = 2 - coretlv.session.add_node(SwitchNode, _id=switch) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + switch_id = 2 + coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) + iface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) - all_links = switch_node.all_link_data() + switch_node = coretlv.session.get_node(switch_id, SwitchNode) + all_links = switch_node.links() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( MessageFlags.DELETE.value, [ - (LinkTlvs.N1_NUMBER, switch), - (LinkTlvs.N2_NUMBER, node_one), - (LinkTlvs.INTERFACE2_NUMBER, 0), + (LinkTlvs.N1_NUMBER, switch_id), + (LinkTlvs.N2_NUMBER, node1_id), + (LinkTlvs.IFACE2_NUMBER, 0), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) - all_links = switch_node.all_link_data() + switch_node = coretlv.session.get_node(switch_id, SwitchNode) + all_links = switch_node.links() assert len(all_links) == 0 def test_session_update(self, coretlv: CoreHandler): @@ -379,7 +382,7 @@ def test_session_delete(self, coretlv: CoreHandler): def test_file_hook_add(self, coretlv: CoreHandler): state = EventTypes.DATACOLLECT_STATE - assert coretlv.session._hooks.get(state) is None + assert coretlv.session.hooks.get(state) is None file_name = "test.sh" file_data = "echo hello" message = coreapi.CoreFileMessage.create( @@ -393,7 +396,7 @@ def test_file_hook_add(self, coretlv: CoreHandler): coretlv.handle_message(message) - hooks = coretlv.session._hooks.get(state) + hooks = coretlv.session.hooks.get(state) assert len(hooks) == 1 name, data = hooks[0] assert file_name == name diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 9736537ea..535ad8372 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -1,6 +1,6 @@ from typing import Tuple -from core.emulator.emudata import IpPrefixes, LinkOptions +from core.emulator.data import IpPrefixes, LinkOptions from core.emulator.session import Session from core.nodes.base import CoreNode from core.nodes.network import SwitchNode @@ -10,126 +10,222 @@ def create_ptp_network( session: Session, ip_prefixes: IpPrefixes ) -> Tuple[CoreNode, CoreNode]: # create nodes - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) # link nodes to net node - interface_one = ip_prefixes.create_interface(node_one) - interface_two = ip_prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface1_data, iface2_data) # instantiate session session.instantiate() - return node_one, node_two + return node1, node2 class TestLinks: - def test_ptp(self, session: Session, ip_prefixes: IpPrefixes): + def test_add_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) - interface_two = ip_prefixes.create_interface(node_two) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) # when - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + session.add_link(node1.id, node2.id, iface1_data, iface2_data) # then - assert node_one.netif(interface_one.id) - assert node_two.netif(interface_two.id) + assert node1.get_iface(iface1_data.id) + assert node2.get_iface(iface2_data.id) - def test_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): + def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given - node_one = session.add_node(CoreNode) - node_two = session.add_node(SwitchNode) - interface_one = ip_prefixes.create_interface(node_one) + node1 = session.add_node(CoreNode) + node2 = session.add_node(SwitchNode) + iface1_data = ip_prefixes.create_iface(node1) # when - session.add_link(node_one.id, node_two.id, interface_one) + session.add_link(node1.id, node2.id, iface1_data=iface1_data) # then - assert node_two.all_link_data() - assert node_one.netif(interface_one.id) + assert node2.links() + assert node1.get_iface(iface1_data.id) - def test_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): + def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given - node_one = session.add_node(SwitchNode) - node_two = session.add_node(CoreNode) - interface_two = ip_prefixes.create_interface(node_two) + node1 = session.add_node(SwitchNode) + node2 = session.add_node(CoreNode) + iface2_data = ip_prefixes.create_iface(node2) # when - session.add_link(node_one.id, node_two.id, interface_two=interface_two) + session.add_link(node1.id, node2.id, iface2_data=iface2_data) # then - assert node_one.all_link_data() - assert node_two.netif(interface_two.id) + assert node1.links() + assert node2.get_iface(iface2_data.id) - def test_net_to_net(self, session): + def test_add_net_to_net(self, session): # given - node_one = session.add_node(SwitchNode) - node_two = session.add_node(SwitchNode) + node1 = session.add_node(SwitchNode) + node2 = session.add_node(SwitchNode) # when - session.add_link(node_one.id, node_two.id) + session.add_link(node1.id, node2.id) # then - assert node_one.all_link_data() + assert node1.links() - def test_link_update(self, session: Session, ip_prefixes: IpPrefixes): + def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given delay = 50 bandwidth = 5000000 - per = 25 + loss = 25 dup = 25 jitter = 10 - node_one = session.add_node(CoreNode) - node_two = session.add_node(SwitchNode) - interface_one_data = ip_prefixes.create_interface(node_one) - session.add_link(node_one.id, node_two.id, interface_one_data) - interface_one = node_one.netif(interface_one_data.id) - assert interface_one.getparam("delay") != delay - assert interface_one.getparam("bw") != bandwidth - assert interface_one.getparam("loss") != per - assert interface_one.getparam("duplicate") != dup - assert interface_one.getparam("jitter") != jitter + node1 = session.add_node(CoreNode) + node2 = session.add_node(SwitchNode) + iface1_data = ip_prefixes.create_iface(node1) + session.add_link(node1.id, node2.id, iface1_data) + iface1 = node1.get_iface(iface1_data.id) + assert iface1.getparam("delay") != delay + assert iface1.getparam("bw") != bandwidth + assert iface1.getparam("loss") != loss + assert iface1.getparam("duplicate") != dup + assert iface1.getparam("jitter") != jitter # when - link_options = LinkOptions() - link_options.delay = delay - link_options.bandwidth = bandwidth - link_options.per = per - link_options.dup = dup - link_options.jitter = jitter + options = LinkOptions( + delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter + ) + session.update_link( + node1.id, node2.id, iface1_id=iface1_data.id, options=options + ) + + # then + assert iface1.getparam("delay") == delay + assert iface1.getparam("bw") == bandwidth + assert iface1.getparam("loss") == loss + assert iface1.getparam("duplicate") == dup + assert iface1.getparam("jitter") == jitter + + def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): + # given + delay = 50 + bandwidth = 5000000 + loss = 25 + dup = 25 + jitter = 10 + node1 = session.add_node(SwitchNode) + node2 = session.add_node(CoreNode) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface2_data=iface2_data) + iface2 = node2.get_iface(iface2_data.id) + assert iface2.getparam("delay") != delay + assert iface2.getparam("bw") != bandwidth + assert iface2.getparam("loss") != loss + assert iface2.getparam("duplicate") != dup + assert iface2.getparam("jitter") != jitter + + # when + options = LinkOptions( + delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter + ) session.update_link( - node_one.id, - node_two.id, - interface_one_id=interface_one_data.id, - options=link_options, + node1.id, node2.id, iface2_id=iface2_data.id, options=options ) # then - assert interface_one.getparam("delay") == delay - assert interface_one.getparam("bw") == bandwidth - assert interface_one.getparam("loss") == per - assert interface_one.getparam("duplicate") == dup - assert interface_one.getparam("jitter") == jitter + assert iface2.getparam("delay") == delay + assert iface2.getparam("bw") == bandwidth + assert iface2.getparam("loss") == loss + assert iface2.getparam("duplicate") == dup + assert iface2.getparam("jitter") == jitter - def test_link_delete(self, session: Session, ip_prefixes: IpPrefixes): + def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) - interface_two = ip_prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_one, interface_two) - assert node_one.netif(interface_one.id) - assert node_two.netif(interface_two.id) + delay = 50 + bandwidth = 5000000 + loss = 25 + dup = 25 + jitter = 10 + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface1_data, iface2_data) + iface1 = node1.get_iface(iface1_data.id) + iface2 = node2.get_iface(iface2_data.id) + assert iface1.getparam("delay") != delay + assert iface1.getparam("bw") != bandwidth + assert iface1.getparam("loss") != loss + assert iface1.getparam("duplicate") != dup + assert iface1.getparam("jitter") != jitter + assert iface2.getparam("delay") != delay + assert iface2.getparam("bw") != bandwidth + assert iface2.getparam("loss") != loss + assert iface2.getparam("duplicate") != dup + assert iface2.getparam("jitter") != jitter # when - session.delete_link( - node_one.id, node_two.id, interface_one.id, interface_two.id + options = LinkOptions( + delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter ) + session.update_link(node1.id, node2.id, iface1_data.id, iface2_data.id, options) + + # then + assert iface1.getparam("delay") == delay + assert iface1.getparam("bw") == bandwidth + assert iface1.getparam("loss") == loss + assert iface1.getparam("duplicate") == dup + assert iface1.getparam("jitter") == jitter + assert iface2.getparam("delay") == delay + assert iface2.getparam("bw") == bandwidth + assert iface2.getparam("loss") == loss + assert iface2.getparam("duplicate") == dup + assert iface2.getparam("jitter") == jitter + + def test_delete_ptp(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface1_data, iface2_data) + assert node1.get_iface(iface1_data.id) + assert node2.get_iface(iface2_data.id) + + # when + session.delete_link(node1.id, node2.id, iface1_data.id, iface2_data.id) + + # then + assert iface1_data.id not in node1.ifaces + assert iface2_data.id not in node2.ifaces + + def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(CoreNode) + node2 = session.add_node(SwitchNode) + iface1_data = ip_prefixes.create_iface(node1) + session.add_link(node1.id, node2.id, iface1_data) + assert node1.get_iface(iface1_data.id) + + # when + session.delete_link(node1.id, node2.id, iface1_id=iface1_data.id) + + # then + assert iface1_data.id not in node1.ifaces + + def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(SwitchNode) + node2 = session.add_node(CoreNode) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface2_data=iface2_data) + assert node2.get_iface(iface2_data.id) + + # when + session.delete_link(node1.id, node2.id, iface2_id=iface2_data.id) # then - assert not node_one.netif(interface_one.id) - assert not node_two.netif(interface_two.id) + assert iface2_data.id not in node2.ifaces diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 26e78367b..8ed21f277 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -1,6 +1,6 @@ import pytest -from core.emulator.emudata import InterfaceData, NodeOptions +from core.emulator.data import InterfaceData, NodeOptions from core.emulator.session import Session from core.errors import CoreError from core.nodes.base import CoreNode @@ -49,61 +49,76 @@ def test_node_delete(self, session: Session): with pytest.raises(CoreError): session.get_node(node.id, CoreNode) - def test_node_sethwaddr(self, session: Session): + @pytest.mark.parametrize( + "mac,expected", + [ + ("AA-AA-AA-FF-FF-FF", "aa:aa:aa:ff:ff:ff"), + ("00:00:00:FF:FF:FF", "00:00:00:ff:ff:ff"), + ], + ) + def test_node_set_mac(self, session: Session, mac: str, expected: str): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) - interface_data = InterfaceData() - index = node.newnetif(switch, interface_data) - interface = node.netif(index) - mac = "aa:aa:aa:ff:ff:ff" + iface_data = InterfaceData() + iface = node.new_iface(switch, iface_data) # when - node.sethwaddr(index, mac) + node.set_mac(iface.node_id, mac) # then - assert interface.hwaddr == mac + assert str(iface.mac) == expected - def test_node_sethwaddr_exception(self, session: Session): + @pytest.mark.parametrize( + "mac", ["AAA:AA:AA:FF:FF:FF", "AA:AA:AA:FF:FF", "AA/AA/AA/FF/FF/FF"] + ) + def test_node_set_mac_exception(self, session: Session, mac: str): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) - interface_data = InterfaceData() - index = node.newnetif(switch, interface_data) - node.netif(index) - mac = "aa:aa:aa:ff:ff:fff" + iface_data = InterfaceData() + iface = node.new_iface(switch, iface_data) # when with pytest.raises(CoreError): - node.sethwaddr(index, mac) - - def test_node_addaddr(self, session: Session): + node.set_mac(iface.node_id, mac) + + @pytest.mark.parametrize( + "ip,expected,is_ip6", + [ + ("127", "127.0.0.0/32", False), + ("10.0.0.1/24", "10.0.0.1/24", False), + ("2001::", "2001::/128", True), + ("2001::/64", "2001::/64", True), + ], + ) + def test_node_add_ip(self, session: Session, ip: str, expected: str, is_ip6: bool): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) - interface_data = InterfaceData() - index = node.newnetif(switch, interface_data) - interface = node.netif(index) - addr = "192.168.0.1/24" + iface_data = InterfaceData() + iface = node.new_iface(switch, iface_data) # when - node.addaddr(index, addr) + node.add_ip(iface.node_id, ip) # then - assert interface.addrlist[0] == addr + if is_ip6: + assert str(iface.get_ip6()) == expected + else: + assert str(iface.get_ip4()) == expected - def test_node_addaddr_exception(self, session): + def test_node_add_ip_exception(self, session): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) - interface_data = InterfaceData() - index = node.newnetif(switch, interface_data) - node.netif(index) - addr = "256.168.0.1/24" + iface_data = InterfaceData() + iface = node.new_iface(switch, iface_data) + ip = "256.168.0.1/24" # when with pytest.raises(CoreError): - node.addaddr(index, addr) + node.add_ip(iface.node_id, ip) @pytest.mark.parametrize("net_type", NET_TYPES) def test_net(self, session, net_type): diff --git a/daemon/tests/test_services.py b/daemon/tests/test_services.py index e304a2750..264a65663 100644 --- a/daemon/tests/test_services.py +++ b/daemon/tests/test_services.py @@ -206,23 +206,23 @@ def test_service_set_file(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) file_name = my_service.configs[0] - file_data_one = "# custom file one" - file_data_two = "# custom file two" + file_data1 = "# custom file one" + file_data2 = "# custom file two" session.services.set_service_file( - node_one.id, my_service.name, file_name, file_data_one + node1.id, my_service.name, file_name, file_data1 ) session.services.set_service_file( - node_two.id, my_service.name, file_name, file_data_two + node2.id, my_service.name, file_name, file_data2 ) # when - custom_service_one = session.services.get_service(node_one.id, my_service.name) - session.services.create_service_files(node_one, custom_service_one) - custom_service_two = session.services.get_service(node_two.id, my_service.name) - session.services.create_service_files(node_two, custom_service_two) + custom_service1 = session.services.get_service(node1.id, my_service.name) + session.services.create_service_files(node1, custom_service1) + custom_service2 = session.services.get_service(node2.id, my_service.name) + session.services.create_service_files(node2, custom_service2) def test_service_import(self): """ diff --git a/daemon/tests/test_utils.py b/daemon/tests/test_utils.py index 3e43b7898..5a4f25a4d 100644 --- a/daemon/tests/test_utils.py +++ b/daemon/tests/test_utils.py @@ -1,8 +1,6 @@ import netaddr -import pytest from core import utils -from core.errors import CoreError class TestUtils: @@ -25,42 +23,6 @@ def test_make_tuple_fromstr(self): assert len(two_args) == 2 assert len(unicode_args) == 3 - @pytest.mark.parametrize( - "data,expected", - [ - ("127", "127.0.0.0/32"), - ("10.0.0.1/24", "10.0.0.1/24"), - ("2001::", "2001::/128"), - ("2001::/64", "2001::/64"), - ], - ) - def test_validate_ip(self, data: str, expected: str): - value = utils.validate_ip(data) - assert value == expected - - @pytest.mark.parametrize("data", ["256", "1270.0.0.1", "127.0.0.0.1"]) - def test_validate_ip_exception(self, data: str): - with pytest.raises(CoreError): - utils.validate_ip("") - - @pytest.mark.parametrize( - "data,expected", - [ - ("AA-AA-AA-FF-FF-FF", "aa:aa:aa:ff:ff:ff"), - ("00:00:00:FF:FF:FF", "00:00:00:ff:ff:ff"), - ], - ) - def test_validate_mac(self, data: str, expected: str): - value = utils.validate_mac(data) - assert value == expected - - @pytest.mark.parametrize( - "data", ["AAA:AA:AA:FF:FF:FF", "AA:AA:AA:FF:FF", "AA/AA/AA/FF/FF/FF"] - ) - def test_validate_mac_exception(self, data: str): - with pytest.raises(CoreError): - utils.validate_mac(data) - def test_random_mac(self): value = utils.random_mac() assert netaddr.EUI(value) is not None diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index c40a9ef3e..fb8bc4d9d 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -3,7 +3,7 @@ import pytest -from core.emulator.emudata import IpPrefixes, LinkOptions, NodeOptions +from core.emulator.data import IpPrefixes, LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes from core.emulator.session import Session from core.errors import CoreError @@ -48,7 +48,7 @@ def test_xml_hooks(self, session: Session, tmpdir: TemporaryFile): session.open_xml(file_path, start=True) # verify nodes have been recreated - runtime_hooks = session._hooks.get(state) + runtime_hooks = session.hooks.get(state) assert runtime_hooks runtime_hook = runtime_hooks[0] assert file_name == runtime_hook[0] @@ -68,20 +68,20 @@ def test_xml_ptp( ptp_node = session.add_node(PtpNet) # create nodes - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) # link nodes to ptp net - for node in [node_one, node_two]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface_one=interface) + for node in [node1, node2]: + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, ptp_node.id, iface1_data=iface_data) # instantiate session session.instantiate() # get ids for nodes - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -97,16 +97,16 @@ def test_xml_ptp( # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) def test_xml_ptp_services( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -123,28 +123,28 @@ def test_xml_ptp_services( # create nodes options = NodeOptions(model="host") - node_one = session.add_node(CoreNode, options=options) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode) # link nodes to ptp net - for node in [node_one, node_two]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface_one=interface) + for node in [node1, node2]: + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, ptp_node.id, iface1_data=iface_data) # set custom values for node service - session.services.set_service(node_one.id, SshService.name) + session.services.set_service(node1.id, SshService.name) service_file = SshService.configs[0] file_data = "# test" session.services.set_service_file( - node_one.id, SshService.name, service_file, file_data + node1.id, SshService.name, service_file, file_data ) # instantiate session session.instantiate() # get ids for nodes - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -160,19 +160,19 @@ def test_xml_ptp_services( # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # retrieve custom service - service = session.services.get_service(node_one.id, SshService.name) + service = session.services.get_service(node1.id, SshService.name) # verify nodes have been recreated - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) assert service.config_data.get(service_file) == file_data def test_xml_mobility( @@ -192,21 +192,21 @@ def test_xml_mobility( # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(CoreNode, options=options) - node_two = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # link nodes - for node in [node_one, node_two]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan_node.id, interface_one=interface) + for node in [node1, node2]: + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, wlan_node.id, iface1_data=iface_data) # instantiate session session.instantiate() # get ids for nodes wlan_id = wlan_node.id - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -222,9 +222,9 @@ def test_xml_mobility( # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) @@ -233,8 +233,8 @@ def test_xml_mobility( value = str(session.mobility.get_config("test", wlan_id, BasicRangeModel.name)) # verify nodes and configuration were restored - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) assert session.get_node(wlan_id, WlanNode) assert value == "1" @@ -246,18 +246,18 @@ def test_network_to_network(self, session: Session, tmpdir: TemporaryFile): :param tmpdir: tmpdir to create data in """ # create nodes - switch_one = session.add_node(SwitchNode) - switch_two = session.add_node(SwitchNode) + switch1 = session.add_node(SwitchNode) + switch2 = session.add_node(SwitchNode) # link nodes - session.add_link(switch_one.id, switch_two.id) + session.add_link(switch1.id, switch2.id) # instantiate session session.instantiate() # get ids for nodes - n1_id = switch_one.id - n2_id = switch_two.id + node1_id = switch1.id + node2_id = switch2.id # save xml xml_file = tmpdir.join("session.xml") @@ -273,19 +273,19 @@ def test_network_to_network(self, session: Session, tmpdir: TemporaryFile): # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, SwitchNode) + assert not session.get_node(node1_id, SwitchNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, SwitchNode) + assert not session.get_node(node2_id, SwitchNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - switch_one = session.get_node(n1_id, SwitchNode) - switch_two = session.get_node(n2_id, SwitchNode) - assert switch_one - assert switch_two - assert len(switch_one.all_link_data() + switch_two.all_link_data()) == 1 + switch1 = session.get_node(node1_id, SwitchNode) + switch2 = session.get_node(node2_id, SwitchNode) + assert switch1 + assert switch2 + assert len(switch1.links() + switch2.links()) == 1 def test_link_options( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -298,25 +298,25 @@ def test_link_options( :param ip_prefixes: generates ip addresses for nodes """ # create nodes - node_one = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) + node1 = session.add_node(CoreNode) + iface1_data = ip_prefixes.create_iface(node1) switch = session.add_node(SwitchNode) # create link - link_options = LinkOptions() - link_options.per = 10.5 - link_options.bandwidth = 50000 - link_options.jitter = 10 - link_options.delay = 30 - link_options.dup = 5 - session.add_link(node_one.id, switch.id, interface_one, options=link_options) + options = LinkOptions() + options.loss = 10.5 + options.bandwidth = 50000 + options.jitter = 10 + options.delay = 30 + options.dup = 5 + session.add_link(node1.id, switch.id, iface1_data, options=options) # instantiate session session.instantiate() # get ids for nodes - n1_id = node_one.id - n2_id = switch.id + node1_id = node1.id + node2_id = switch.id # save xml xml_file = tmpdir.join("session.xml") @@ -332,26 +332,26 @@ def test_link_options( # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, SwitchNode) + assert not session.get_node(node2_id, SwitchNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, SwitchNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, SwitchNode) links = [] for node_id in session.nodes: node = session.nodes[node_id] - links += node.all_link_data() + links += node.links() link = links[0] - assert link_options.per == link.per - assert link_options.bandwidth == link.bandwidth - assert link_options.jitter == link.jitter - assert link_options.delay == link.delay - assert link_options.dup == link.dup + assert options.loss == link.options.loss + assert options.bandwidth == link.options.bandwidth + assert options.jitter == link.options.jitter + assert options.delay == link.options.delay + assert options.dup == link.options.dup def test_link_options_ptp( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -364,28 +364,26 @@ def test_link_options_ptp( :param ip_prefixes: generates ip addresses for nodes """ # create nodes - node_one = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) - node_two = session.add_node(CoreNode) - interface_two = ip_prefixes.create_interface(node_two) + node1 = session.add_node(CoreNode) + iface1_data = ip_prefixes.create_iface(node1) + node2 = session.add_node(CoreNode) + iface2_data = ip_prefixes.create_iface(node2) # create link - link_options = LinkOptions() - link_options.per = 10.5 - link_options.bandwidth = 50000 - link_options.jitter = 10 - link_options.delay = 30 - link_options.dup = 5 - session.add_link( - node_one.id, node_two.id, interface_one, interface_two, link_options - ) + options = LinkOptions() + options.loss = 10.5 + options.bandwidth = 50000 + options.jitter = 10 + options.delay = 30 + options.dup = 5 + session.add_link(node1.id, node2.id, iface1_data, iface2_data, options) # instantiate session session.instantiate() # get ids for nodes - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -401,26 +399,26 @@ def test_link_options_ptp( # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) links = [] for node_id in session.nodes: node = session.nodes[node_id] - links += node.all_link_data() + links += node.links() link = links[0] - assert link_options.per == link.per - assert link_options.bandwidth == link.bandwidth - assert link_options.jitter == link.jitter - assert link_options.delay == link.delay - assert link_options.dup == link.dup + assert options.loss == link.options.loss + assert options.bandwidth == link.options.bandwidth + assert options.jitter == link.options.jitter + assert options.delay == link.options.delay + assert options.dup == link.options.dup def test_link_options_bidirectional( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -433,43 +431,37 @@ def test_link_options_bidirectional( :param ip_prefixes: generates ip addresses for nodes """ # create nodes - node_one = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) - node_two = session.add_node(CoreNode) - interface_two = ip_prefixes.create_interface(node_two) + node1 = session.add_node(CoreNode) + iface1_data = ip_prefixes.create_iface(node1) + node2 = session.add_node(CoreNode) + iface2_data = ip_prefixes.create_iface(node2) # create link - link_options_one = LinkOptions() - link_options_one.unidirectional = 1 - link_options_one.bandwidth = 5000 - link_options_one.delay = 10 - link_options_one.per = 10.5 - link_options_one.dup = 5 - link_options_one.jitter = 5 - session.add_link( - node_one.id, node_two.id, interface_one, interface_two, link_options_one - ) - link_options_two = LinkOptions() - link_options_two.unidirectional = 1 - link_options_two.bandwidth = 10000 - link_options_two.delay = 20 - link_options_two.per = 10 - link_options_two.dup = 10 - link_options_two.jitter = 10 + options1 = LinkOptions() + options1.unidirectional = 1 + options1.bandwidth = 5000 + options1.delay = 10 + options1.loss = 10.5 + options1.dup = 5 + options1.jitter = 5 + session.add_link(node1.id, node2.id, iface1_data, iface2_data, options1) + options2 = LinkOptions() + options2.unidirectional = 1 + options2.bandwidth = 10000 + options2.delay = 20 + options2.loss = 10 + options2.dup = 10 + options2.jitter = 10 session.update_link( - node_two.id, - node_one.id, - interface_two.id, - interface_one.id, - link_options_two, + node2.id, node1.id, iface2_data.id, iface1_data.id, options2 ) # instantiate session session.instantiate() # get ids for nodes - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -485,30 +477,30 @@ def test_link_options_bidirectional( # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) links = [] for node_id in session.nodes: node = session.nodes[node_id] - links += node.all_link_data() + links += node.links() assert len(links) == 2 - link_one = links[0] - link_two = links[1] - assert link_options_one.bandwidth == link_one.bandwidth - assert link_options_one.delay == link_one.delay - assert link_options_one.per == link_one.per - assert link_options_one.dup == link_one.dup - assert link_options_one.jitter == link_one.jitter - assert link_options_two.bandwidth == link_two.bandwidth - assert link_options_two.delay == link_two.delay - assert link_options_two.per == link_two.per - assert link_options_two.dup == link_two.dup - assert link_options_two.jitter == link_two.jitter + link1 = links[0] + link2 = links[1] + assert options1.bandwidth == link1.options.bandwidth + assert options1.delay == link1.options.delay + assert options1.loss == link1.options.loss + assert options1.dup == link1.options.dup + assert options1.jitter == link1.options.jitter + assert options2.bandwidth == link2.options.bandwidth + assert options2.delay == link2.options.delay + assert options2.loss == link2.options.loss + assert options2.dup == link2.options.dup + assert options2.jitter == link2.options.jitter diff --git a/docs/devguide.md b/docs/devguide.md index c10bb0077..ba34a211c 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -16,7 +16,6 @@ daemon. Here is a brief description of the source directories. |gui|Tcl/Tk GUI| |man|Template files for creating man pages for various CORE command line utilities| |netns|C program for creating CORE containers| -|scripts|Template files used for running CORE as a service| ## Getting started @@ -34,21 +33,11 @@ git checkout develop ## Install the Development Environment This command will automatically install system dependencies, clone and build OSPF-MDR, -build CORE, setup the CORE pipenv environment, and install pre-commit hooks. - -This script is currently compatible with Ubuntu and CentOS, tested on Ubuntu 18.04 and -CentOS 7.6. The script also currently defaults to using python3.6, but a different -version of python can be targeted if python3.6 is not available on your system. +build CORE, setup the CORE poetry environment, and install pre-commit hooks. You can +refer to the [install docs](install.md) for issues related to different distributions. ```shell -# default dev install using python3.6 -./install.sh -d - -# providing a newer python version for ubuntu -./install.sh -d -v 3.7 - -# providing a newer python version for centos -./install.sh -d -v 37 +./install -d ``` ### pre-commit @@ -57,42 +46,24 @@ pre-commit hooks help automate running tools to check modified code. Every time python utilities will be ran to check validity of code, potentially failing and backing out the commit. These changes are currently mandated as part of the current CI, so add the changes and commit again. -### Adding EMANE to Pipenv - -EMANE bindings are not available through pip, you will need to build and install from source. - -[Build EMANE](https://github.com/adjacentlink/emane/wiki/Build#general-build-instructions) - -```shell -# clone emane repo -git clone https://github.com/adjacentlink/emane.git - -# install emane build deps -sudo apt install libxml2-dev libprotobuf-dev uuid-dev libpcap-dev protobuf-compiler - -# build emane -./autogen.sh -./configure --prefix=/usr -make -j8 - -# install emane binding in pipenv -# NOTE: this will mody pipenv Pipfiles and we do not want that, use git checkout -- Pipfile*, to remove changes -python3 -m pipenv pip install $EMANEREPO/src/python -``` - ## Running CORE -Commands below can be used to run the core-daemon, the new core gui, and tests. +You can now run core as you normally would, or leverage some of the invoke tasks to +conveniently run tests, etc. ```shell -# runs for daemon -sudo python3 -m pipenv run core +# run core-daemon +sudo core-daemon + +# run python gui +core-pygui -# runs coretk gui -python3 -m pipenv run core-pygui +# run tcl gui +core-gui -# runs mocked unit tests -python3 -m pipenv run test-mock +# run mocked unit tests +cd +inv test-mock ``` ## Linux Network Namespace Commands diff --git a/docs/distributed.md b/docs/distributed.md index f36efc722..ad3d61f8c 100644 --- a/docs/distributed.md +++ b/docs/distributed.md @@ -12,6 +12,28 @@ run on one of the emulation servers or on a separate machine. Each machine that will act as an emulation will require the installation of a distributed CORE package and some configuration to allow SSH as root. +## CORE Configuration + +CORE configuration settings required for using distributed functionality. + +Edit **/etc/core/core.conf** or specific configuration file being used. + +```shell +# uncomment and set this to the address that remote servers +# use to get back to the main host, example below +distributed_address = 129.168.0.101 +``` + +### EMANE Specific Configurations + +EMANE needs to have controlnet configured in **core.conf** in order to startup correctly. +The names before the addresses need to match the names of distributed servers configured. + +```shell +controlnet = core1:172.16.1.0/24 core2:172.16.2.0/24 core3:172.16.3.0/24 core4:172.16.4.0/24 core5:172.16.5.0/24 +emane_event_generate = True +``` + ## Configuring SSH Distributed CORE works using the python fabric library to run commands on @@ -88,6 +110,16 @@ PermitRootLogin without-password sudo systemctl restart sshd ``` +### Fabric Config File + +Make sure the value used below is the absolute path to the file +generated above **~/.ssh/core**" + +Add/update the fabric configuration file **/etc/fabric.yml**: +```yaml +connect_kwargs: {"key_filename": "/home/user/.ssh/core"} +``` + ## Add Emulation Servers in GUI Within the core-gui navigate to menu option: @@ -152,26 +184,13 @@ to arrange the topology such that the number of tunnels is minimized. The tunnels carry data between servers to connect nodes as specified in the topology. These tunnels are created using GRE tunneling, similar to the Tunnel Tool. -### EMANE Configuration and Issues - -EMANE needs to have controlnet configured in **core.conf** in order to startup correctly. -The names before the addresses need to match the servers configured in -**~/.core/servers.conf** previously. - -```shell -controlnet = core1:172.16.1.0/24 core2:172.16.2.0/24 core3:172.16.3.0/24 core4:172.16.4.0/24 core5:172.16.5.0/24 -``` - -```shell -emane_event_generate = True -``` - ## Distributed Checklist 1. Install CORE on master server 1. Install distributed CORE package on all servers needed 1. Installed and configure public-key SSH access on all servers (if you want to use double-click shells or Widgets.) for both the GUI user (for terminals) and root for running CORE commands +1. Update CORE configuration as needed 1. Choose the servers that participate in distributed emulation. 1. Assign nodes to desired servers, empty for master server. 1. Press the **Start** button to launch the distributed emulation. diff --git a/docs/grpc.md b/docs/grpc.md index 69cf4aedc..ca80256ee 100644 --- a/docs/grpc.md +++ b/docs/grpc.md @@ -15,20 +15,23 @@ properly account for this issue or clear out your proxy when running if needed. ## Python Client A python client wrapper is provided at -[CoreGrpcClient](../daemon/core/api/grpc/client.py) to help provide some -conveniences when using the API. +[CoreGrpcClient](https://github.com/coreemu/core/blob/master/daemon/core/api/grpc/client.py) +to help provide some conveniences when using the API. ## Proto Files Proto files are used to define the API and protobuf messages that are used for interfaces with this API. -They can be found [here](../daemon/proto/core/api/grpc) to see the specifics of +They can be found +[here](https://github.com/coreemu/core/tree/master/daemon/proto/core/api/grpc) +to see the specifics of what is going on and response message values that would be returned. ## Examples -Example usage of this API can be found [here](../daemon/examples/grpc). These -examples will create a session using the gRPC API when the core-daemon is running. +Example usage of this API can be found +[here](https://github.com/coreemu/core/tree/master/daemon/examples/grpc). +These examples will create a session using the gRPC API when the core-daemon is running. You can then switch to and attach to these sessions using either of the CORE GUIs. diff --git a/docs/install.md b/docs/install.md index 4a39218dd..604ac509b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,300 +1,222 @@ -# CORE Installation +# Installation * Table of Contents {:toc} ## Overview -This section will describe how to install CORE from source or from a pre-built package. -CORE has been vetted on Ubuntu 18 and CentOS 7.6. Other versions and distributions -can work, assuming you can get the required packages and versions similar to those -noted below for the tested distributions. +CORE provides a script to help automate installing all required software +to build and run, including a python virtual environment to run it all in. -> **NOTE:** iproute2 4.5+ is a requirement for bridge related commands +The following tools will be leveraged during installation: + +|Tool|Description| +|---|---| +|[pip](https://pip.pypa.io/en/stable/)|used to install pipx| +|[pipx](https://pipxproject.github.io/pipx/)|used to install standalone python tools (invoke, poetry)| +|[invoke](http://www.pyinvoke.org/)|used to run provided tasks (install, daemon, gui, tests, etc)| +|[poetry](https://python-poetry.org/)|used to install the managed python virtual environment for running CORE| ## Required Hardware Any computer capable of running Linux should be able to run CORE. Since the physical machine will be hosting numerous containers, as a general rule you should select a machine having as much RAM and CPU resources as possible. -## Operating System - -CORE requires a Linux operating system because it uses namespacing provided by the kernel. It does not run on -Windows or Mac OS X operating systems (unless it is running within a virtual machine guest.) The -technology that CORE currently uses is Linux network namespaces. - -Ubuntu and CentOS Linux are the recommended distributions for running CORE. However, these distributions are -not strictly required. CORE will likely work on other flavors of Linux as well, assuming dependencies are met. - -> **NOTE:** CORE Services determine what run on each node. You may require other software packages depending on the -services you wish to use. For example, the HTTP service will require the apache2 package. - -## Installed Files +## Supported Linux Distributions -CORE files are installed to the following directories by default, when the installation prefix is **/usr**. +Plan is to support recent Ubuntu and CentOS LTS releases. -Install Path | Description --------------|------------ -/usr/bin/core-gui|GUI startup command -/usr/bin/coretk-gui|BETA Python GUI -/usr/bin/core-daemon|Daemon startup command -/usr/bin/{core-cleanup, coresendmsg, core-manage}|Misc. helper commands/scripts -/usr/lib/core|GUI files -/usr/lib/python{3.6+}/dist-packages/core|Python modules for daemon/scripts -/etc/core/|Daemon and log configuration files -~/.core/|User-specific GUI preferences and scenario files -/usr/share/core/|Example scripts and scenarios -/usr/share/man/man1/|Command man pages -/etc/init.d/core-daemon|SysV startup script for daemon -/usr/lib/systemd/system/core-daemon.service|Systemd startup script for daemon +Verified: +* Ubuntu - 18.04, 20.04 +* CentOS - 7.8, 8.0* -## Automated Install +> **NOTE:** Ubuntu 20.04 requires installing legacy ebtables for WLAN +> functionality -There is a helper script in the root of the repository that can help automate -the CORE installation. Some steps require commands be ran as sudo and you -will be prompted for a password. This should work on Ubuntu/CentOS and will -install system dependencies, python dependencies, and CORE. This will target -system installations of python 3.6. - -```shell -git clone https://github.com/coreemu/core.git -cd core -./install.sh -``` +> **NOTE:** CentOS 8 does not provide legacy ebtables support, WLAN will not +> function properly -You can target newer system python versions using the **-v** flag. Assuming -these versions are actually available on your system. +> **NOTE:** CentOS 8 does not have the netem kernel mod available by default +CentOS 8 Enabled netem: ```shell -# ubuntu 3.7 -./install.sh -v 3.7 -# centos 3.7 -./install.sh -v 37 +sudo yum update +# restart into updated kernel +sudo yum install -y kernel-modules-extra +sudo modprobe sch_netem ``` -## Pre-Req Installing Python - -Python 3.6 is the minimum required python version. Newer versions can be used if available. -These steps are needed, since the system packages can not provide all the -dependencies needed by CORE. +## Utility Requirements -### Ubuntu +* iproute2 4.5+ is a requirement for bridge related commands +* ebtables not backed by nftables -```shell -sudo apt install python3.6 -sudo apt install python3-pip -``` +## Upgrading -### CentOS +Please make sure to uninstall the previous installation of CORE cleanly +before proceeding to install. +Previous install was built from source: ```shell -sudo yum install python36 -sudo yum install python3-pip +cd +sudo make uninstall +make clean +./bootstrap.sh clean ``` -### Dependencies - -Install the current python dependencies. - +Installed from previously built packages: ```shell -sudo python3 -m pip install -r requirements.txt +# centos +sudo yum remove core +# ubuntu +sudo apt remove core ``` -## Pre-Req Installing OSPF MDR +## Automated Installation -Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing -tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by -default when the blue router node type is used. +The automated install will install the various tools needed to help automate +the CORE installation (python3, pip, pipx, invoke, poetry). The script will +also automatically clone, build, and install the latest version of OSPF MDR. +Finally it will install CORE scripts and a systemd service, which have +been modified to use the installed poetry created virtual environment. -* [OSPF MANET Designated Routers](https://github.com/USNavalResearchLaboratory/ospf-mdr) (MDR) - the Quagga routing -suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type -(and the MDR service) requires this variant of Quagga. +After installation has completed you should be able to run the various +CORE scripts for running core. -### Ubuntu +> **NOTE:** provide a prefix that will be found on path when running as sudo +> if the default prefix is not valid ```shell -sudo apt install libtool gawk libreadline-dev -``` - -### CentOS - -```shell -sudo yum install libtool gawk readline-devel -``` - -### Build and Install - -```shell -git clone https://github.com/USNavalResearchLaboratory/ospf-mdr -cd ospf-mdr -./bootstrap.sh -./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ - --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ - --localstatedir=/var/run/quagga -make -sudo make install -``` - -Note that the configuration directory */usr/local/etc/quagga* shown for Quagga above could be */etc/quagga*, -if you create a symbolic link from */etc/quagga/Quagga.conf -> /usr/local/etc/quagga/Quagga.conf* on the host. -The *quaggaboot.sh* script in a Linux network namespace will try and do this for you if needed. - -If you try to run quagga after installing from source and get an error such as: - -```shell -error while loading shared libraries libzebra.so.0 -``` - -this is usually a sign that you have to run ```sudo ldconfig```` to refresh the cache file. - -## Installing from Packages - -The easiest way to install CORE is using the pre-built packages. The package managers on Ubuntu or CentOS -will help in automatically installing most dependencies, except for the python ones described previously. - -You can obtain the CORE packages from [CORE Releases](https://github.com/coreemu/core/releases). - -### Ubuntu - -Ubuntu package defaults to using systemd for running as a service. +# clone CORE repo +git clone https://github.com/coreemu/core.git +cd core -```shell -sudo apt install ./core_$VERSION_amd64.deb +# run install script +# script usage: install.sh [-d] [-v] +# +# -v enable verbose install +# -d enable developer install +# -p install prefix, defaults to /usr/local +./install.sh ``` -### CentOS +### Unsupported Linux Distribution -**NOTE: tkimg is not required for the core-gui, but if you get an error message about it you can install the package -on CentOS <= 6, or build from source otherwise** +If you are on an unsupported distribution, you can look into the +[install.sh](https://github.com/coreemu/core/blob/master/install.sh) +and +[tasks.py](https://github.com/coreemu/core/blob/master/tasks.py) +files to see the various commands ran to install CORE and translate them to +your use case, assuming it is possible. -```shell -yum install ./core_$VERSION_x86_64.rpm -``` +If you get install down entirely, feel free to contribute and help others. -Disabling SELINUX: +## Installed Scripts -```shell -# change the following in /etc/sysconfig/selinux -SELINUX=disabled +After the installation complete it will have installed the following scripts. -# add the following to the kernel line in /etc/grub.conf -selinux=0 -``` +| Name | Description | +|---|---| +| core-daemon | runs the backed core server providing TLV and gRPC APIs | +| core-gui | runs the legacy tcl/tk based GUI | +| core-pygui | runs the new python/tk based GUI | +| core-cleanup | tool to help removed lingering core created containers, bridges, directories | +| core-imn-to-xml | tool to help automate converting a .imn file to .xml format | +| core-route-monitor | tool to help monitor traffic across nodes and feed that to SDT | +| core-service-update | tool to update automate modifying a legacy service to match current naming | +| coresendmsg | tool to send TLV API commands from command line | +| core-cli | tool to query, open xml files, and send commands using gRPC | +| core-manage | tool to add, remove, or check for services, models, and node types | -Turn off firewalls: +## Running User Scripts -```shell -systemctl disable firewalld -systemctl disable iptables.service -systemctl disable ip6tables.service -chkconfig iptables off -chkconfig ip6tables off -``` +If you create your own python scripts to run CORE directly or using the gRPC/TLV +APIs you will need to make sure you are running them within context of the +installed virtual environment. -You need to reboot after making these changes, or flush the firewall using +> **NOTE:** the following assumes CORE has been installed successfully +There is an invoke task to help with this case. ```shell -iptables -F -ip6tables -F -``` - -## Installing from Source +cd +inv -h run +Usage: inv[oke] [--core-opts] run [--options] [other tasks here ...] -Steps for building from cloned source code. Python 3.6 is the minimum required version -a newer version can be used below if available. +Docstring: + runs a user script in the core virtual environment -### Distro Requirements - -System packages required to build from source. - -#### Ubuntu - -```shell -sudo apt install git automake pkg-config gcc libev-dev ebtables iproute2 \ - python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool autoconf +Options: + -f STRING, --file=STRING script file to run in the core virtual environment + -s, --sudo run script as sudo ``` -#### CentOS - +Another way would be to enable the core virtual environment shell. Which +would allow you to run scripts in a more **normal** way. ```shell -sudo yum install git automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ - python36 python36-devel python3-pip python3-tkinter tk ethtool autoconf +cd /daemon +poetry shell +python run /path/to/script.py ``` -### Clone Repository - -Clone the CORE repository for building from source. +## Manually Install EMANE -```shell -git clone https://github.com/coreemu/core.git -``` +EMANE can be installed from deb or RPM packages or from source. See the +[EMANE GitHub](https://github.com/adjacentlink/emane) for full details. -### Install grpcio-tools - -Python module grpcio-tools is currently needed to generate gRPC protobuf code. +There is an invoke task to help with installing EMANE, but has issues, +which attempts to build EMANE from source, but has issue on systems with + older protobuf-compilers. ```shell -sudo python3 -m pip install grpcio-tools +cd +inv install-emane ``` -### Build and Install +Alternatively, you can +[build EMANE](https://github.com/adjacentlink/emane/wiki/Build) +from source and install the python +bindings into the core virtual environment. +The following would install the EMANE python bindings after being +successfully built. ```shell -./bootstrap.sh -./configure -make -sudo make install +cd /daemon +poetry run pip install /src/python ``` -## Building Documentation +## Using Invoke Tasks -Building documentation requires python-sphinx not noted above. +The invoke tool installed by way of pipx provides conveniences for running +CORE tasks to help ensure usage of the create python virtual environment. ```shell -sudo apt install python3-sphinx -sudo yum install python3-sphinx - -./bootstrap.sh -./configure -make doc -``` - -## Building Packages -Build package commands, DESTDIR is used to make install into and then for packaging by fpm. +inv --list -**NOTE: clean the DESTDIR if re-using the same directory** +Available tasks: -* Install [fpm](http://fpm.readthedocs.io/en/latest/installing.html) - -```shell -./bootstrap.sh -./configure -make -mkdir /tmp/core-build -make fpm DESTDIR=/tmp/core-build + daemon start core-daemon + install install core, poetry, scripts, service, and ospf mdr + install-emane install emane and the python bindings + install-scripts install core script files, modified to leverage virtual environment + install-service install systemd core service + run runs a user script in the core virtual environment + test run core tests + test-emane run core emane tests + test-mock run core tests using mock to avoid running as sudo + uninstall uninstall core, scripts, service, virtual environment, and clean build directory ``` -This will produce and RPM and Deb package for the currently configured python version. - -## Running CORE - -Start the CORE daemon. - +Print help for a given task: ```shell -# systemd -sudo systemctl daemon-reload -sudo systemctl start core-daemon - -# sysv -sudo service core-daemon start -``` +inv -h install -Run the GUI +Usage: inv[oke] [--core-opts] install [--options] [other tasks here ...] -```shell -# default gui -core-gui +Docstring: + install core, poetry, scripts, service, and ospf mdr -# new beta gui -coretk-gui +Options: + -d, --dev install development mode + -p STRING, --prefix=STRING prefix where scripts are installed, default is /usr/local + -v, --verbose enable verbose ``` diff --git a/docs/scripting.md b/docs/scripting.md index 8c1a705c0..06ca483ac 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -36,7 +36,7 @@ interact with the GUI. import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes +from core.emulator.data import IpPrefixes from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.network import SwitchNode @@ -61,8 +61,8 @@ def main(): # create nodes for _ in range(NODES): node = session.add_node(CoreNode) - interface = prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface_one=interface) + iface_data = prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface1_data=iface_data) # instantiate session session.instantiate() @@ -137,7 +137,7 @@ session = coreemu.create_session() # create node with custom services options = NodeOptions(services=["ServiceName"]) -node = session.add_node(options=options) +node = session.add_node(CoreNode, options=options) # set custom file data session.services.set_service_file(node.id, "ServiceName", "FileName", "custom file data") diff --git a/docs/services.md b/docs/services.md index 9f47ae482..2ce52e997 100644 --- a/docs/services.md +++ b/docs/services.md @@ -263,7 +263,7 @@ class MyService(CoreService): if filename == cls.configs[0]: cfg += "# auto-generated by MyService (sample.py)\n" - for ifc in node.netifs(): + for ifc in node.get_ifaces(): cfg += f'echo "Node {node.name} has interface {ifc.name}"\n' elif filename == cls.configs[1]: cfg += "echo hello" diff --git a/gui/plugins.tcl b/gui/plugins.tcl index 95c1a2037..fdb5c4541 100644 --- a/gui/plugins.tcl +++ b/gui/plugins.tcl @@ -672,11 +672,11 @@ proc popupCapabilityConfig { channel wlan model types values captions bmp possib pack $windowScroll -fill y -side right pack $windowCanvas -expand yes -fill both -side top - frame $windowCanvas.notebookFrame -width 700 -height 1200 + frame $windowCanvas.notebookFrame -width 700 -height 2400 set notebookFrame $windowCanvas.notebookFrame pack $notebookFrame -fill both -expand yes -padx 5 -pady 5 - ttk::notebook $notebookFrame.vals -width 690 -height 1200 + ttk::notebook $notebookFrame.vals -width 690 -height 2400 set configNotebook $notebookFrame.vals ttk::notebook::enableTraversal $configNotebook pack $configNotebook -fill both -expand yes diff --git a/install.sh b/install.sh index a12072f1f..5e5a9b117 100755 --- a/install.sh +++ b/install.sh @@ -3,53 +3,6 @@ # exit on error set -e -ubuntu_py=3.6 -centos_py=36 -reinstall= - -function install_python_depencencies() { - sudo python3 -m pip install -r daemon/requirements.txt -} - -function install_ospf_mdr() { - rm -rf /tmp/ospf-mdr - git clone https://github.com/USNavalResearchLaboratory/ospf-mdr /tmp/ospf-mdr - cd /tmp/ospf-mdr - ./bootstrap.sh - ./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ - --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ - --localstatedir=/var/run/quagga - make -j8 - sudo make install - cd - -} - -function build_core() { - ./bootstrap.sh - ./configure $1 - make -j8 -} - -function install_core() { - sudo make install -} - -function uninstall_core() { - sudo make uninstall - make clean - ./bootstrap.sh clean -} - -function install_dev_core() { - cd gui - sudo make install - cd - - cd netns - sudo make install - cd - - cd daemon -} - # detect os/ver for install type os="" if [[ -f /etc/os-release ]]; then @@ -58,100 +11,47 @@ if [[ -f /etc/os-release ]]; then fi # parse arguments -while getopts "drv:" opt; do +dev="" +verbose="" +prefix="" +while getopts "dvp:" opt; do case ${opt} in d) - dev=1 + dev="-d" ;; v) - ubuntu_py=${OPTARG} - centos_py=${OPTARG} + verbose="-v" ;; - r) - reinstall=1 + p) + prefix="-p ${OPTARG}" ;; \?) - echo "script usage: $(basename $0) [-d] [-r] [-v python version]" >&2 + echo "script usage: $(basename $0) [-d] [-v]" >&2 + echo "" >&2 + echo "-v enable verbose install" >&2 + echo "-d enable developer install" >&2 + echo "-p install prefix, defaults to /usr/local" >&2 exit 1 ;; esac done shift $((OPTIND - 1)) -# check if we are reinstalling or installing -if [ -z "${reinstall}" ]; then - echo "installing CORE for ${os}" - case ${os} in - "ubuntu") - echo "installing core system dependencies" - sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ - python${ubuntu_py} python${ubuntu_py}-dev python3-pip python3-tk tk libtk-img ethtool autoconf - python3 -m pip install grpcio-tools - echo "installing ospf-mdr system dependencies" - sudo apt install -y libtool gawk libreadline-dev - install_ospf_mdr - if [[ -z ${dev} ]]; then - echo "normal install" - install_python_depencencies - build_core - install_core - else - echo "dev install" - python3 -m pip install pipenv - build_core - install_dev_core - python3 -m pipenv sync --dev - python3 -m pipenv run pre-commit install - fi - ;; - "centos") - echo "installing core system dependencies" - sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ - python${centos_py} python${centos_py}-devel python3-pip python3-tkinter tk ethtool autoconf - sudo python3 -m pip install grpcio-tools - echo "installing ospf-mdr system dependencies" - sudo yum install -y libtool gawk readline-devel - install_ospf_mdr - if [[ -z ${dev} ]]; then - echo "normal install" - install_python_depencencies - build_core --prefix=/usr - install_core - else - echo "dev install" - sudo python3 -m pip install pipenv - build_core --prefix=/usr - install_dev_core - sudo python3 -m pipenv sync --dev - python3 -m pipenv sync --dev - python3 -m pipenv run pre-commit install - fi - ;; - *) - echo "unknown OS ID ${os} cannot install" - ;; - esac -else - branch=$(git symbolic-ref --short HEAD) - echo "reinstalling CORE on ${os} with latest ${branch}" - echo "uninstalling CORE" - uninstall_core - echo "pulling latest code" - git pull - echo "installing python dependencies" - install_python_depencencies - echo "building CORE" - case ${os} in - "ubuntu") - build_core - ;; - "centos") - build_core --prefix=/usr - ;; - *) - echo "unknown OS ID ${os} cannot reinstall" - ;; - esac - echo "installing CORE" - install_core -fi +echo "installing CORE for ${os}" +case ${os} in +"ubuntu") + sudo apt install -y python3-pip python3-venv + ;; +"centos") + sudo yum install -y python3-pip + ;; +*) + echo "unknown OS ID ${os} cannot install" + ;; +esac + +python3 -m pip install --user pipx +python3 -m pipx ensurepath +export PATH=$PATH:~/.local/bin +pipx install invoke +inv install ${dev} ${verbose} ${prefix} diff --git a/scripts/.gitignore b/scripts/.gitignore deleted file mode 100644 index 86129f95b..000000000 --- a/scripts/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -core-daemon -core-daemon.service diff --git a/scripts/Makefile.am b/scripts/Makefile.am deleted file mode 100644 index abdef40da..000000000 --- a/scripts/Makefile.am +++ /dev/null @@ -1,31 +0,0 @@ -# CORE -# (c)2011-2013 the Boeing Company. -# See the LICENSE file included in this distribution. -# -# author: Jeff Ahrenholz -# -# Makefile for installing scripts. -# - -CLEANFILES = core-daemon - -DISTCLEANFILES = Makefile.in core-daemon.service core-daemon - -EXTRA_DIST = core-daemon.in core-daemon.service.in - -SUBDIRS = - -# install startup scripts based on --with-startup=option configure option -# init.d (default), systemd -if WANT_INITD -startupdir = /etc/init.d -startup_SCRIPTS = core-daemon -endif -if WANT_SYSTEMD -startupdir = /usr/lib/systemd/system -startup_SCRIPTS = core-daemon.service -endif - -# remove extra scripts and their directories if they are empty -uninstall-hook: - rmdir -p $(startupdir) || true diff --git a/scripts/core-daemon.in b/scripts/core-daemon.in deleted file mode 100644 index 0a988f0f2..000000000 --- a/scripts/core-daemon.in +++ /dev/null @@ -1,112 +0,0 @@ -#!/bin/sh -### BEGIN INIT INFO -# Provides: core-daemon -# Required-Start: $network $remote_fs -# Required-Stop: $network $remote_fs -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Start the core-daemon CORE daemon at boot time -# Description: Starts and stops the core-daemon CORE daemon used to -# provide network emulation services for the CORE GUI -# or scripts. -### END INIT INFO -# -# chkconfig: 35 90 03 -# description: Starts and stops the CORE daemon \ -# used to provide network emulation services. -# -# config: /etc/core/ - -NAME=`basename $0` -PIDFILE="@CORE_STATE_DIR@/run/$NAME.pid" -LOG="@CORE_STATE_DIR@/log/$NAME.log" -CMD="@bindir@/$NAME" - -get_pid() { - cat "$PIDFILE" -} - -is_alive() { - [ -f "$PIDFILE" ] && ps -p `get_pid` > /dev/null 2>&1 -} - -corestart() { - if is_alive; then - echo "$NAME already started" - else - echo "starting $NAME" - $CMD 2>&1 >> "$LOG" & - fi - - echo $! > "$PIDFILE" - if ! is_alive; then - echo "unable to start $NAME, see $LOG" - exit 1 - fi -} - -corestop() { - if is_alive; then - echo -n "stopping $NAME.." - kill `get_pid` - for i in 1 2 3 4 5; do - sleep 1 - if ! is_alive; then - break - fi - echo -n "." - done - echo - - if is_alive; then - echo "not stopped; may still be shutting down" - exit 1 - else - echo "stopped" - if [ -f "$PIDFILE" ]; then - rm -f "$PIDFILE" - fi - fi - else - echo "$NAME not running" - fi -} - -corerestart() { - corestop - corestart -} - -corestatus() { - if is_alive; then - echo "$NAME is running" - else - echo "$NAME is stopped" - exit 1 - fi -} - - -case "$1" in - start) - corestart - ;; - stop) - corestop - ;; - restart) - corerestart - ;; - force-reload) - corerestart - ;; - status) - corestatus - ;; - *) - echo "Usage: $0 {start|stop|restart|status}" - exit 1 -esac - -exit $? - diff --git a/scripts/core-daemon.service.in b/scripts/core-daemon.service.in deleted file mode 100644 index cd53cfad2..000000000 --- a/scripts/core-daemon.service.in +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Common Open Research Emulator Service -After=network.target - -[Service] -Type=simple -ExecStart=@bindir@/core-daemon -TasksMax=infinity - -[Install] -WantedBy=multi-user.target diff --git a/tasks.py b/tasks.py new file mode 100644 index 000000000..5f52f4440 --- /dev/null +++ b/tasks.py @@ -0,0 +1,504 @@ +import inspect +import itertools +import os +import sys +import threading +import time +from contextlib import contextmanager +from enum import Enum +from pathlib import Path +from tempfile import NamedTemporaryFile +from typing import Optional + +from invoke import task, Context + +DAEMON_DIR: str = "daemon" +DEFAULT_PREFIX: str = "/usr/local" + + +class Progress: + cycles = itertools.cycle(["-", "/", "|", "\\"]) + + def __init__(self, verbose: bool) -> None: + self.verbose: bool = verbose + self.thread: Optional[threading.Thread] = None + self.running: bool = False + + @contextmanager + def start(self, message: str) -> None: + if not self.verbose: + print(f"{message} ... ", end="") + self.running = True + self.thread = threading.Thread(target=self.run, daemon=True) + self.thread.start() + yield + self.stop() + + def run(self) -> None: + while self.running: + sys.stdout.write(next(self.cycles)) + sys.stdout.flush() + sys.stdout.write("\b") + time.sleep(0.1) + + def stop(self) -> None: + if not self.verbose: + print("done") + if self.thread: + self.running = False + self.thread.join() + self.thread = None + + +class OsName(Enum): + UBUNTU = "ubuntu" + CENTOS = "centos" + + +class OsLike(Enum): + DEBIAN = "debian" + REDHAT = "rhel fedora" + + +class OsInfo: + def __init__(self, name: OsName, like: OsLike, version: float) -> None: + self.name: OsName = name + self.like: OsLike = like + self.version: float = version + + +def get_python(c: Context, warn: bool = False) -> str: + with c.cd(DAEMON_DIR): + r = c.run("poetry env info -p", warn=warn, hide=True) + if r.ok: + venv = r.stdout.strip() + return os.path.join(venv, "bin", "python") + else: + return "" + + +def get_pytest(c: Context) -> str: + with c.cd(DAEMON_DIR): + venv = c.run("poetry env info -p", hide=True).stdout.strip() + return os.path.join(venv, "bin", "pytest") + + +def get_os() -> OsInfo: + d = {} + with open("/etc/os-release", "r") as f: + for line in f.readlines(): + line = line.strip() + if not line: + continue + key, value = line.split("=") + d[key] = value.strip("\"") + name_value = d["ID"] + like_value = d["ID_LIKE"] + version_value = d["VERSION_ID"] + try: + name = OsName(name_value) + like = OsLike(like_value) + version = float(version_value) + except ValueError: + print( + f"unsupported os({name_value}) like({like_value}) version({version_value}" + ) + sys.exit(1) + return OsInfo(name, like, version) + + +def check_existing_core(c: Context, hide: bool) -> None: + if c.run("python -c \"import core\"", warn=True, hide=hide): + raise SystemError("existing python2 core installation detected, please remove") + if c.run("python3 -c \"import core\"", warn=True, hide=hide): + raise SystemError("existing python3 core installation detected, please remove") + if c.run("which core-daemon", warn=True, hide=hide): + raise SystemError("core scripts found, please remove old installation") + + +def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: + if os_info.like == OsLike.DEBIAN: + c.run( + "sudo apt install -y automake pkg-config gcc libev-dev ebtables " + "iproute2 ethtool tk python3-tk", + hide=hide + ) + elif os_info.like == OsLike.REDHAT: + c.run( + "sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ " + "libev-devel iptables-ebtables iproute python3-devel python3-tkinter " + "tk ethtool make", + hide=hide + ) + # centos 8+ does not support netem by default + if os_info.name == OsName.CENTOS and os_info.version >= 8: + c.run("sudo yum install -y kernel-modules-extra", hide=hide) + if not c.run("sudo modprobe sch_netem", warn=True, hide=hide): + print("\nERROR: you need to install the latest kernel") + print("run the following, restart, and try again") + print("sudo yum update") + sys.exit(1) + + # attempt to setup legacy ebtables when an nftables based version is found + r = c.run("ebtables -V", hide=hide) + if "nf_tables" in r.stdout: + if not c.run( + "sudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy", + warn=True, + hide=hide + ): + print( + "\nWARNING: unable to setup ebtables-legacy, WLAN will not work" + ) + + +def install_grpcio(c: Context, hide: bool) -> None: + c.run( + "python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2", + hide=hide, + ) + + +def build_core(c: Context, hide: bool, prefix: str = DEFAULT_PREFIX) -> None: + c.run("./bootstrap.sh", hide=hide) + c.run(f"./configure --prefix={prefix}", hide=hide) + c.run("make -j$(nproc)", hide=hide) + + +def install_core(c: Context, hide: bool) -> None: + c.run("sudo make install", hide=hide) + + +def install_poetry(c: Context, dev: bool, hide: bool) -> None: + c.run("pipx install poetry", hide=hide) + args = "" if dev else "--no-dev" + with c.cd(DAEMON_DIR): + c.run(f"poetry install {args}", hide=hide) + if dev: + c.run("poetry run pre-commit install", hide=hide) + + +def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: + if c.run("which zebra", warn=True, hide=hide): + print("\nquagga already installed, skipping ospf mdr") + return + if os_info.like == OsLike.DEBIAN: + c.run("sudo apt install -y libtool gawk libreadline-dev git", hide=hide) + elif os_info.like == OsLike.REDHAT: + c.run("sudo yum install -y libtool gawk readline-devel git", hide=hide) + clone_dir = "/tmp/ospf-mdr" + c.run( + f"git clone https://github.com/USNavalResearchLaboratory/ospf-mdr {clone_dir}", + hide=hide + ) + with c.cd(clone_dir): + c.run("./bootstrap.sh", hide=hide) + c.run( + "./configure --disable-doc --enable-user=root --enable-group=root " + "--with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh " + "--localstatedir=/var/run/quagga", + hide=hide + ) + c.run("make -j$(nproc)", hide=hide) + c.run("sudo make install", hide=hide) + + +@task( + help={ + "verbose": "enable verbose", + "prefix": f"prefix where scripts are installed, default is {DEFAULT_PREFIX}" + }, +) +def install_service(c, verbose=False, prefix=DEFAULT_PREFIX): + """ + install systemd core service + """ + hide = not verbose + bin_dir = Path(prefix).joinpath("bin") + systemd_dir = Path("/lib/systemd/system/") + service_file = systemd_dir.joinpath("core-daemon.service") + if systemd_dir.exists(): + service_data = inspect.cleandoc(f""" + [Unit] + Description=Common Open Research Emulator Service + After=network.target + + [Service] + Type=simple + ExecStart={bin_dir}/core-daemon + TasksMax=infinity + + [Install] + WantedBy=multi-user.target + """) + temp = NamedTemporaryFile("w", delete=False) + temp.write(service_data) + temp.close() + c.run(f"sudo cp {temp.name} {service_file}", hide=hide) + else: + print(f"ERROR: systemd service path not found: {systemd_dir}") + + +@task( + help={ + "verbose": "enable verbose", + "prefix": f"prefix where scripts are installed, default is {DEFAULT_PREFIX}" + }, +) +def install_scripts(c, verbose=False, prefix=DEFAULT_PREFIX): + """ + install core script files, modified to leverage virtual environment + """ + hide = not verbose + python = get_python(c) + bin_dir = Path(prefix).joinpath("bin") + for script in Path("daemon/scripts").iterdir(): + dest = bin_dir.joinpath(script.name) + with open(script, "r") as f: + lines = f.readlines() + first = lines[0].strip() + # modify python scripts to point to virtual environment + if first == "#!/usr/bin/env python3": + lines[0] = f"#!{python}\n" + temp = NamedTemporaryFile("w", delete=False) + for line in lines: + temp.write(line) + temp.close() + c.run(f"sudo cp {temp.name} {dest}", hide=hide) + c.run(f"sudo chmod 755 {dest}", hide=hide) + os.unlink(temp.name) + # copy normal links + else: + c.run(f"sudo cp {script} {dest}", hide=hide) + + # install core configuration file + config_dir = "/etc/core" + c.run(f"sudo mkdir -p {config_dir}", hide=hide) + c.run(f"sudo cp -n daemon/data/core.conf {config_dir}", hide=hide) + c.run(f"sudo cp -n daemon/data/logging.conf {config_dir}", hide=hide) + + +@task( + help={ + "dev": "install development mode", + "verbose": "enable verbose", + "prefix": f"prefix where scripts are installed, default is {DEFAULT_PREFIX}" + }, +) +def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): + """ + install core, poetry, scripts, service, and ospf mdr + """ + print(f"installing core with prefix: {prefix}") + c.run("sudo -v", hide=True) + p = Progress(verbose) + hide = not verbose + os_info = get_os() + with p.start("checking for old installations"): + check_existing_core(c, hide) + with p.start("installing system dependencies"): + install_system(c, os_info, hide) + with p.start("installing system grpcio-tools"): + install_grpcio(c, hide) + with p.start("building core"): + build_core(c, hide, prefix) + with p.start("installing vcmd/gui"): + install_core(c, hide) + with p.start("installing poetry virtual environment"): + install_poetry(c, dev, hide) + with p.start("installing scripts and /etc/core"): + install_scripts(c, hide, prefix) + with p.start("installing systemd service"): + install_service(c, hide, prefix) + with p.start("installing ospf mdr"): + install_ospf_mdr(c, os_info, hide) + print("\nyou may need to open a new terminal to leverage invoke for running core") + + +@task( + help={ + "verbose": "enable verbose", + }, +) +def install_emane(c, verbose=False): + """ + install emane and the python bindings + """ + c.run("sudo -v", hide=True) + p = Progress(verbose) + hide = not verbose + os_info = get_os() + emane_dir = "/tmp/emane" + with p.start("installing system dependencies"): + if os_info.like == OsLike.DEBIAN: + c.run( + "sudo apt install -y gcc g++ automake libtool libxml2-dev " + "libprotobuf-dev libpcap-dev libpcre3-dev uuid-dev pkg-config " + "protobuf-compiler git python3-protobuf python3-setuptools", + hide=hide, + ) + elif os_info.like == OsLike.REDHAT: + if os_info.name == OsName.CENTOS and os_info.version >= 8: + c.run("sudo yum config-manager --set-enabled PowerTools", hide=hide) + c.run( + "sudo yum install -y autoconf automake git libtool libxml2-devel " + "libpcap-devel pcre-devel libuuid-devel make gcc-c++ protobuf-compiler " + "protobuf-devel python3-setuptools", + hide=hide, + ) + with p.start("cloning emane"): + c.run( + f"git clone https://github.com/adjacentlink/emane.git {emane_dir}", + hide=hide + ) + with p.start("building emane"): + with c.cd(emane_dir): + c.run("./autogen.sh", hide=hide) + c.run("PYTHON=python3 ./configure --prefix=/usr", hide=hide) + c.run("make -j$(nproc)", hide=hide) + with p.start("installing emane"): + with c.cd(emane_dir): + c.run("sudo make install", hide=hide) + with p.start("installing python binding for core"): + with c.cd(DAEMON_DIR): + c.run(f"poetry run pip install {emane_dir}/src/python", hide=hide) + + +@task( + help={ + "dev": "uninstall development mode", + "verbose": "enable verbose", + "prefix": f"prefix where scripts are installed, default is {DEFAULT_PREFIX}" + }, +) +def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): + """ + uninstall core, scripts, service, virtual environment, and clean build directory + """ + print(f"uninstalling core with prefix: {prefix}") + hide = not verbose + p = Progress(verbose) + c.run("sudo -v", hide=True) + with p.start("uninstalling core"): + c.run("sudo make uninstall", hide=hide) + + with p.start("cleaning build directory"): + c.run("make clean", hide=hide) + c.run("./bootstrap.sh clean", hide=hide) + + python = get_python(c, warn=True) + if python: + with c.cd(DAEMON_DIR): + if dev: + with p.start("uninstalling pre-commit"): + c.run("poetry run pre-commit uninstall", hide=hide) + with p.start("uninstalling poetry virtual environment"): + c.run(f"poetry env remove {python}", hide=hide) + + # remove installed files + bin_dir = Path(prefix).joinpath("bin") + with p.start("uninstalling script files"): + for script in Path("daemon/scripts").iterdir(): + dest = bin_dir.joinpath(script.name) + c.run(f"sudo rm -f {dest}", hide=hide) + + # install service + systemd_dir = Path("/lib/systemd/system/") + service_name = "core-daemon.service" + service_file = systemd_dir.joinpath(service_name) + if service_file.exists(): + with p.start(f"uninstalling service {service_file}"): + c.run(f"sudo systemctl disable {service_name}", hide=hide) + c.run(f"sudo rm -f {service_file}", hide=hide) + + +@task( + help={ + "dev": "reinstall development mode", + "verbose": "enable verbose", + "prefix": f"prefix where scripts are installed, default is {DEFAULT_PREFIX}", + "branch": "branch to install latest code from, default is current branch" + }, +) +def reinstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX, branch=None): + """ + run the uninstall task, get latest from specified branch, and run install task + """ + uninstall(c, dev, verbose, prefix) + hide = not verbose + p = Progress(verbose) + with p.start("pulling latest code"): + current = c.run("git rev-parse --abbrev-ref HEAD", hide=hide).stdout.strip() + if branch and branch != current: + c.run(f"git checkout {branch}") + else: + branch = current + c.run("git pull", hide=hide) + if not Path("tasks.py").exists(): + raise FileNotFoundError(f"missing tasks.py on branch: {branch}") + install(c, dev, verbose, prefix) + + +@task +def daemon(c): + """ + start core-daemon + """ + python = get_python(c) + with c.cd(DAEMON_DIR): + c.run( + f"sudo {python} scripts/core-daemon " + "-f data/core.conf -l data/logging.conf", + pty=True + ) + + +@task( + help={ + "sudo": "run script as sudo", + "file": "script file to run in the core virtual environment" + }, +) +def run(c, file, sudo=False): + """ + runs a user script in the core virtual environment + """ + if not file: + print("no script was provided") + return + python = get_python(c) + path = Path(file).absolute() + with c.cd(DAEMON_DIR): + cmd = f"{python} {path}" + if sudo: + cmd = f"sudo {cmd}" + c.run(cmd, pty=True) + + +@task +def test(c): + """ + run core tests + """ + pytest = get_pytest(c) + with c.cd(DAEMON_DIR): + c.run(f"sudo {pytest} -v --lf -x tests", pty=True) + + +@task +def test_mock(c): + """ + run core tests using mock to avoid running as sudo + """ + with c.cd(DAEMON_DIR): + c.run("poetry run pytest -v --mock --lf -x tests", pty=True) + + +@task +def test_emane(c): + """ + run core emane tests + """ + pytest = get_pytest(c) + with c.cd(DAEMON_DIR): + c.run(f"{pytest} -v --lf -x tests/emane", pty=True)