commit
eb70386238
209 changed files with 10343 additions and 9561 deletions
19
.github/workflows/daemon-checks.yml
vendored
19
.github/workflows/daemon-checks.yml
vendored
|
@ -11,32 +11,31 @@ jobs:
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v1
|
||||||
with:
|
with:
|
||||||
python-version: 3.6
|
python-version: 3.6
|
||||||
- name: Install pipenv
|
- name: install poetry
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install pipenv
|
pip install poetry
|
||||||
cd daemon
|
cd daemon
|
||||||
cp setup.py.in setup.py
|
|
||||||
cp core/constants.py.in core/constants.py
|
cp core/constants.py.in core/constants.py
|
||||||
sed -i 's/True/False/g' core/constants.py
|
sed -i 's/required=True/required=False/g' core/emulator/coreemu.py
|
||||||
pipenv sync --dev
|
poetry install
|
||||||
- name: isort
|
- name: isort
|
||||||
run: |
|
run: |
|
||||||
cd daemon
|
cd daemon
|
||||||
pipenv run isort -c -df
|
poetry run isort -c -df
|
||||||
- name: black
|
- name: black
|
||||||
run: |
|
run: |
|
||||||
cd daemon
|
cd daemon
|
||||||
pipenv run black --check --exclude ".+_pb2.*.py|doc|build|utm\.py|setup\.py" .
|
poetry run black --check .
|
||||||
- name: flake8
|
- name: flake8
|
||||||
run: |
|
run: |
|
||||||
cd daemon
|
cd daemon
|
||||||
pipenv run flake8
|
poetry run flake8
|
||||||
- name: grpc
|
- name: grpc
|
||||||
run: |
|
run: |
|
||||||
cd daemon/proto
|
cd daemon/proto
|
||||||
pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto
|
poetry run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto
|
||||||
- name: test
|
- name: test
|
||||||
run: |
|
run: |
|
||||||
cd daemon
|
cd daemon
|
||||||
pipenv run test --mock
|
poetry run pytest --mock tests
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -39,6 +39,7 @@ coverage.xml
|
||||||
|
|
||||||
# python files
|
# python files
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# ignore package files
|
# ignore package files
|
||||||
*.rpm
|
*.rpm
|
||||||
|
@ -55,8 +56,5 @@ coverage.xml
|
||||||
netns/setup.py
|
netns/setup.py
|
||||||
daemon/setup.py
|
daemon/setup.py
|
||||||
|
|
||||||
# ignore corefx build
|
|
||||||
corefx/target
|
|
||||||
|
|
||||||
# python
|
# python
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
50
CHANGELOG.md
50
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
|
## 2020-06-11 CORE 6.5.0
|
||||||
* Breaking Changes
|
* Breaking Changes
|
||||||
* CoreNode.newnetif - both parameters are required and now takes an InterfaceData object as its second parameter
|
* CoreNode.newnetif - both parameters are required and now takes an InterfaceData object as its second parameter
|
||||||
|
|
63
Makefile.am
63
Makefile.am
|
@ -11,7 +11,7 @@ if WANT_GUI
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if WANT_DAEMON
|
if WANT_DAEMON
|
||||||
DAEMON = scripts daemon
|
DAEMON = daemon
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if WANT_NETNS
|
if WANT_NETNS
|
||||||
|
@ -44,58 +44,6 @@ DISTCLEANFILES = aclocal.m4 \
|
||||||
MAINTAINERCLEANFILES = .version \
|
MAINTAINERCLEANFILES = .version \
|
||||||
.version.date
|
.version.date
|
||||||
|
|
||||||
define fpm-rpm =
|
|
||||||
fpm -s dir -t rpm -n core \
|
|
||||||
-m "$(PACKAGE_MAINTAINERS)" \
|
|
||||||
--license "BSD" \
|
|
||||||
--description "Common Open Research Emulator" \
|
|
||||||
--url https://github.com/coreemu/core \
|
|
||||||
--vendor "$(PACKAGE_VENDOR)" \
|
|
||||||
-p core_VERSION_ARCH.rpm \
|
|
||||||
-v $(PACKAGE_VERSION) \
|
|
||||||
--rpm-init scripts/core-daemon \
|
|
||||||
--config-files "/etc/core" \
|
|
||||||
-d "ethtool" \
|
|
||||||
-d "tcl" \
|
|
||||||
-d "tk" \
|
|
||||||
-d "procps-ng" \
|
|
||||||
-d "bash >= 3.0" \
|
|
||||||
-d "ebtables" \
|
|
||||||
-d "iproute" \
|
|
||||||
-d "libev" \
|
|
||||||
-d "net-tools" \
|
|
||||||
-d "python3 >= 3.6" \
|
|
||||||
-d "python3-tkinter" \
|
|
||||||
-C $(DESTDIR)
|
|
||||||
endef
|
|
||||||
|
|
||||||
define fpm-deb =
|
|
||||||
fpm -s dir -t deb -n core \
|
|
||||||
-m "$(PACKAGE_MAINTAINERS)" \
|
|
||||||
--license "BSD" \
|
|
||||||
--description "Common Open Research Emulator" \
|
|
||||||
--url https://github.com/coreemu/core \
|
|
||||||
--vendor "$(PACKAGE_VENDOR)" \
|
|
||||||
-p core_VERSION_ARCH.deb \
|
|
||||||
-v $(PACKAGE_VERSION) \
|
|
||||||
--deb-systemd scripts/core-daemon.service \
|
|
||||||
--deb-no-default-config-files \
|
|
||||||
--config-files "/etc/core" \
|
|
||||||
-d "ethtool" \
|
|
||||||
-d "tcl" \
|
|
||||||
-d "tk" \
|
|
||||||
-d "libtk-img" \
|
|
||||||
-d "procps" \
|
|
||||||
-d "libc6 >= 2.14" \
|
|
||||||
-d "bash >= 3.0" \
|
|
||||||
-d "ebtables" \
|
|
||||||
-d "iproute2" \
|
|
||||||
-d "libev4" \
|
|
||||||
-d "python3 >= 3.6" \
|
|
||||||
-d "python3-tk" \
|
|
||||||
-C $(DESTDIR)
|
|
||||||
endef
|
|
||||||
|
|
||||||
define fpm-distributed-deb =
|
define fpm-distributed-deb =
|
||||||
fpm -s dir -t deb -n core-distributed \
|
fpm -s dir -t deb -n core-distributed \
|
||||||
-m "$(PACKAGE_MAINTAINERS)" \
|
-m "$(PACKAGE_MAINTAINERS)" \
|
||||||
|
@ -138,12 +86,6 @@ fpm -s dir -t rpm -n core-distributed \
|
||||||
-C $(DESTDIR)
|
-C $(DESTDIR)
|
||||||
endef
|
endef
|
||||||
|
|
||||||
.PHONY: fpm
|
|
||||||
fpm: clean-local-fpm
|
|
||||||
$(MAKE) install DESTDIR=$(DESTDIR)
|
|
||||||
$(call fpm-deb)
|
|
||||||
$(call fpm-rpm)
|
|
||||||
|
|
||||||
.PHONY: fpm-distributed
|
.PHONY: fpm-distributed
|
||||||
fpm-distributed: clean-local-fpm
|
fpm-distributed: clean-local-fpm
|
||||||
$(MAKE) -C netns install DESTDIR=$(DESTDIR)
|
$(MAKE) -C netns install DESTDIR=$(DESTDIR)
|
||||||
|
@ -182,11 +124,8 @@ all: change-files
|
||||||
.PHONY: change-files
|
.PHONY: change-files
|
||||||
change-files:
|
change-files:
|
||||||
$(call change-files,gui/core-gui)
|
$(call change-files,gui/core-gui)
|
||||||
$(call change-files,scripts/core-daemon.service)
|
|
||||||
$(call change-files,scripts/core-daemon)
|
|
||||||
$(call change-files,daemon/core/constants.py)
|
$(call change-files,daemon/core/constants.py)
|
||||||
$(call change-files,netns/setup.py)
|
$(call change-files,netns/setup.py)
|
||||||
$(call change-files,daemon/setup.py)
|
|
||||||
|
|
||||||
CORE_DOC_SRC = core-python-$(PACKAGE_VERSION)
|
CORE_DOC_SRC = core-python-$(PACKAGE_VERSION)
|
||||||
.PHONY: doc
|
.PHONY: doc
|
||||||
|
|
28
configure.ac
28
configure.ac
|
@ -2,7 +2,7 @@
|
||||||
# Process this file with autoconf to produce a configure script.
|
# Process this file with autoconf to produce a configure script.
|
||||||
|
|
||||||
# this defines the CORE version number, must be static for AC_INIT
|
# this defines the CORE version number, must be static for AC_INIT
|
||||||
AC_INIT(core, 6.5.0)
|
AC_INIT(core, 7.0.0)
|
||||||
|
|
||||||
# autoconf and automake initialization
|
# autoconf and automake initialization
|
||||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||||
|
@ -167,18 +167,6 @@ if test "x$enable_daemon" = "xyes"; then
|
||||||
if test "x$ovs_of_path" = "xno" ; then
|
if test "x$ovs_of_path" = "xno" ; then
|
||||||
AC_MSG_WARN([Could not locate ovs-ofctl cannot use OVS mode])
|
AC_MSG_WARN([Could not locate ovs-ofctl cannot use OVS mode])
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CFLAGS_save=$CFLAGS
|
|
||||||
CPPFLAGS_save=$CPPFLAGS
|
|
||||||
if test "x$PYTHON_INCLUDE_DIR" = "x"; then
|
|
||||||
PYTHON_INCLUDE_DIR=`$PYTHON -c "import distutils.sysconfig; print(distutils.sysconfig.get_python_inc())"`
|
|
||||||
fi
|
|
||||||
CFLAGS="-I$PYTHON_INCLUDE_DIR"
|
|
||||||
CPPFLAGS="-I$PYTHON_INCLUDE_DIR"
|
|
||||||
AC_CHECK_HEADERS([Python.h], [],
|
|
||||||
AC_MSG_ERROR([Python bindings require Python development headers (try installing your 'python-devel' or 'python-dev' package)]))
|
|
||||||
CFLAGS=$CFLAGS_save
|
|
||||||
CPPFLAGS=$CPPFLAGS_save
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; then
|
if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; then
|
||||||
|
@ -220,22 +208,12 @@ if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then
|
||||||
AS_IF([$PYTHON -c "import sphinx_rtd_theme" &> /dev/null], [], [AC_MSG_ERROR([doc dependency missing, please install python3 -m pip install sphinx-rtd-theme])])
|
AS_IF([$PYTHON -c "import sphinx_rtd_theme" &> /dev/null], [], [AC_MSG_ERROR([doc dependency missing, please install python3 -m pip install sphinx-rtd-theme])])
|
||||||
fi
|
fi
|
||||||
|
|
||||||
AC_ARG_WITH([startup],
|
|
||||||
[AS_HELP_STRING([--with-startup=option],
|
|
||||||
[option=systemd,suse,none to install systemd/SUSE init scripts])],
|
|
||||||
[with_startup=$with_startup],
|
|
||||||
[with_startup=initd])
|
|
||||||
AC_SUBST(with_startup)
|
|
||||||
AC_MSG_RESULT([using startup option $with_startup])
|
|
||||||
|
|
||||||
# Variable substitutions
|
# Variable substitutions
|
||||||
AM_CONDITIONAL(WANT_GUI, test x$enable_gui = xyes)
|
AM_CONDITIONAL(WANT_GUI, test x$enable_gui = xyes)
|
||||||
AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes)
|
AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes)
|
||||||
AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes)
|
AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes)
|
||||||
AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes)
|
AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes)
|
||||||
AM_CONDITIONAL(WANT_NETNS, test x$want_linux_netns = xyes)
|
AM_CONDITIONAL(WANT_NETNS, test x$want_linux_netns = xyes)
|
||||||
AM_CONDITIONAL(WANT_INITD, test x$with_startup = xinitd)
|
|
||||||
AM_CONDITIONAL(WANT_SYSTEMD, test x$with_startup = xsystemd)
|
|
||||||
AM_CONDITIONAL(WANT_VNODEDONLY, test x$enable_vnodedonly = xyes)
|
AM_CONDITIONAL(WANT_VNODEDONLY, test x$enable_vnodedonly = xyes)
|
||||||
|
|
||||||
if test $cross_compiling = no; then
|
if test $cross_compiling = no; then
|
||||||
|
@ -249,7 +227,6 @@ AC_CONFIG_FILES([Makefile
|
||||||
gui/version.tcl
|
gui/version.tcl
|
||||||
gui/Makefile
|
gui/Makefile
|
||||||
gui/icons/Makefile
|
gui/icons/Makefile
|
||||||
scripts/Makefile
|
|
||||||
man/Makefile
|
man/Makefile
|
||||||
docs/Makefile
|
docs/Makefile
|
||||||
daemon/Makefile
|
daemon/Makefile
|
||||||
|
@ -279,9 +256,6 @@ Daemon:
|
||||||
Daemon path: ${bindir}
|
Daemon path: ${bindir}
|
||||||
Daemon config: ${CORE_CONF_DIR}
|
Daemon config: ${CORE_CONF_DIR}
|
||||||
Python: ${PYTHON}
|
Python: ${PYTHON}
|
||||||
Logs: ${CORE_STATE_DIR}/log
|
|
||||||
|
|
||||||
Startup: ${with_startup}
|
|
||||||
|
|
||||||
Features to build:
|
Features to build:
|
||||||
Build GUI: ${enable_gui}
|
Build GUI: ${enable_gui}
|
||||||
|
|
2
daemon/.gitignore
vendored
2
daemon/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
*.pyc
|
|
||||||
build
|
|
|
@ -5,19 +5,19 @@ repos:
|
||||||
name: isort
|
name: isort
|
||||||
stages: [commit]
|
stages: [commit]
|
||||||
language: system
|
language: system
|
||||||
entry: bash -c 'cd daemon && pipenv run isort --atomic -y'
|
entry: bash -c 'cd daemon && poetry run isort --atomic -y'
|
||||||
types: [python]
|
types: [python]
|
||||||
|
|
||||||
- id: black
|
- id: black
|
||||||
name: black
|
name: black
|
||||||
stages: [commit]
|
stages: [commit]
|
||||||
language: system
|
language: system
|
||||||
entry: bash -c 'cd daemon && pipenv run black --exclude ".+_pb2.*.py|doc|build|utm\.py" .'
|
entry: bash -c 'cd daemon && poetry run black .'
|
||||||
types: [python]
|
types: [python]
|
||||||
|
|
||||||
- id: flake8
|
- id: flake8
|
||||||
name: flake8
|
name: flake8
|
||||||
stages: [commit]
|
stages: [commit]
|
||||||
language: system
|
language: system
|
||||||
entry: bash -c 'cd daemon && pipenv run flake8'
|
entry: bash -c 'cd daemon && poetry run flake8'
|
||||||
types: [python]
|
types: [python]
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
graft core/gui/data
|
|
||||||
graft core/configservices/*/templates
|
|
|
@ -7,43 +7,12 @@
|
||||||
# Makefile for building netns components.
|
# Makefile for building netns components.
|
||||||
#
|
#
|
||||||
|
|
||||||
SETUPPY = setup.py
|
|
||||||
SETUPPYFLAGS = -v
|
|
||||||
|
|
||||||
if WANT_DOCS
|
if WANT_DOCS
|
||||||
DOCS = doc
|
DOCS = doc
|
||||||
endif
|
endif
|
||||||
|
|
||||||
SUBDIRS = proto $(DOCS)
|
SUBDIRS = proto $(DOCS)
|
||||||
|
|
||||||
SCRIPT_FILES := $(notdir $(wildcard scripts/*))
|
|
||||||
MAN_FILES := $(notdir $(wildcard ../man/*.1))
|
|
||||||
|
|
||||||
# Python package build
|
|
||||||
noinst_SCRIPTS = build
|
|
||||||
build:
|
|
||||||
$(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) build
|
|
||||||
|
|
||||||
# Python package install
|
|
||||||
install-exec-hook:
|
|
||||||
$(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) install \
|
|
||||||
--root=/$(DESTDIR) \
|
|
||||||
--prefix=$(prefix) \
|
|
||||||
--single-version-externally-managed
|
|
||||||
|
|
||||||
# Python package uninstall
|
|
||||||
uninstall-hook:
|
|
||||||
rm -rf $(DESTDIR)/etc/core
|
|
||||||
rm -rf $(DESTDIR)/$(datadir)/core
|
|
||||||
rm -f $(addprefix $(DESTDIR)/$(datarootdir)/man/man1/, $(MAN_FILES))
|
|
||||||
rm -f $(addprefix $(DESTDIR)/$(bindir)/,$(SCRIPT_FILES))
|
|
||||||
rm -rf $(DESTDIR)/$(pythondir)/core-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg-info
|
|
||||||
rm -rf $(DESTDIR)/$(pythondir)/core
|
|
||||||
|
|
||||||
# Python package cleanup
|
|
||||||
clean-local:
|
|
||||||
-rm -rf build
|
|
||||||
|
|
||||||
# because we include entire directories with EXTRA_DIST, we need to clean up
|
# because we include entire directories with EXTRA_DIST, we need to clean up
|
||||||
# the source control files
|
# the source control files
|
||||||
dist-hook:
|
dist-hook:
|
||||||
|
@ -52,17 +21,15 @@ dist-hook:
|
||||||
distclean-local:
|
distclean-local:
|
||||||
-rm -rf core.egg-info
|
-rm -rf core.egg-info
|
||||||
|
|
||||||
|
|
||||||
DISTCLEANFILES = Makefile.in
|
DISTCLEANFILES = Makefile.in
|
||||||
|
|
||||||
# files to include with distribution tarball
|
# files to include with distribution tarball
|
||||||
EXTRA_DIST = $(SETUPPY) \
|
EXTRA_DIST = core \
|
||||||
core \
|
|
||||||
data \
|
data \
|
||||||
doc/conf.py.in \
|
doc/conf.py.in \
|
||||||
examples \
|
examples \
|
||||||
scripts \
|
scripts \
|
||||||
tests \
|
tests \
|
||||||
test.py \
|
|
||||||
setup.cfg \
|
setup.cfg \
|
||||||
requirements.txt
|
poetry.lock \
|
||||||
|
pyproject.toml
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
[[source]]
|
|
||||||
name = "pypi"
|
|
||||||
url = "https://pypi.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
|
|
||||||
[scripts]
|
|
||||||
core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf"
|
|
||||||
core-pygui = "python scripts/core-pygui"
|
|
||||||
test = "pytest -v tests"
|
|
||||||
test-mock = "pytest -v --mock tests"
|
|
||||||
test-emane = "pytest -v tests/emane"
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
grpcio-tools = "*"
|
|
||||||
isort = "*"
|
|
||||||
pre-commit = "*"
|
|
||||||
flake8 = "*"
|
|
||||||
black = "==19.3b0"
|
|
||||||
pytest = "*"
|
|
||||||
mock = "*"
|
|
||||||
|
|
||||||
[packages]
|
|
||||||
core = {editable = true,path = "."}
|
|
732
daemon/Pipfile.lock
generated
732
daemon/Pipfile.lock
generated
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,7 @@ gRpc client for interfacing with CORE.
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any, Callable, Dict, Generator, Iterable, List
|
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ from core.api.grpc.wlan_pb2 import (
|
||||||
WlanLinkRequest,
|
WlanLinkRequest,
|
||||||
WlanLinkResponse,
|
WlanLinkResponse,
|
||||||
)
|
)
|
||||||
from core.emulator.emudata import IpPrefixes
|
from core.emulator.data import IpPrefixes
|
||||||
|
|
||||||
|
|
||||||
class InterfaceHelper:
|
class InterfaceHelper:
|
||||||
|
@ -108,29 +108,29 @@ class InterfaceHelper:
|
||||||
:param ip6_prefix: ip6 prefix to use for generation
|
:param ip6_prefix: ip6 prefix to use for generation
|
||||||
:raises ValueError: when both ip4 and ip6 prefixes have not been provided
|
:raises ValueError: when both ip4 and ip6 prefixes have not been provided
|
||||||
"""
|
"""
|
||||||
self.prefixes = IpPrefixes(ip4_prefix, ip6_prefix)
|
self.prefixes: IpPrefixes = IpPrefixes(ip4_prefix, ip6_prefix)
|
||||||
|
|
||||||
def create_interface(
|
def create_iface(
|
||||||
self, node_id: int, interface_id: int, name: str = None, mac: str = None
|
self, node_id: int, iface_id: int, name: str = None, mac: str = None
|
||||||
) -> core_pb2.Interface:
|
) -> core_pb2.Interface:
|
||||||
"""
|
"""
|
||||||
Create an interface protobuf object.
|
Create an interface protobuf object.
|
||||||
|
|
||||||
:param node_id: node id to create interface for
|
:param node_id: node id to create interface for
|
||||||
:param interface_id: interface id
|
:param iface_id: interface id
|
||||||
:param name: name of interface
|
:param name: name of interface
|
||||||
:param mac: mac address for interface
|
:param mac: mac address for interface
|
||||||
:return: interface protobuf
|
:return: interface protobuf
|
||||||
"""
|
"""
|
||||||
interface_data = self.prefixes.gen_interface(node_id, name, mac)
|
iface_data = self.prefixes.gen_iface(node_id, name, mac)
|
||||||
return core_pb2.Interface(
|
return core_pb2.Interface(
|
||||||
id=interface_id,
|
id=iface_id,
|
||||||
name=interface_data.name,
|
name=iface_data.name,
|
||||||
ip4=interface_data.ip4,
|
ip4=iface_data.ip4,
|
||||||
ip4mask=interface_data.ip4_mask,
|
ip4_mask=iface_data.ip4_mask,
|
||||||
ip6=interface_data.ip6,
|
ip6=iface_data.ip6,
|
||||||
ip6mask=interface_data.ip6_mask,
|
ip6_mask=iface_data.ip6_mask,
|
||||||
mac=interface_data.mac,
|
mac=iface_data.mac,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -177,10 +177,10 @@ class CoreGrpcClient:
|
||||||
|
|
||||||
:param address: grpc server address to connect to
|
:param address: grpc server address to connect to
|
||||||
"""
|
"""
|
||||||
self.address = address
|
self.address: str = address
|
||||||
self.stub = None
|
self.stub: Optional[core_pb2_grpc.CoreApiStub] = None
|
||||||
self.channel = None
|
self.channel: Optional[grpc.Channel] = None
|
||||||
self.proxy = proxy
|
self.proxy: bool = proxy
|
||||||
|
|
||||||
def start_session(
|
def start_session(
|
||||||
self,
|
self,
|
||||||
|
@ -414,6 +414,20 @@ class CoreGrpcClient:
|
||||||
request = core_pb2.SetSessionStateRequest(session_id=session_id, state=state)
|
request = core_pb2.SetSessionStateRequest(session_id=session_id, state=state)
|
||||||
return self.stub.SetSessionState(request)
|
return self.stub.SetSessionState(request)
|
||||||
|
|
||||||
|
def set_session_user(
|
||||||
|
self, session_id: int, user: str
|
||||||
|
) -> core_pb2.SetSessionUserResponse:
|
||||||
|
"""
|
||||||
|
Set session user, used for helping to find files without full paths.
|
||||||
|
|
||||||
|
:param session_id: id of session
|
||||||
|
:param user: user to set for session
|
||||||
|
:return: response with result of success or failure
|
||||||
|
:raises grpc.RpcError: when session doesn't exist
|
||||||
|
"""
|
||||||
|
request = core_pb2.SetSessionUserRequest(session_id=session_id, user=user)
|
||||||
|
return self.stub.SetSessionUser(request)
|
||||||
|
|
||||||
def add_session_server(
|
def add_session_server(
|
||||||
self, session_id: int, name: str, host: str
|
self, session_id: int, name: str, host: str
|
||||||
) -> core_pb2.AddSessionServerResponse:
|
) -> core_pb2.AddSessionServerResponse:
|
||||||
|
@ -431,12 +445,29 @@ class CoreGrpcClient:
|
||||||
)
|
)
|
||||||
return self.stub.AddSessionServer(request)
|
return self.stub.AddSessionServer(request)
|
||||||
|
|
||||||
|
def alert(
|
||||||
|
self,
|
||||||
|
session_id: int,
|
||||||
|
level: core_pb2.ExceptionLevel,
|
||||||
|
source: str,
|
||||||
|
text: str,
|
||||||
|
node_id: int = None,
|
||||||
|
) -> core_pb2.SessionAlertResponse:
|
||||||
|
request = core_pb2.SessionAlertRequest(
|
||||||
|
session_id=session_id,
|
||||||
|
level=level,
|
||||||
|
source=source,
|
||||||
|
text=text,
|
||||||
|
node_id=node_id,
|
||||||
|
)
|
||||||
|
return self.stub.SessionAlert(request)
|
||||||
|
|
||||||
def events(
|
def events(
|
||||||
self,
|
self,
|
||||||
session_id: int,
|
session_id: int,
|
||||||
handler: Callable[[core_pb2.Event], None],
|
handler: Callable[[core_pb2.Event], None],
|
||||||
events: List[core_pb2.Event] = None,
|
events: List[core_pb2.Event] = None,
|
||||||
) -> Any:
|
) -> grpc.Future:
|
||||||
"""
|
"""
|
||||||
Listen for session events.
|
Listen for session events.
|
||||||
|
|
||||||
|
@ -453,7 +484,7 @@ class CoreGrpcClient:
|
||||||
|
|
||||||
def throughputs(
|
def throughputs(
|
||||||
self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None]
|
self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None]
|
||||||
) -> Any:
|
) -> grpc.Future:
|
||||||
"""
|
"""
|
||||||
Listen for throughput events with information for interfaces and bridges.
|
Listen for throughput events with information for interfaces and bridges.
|
||||||
|
|
||||||
|
@ -467,18 +498,36 @@ class CoreGrpcClient:
|
||||||
start_streamer(stream, handler)
|
start_streamer(stream, handler)
|
||||||
return stream
|
return stream
|
||||||
|
|
||||||
|
def cpu_usage(
|
||||||
|
self, delay: int, handler: Callable[[core_pb2.CpuUsageEvent], None]
|
||||||
|
) -> grpc.Future:
|
||||||
|
"""
|
||||||
|
Listen for cpu usage events with the given repeat delay.
|
||||||
|
|
||||||
|
:param delay: delay between receiving events
|
||||||
|
:param handler: handler for every event
|
||||||
|
:return: stream processing events, can be used to cancel stream
|
||||||
|
"""
|
||||||
|
request = core_pb2.CpuUsageRequest(delay=delay)
|
||||||
|
stream = self.stub.CpuUsage(request)
|
||||||
|
start_streamer(stream, handler)
|
||||||
|
return stream
|
||||||
|
|
||||||
def add_node(
|
def add_node(
|
||||||
self, session_id: int, node: core_pb2.Node
|
self, session_id: int, node: core_pb2.Node, source: str = None
|
||||||
) -> core_pb2.AddNodeResponse:
|
) -> core_pb2.AddNodeResponse:
|
||||||
"""
|
"""
|
||||||
Add node to session.
|
Add node to session.
|
||||||
|
|
||||||
:param session_id: session id
|
:param session_id: session id
|
||||||
:param node: node to add
|
:param node: node to add
|
||||||
|
:param source: source application
|
||||||
:return: response with node id
|
:return: response with node id
|
||||||
:raises grpc.RpcError: when session doesn't exist
|
:raises grpc.RpcError: when session doesn't exist
|
||||||
"""
|
"""
|
||||||
request = core_pb2.AddNodeRequest(session_id=session_id, node=node)
|
request = core_pb2.AddNodeRequest(
|
||||||
|
session_id=session_id, node=node, source=source
|
||||||
|
)
|
||||||
return self.stub.AddNode(request)
|
return self.stub.AddNode(request)
|
||||||
|
|
||||||
def get_node(self, session_id: int, node_id: int) -> core_pb2.GetNodeResponse:
|
def get_node(self, session_id: int, node_id: int) -> core_pb2.GetNodeResponse:
|
||||||
|
@ -499,8 +548,8 @@ class CoreGrpcClient:
|
||||||
node_id: int,
|
node_id: int,
|
||||||
position: core_pb2.Position = None,
|
position: core_pb2.Position = None,
|
||||||
icon: str = None,
|
icon: str = None,
|
||||||
source: str = None,
|
|
||||||
geo: core_pb2.Geo = None,
|
geo: core_pb2.Geo = None,
|
||||||
|
source: str = None,
|
||||||
) -> core_pb2.EditNodeResponse:
|
) -> core_pb2.EditNodeResponse:
|
||||||
"""
|
"""
|
||||||
Edit a node, currently only changes position.
|
Edit a node, currently only changes position.
|
||||||
|
@ -509,8 +558,8 @@ class CoreGrpcClient:
|
||||||
:param node_id: node id
|
:param node_id: node id
|
||||||
:param position: position to set node to
|
:param position: position to set node to
|
||||||
:param icon: path to icon for gui to use for node
|
:param icon: path to icon for gui to use for node
|
||||||
:param source: application source editing node
|
|
||||||
:param geo: lon,lat,alt location for node
|
:param geo: lon,lat,alt location for node
|
||||||
|
:param source: application source
|
||||||
:return: response with result of success or failure
|
:return: response with result of success or failure
|
||||||
:raises grpc.RpcError: when session or node doesn't exist
|
:raises grpc.RpcError: when session or node doesn't exist
|
||||||
"""
|
"""
|
||||||
|
@ -536,16 +585,21 @@ class CoreGrpcClient:
|
||||||
"""
|
"""
|
||||||
return self.stub.MoveNodes(move_iterator)
|
return self.stub.MoveNodes(move_iterator)
|
||||||
|
|
||||||
def delete_node(self, session_id: int, node_id: int) -> core_pb2.DeleteNodeResponse:
|
def delete_node(
|
||||||
|
self, session_id: int, node_id: int, source: str = None
|
||||||
|
) -> core_pb2.DeleteNodeResponse:
|
||||||
"""
|
"""
|
||||||
Delete node from session.
|
Delete node from session.
|
||||||
|
|
||||||
:param session_id: session id
|
:param session_id: session id
|
||||||
:param node_id: node id
|
:param node_id: node id
|
||||||
|
:param source: application source
|
||||||
:return: response with result of success or failure
|
:return: response with result of success or failure
|
||||||
:raises grpc.RpcError: when session doesn't exist
|
:raises grpc.RpcError: when session doesn't exist
|
||||||
"""
|
"""
|
||||||
request = core_pb2.DeleteNodeRequest(session_id=session_id, node_id=node_id)
|
request = core_pb2.DeleteNodeRequest(
|
||||||
|
session_id=session_id, node_id=node_id, source=source
|
||||||
|
)
|
||||||
return self.stub.DeleteNode(request)
|
return self.stub.DeleteNode(request)
|
||||||
|
|
||||||
def node_command(
|
def node_command(
|
||||||
|
@ -609,91 +663,101 @@ class CoreGrpcClient:
|
||||||
def add_link(
|
def add_link(
|
||||||
self,
|
self,
|
||||||
session_id: int,
|
session_id: int,
|
||||||
node_one_id: int,
|
node1_id: int,
|
||||||
node_two_id: int,
|
node2_id: int,
|
||||||
interface_one: core_pb2.Interface = None,
|
iface1: core_pb2.Interface = None,
|
||||||
interface_two: core_pb2.Interface = None,
|
iface2: core_pb2.Interface = None,
|
||||||
options: core_pb2.LinkOptions = None,
|
options: core_pb2.LinkOptions = None,
|
||||||
|
source: str = None,
|
||||||
) -> core_pb2.AddLinkResponse:
|
) -> core_pb2.AddLinkResponse:
|
||||||
"""
|
"""
|
||||||
Add a link between nodes.
|
Add a link between nodes.
|
||||||
|
|
||||||
:param session_id: session id
|
:param session_id: session id
|
||||||
:param node_one_id: node one id
|
:param node1_id: node one id
|
||||||
:param node_two_id: node two id
|
:param node2_id: node two id
|
||||||
:param interface_one: node one interface data
|
:param iface1: node one interface data
|
||||||
:param interface_two: node two interface data
|
:param iface2: node two interface data
|
||||||
:param options: options for link (jitter, bandwidth, etc)
|
:param options: options for link (jitter, bandwidth, etc)
|
||||||
|
:param source: application source
|
||||||
:return: response with result of success or failure
|
:return: response with result of success or failure
|
||||||
:raises grpc.RpcError: when session or one of the nodes don't exist
|
:raises grpc.RpcError: when session or one of the nodes don't exist
|
||||||
"""
|
"""
|
||||||
link = core_pb2.Link(
|
link = core_pb2.Link(
|
||||||
node_one_id=node_one_id,
|
node1_id=node1_id,
|
||||||
node_two_id=node_two_id,
|
node2_id=node2_id,
|
||||||
type=core_pb2.LinkType.WIRED,
|
type=core_pb2.LinkType.WIRED,
|
||||||
interface_one=interface_one,
|
iface1=iface1,
|
||||||
interface_two=interface_two,
|
iface2=iface2,
|
||||||
options=options,
|
options=options,
|
||||||
)
|
)
|
||||||
request = core_pb2.AddLinkRequest(session_id=session_id, link=link)
|
request = core_pb2.AddLinkRequest(
|
||||||
|
session_id=session_id, link=link, source=source
|
||||||
|
)
|
||||||
return self.stub.AddLink(request)
|
return self.stub.AddLink(request)
|
||||||
|
|
||||||
def edit_link(
|
def edit_link(
|
||||||
self,
|
self,
|
||||||
session_id: int,
|
session_id: int,
|
||||||
node_one_id: int,
|
node1_id: int,
|
||||||
node_two_id: int,
|
node2_id: int,
|
||||||
options: core_pb2.LinkOptions,
|
options: core_pb2.LinkOptions,
|
||||||
interface_one_id: int = None,
|
iface1_id: int = None,
|
||||||
interface_two_id: int = None,
|
iface2_id: int = None,
|
||||||
|
source: str = None,
|
||||||
) -> core_pb2.EditLinkResponse:
|
) -> core_pb2.EditLinkResponse:
|
||||||
"""
|
"""
|
||||||
Edit a link between nodes.
|
Edit a link between nodes.
|
||||||
|
|
||||||
:param session_id: session id
|
:param session_id: session id
|
||||||
:param node_one_id: node one id
|
:param node1_id: node one id
|
||||||
:param node_two_id: node two id
|
:param node2_id: node two id
|
||||||
:param options: options for link (jitter, bandwidth, etc)
|
:param options: options for link (jitter, bandwidth, etc)
|
||||||
:param interface_one_id: node one interface id
|
:param iface1_id: node one interface id
|
||||||
:param interface_two_id: node two interface id
|
:param iface2_id: node two interface id
|
||||||
|
:param source: application source
|
||||||
:return: response with result of success or failure
|
:return: response with result of success or failure
|
||||||
:raises grpc.RpcError: when session or one of the nodes don't exist
|
:raises grpc.RpcError: when session or one of the nodes don't exist
|
||||||
"""
|
"""
|
||||||
request = core_pb2.EditLinkRequest(
|
request = core_pb2.EditLinkRequest(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
node_one_id=node_one_id,
|
node1_id=node1_id,
|
||||||
node_two_id=node_two_id,
|
node2_id=node2_id,
|
||||||
options=options,
|
options=options,
|
||||||
interface_one_id=interface_one_id,
|
iface1_id=iface1_id,
|
||||||
interface_two_id=interface_two_id,
|
iface2_id=iface2_id,
|
||||||
|
source=source,
|
||||||
)
|
)
|
||||||
return self.stub.EditLink(request)
|
return self.stub.EditLink(request)
|
||||||
|
|
||||||
def delete_link(
|
def delete_link(
|
||||||
self,
|
self,
|
||||||
session_id: int,
|
session_id: int,
|
||||||
node_one_id: int,
|
node1_id: int,
|
||||||
node_two_id: int,
|
node2_id: int,
|
||||||
interface_one_id: int = None,
|
iface1_id: int = None,
|
||||||
interface_two_id: int = None,
|
iface2_id: int = None,
|
||||||
|
source: str = None,
|
||||||
) -> core_pb2.DeleteLinkResponse:
|
) -> core_pb2.DeleteLinkResponse:
|
||||||
"""
|
"""
|
||||||
Delete a link between nodes.
|
Delete a link between nodes.
|
||||||
|
|
||||||
:param session_id: session id
|
:param session_id: session id
|
||||||
:param node_one_id: node one id
|
:param node1_id: node one id
|
||||||
:param node_two_id: node two id
|
:param node2_id: node two id
|
||||||
:param interface_one_id: node one interface id
|
:param iface1_id: node one interface id
|
||||||
:param interface_two_id: node two interface id
|
:param iface2_id: node two interface id
|
||||||
|
:param source: application source
|
||||||
:return: response with result of success or failure
|
:return: response with result of success or failure
|
||||||
:raises grpc.RpcError: when session doesn't exist
|
:raises grpc.RpcError: when session doesn't exist
|
||||||
"""
|
"""
|
||||||
request = core_pb2.DeleteLinkRequest(
|
request = core_pb2.DeleteLinkRequest(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
node_one_id=node_one_id,
|
node1_id=node1_id,
|
||||||
node_two_id=node_two_id,
|
node2_id=node2_id,
|
||||||
interface_one_id=interface_one_id,
|
iface1_id=iface1_id,
|
||||||
interface_two_id=interface_two_id,
|
iface2_id=iface2_id,
|
||||||
|
source=source,
|
||||||
)
|
)
|
||||||
return self.stub.DeleteLink(request)
|
return self.stub.DeleteLink(request)
|
||||||
|
|
||||||
|
@ -1028,7 +1092,7 @@ class CoreGrpcClient:
|
||||||
return self.stub.GetEmaneModels(request)
|
return self.stub.GetEmaneModels(request)
|
||||||
|
|
||||||
def get_emane_model_config(
|
def get_emane_model_config(
|
||||||
self, session_id: int, node_id: int, model: str, interface_id: int = -1
|
self, session_id: int, node_id: int, model: str, iface_id: int = -1
|
||||||
) -> GetEmaneModelConfigResponse:
|
) -> GetEmaneModelConfigResponse:
|
||||||
"""
|
"""
|
||||||
Get emane model configuration for a node or a node's interface.
|
Get emane model configuration for a node or a node's interface.
|
||||||
|
@ -1036,12 +1100,12 @@ class CoreGrpcClient:
|
||||||
:param session_id: session id
|
:param session_id: session id
|
||||||
:param node_id: node id
|
:param node_id: node id
|
||||||
:param model: emane model name
|
:param model: emane model name
|
||||||
:param interface_id: node interface id
|
:param iface_id: node interface id
|
||||||
:return: response with a list of configuration groups
|
:return: response with a list of configuration groups
|
||||||
:raises grpc.RpcError: when session doesn't exist
|
:raises grpc.RpcError: when session doesn't exist
|
||||||
"""
|
"""
|
||||||
request = GetEmaneModelConfigRequest(
|
request = GetEmaneModelConfigRequest(
|
||||||
session_id=session_id, node_id=node_id, model=model, interface=interface_id
|
session_id=session_id, node_id=node_id, model=model, iface_id=iface_id
|
||||||
)
|
)
|
||||||
return self.stub.GetEmaneModelConfig(request)
|
return self.stub.GetEmaneModelConfig(request)
|
||||||
|
|
||||||
|
@ -1051,7 +1115,7 @@ class CoreGrpcClient:
|
||||||
node_id: int,
|
node_id: int,
|
||||||
model: str,
|
model: str,
|
||||||
config: Dict[str, str] = None,
|
config: Dict[str, str] = None,
|
||||||
interface_id: int = -1,
|
iface_id: int = -1,
|
||||||
) -> SetEmaneModelConfigResponse:
|
) -> SetEmaneModelConfigResponse:
|
||||||
"""
|
"""
|
||||||
Set emane model configuration for a node or a node's interface.
|
Set emane model configuration for a node or a node's interface.
|
||||||
|
@ -1060,12 +1124,12 @@ class CoreGrpcClient:
|
||||||
:param node_id: node id
|
:param node_id: node id
|
||||||
:param model: emane model name
|
:param model: emane model name
|
||||||
:param config: emane model configuration
|
:param config: emane model configuration
|
||||||
:param interface_id: node interface id
|
:param iface_id: node interface id
|
||||||
:return: response with result of success or failure
|
:return: response with result of success or failure
|
||||||
:raises grpc.RpcError: when session doesn't exist
|
:raises grpc.RpcError: when session doesn't exist
|
||||||
"""
|
"""
|
||||||
model_config = EmaneModelConfig(
|
model_config = EmaneModelConfig(
|
||||||
node_id=node_id, model=model, config=config, interface_id=interface_id
|
node_id=node_id, model=model, config=config, iface_id=iface_id
|
||||||
)
|
)
|
||||||
request = SetEmaneModelConfigRequest(
|
request = SetEmaneModelConfigRequest(
|
||||||
session_id=session_id, emane_model_config=model_config
|
session_id=session_id, emane_model_config=model_config
|
||||||
|
@ -1111,24 +1175,24 @@ class CoreGrpcClient:
|
||||||
return self.stub.OpenXml(request)
|
return self.stub.OpenXml(request)
|
||||||
|
|
||||||
def emane_link(
|
def emane_link(
|
||||||
self, session_id: int, nem_one: int, nem_two: int, linked: bool
|
self, session_id: int, nem1: int, nem2: int, linked: bool
|
||||||
) -> EmaneLinkResponse:
|
) -> EmaneLinkResponse:
|
||||||
"""
|
"""
|
||||||
Helps broadcast wireless link/unlink between EMANE nodes.
|
Helps broadcast wireless link/unlink between EMANE nodes.
|
||||||
|
|
||||||
:param session_id: session to emane link
|
:param session_id: session to emane link
|
||||||
:param nem_one: first nem for emane link
|
:param nem1: first nem for emane link
|
||||||
:param nem_two: second nem for emane link
|
:param nem2: second nem for emane link
|
||||||
:param linked: True to link, False to unlink
|
:param linked: True to link, False to unlink
|
||||||
:return: get emane link response
|
:return: get emane link response
|
||||||
:raises grpc.RpcError: when session or nodes related to nems do not exist
|
:raises grpc.RpcError: when session or nodes related to nems do not exist
|
||||||
"""
|
"""
|
||||||
request = EmaneLinkRequest(
|
request = EmaneLinkRequest(
|
||||||
session_id=session_id, nem_one=nem_one, nem_two=nem_two, linked=linked
|
session_id=session_id, nem1=nem1, nem2=nem2, linked=linked
|
||||||
)
|
)
|
||||||
return self.stub.EmaneLink(request)
|
return self.stub.EmaneLink(request)
|
||||||
|
|
||||||
def get_interfaces(self) -> core_pb2.GetInterfacesResponse:
|
def get_ifaces(self) -> core_pb2.GetInterfacesResponse:
|
||||||
"""
|
"""
|
||||||
Retrieves a list of interfaces available on the host machine that are not
|
Retrieves a list of interfaces available on the host machine that are not
|
||||||
a part of a CORE session.
|
a part of a CORE session.
|
||||||
|
@ -1243,24 +1307,24 @@ class CoreGrpcClient:
|
||||||
return self.stub.ExecuteScript(request)
|
return self.stub.ExecuteScript(request)
|
||||||
|
|
||||||
def wlan_link(
|
def wlan_link(
|
||||||
self, session_id: int, wlan: int, node_one: int, node_two: int, linked: bool
|
self, session_id: int, wlan_id: int, node1_id: int, node2_id: int, linked: bool
|
||||||
) -> WlanLinkResponse:
|
) -> WlanLinkResponse:
|
||||||
"""
|
"""
|
||||||
Links/unlinks nodes on the same WLAN.
|
Links/unlinks nodes on the same WLAN.
|
||||||
|
|
||||||
:param session_id: session id containing wlan and nodes
|
:param session_id: session id containing wlan and nodes
|
||||||
:param wlan: wlan nodes must belong to
|
:param wlan_id: wlan nodes must belong to
|
||||||
:param node_one: first node of pair to link/unlink
|
:param node1_id: first node of pair to link/unlink
|
||||||
:param node_two: second node of pair to link/unlin
|
:param node2_id: second node of pair to link/unlin
|
||||||
:param linked: True to link, False to unlink
|
:param linked: True to link, False to unlink
|
||||||
:return: wlan link response
|
:return: wlan link response
|
||||||
:raises grpc.RpcError: when session or one of the nodes do not exist
|
:raises grpc.RpcError: when session or one of the nodes do not exist
|
||||||
"""
|
"""
|
||||||
request = WlanLinkRequest(
|
request = WlanLinkRequest(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
wlan=wlan,
|
wlan=wlan_id,
|
||||||
node_one=node_one,
|
node1_id=node1_id,
|
||||||
node_two=node_two,
|
node2_id=node2_id,
|
||||||
linked=linked,
|
linked=linked,
|
||||||
)
|
)
|
||||||
return self.stub.WlanLink(request)
|
return self.stub.WlanLink(request)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
from queue import Empty, Queue
|
from queue import Empty, Queue
|
||||||
from typing import Iterable
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
from core.api.grpc.grpcutils import convert_link
|
from core.api.grpc.grpcutils import convert_link
|
||||||
|
@ -15,115 +15,127 @@ from core.emulator.data import (
|
||||||
from core.emulator.session import Session
|
from core.emulator.session import Session
|
||||||
|
|
||||||
|
|
||||||
def handle_node_event(event: NodeData) -> core_pb2.NodeEvent:
|
def handle_node_event(node_data: NodeData) -> core_pb2.Event:
|
||||||
"""
|
"""
|
||||||
Handle node event when there is a node event
|
Handle node event when there is a node event
|
||||||
|
|
||||||
:param event: node data
|
:param node_data: node data
|
||||||
:return: node event that contains node id, name, model, position, and services
|
:return: node event that contains node id, name, model, position, and services
|
||||||
"""
|
"""
|
||||||
position = core_pb2.Position(x=event.x_position, y=event.y_position)
|
node = node_data.node
|
||||||
geo = core_pb2.Geo(lat=event.latitude, lon=event.longitude, alt=event.altitude)
|
x, y, _ = node.position.get()
|
||||||
|
position = core_pb2.Position(x=x, y=y)
|
||||||
|
lon, lat, alt = node.position.get_geo()
|
||||||
|
geo = core_pb2.Geo(lon=lon, lat=lat, alt=alt)
|
||||||
|
services = [x.name for x in node.services]
|
||||||
node_proto = core_pb2.Node(
|
node_proto = core_pb2.Node(
|
||||||
id=event.id,
|
id=node.id,
|
||||||
name=event.name,
|
name=node.name,
|
||||||
model=event.model,
|
model=node.type,
|
||||||
position=position,
|
position=position,
|
||||||
geo=geo,
|
geo=geo,
|
||||||
services=event.services,
|
services=services,
|
||||||
)
|
)
|
||||||
return core_pb2.NodeEvent(node=node_proto, source=event.source)
|
message_type = node_data.message_type.value
|
||||||
|
node_event = core_pb2.NodeEvent(message_type=message_type, node=node_proto)
|
||||||
|
return core_pb2.Event(node_event=node_event, source=node_data.source)
|
||||||
|
|
||||||
|
|
||||||
def handle_link_event(event: LinkData) -> core_pb2.LinkEvent:
|
def handle_link_event(link_data: LinkData) -> core_pb2.Event:
|
||||||
"""
|
"""
|
||||||
Handle link event when there is a link event
|
Handle link event when there is a link event
|
||||||
|
|
||||||
:param event: link data
|
:param link_data: link data
|
||||||
:return: link event that has message type and link information
|
:return: link event that has message type and link information
|
||||||
"""
|
"""
|
||||||
link = convert_link(event)
|
link = convert_link(link_data)
|
||||||
return core_pb2.LinkEvent(message_type=event.message_type.value, link=link)
|
message_type = link_data.message_type.value
|
||||||
|
link_event = core_pb2.LinkEvent(message_type=message_type, link=link)
|
||||||
|
return core_pb2.Event(link_event=link_event, source=link_data.source)
|
||||||
|
|
||||||
|
|
||||||
def handle_session_event(event: EventData) -> core_pb2.SessionEvent:
|
def handle_session_event(event_data: EventData) -> core_pb2.Event:
|
||||||
"""
|
"""
|
||||||
Handle session event when there is a session event
|
Handle session event when there is a session event
|
||||||
|
|
||||||
:param event: event data
|
:param event_data: event data
|
||||||
:return: session event
|
:return: session event
|
||||||
"""
|
"""
|
||||||
event_time = event.time
|
event_time = event_data.time
|
||||||
if event_time is not None:
|
if event_time is not None:
|
||||||
event_time = float(event_time)
|
event_time = float(event_time)
|
||||||
return core_pb2.SessionEvent(
|
session_event = core_pb2.SessionEvent(
|
||||||
node_id=event.node,
|
node_id=event_data.node,
|
||||||
event=event.event_type.value,
|
event=event_data.event_type.value,
|
||||||
name=event.name,
|
name=event_data.name,
|
||||||
data=event.data,
|
data=event_data.data,
|
||||||
time=event_time,
|
time=event_time,
|
||||||
)
|
)
|
||||||
|
return core_pb2.Event(session_event=session_event)
|
||||||
|
|
||||||
|
|
||||||
def handle_config_event(event: ConfigData) -> core_pb2.ConfigEvent:
|
def handle_config_event(config_data: ConfigData) -> core_pb2.Event:
|
||||||
"""
|
"""
|
||||||
Handle configuration event when there is configuration event
|
Handle configuration event when there is configuration event
|
||||||
|
|
||||||
:param event: configuration data
|
:param config_data: configuration data
|
||||||
:return: configuration event
|
:return: configuration event
|
||||||
"""
|
"""
|
||||||
return core_pb2.ConfigEvent(
|
config_event = core_pb2.ConfigEvent(
|
||||||
message_type=event.message_type,
|
message_type=config_data.message_type,
|
||||||
node_id=event.node,
|
node_id=config_data.node,
|
||||||
object=event.object,
|
object=config_data.object,
|
||||||
type=event.type,
|
type=config_data.type,
|
||||||
captions=event.captions,
|
captions=config_data.captions,
|
||||||
bitmap=event.bitmap,
|
bitmap=config_data.bitmap,
|
||||||
data_values=event.data_values,
|
data_values=config_data.data_values,
|
||||||
possible_values=event.possible_values,
|
possible_values=config_data.possible_values,
|
||||||
groups=event.groups,
|
groups=config_data.groups,
|
||||||
interface=event.interface_number,
|
iface_id=config_data.iface_id,
|
||||||
network_id=event.network_id,
|
network_id=config_data.network_id,
|
||||||
opaque=event.opaque,
|
opaque=config_data.opaque,
|
||||||
data_types=event.data_types,
|
data_types=config_data.data_types,
|
||||||
)
|
)
|
||||||
|
return core_pb2.Event(config_event=config_event)
|
||||||
|
|
||||||
|
|
||||||
def handle_exception_event(event: ExceptionData) -> core_pb2.ExceptionEvent:
|
def handle_exception_event(exception_data: ExceptionData) -> core_pb2.Event:
|
||||||
"""
|
"""
|
||||||
Handle exception event when there is exception event
|
Handle exception event when there is exception event
|
||||||
|
|
||||||
:param event: exception data
|
:param exception_data: exception data
|
||||||
:return: exception event
|
:return: exception event
|
||||||
"""
|
"""
|
||||||
return core_pb2.ExceptionEvent(
|
exception_event = core_pb2.ExceptionEvent(
|
||||||
node_id=event.node,
|
node_id=exception_data.node,
|
||||||
level=event.level.value,
|
level=exception_data.level.value,
|
||||||
source=event.source,
|
source=exception_data.source,
|
||||||
date=event.date,
|
date=exception_data.date,
|
||||||
text=event.text,
|
text=exception_data.text,
|
||||||
opaque=event.opaque,
|
opaque=exception_data.opaque,
|
||||||
)
|
)
|
||||||
|
return core_pb2.Event(exception_event=exception_event)
|
||||||
|
|
||||||
|
|
||||||
def handle_file_event(event: FileData) -> core_pb2.FileEvent:
|
def handle_file_event(file_data: FileData) -> core_pb2.Event:
|
||||||
"""
|
"""
|
||||||
Handle file event
|
Handle file event
|
||||||
|
|
||||||
:param event: file data
|
:param file_data: file data
|
||||||
:return: file event
|
:return: file event
|
||||||
"""
|
"""
|
||||||
return core_pb2.FileEvent(
|
file_event = core_pb2.FileEvent(
|
||||||
message_type=event.message_type.value,
|
message_type=file_data.message_type.value,
|
||||||
node_id=event.node,
|
node_id=file_data.node,
|
||||||
name=event.name,
|
name=file_data.name,
|
||||||
mode=event.mode,
|
mode=file_data.mode,
|
||||||
number=event.number,
|
number=file_data.number,
|
||||||
type=event.type,
|
type=file_data.type,
|
||||||
source=event.source,
|
source=file_data.source,
|
||||||
data=event.data,
|
data=file_data.data,
|
||||||
compressed_data=event.compressed_data,
|
compressed_data=file_data.compressed_data,
|
||||||
)
|
)
|
||||||
|
return core_pb2.Event(file_event=file_event)
|
||||||
|
|
||||||
|
|
||||||
class EventStreamer:
|
class EventStreamer:
|
||||||
|
@ -140,9 +152,9 @@ class EventStreamer:
|
||||||
:param session: session to process events for
|
:param session: session to process events for
|
||||||
:param event_types: types of events to process
|
:param event_types: types of events to process
|
||||||
"""
|
"""
|
||||||
self.session = session
|
self.session: Session = session
|
||||||
self.event_types = event_types
|
self.event_types: Iterable[core_pb2.EventType] = event_types
|
||||||
self.queue = Queue()
|
self.queue: Queue = Queue()
|
||||||
self.add_handlers()
|
self.add_handlers()
|
||||||
|
|
||||||
def add_handlers(self) -> None:
|
def add_handlers(self) -> None:
|
||||||
|
@ -164,32 +176,33 @@ class EventStreamer:
|
||||||
if core_pb2.EventType.SESSION in self.event_types:
|
if core_pb2.EventType.SESSION in self.event_types:
|
||||||
self.session.event_handlers.append(self.queue.put)
|
self.session.event_handlers.append(self.queue.put)
|
||||||
|
|
||||||
def process(self) -> core_pb2.Event:
|
def process(self) -> Optional[core_pb2.Event]:
|
||||||
"""
|
"""
|
||||||
Process the next event in the queue.
|
Process the next event in the queue.
|
||||||
|
|
||||||
:return: grpc event, or None when invalid event or queue timeout
|
:return: grpc event, or None when invalid event or queue timeout
|
||||||
"""
|
"""
|
||||||
event = core_pb2.Event(session_id=self.session.id)
|
event = None
|
||||||
try:
|
try:
|
||||||
data = self.queue.get(timeout=1)
|
data = self.queue.get(timeout=1)
|
||||||
if isinstance(data, NodeData):
|
if isinstance(data, NodeData):
|
||||||
event.node_event.CopyFrom(handle_node_event(data))
|
event = handle_node_event(data)
|
||||||
elif isinstance(data, LinkData):
|
elif isinstance(data, LinkData):
|
||||||
event.link_event.CopyFrom(handle_link_event(data))
|
event = handle_link_event(data)
|
||||||
elif isinstance(data, EventData):
|
elif isinstance(data, EventData):
|
||||||
event.session_event.CopyFrom(handle_session_event(data))
|
event = handle_session_event(data)
|
||||||
elif isinstance(data, ConfigData):
|
elif isinstance(data, ConfigData):
|
||||||
event.config_event.CopyFrom(handle_config_event(data))
|
event = handle_config_event(data)
|
||||||
elif isinstance(data, ExceptionData):
|
elif isinstance(data, ExceptionData):
|
||||||
event.exception_event.CopyFrom(handle_exception_event(data))
|
event = handle_exception_event(data)
|
||||||
elif isinstance(data, FileData):
|
elif isinstance(data, FileData):
|
||||||
event.file_event.CopyFrom(handle_file_event(data))
|
event = handle_file_event(data)
|
||||||
else:
|
else:
|
||||||
logging.error("unknown event: %s", data)
|
logging.error("unknown event: %s", data)
|
||||||
event = None
|
|
||||||
except Empty:
|
except Empty:
|
||||||
event = None
|
pass
|
||||||
|
if event:
|
||||||
|
event.session_id = self.session.id
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def remove_handlers(self) -> None:
|
def remove_handlers(self) -> None:
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Tuple, Type
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Tuple, Type, Union
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
import netaddr
|
|
||||||
from grpc import ServicerContext
|
from grpc import ServicerContext
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
@ -11,8 +11,7 @@ from core.api.grpc import common_pb2, core_pb2
|
||||||
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig
|
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig
|
||||||
from core.config import ConfigurableOptions
|
from core.config import ConfigurableOptions
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.emulator.data import LinkData
|
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
|
||||||
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
|
|
||||||
from core.emulator.enumerations import LinkTypes, NodeTypes
|
from core.emulator.enumerations import LinkTypes, NodeTypes
|
||||||
from core.emulator.session import Session
|
from core.emulator.session import Session
|
||||||
from core.nodes.base import CoreNode, NodeBase
|
from core.nodes.base import CoreNode, NodeBase
|
||||||
|
@ -22,6 +21,25 @@ from core.services.coreservices import CoreService
|
||||||
WORKERS = 10
|
WORKERS = 10
|
||||||
|
|
||||||
|
|
||||||
|
class CpuUsage:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.stat_file: Path = Path("/proc/stat")
|
||||||
|
self.prev_idle: int = 0
|
||||||
|
self.prev_total: int = 0
|
||||||
|
|
||||||
|
def run(self) -> float:
|
||||||
|
lines = self.stat_file.read_text().splitlines()[0]
|
||||||
|
values = [int(x) for x in lines.split()[1:]]
|
||||||
|
idle = sum(values[3:5])
|
||||||
|
non_idle = sum(values[:3] + values[5:8])
|
||||||
|
total = idle + non_idle
|
||||||
|
total_diff = total - self.prev_total
|
||||||
|
idle_diff = idle - self.prev_idle
|
||||||
|
self.prev_idle = idle
|
||||||
|
self.prev_total = total
|
||||||
|
return (total_diff - idle_diff) / total_diff
|
||||||
|
|
||||||
|
|
||||||
def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOptions]:
|
def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOptions]:
|
||||||
"""
|
"""
|
||||||
Convert node protobuf message to data for creating a node.
|
Convert node protobuf message to data for creating a node.
|
||||||
|
@ -35,7 +53,6 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
|
||||||
name=node_proto.name,
|
name=node_proto.name,
|
||||||
model=node_proto.model,
|
model=node_proto.model,
|
||||||
icon=node_proto.icon,
|
icon=node_proto.icon,
|
||||||
opaque=node_proto.opaque,
|
|
||||||
image=node_proto.image,
|
image=node_proto.image,
|
||||||
services=node_proto.services,
|
services=node_proto.services,
|
||||||
config_services=node_proto.config_services,
|
config_services=node_proto.config_services,
|
||||||
|
@ -52,58 +69,57 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
|
||||||
return _type, _id, options
|
return _type, _id, options
|
||||||
|
|
||||||
|
|
||||||
def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData:
|
def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
|
||||||
"""
|
"""
|
||||||
Create interface data from interface proto.
|
Create interface data from interface proto.
|
||||||
|
|
||||||
:param interface_proto: interface proto
|
:param iface_proto: interface proto
|
||||||
:return: interface data
|
:return: interface data
|
||||||
"""
|
"""
|
||||||
interface = None
|
iface_data = None
|
||||||
if interface_proto:
|
if iface_proto:
|
||||||
name = interface_proto.name if interface_proto.name else None
|
name = iface_proto.name if iface_proto.name else None
|
||||||
mac = interface_proto.mac if interface_proto.mac else None
|
mac = iface_proto.mac if iface_proto.mac else None
|
||||||
ip4 = interface_proto.ip4 if interface_proto.ip4 else None
|
ip4 = iface_proto.ip4 if iface_proto.ip4 else None
|
||||||
ip6 = interface_proto.ip6 if interface_proto.ip6 else None
|
ip6 = iface_proto.ip6 if iface_proto.ip6 else None
|
||||||
interface = InterfaceData(
|
iface_data = InterfaceData(
|
||||||
id=interface_proto.id,
|
id=iface_proto.id,
|
||||||
name=name,
|
name=name,
|
||||||
mac=mac,
|
mac=mac,
|
||||||
ip4=ip4,
|
ip4=ip4,
|
||||||
ip4_mask=interface_proto.ip4mask,
|
ip4_mask=iface_proto.ip4_mask,
|
||||||
ip6=ip6,
|
ip6=ip6,
|
||||||
ip6_mask=interface_proto.ip6mask,
|
ip6_mask=iface_proto.ip6_mask,
|
||||||
)
|
)
|
||||||
return interface
|
return iface_data
|
||||||
|
|
||||||
|
|
||||||
def add_link_data(
|
def add_link_data(
|
||||||
link_proto: core_pb2.Link
|
link_proto: core_pb2.Link
|
||||||
) -> Tuple[InterfaceData, InterfaceData, LinkOptions]:
|
) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]:
|
||||||
"""
|
"""
|
||||||
Convert link proto to link interfaces and options data.
|
Convert link proto to link interfaces and options data.
|
||||||
|
|
||||||
:param link_proto: link proto
|
:param link_proto: link proto
|
||||||
:return: link interfaces and options
|
:return: link interfaces and options
|
||||||
"""
|
"""
|
||||||
interface_one = link_interface(link_proto.interface_one)
|
iface1_data = link_iface(link_proto.iface1)
|
||||||
interface_two = link_interface(link_proto.interface_two)
|
iface2_data = link_iface(link_proto.iface2)
|
||||||
link_type = LinkTypes(link_proto.type)
|
link_type = LinkTypes(link_proto.type)
|
||||||
options = LinkOptions(type=link_type)
|
options = LinkOptions()
|
||||||
options_data = link_proto.options
|
options_proto = link_proto.options
|
||||||
if options_data:
|
if options_proto:
|
||||||
options.delay = options_data.delay
|
options.delay = options_proto.delay
|
||||||
options.bandwidth = options_data.bandwidth
|
options.bandwidth = options_proto.bandwidth
|
||||||
options.per = options_data.per
|
options.loss = options_proto.loss
|
||||||
options.dup = options_data.dup
|
options.dup = options_proto.dup
|
||||||
options.jitter = options_data.jitter
|
options.jitter = options_proto.jitter
|
||||||
options.mer = options_data.mer
|
options.mer = options_proto.mer
|
||||||
options.burst = options_data.burst
|
options.burst = options_proto.burst
|
||||||
options.mburst = options_data.mburst
|
options.mburst = options_proto.mburst
|
||||||
options.unidirectional = options_data.unidirectional
|
options.unidirectional = options_proto.unidirectional
|
||||||
options.key = options_data.key
|
options.key = options_proto.key
|
||||||
options.opaque = options_data.opaque
|
return iface1_data, iface2_data, options, link_type
|
||||||
return interface_one, interface_two, options
|
|
||||||
|
|
||||||
|
|
||||||
def create_nodes(
|
def create_nodes(
|
||||||
|
@ -141,10 +157,10 @@ def create_links(
|
||||||
"""
|
"""
|
||||||
funcs = []
|
funcs = []
|
||||||
for link_proto in link_protos:
|
for link_proto in link_protos:
|
||||||
node_one_id = link_proto.node_one_id
|
node1_id = link_proto.node1_id
|
||||||
node_two_id = link_proto.node_two_id
|
node2_id = link_proto.node2_id
|
||||||
interface_one, interface_two, options = add_link_data(link_proto)
|
iface1, iface2, options, link_type = add_link_data(link_proto)
|
||||||
args = (node_one_id, node_two_id, interface_one, interface_two, options)
|
args = (node1_id, node2_id, iface1, iface2, options, link_type)
|
||||||
funcs.append((session.add_link, args, {}))
|
funcs.append((session.add_link, args, {}))
|
||||||
start = time.monotonic()
|
start = time.monotonic()
|
||||||
results, exceptions = utils.threadpool(funcs)
|
results, exceptions = utils.threadpool(funcs)
|
||||||
|
@ -165,10 +181,10 @@ def edit_links(
|
||||||
"""
|
"""
|
||||||
funcs = []
|
funcs = []
|
||||||
for link_proto in link_protos:
|
for link_proto in link_protos:
|
||||||
node_one_id = link_proto.node_one_id
|
node1_id = link_proto.node1_id
|
||||||
node_two_id = link_proto.node_two_id
|
node2_id = link_proto.node2_id
|
||||||
interface_one, interface_two, options = add_link_data(link_proto)
|
iface1, iface2, options, link_type = add_link_data(link_proto)
|
||||||
args = (node_one_id, node_two_id, interface_one.id, interface_two.id, options)
|
args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type)
|
||||||
funcs.append((session.update_link, args, {}))
|
funcs.append((session.update_link, args, {}))
|
||||||
start = time.monotonic()
|
start = time.monotonic()
|
||||||
results, exceptions = utils.threadpool(funcs)
|
results, exceptions = utils.threadpool(funcs)
|
||||||
|
@ -190,7 +206,8 @@ def convert_value(value: Any) -> str:
|
||||||
|
|
||||||
|
|
||||||
def get_config_options(
|
def get_config_options(
|
||||||
config: Dict[str, str], configurable_options: Type[ConfigurableOptions]
|
config: Dict[str, str],
|
||||||
|
configurable_options: Union[ConfigurableOptions, Type[ConfigurableOptions]],
|
||||||
) -> Dict[str, common_pb2.ConfigOption]:
|
) -> Dict[str, common_pb2.ConfigOption]:
|
||||||
"""
|
"""
|
||||||
Retrieve configuration options in a form that is used by the grpc server.
|
Retrieve configuration options in a form that is used by the grpc server.
|
||||||
|
@ -272,22 +289,22 @@ def get_links(node: NodeBase):
|
||||||
:return: protobuf links
|
:return: protobuf links
|
||||||
"""
|
"""
|
||||||
links = []
|
links = []
|
||||||
for link_data in node.all_link_data():
|
for link in node.links():
|
||||||
link = convert_link(link_data)
|
link_proto = convert_link(link)
|
||||||
links.append(link)
|
links.append(link_proto)
|
||||||
return links
|
return links
|
||||||
|
|
||||||
|
|
||||||
def get_emane_model_id(node_id: int, interface_id: int) -> int:
|
def get_emane_model_id(node_id: int, iface_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
Get EMANE model id
|
Get EMANE model id
|
||||||
|
|
||||||
:param node_id: node id
|
:param node_id: node id
|
||||||
:param interface_id: interface id
|
:param iface_id: interface id
|
||||||
:return: EMANE model id
|
:return: EMANE model id
|
||||||
"""
|
"""
|
||||||
if interface_id >= 0:
|
if iface_id >= 0:
|
||||||
return node_id * 1000 + interface_id
|
return node_id * 1000 + iface_id
|
||||||
else:
|
else:
|
||||||
return node_id
|
return node_id
|
||||||
|
|
||||||
|
@ -299,12 +316,39 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]:
|
||||||
:param _id: id to parse
|
:param _id: id to parse
|
||||||
:return: node id and interface id
|
:return: node id and interface id
|
||||||
"""
|
"""
|
||||||
interface = -1
|
iface_id = -1
|
||||||
node_id = _id
|
node_id = _id
|
||||||
if _id >= 1000:
|
if _id >= 1000:
|
||||||
interface = _id % 1000
|
iface_id = _id % 1000
|
||||||
node_id = int(_id / 1000)
|
node_id = int(_id / 1000)
|
||||||
return node_id, interface
|
return node_id, iface_id
|
||||||
|
|
||||||
|
|
||||||
|
def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface:
|
||||||
|
return core_pb2.Interface(
|
||||||
|
id=iface_data.id,
|
||||||
|
name=iface_data.name,
|
||||||
|
mac=iface_data.mac,
|
||||||
|
ip4=iface_data.ip4,
|
||||||
|
ip4_mask=iface_data.ip4_mask,
|
||||||
|
ip6=iface_data.ip6,
|
||||||
|
ip6_mask=iface_data.ip6_mask,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions:
|
||||||
|
return core_pb2.LinkOptions(
|
||||||
|
jitter=options_data.jitter,
|
||||||
|
key=options_data.key,
|
||||||
|
mburst=options_data.mburst,
|
||||||
|
mer=options_data.mer,
|
||||||
|
loss=options_data.loss,
|
||||||
|
bandwidth=options_data.bandwidth,
|
||||||
|
burst=options_data.burst,
|
||||||
|
delay=options_data.delay,
|
||||||
|
dup=options_data.dup,
|
||||||
|
unidirectional=options_data.unidirectional,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def convert_link(link_data: LinkData) -> core_pb2.Link:
|
def convert_link(link_data: LinkData) -> core_pb2.Link:
|
||||||
|
@ -314,47 +358,19 @@ def convert_link(link_data: LinkData) -> core_pb2.Link:
|
||||||
:param link_data: link to convert
|
:param link_data: link to convert
|
||||||
:return: core protobuf Link
|
:return: core protobuf Link
|
||||||
"""
|
"""
|
||||||
interface_one = None
|
iface1 = None
|
||||||
if link_data.interface1_id is not None:
|
if link_data.iface1 is not None:
|
||||||
interface_one = core_pb2.Interface(
|
iface1 = convert_iface(link_data.iface1)
|
||||||
id=link_data.interface1_id,
|
iface2 = None
|
||||||
name=link_data.interface1_name,
|
if link_data.iface2 is not None:
|
||||||
mac=convert_value(link_data.interface1_mac),
|
iface2 = convert_iface(link_data.iface2)
|
||||||
ip4=convert_value(link_data.interface1_ip4),
|
options = convert_link_options(link_data.options)
|
||||||
ip4mask=link_data.interface1_ip4_mask,
|
|
||||||
ip6=convert_value(link_data.interface1_ip6),
|
|
||||||
ip6mask=link_data.interface1_ip6_mask,
|
|
||||||
)
|
|
||||||
interface_two = None
|
|
||||||
if link_data.interface2_id is not None:
|
|
||||||
interface_two = core_pb2.Interface(
|
|
||||||
id=link_data.interface2_id,
|
|
||||||
name=link_data.interface2_name,
|
|
||||||
mac=convert_value(link_data.interface2_mac),
|
|
||||||
ip4=convert_value(link_data.interface2_ip4),
|
|
||||||
ip4mask=link_data.interface2_ip4_mask,
|
|
||||||
ip6=convert_value(link_data.interface2_ip6),
|
|
||||||
ip6mask=link_data.interface2_ip6_mask,
|
|
||||||
)
|
|
||||||
options = core_pb2.LinkOptions(
|
|
||||||
opaque=link_data.opaque,
|
|
||||||
jitter=link_data.jitter,
|
|
||||||
key=link_data.key,
|
|
||||||
mburst=link_data.mburst,
|
|
||||||
mer=link_data.mer,
|
|
||||||
per=link_data.per,
|
|
||||||
bandwidth=link_data.bandwidth,
|
|
||||||
burst=link_data.burst,
|
|
||||||
delay=link_data.delay,
|
|
||||||
dup=link_data.dup,
|
|
||||||
unidirectional=link_data.unidirectional,
|
|
||||||
)
|
|
||||||
return core_pb2.Link(
|
return core_pb2.Link(
|
||||||
type=link_data.link_type.value,
|
type=link_data.type.value,
|
||||||
node_one_id=link_data.node1_id,
|
node1_id=link_data.node1_id,
|
||||||
node_two_id=link_data.node2_id,
|
node2_id=link_data.node2_id,
|
||||||
interface_one=interface_one,
|
iface1=iface1,
|
||||||
interface_two=interface_two,
|
iface2=iface2,
|
||||||
options=options,
|
options=options,
|
||||||
network_id=link_data.network_id,
|
network_id=link_data.network_id,
|
||||||
label=link_data.label,
|
label=link_data.label,
|
||||||
|
@ -418,7 +434,7 @@ def service_configuration(session: Session, config: ServiceConfig) -> None:
|
||||||
service.shutdown = tuple(config.shutdown)
|
service.shutdown = tuple(config.shutdown)
|
||||||
|
|
||||||
|
|
||||||
def get_service_configuration(service: Type[CoreService]) -> NodeServiceData:
|
def get_service_configuration(service: CoreService) -> NodeServiceData:
|
||||||
"""
|
"""
|
||||||
Convenience for converting a service to service data proto.
|
Convenience for converting a service to service data proto.
|
||||||
|
|
||||||
|
@ -439,58 +455,84 @@ def get_service_configuration(service: Type[CoreService]) -> NodeServiceData:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface:
|
def iface_to_data(iface: CoreInterface) -> InterfaceData:
|
||||||
"""
|
ip4 = iface.get_ip4()
|
||||||
Convenience for converting a core interface to the protobuf representation.
|
ip4_addr = str(ip4.ip) if ip4 else None
|
||||||
:param interface: interface to convert
|
ip4_mask = ip4.prefixlen if ip4 else None
|
||||||
:return: interface proto
|
ip6 = iface.get_ip6()
|
||||||
"""
|
ip6_addr = str(ip6.ip) if ip6 else None
|
||||||
net_id = None
|
ip6_mask = ip6.prefixlen if ip6 else None
|
||||||
if interface.net:
|
return InterfaceData(
|
||||||
net_id = interface.net.id
|
id=iface.node_id,
|
||||||
ip4 = None
|
name=iface.name,
|
||||||
ip4mask = None
|
mac=str(iface.mac),
|
||||||
ip6 = None
|
ip4=ip4_addr,
|
||||||
ip6mask = None
|
ip4_mask=ip4_mask,
|
||||||
for addr in interface.addrlist:
|
ip6=ip6_addr,
|
||||||
network = netaddr.IPNetwork(addr)
|
ip6_mask=ip6_mask,
|
||||||
mask = network.prefixlen
|
|
||||||
ip = str(network.ip)
|
|
||||||
if netaddr.valid_ipv4(ip) and not ip4:
|
|
||||||
ip4 = ip
|
|
||||||
ip4mask = mask
|
|
||||||
elif netaddr.valid_ipv6(ip) and not ip6:
|
|
||||||
ip6 = ip
|
|
||||||
ip6mask = mask
|
|
||||||
return core_pb2.Interface(
|
|
||||||
id=interface.netindex,
|
|
||||||
netid=net_id,
|
|
||||||
name=interface.name,
|
|
||||||
mac=str(interface.hwaddr),
|
|
||||||
mtu=interface.mtu,
|
|
||||||
flowid=interface.flow_id,
|
|
||||||
ip4=ip4,
|
|
||||||
ip4mask=ip4mask,
|
|
||||||
ip6=ip6,
|
|
||||||
ip6mask=ip6mask,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_nem_id(node: CoreNode, netif_id: int, context: ServicerContext) -> int:
|
def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
|
||||||
|
"""
|
||||||
|
Convenience for converting a core interface to the protobuf representation.
|
||||||
|
|
||||||
|
:param node_id: id of node to convert interface for
|
||||||
|
:param iface: interface to convert
|
||||||
|
:return: interface proto
|
||||||
|
"""
|
||||||
|
if iface.node and iface.node.id == node_id:
|
||||||
|
_id = iface.node_id
|
||||||
|
else:
|
||||||
|
_id = iface.net_id
|
||||||
|
net_id = iface.net.id if iface.net else None
|
||||||
|
node_id = iface.node.id if iface.node else None
|
||||||
|
net2_id = iface.othernet.id if iface.othernet else None
|
||||||
|
ip4_net = iface.get_ip4()
|
||||||
|
ip4 = str(ip4_net.ip) if ip4_net else None
|
||||||
|
ip4_mask = ip4_net.prefixlen if ip4_net else None
|
||||||
|
ip6_net = iface.get_ip6()
|
||||||
|
ip6 = str(ip6_net.ip) if ip6_net else None
|
||||||
|
ip6_mask = ip6_net.prefixlen if ip6_net else None
|
||||||
|
mac = str(iface.mac) if iface.mac else None
|
||||||
|
return core_pb2.Interface(
|
||||||
|
id=_id,
|
||||||
|
net_id=net_id,
|
||||||
|
net2_id=net2_id,
|
||||||
|
node_id=node_id,
|
||||||
|
name=iface.name,
|
||||||
|
mac=mac,
|
||||||
|
mtu=iface.mtu,
|
||||||
|
flow_id=iface.flow_id,
|
||||||
|
ip4=ip4,
|
||||||
|
ip4_mask=ip4_mask,
|
||||||
|
ip6=ip6,
|
||||||
|
ip6_mask=ip6_mask,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_nem_id(
|
||||||
|
session: Session, node: CoreNode, iface_id: int, context: ServicerContext
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Get nem id for a given node and interface id.
|
Get nem id for a given node and interface id.
|
||||||
|
|
||||||
|
:param session: session node belongs to
|
||||||
:param node: node to get nem id for
|
:param node: node to get nem id for
|
||||||
:param netif_id: id of interface on node to get nem id for
|
:param iface_id: id of interface on node to get nem id for
|
||||||
:param context: request context
|
:param context: request context
|
||||||
:return: nem id
|
:return: nem id
|
||||||
"""
|
"""
|
||||||
netif = node.netif(netif_id)
|
iface = node.ifaces.get(iface_id)
|
||||||
if not netif:
|
if not iface:
|
||||||
message = f"{node.name} missing interface {netif_id}"
|
message = f"{node.name} missing interface {iface_id}"
|
||||||
context.abort(grpc.StatusCode.NOT_FOUND, message)
|
context.abort(grpc.StatusCode.NOT_FOUND, message)
|
||||||
net = netif.net
|
net = iface.net
|
||||||
if not isinstance(net, EmaneNet):
|
if not isinstance(net, EmaneNet):
|
||||||
message = f"{node.name} interface {netif_id} is not an EMANE network"
|
message = f"{node.name} interface {iface_id} is not an EMANE network"
|
||||||
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
|
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
|
||||||
return net.getnemid(netif)
|
nem_id = session.emane.get_nem_id(iface)
|
||||||
|
if nem_id is None:
|
||||||
|
message = f"{node.name} interface {iface_id} nem id does not exist"
|
||||||
|
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
|
||||||
|
return nem_id
|
||||||
|
|
|
@ -6,7 +6,7 @@ import tempfile
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from concurrent import futures
|
from concurrent import futures
|
||||||
from typing import Iterable, Type
|
from typing import Iterable, Optional, Pattern, Type
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
from grpc import ServicerContext
|
from grpc import ServicerContext
|
||||||
|
@ -108,18 +108,22 @@ from core.api.grpc.wlan_pb2 import (
|
||||||
WlanLinkResponse,
|
WlanLinkResponse,
|
||||||
)
|
)
|
||||||
from core.emulator.coreemu import CoreEmu
|
from core.emulator.coreemu import CoreEmu
|
||||||
from core.emulator.data import LinkData
|
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
|
||||||
from core.emulator.emudata import LinkOptions, NodeOptions
|
from core.emulator.enumerations import (
|
||||||
from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
|
EventTypes,
|
||||||
|
ExceptionLevels,
|
||||||
|
LinkTypes,
|
||||||
|
MessageFlags,
|
||||||
|
)
|
||||||
from core.emulator.session import NT, Session
|
from core.emulator.session import NT, Session
|
||||||
from core.errors import CoreCommandError, CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||||
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
||||||
from core.nodes.network import WlanNode
|
from core.nodes.network import PtpNet, WlanNode
|
||||||
from core.services.coreservices import ServiceManager
|
from core.services.coreservices import ServiceManager
|
||||||
|
|
||||||
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
|
_ONE_DAY_IN_SECONDS: int = 60 * 60 * 24
|
||||||
_INTERFACE_REGEX = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
|
_INTERFACE_REGEX: Pattern = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
|
||||||
|
|
||||||
|
|
||||||
class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
|
@ -131,9 +135,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
|
|
||||||
def __init__(self, coreemu: CoreEmu) -> None:
|
def __init__(self, coreemu: CoreEmu) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.coreemu = coreemu
|
self.coreemu: CoreEmu = coreemu
|
||||||
self.running = True
|
self.running: bool = True
|
||||||
self.server = None
|
self.server: Optional[grpc.Server] = None
|
||||||
atexit.register(self._exit_handler)
|
atexit.register(self._exit_handler)
|
||||||
|
|
||||||
def _exit_handler(self) -> None:
|
def _exit_handler(self) -> None:
|
||||||
|
@ -246,7 +250,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
config = session.emane.get_configs()
|
config = session.emane.get_configs()
|
||||||
config.update(request.emane_config)
|
config.update(request.emane_config)
|
||||||
for config in request.emane_model_configs:
|
for config in request.emane_model_configs:
|
||||||
_id = get_emane_model_id(config.node_id, config.interface_id)
|
_id = get_emane_model_id(config.node_id, config.iface_id)
|
||||||
session.emane.set_model_config(_id, config.model, config.config)
|
session.emane.set_model_config(_id, config.model, config.config)
|
||||||
|
|
||||||
# wlan configs
|
# wlan configs
|
||||||
|
@ -449,6 +453,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
|
|
||||||
return core_pb2.SetSessionStateResponse(result=result)
|
return core_pb2.SetSessionStateResponse(result=result)
|
||||||
|
|
||||||
|
def SetSessionUser(
|
||||||
|
self, request: core_pb2.SetSessionUserRequest, context: ServicerContext
|
||||||
|
) -> core_pb2.SetSessionUserResponse:
|
||||||
|
"""
|
||||||
|
Sets the user for a session.
|
||||||
|
|
||||||
|
:param request: set session user request
|
||||||
|
:param context: context object
|
||||||
|
:return: set session user response
|
||||||
|
"""
|
||||||
|
logging.debug("set session user: %s", request)
|
||||||
|
session = self.get_session(request.session_id, context)
|
||||||
|
session.user = request.user
|
||||||
|
return core_pb2.SetSessionUserResponse(result=True)
|
||||||
|
|
||||||
def GetSessionOptions(
|
def GetSessionOptions(
|
||||||
self, request: core_pb2.GetSessionOptionsRequest, context: ServicerContext
|
self, request: core_pb2.GetSessionOptionsRequest, context: ServicerContext
|
||||||
) -> core_pb2.GetSessionOptionsResponse:
|
) -> core_pb2.GetSessionOptionsResponse:
|
||||||
|
@ -544,10 +563,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
nodes = []
|
nodes = []
|
||||||
for _id in session.nodes:
|
for _id in session.nodes:
|
||||||
node = session.nodes[_id]
|
node = session.nodes[_id]
|
||||||
if not isinstance(node.id, int):
|
if not isinstance(node, PtpNet):
|
||||||
continue
|
node_proto = grpcutils.get_node_proto(session, node)
|
||||||
node_proto = grpcutils.get_node_proto(session, node)
|
nodes.append(node_proto)
|
||||||
nodes.append(node_proto)
|
|
||||||
node_links = get_links(node)
|
node_links = get_links(node)
|
||||||
links.extend(node_links)
|
links.extend(node_links)
|
||||||
|
|
||||||
|
@ -571,6 +589,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
session.distributed.add_server(request.name, request.host)
|
session.distributed.add_server(request.name, request.host)
|
||||||
return core_pb2.AddSessionServerResponse(result=True)
|
return core_pb2.AddSessionServerResponse(result=True)
|
||||||
|
|
||||||
|
def SessionAlert(
|
||||||
|
self, request: core_pb2.SessionAlertRequest, context: ServicerContext
|
||||||
|
) -> core_pb2.SessionAlertResponse:
|
||||||
|
session = self.get_session(request.session_id, context)
|
||||||
|
level = ExceptionLevels(request.level)
|
||||||
|
node_id = request.node_id if request.node_id else None
|
||||||
|
session.exception(level, request.source, request.text, node_id)
|
||||||
|
return core_pb2.SessionAlertResponse(result=True)
|
||||||
|
|
||||||
def Events(self, request: core_pb2.EventsRequest, context: ServicerContext) -> None:
|
def Events(self, request: core_pb2.EventsRequest, context: ServicerContext) -> None:
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
event_types = set(request.events)
|
event_types = set(request.events)
|
||||||
|
@ -625,16 +652,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
key = key.split(".")
|
key = key.split(".")
|
||||||
node_id = _INTERFACE_REGEX.search(key[0]).group("node")
|
node_id = _INTERFACE_REGEX.search(key[0]).group("node")
|
||||||
node_id = int(node_id, base=16)
|
node_id = int(node_id, base=16)
|
||||||
interface_id = int(key[1], base=16)
|
iface_id = int(key[1], base=16)
|
||||||
session_id = int(key[2], base=16)
|
session_id = int(key[2], base=16)
|
||||||
if session.id != session_id:
|
if session.id != session_id:
|
||||||
continue
|
continue
|
||||||
interface_throughput = (
|
iface_throughput = throughputs_event.iface_throughputs.add()
|
||||||
throughputs_event.interface_throughputs.add()
|
iface_throughput.node_id = node_id
|
||||||
)
|
iface_throughput.iface_id = iface_id
|
||||||
interface_throughput.node_id = node_id
|
iface_throughput.throughput = throughput
|
||||||
interface_throughput.interface_id = interface_id
|
|
||||||
interface_throughput.throughput = throughput
|
|
||||||
elif key.startswith("b."):
|
elif key.startswith("b."):
|
||||||
try:
|
try:
|
||||||
key = key.split(".")
|
key = key.split(".")
|
||||||
|
@ -656,6 +681,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
last_stats = stats
|
last_stats = stats
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
|
def CpuUsage(
|
||||||
|
self, request: core_pb2.CpuUsageRequest, context: ServicerContext
|
||||||
|
) -> None:
|
||||||
|
cpu_usage = grpcutils.CpuUsage()
|
||||||
|
while self._is_running(context):
|
||||||
|
usage = cpu_usage.run()
|
||||||
|
yield core_pb2.CpuUsageEvent(usage=usage)
|
||||||
|
time.sleep(request.delay)
|
||||||
|
|
||||||
def AddNode(
|
def AddNode(
|
||||||
self, request: core_pb2.AddNodeRequest, context: ServicerContext
|
self, request: core_pb2.AddNodeRequest, context: ServicerContext
|
||||||
) -> core_pb2.AddNodeResponse:
|
) -> core_pb2.AddNodeResponse:
|
||||||
|
@ -671,6 +705,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
_type, _id, options = grpcutils.add_node_data(request.node)
|
_type, _id, options = grpcutils.add_node_data(request.node)
|
||||||
_class = session.get_node_class(_type)
|
_class = session.get_node_class(_type)
|
||||||
node = session.add_node(_class, _id, options)
|
node = session.add_node(_class, _id, options)
|
||||||
|
source = request.source if request.source else None
|
||||||
|
session.broadcast_node(node, MessageFlags.ADD, source)
|
||||||
return core_pb2.AddNodeResponse(node_id=node.id)
|
return core_pb2.AddNodeResponse(node_id=node.id)
|
||||||
|
|
||||||
def GetNode(
|
def GetNode(
|
||||||
|
@ -686,13 +722,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
logging.debug("get node: %s", request)
|
logging.debug("get node: %s", request)
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
node = self.get_node(session, request.node_id, context, NodeBase)
|
node = self.get_node(session, request.node_id, context, NodeBase)
|
||||||
interfaces = []
|
ifaces = []
|
||||||
for interface_id in node._netif:
|
for iface_id in node.ifaces:
|
||||||
interface = node._netif[interface_id]
|
iface = node.ifaces[iface_id]
|
||||||
interface_proto = grpcutils.interface_to_proto(interface)
|
iface_proto = grpcutils.iface_to_proto(request.node_id, iface)
|
||||||
interfaces.append(interface_proto)
|
ifaces.append(iface_proto)
|
||||||
node_proto = grpcutils.get_node_proto(session, node)
|
node_proto = grpcutils.get_node_proto(session, node)
|
||||||
return core_pb2.GetNodeResponse(node=node_proto, interfaces=interfaces)
|
return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces)
|
||||||
|
|
||||||
def MoveNodes(
|
def MoveNodes(
|
||||||
self,
|
self,
|
||||||
|
@ -778,7 +814,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
"""
|
"""
|
||||||
logging.debug("delete node: %s", request)
|
logging.debug("delete node: %s", request)
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
result = session.delete_node(request.node_id)
|
result = False
|
||||||
|
if request.node_id in session.nodes:
|
||||||
|
node = self.get_node(session, request.node_id, context, NodeBase)
|
||||||
|
result = session.delete_node(node.id)
|
||||||
|
source = request.source if request.source else None
|
||||||
|
session.broadcast_node(node, MessageFlags.DELETE, source)
|
||||||
return core_pb2.DeleteNodeResponse(result=result)
|
return core_pb2.DeleteNodeResponse(result=result)
|
||||||
|
|
||||||
def NodeCommand(
|
def NodeCommand(
|
||||||
|
@ -845,27 +886,42 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
:return: add-link response
|
:return: add-link response
|
||||||
"""
|
"""
|
||||||
logging.debug("add link: %s", request)
|
logging.debug("add link: %s", request)
|
||||||
# validate session and nodes
|
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
self.get_node(session, request.link.node_one_id, context, NodeBase)
|
node1_id = request.link.node1_id
|
||||||
self.get_node(session, request.link.node_two_id, context, NodeBase)
|
node2_id = request.link.node2_id
|
||||||
|
self.get_node(session, node1_id, context, NodeBase)
|
||||||
node_one_id = request.link.node_one_id
|
self.get_node(session, node2_id, context, NodeBase)
|
||||||
node_two_id = request.link.node_two_id
|
iface1_data, iface2_data, options, link_type = grpcutils.add_link_data(
|
||||||
interface_one, interface_two, options = grpcutils.add_link_data(request.link)
|
request.link
|
||||||
node_one_interface, node_two_interface = session.add_link(
|
|
||||||
node_one_id, node_two_id, interface_one, interface_two, options=options
|
|
||||||
)
|
)
|
||||||
interface_one_proto = None
|
node1_iface, node2_iface = session.add_link(
|
||||||
interface_two_proto = None
|
node1_id, node2_id, iface1_data, iface2_data, options, link_type
|
||||||
if node_one_interface:
|
)
|
||||||
interface_one_proto = grpcutils.interface_to_proto(node_one_interface)
|
iface1_data = None
|
||||||
if node_two_interface:
|
if node1_iface:
|
||||||
interface_two_proto = grpcutils.interface_to_proto(node_two_interface)
|
iface1_data = grpcutils.iface_to_data(node1_iface)
|
||||||
|
iface2_data = None
|
||||||
|
if node2_iface:
|
||||||
|
iface2_data = grpcutils.iface_to_data(node2_iface)
|
||||||
|
source = request.source if request.source else None
|
||||||
|
link_data = LinkData(
|
||||||
|
message_type=MessageFlags.ADD,
|
||||||
|
node1_id=node1_id,
|
||||||
|
node2_id=node2_id,
|
||||||
|
iface1=iface1_data,
|
||||||
|
iface2=iface2_data,
|
||||||
|
options=options,
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
session.broadcast_link(link_data)
|
||||||
|
iface1_proto = None
|
||||||
|
iface2_proto = None
|
||||||
|
if node1_iface:
|
||||||
|
iface1_proto = grpcutils.iface_to_proto(node1_id, node1_iface)
|
||||||
|
if node2_iface:
|
||||||
|
iface2_proto = grpcutils.iface_to_proto(node2_id, node2_iface)
|
||||||
return core_pb2.AddLinkResponse(
|
return core_pb2.AddLinkResponse(
|
||||||
result=True,
|
result=True, iface1=iface1_proto, iface2=iface2_proto
|
||||||
interface_one=interface_one_proto,
|
|
||||||
interface_two=interface_two_proto,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def EditLink(
|
def EditLink(
|
||||||
|
@ -880,26 +936,37 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
"""
|
"""
|
||||||
logging.debug("edit link: %s", request)
|
logging.debug("edit link: %s", request)
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
node_one_id = request.node_one_id
|
node1_id = request.node1_id
|
||||||
node_two_id = request.node_two_id
|
node2_id = request.node2_id
|
||||||
interface_one_id = request.interface_one_id
|
iface1_id = request.iface1_id
|
||||||
interface_two_id = request.interface_two_id
|
iface2_id = request.iface2_id
|
||||||
options_data = request.options
|
options_proto = request.options
|
||||||
link_options = LinkOptions()
|
options = LinkOptions(
|
||||||
link_options.delay = options_data.delay
|
delay=options_proto.delay,
|
||||||
link_options.bandwidth = options_data.bandwidth
|
bandwidth=options_proto.bandwidth,
|
||||||
link_options.per = options_data.per
|
loss=options_proto.loss,
|
||||||
link_options.dup = options_data.dup
|
dup=options_proto.dup,
|
||||||
link_options.jitter = options_data.jitter
|
jitter=options_proto.jitter,
|
||||||
link_options.mer = options_data.mer
|
mer=options_proto.mer,
|
||||||
link_options.burst = options_data.burst
|
burst=options_proto.burst,
|
||||||
link_options.mburst = options_data.mburst
|
mburst=options_proto.mburst,
|
||||||
link_options.unidirectional = options_data.unidirectional
|
unidirectional=options_proto.unidirectional,
|
||||||
link_options.key = options_data.key
|
key=options_proto.key,
|
||||||
link_options.opaque = options_data.opaque
|
|
||||||
session.update_link(
|
|
||||||
node_one_id, node_two_id, interface_one_id, interface_two_id, link_options
|
|
||||||
)
|
)
|
||||||
|
session.update_link(node1_id, node2_id, iface1_id, iface2_id, options)
|
||||||
|
iface1 = InterfaceData(id=iface1_id)
|
||||||
|
iface2 = InterfaceData(id=iface2_id)
|
||||||
|
source = request.source if request.source else None
|
||||||
|
link_data = LinkData(
|
||||||
|
message_type=MessageFlags.NONE,
|
||||||
|
node1_id=node1_id,
|
||||||
|
node2_id=node2_id,
|
||||||
|
iface1=iface1,
|
||||||
|
iface2=iface2,
|
||||||
|
options=options,
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
session.broadcast_link(link_data)
|
||||||
return core_pb2.EditLinkResponse(result=True)
|
return core_pb2.EditLinkResponse(result=True)
|
||||||
|
|
||||||
def DeleteLink(
|
def DeleteLink(
|
||||||
|
@ -914,13 +981,23 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
"""
|
"""
|
||||||
logging.debug("delete link: %s", request)
|
logging.debug("delete link: %s", request)
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
node_one_id = request.node_one_id
|
node1_id = request.node1_id
|
||||||
node_two_id = request.node_two_id
|
node2_id = request.node2_id
|
||||||
interface_one_id = request.interface_one_id
|
iface1_id = request.iface1_id
|
||||||
interface_two_id = request.interface_two_id
|
iface2_id = request.iface2_id
|
||||||
session.delete_link(
|
session.delete_link(node1_id, node2_id, iface1_id, iface2_id)
|
||||||
node_one_id, node_two_id, interface_one_id, interface_two_id
|
iface1 = InterfaceData(id=iface1_id)
|
||||||
|
iface2 = InterfaceData(id=iface2_id)
|
||||||
|
source = request.source if request.source else None
|
||||||
|
link_data = LinkData(
|
||||||
|
message_type=MessageFlags.DELETE,
|
||||||
|
node1_id=node1_id,
|
||||||
|
node2_id=node2_id,
|
||||||
|
iface1=iface1,
|
||||||
|
iface2=iface2,
|
||||||
|
source=source,
|
||||||
)
|
)
|
||||||
|
session.broadcast_link(link_data)
|
||||||
return core_pb2.DeleteLinkResponse(result=True)
|
return core_pb2.DeleteLinkResponse(result=True)
|
||||||
|
|
||||||
def GetHooks(
|
def GetHooks(
|
||||||
|
@ -936,8 +1013,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
logging.debug("get hooks: %s", request)
|
logging.debug("get hooks: %s", request)
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
hooks = []
|
hooks = []
|
||||||
for state in session._hooks:
|
for state in session.hooks:
|
||||||
state_hooks = session._hooks[state]
|
state_hooks = session.hooks[state]
|
||||||
for file_name, file_data in state_hooks:
|
for file_name, file_data in state_hooks:
|
||||||
hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data)
|
hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data)
|
||||||
hooks.append(hook)
|
hooks.append(hook)
|
||||||
|
@ -1304,13 +1381,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
"""
|
"""
|
||||||
logging.debug("set wlan config: %s", request)
|
logging.debug("set wlan config: %s", request)
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
wlan_config = request.wlan_config
|
node_id = request.wlan_config.node_id
|
||||||
session.mobility.set_model_config(
|
config = request.wlan_config.config
|
||||||
wlan_config.node_id, BasicRangeModel.name, wlan_config.config
|
session.mobility.set_model_config(node_id, BasicRangeModel.name, config)
|
||||||
)
|
|
||||||
if session.state == EventTypes.RUNTIME_STATE:
|
if session.state == EventTypes.RUNTIME_STATE:
|
||||||
node = self.get_node(session, wlan_config.node_id, context, WlanNode)
|
node = self.get_node(session, node_id, context, WlanNode)
|
||||||
node.updatemodel(wlan_config.config)
|
node.updatemodel(config)
|
||||||
return SetWlanConfigResponse(result=True)
|
return SetWlanConfigResponse(result=True)
|
||||||
|
|
||||||
def GetEmaneConfig(
|
def GetEmaneConfig(
|
||||||
|
@ -1378,7 +1454,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
logging.debug("get emane model config: %s", request)
|
logging.debug("get emane model config: %s", request)
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
model = session.emane.models[request.model]
|
model = session.emane.models[request.model]
|
||||||
_id = get_emane_model_id(request.node_id, request.interface)
|
_id = get_emane_model_id(request.node_id, request.iface_id)
|
||||||
current_config = session.emane.get_model_config(_id, request.model)
|
current_config = session.emane.get_model_config(_id, request.model)
|
||||||
config = get_config_options(current_config, model)
|
config = get_config_options(current_config, model)
|
||||||
return GetEmaneModelConfigResponse(config=config)
|
return GetEmaneModelConfigResponse(config=config)
|
||||||
|
@ -1397,7 +1473,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
logging.debug("set emane model config: %s", request)
|
logging.debug("set emane model config: %s", request)
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
model_config = request.emane_model_config
|
model_config = request.emane_model_config
|
||||||
_id = get_emane_model_id(model_config.node_id, model_config.interface_id)
|
_id = get_emane_model_id(model_config.node_id, model_config.iface_id)
|
||||||
session.emane.set_model_config(_id, model_config.model, model_config.config)
|
session.emane.set_model_config(_id, model_config.model, model_config.config)
|
||||||
return SetEmaneModelConfigResponse(result=True)
|
return SetEmaneModelConfigResponse(result=True)
|
||||||
|
|
||||||
|
@ -1426,12 +1502,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
model = session.emane.models[model_name]
|
model = session.emane.models[model_name]
|
||||||
current_config = session.emane.get_model_config(_id, model_name)
|
current_config = session.emane.get_model_config(_id, model_name)
|
||||||
config = get_config_options(current_config, model)
|
config = get_config_options(current_config, model)
|
||||||
node_id, interface = grpcutils.parse_emane_model_id(_id)
|
node_id, iface_id = grpcutils.parse_emane_model_id(_id)
|
||||||
model_config = GetEmaneModelConfigsResponse.ModelConfig(
|
model_config = GetEmaneModelConfigsResponse.ModelConfig(
|
||||||
node_id=node_id,
|
node_id=node_id, model=model_name, iface_id=iface_id, config=config
|
||||||
model=model_name,
|
|
||||||
interface=interface,
|
|
||||||
config=config,
|
|
||||||
)
|
)
|
||||||
configs.append(model_config)
|
configs.append(model_config)
|
||||||
return GetEmaneModelConfigsResponse(configs=configs)
|
return GetEmaneModelConfigsResponse(configs=configs)
|
||||||
|
@ -1496,16 +1569,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
:param context: context object
|
:param context: context object
|
||||||
:return: get-interfaces response that has all the system's interfaces
|
:return: get-interfaces response that has all the system's interfaces
|
||||||
"""
|
"""
|
||||||
interfaces = []
|
ifaces = []
|
||||||
for interface in os.listdir("/sys/class/net"):
|
for iface in os.listdir("/sys/class/net"):
|
||||||
if (
|
if iface.startswith("b.") or iface.startswith("veth") or iface == "lo":
|
||||||
interface.startswith("b.")
|
|
||||||
or interface.startswith("veth")
|
|
||||||
or interface == "lo"
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
interfaces.append(interface)
|
ifaces.append(iface)
|
||||||
return core_pb2.GetInterfacesResponse(interfaces=interfaces)
|
return core_pb2.GetInterfacesResponse(ifaces=ifaces)
|
||||||
|
|
||||||
def EmaneLink(
|
def EmaneLink(
|
||||||
self, request: EmaneLinkRequest, context: ServicerContext
|
self, request: EmaneLinkRequest, context: ServicerContext
|
||||||
|
@ -1519,30 +1588,30 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
"""
|
"""
|
||||||
logging.debug("emane link: %s", request)
|
logging.debug("emane link: %s", request)
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
nem_one = request.nem_one
|
nem1 = request.nem1
|
||||||
emane_one, netif = session.emane.nemlookup(nem_one)
|
iface1 = session.emane.get_iface(nem1)
|
||||||
if not emane_one or not netif:
|
if not iface1:
|
||||||
context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem_one} not found")
|
context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found")
|
||||||
node_one = netif.node
|
node1 = iface1.node
|
||||||
|
|
||||||
nem_two = request.nem_two
|
nem2 = request.nem2
|
||||||
emane_two, netif = session.emane.nemlookup(nem_two)
|
iface2 = session.emane.get_iface(nem2)
|
||||||
if not emane_two or not netif:
|
if not iface2:
|
||||||
context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem_two} not found")
|
context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found")
|
||||||
node_two = netif.node
|
node2 = iface2.node
|
||||||
|
|
||||||
if emane_one.id == emane_two.id:
|
if iface1.net == iface2.net:
|
||||||
if request.linked:
|
if request.linked:
|
||||||
flag = MessageFlags.ADD
|
flag = MessageFlags.ADD
|
||||||
else:
|
else:
|
||||||
flag = MessageFlags.DELETE
|
flag = MessageFlags.DELETE
|
||||||
color = session.get_link_color(emane_one.id)
|
color = session.get_link_color(iface1.net.id)
|
||||||
link = LinkData(
|
link = LinkData(
|
||||||
message_type=flag,
|
message_type=flag,
|
||||||
link_type=LinkTypes.WIRELESS,
|
type=LinkTypes.WIRELESS,
|
||||||
node1_id=node_one.id,
|
node1_id=node1.id,
|
||||||
node2_id=node_two.id,
|
node2_id=node2.id,
|
||||||
network_id=emane_one.id,
|
network_id=iface1.net.id,
|
||||||
color=color,
|
color=color,
|
||||||
)
|
)
|
||||||
session.broadcast_link(link)
|
session.broadcast_link(link)
|
||||||
|
@ -1739,21 +1808,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
grpc.StatusCode.NOT_FOUND,
|
grpc.StatusCode.NOT_FOUND,
|
||||||
f"wlan node {request.wlan} does not using BasicRangeModel",
|
f"wlan node {request.wlan} does not using BasicRangeModel",
|
||||||
)
|
)
|
||||||
n1 = self.get_node(session, request.node_one, context, CoreNode)
|
node1 = self.get_node(session, request.node1_id, context, CoreNode)
|
||||||
n2 = self.get_node(session, request.node_two, context, CoreNode)
|
node2 = self.get_node(session, request.node2_id, context, CoreNode)
|
||||||
n1_netif, n2_netif = None, None
|
node1_iface, node2_iface = None, None
|
||||||
for net, netif1, netif2 in n1.commonnets(n2):
|
for net, iface1, iface2 in node1.commonnets(node2):
|
||||||
if net == wlan:
|
if net == wlan:
|
||||||
n1_netif = netif1
|
node1_iface = iface1
|
||||||
n2_netif = netif2
|
node2_iface = iface2
|
||||||
break
|
break
|
||||||
result = False
|
result = False
|
||||||
if n1_netif and n2_netif:
|
if node1_iface and node2_iface:
|
||||||
if request.linked:
|
if request.linked:
|
||||||
wlan.link(n1_netif, n2_netif)
|
wlan.link(node1_iface, node2_iface)
|
||||||
else:
|
else:
|
||||||
wlan.unlink(n1_netif, n2_netif)
|
wlan.unlink(node1_iface, node2_iface)
|
||||||
wlan.model.sendlinkmsg(n1_netif, n2_netif, unlink=not request.linked)
|
wlan.model.sendlinkmsg(node1_iface, node2_iface, unlink=not request.linked)
|
||||||
result = True
|
result = True
|
||||||
return WlanLinkResponse(result=result)
|
return WlanLinkResponse(result=result)
|
||||||
|
|
||||||
|
@ -1764,9 +1833,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
) -> EmanePathlossesResponse:
|
) -> EmanePathlossesResponse:
|
||||||
for request in request_iterator:
|
for request in request_iterator:
|
||||||
session = self.get_session(request.session_id, context)
|
session = self.get_session(request.session_id, context)
|
||||||
n1 = self.get_node(session, request.node_one, context, CoreNode)
|
node1 = self.get_node(session, request.node1_id, context, CoreNode)
|
||||||
nem1 = grpcutils.get_nem_id(n1, request.interface_one_id, context)
|
nem1 = grpcutils.get_nem_id(session, node1, request.iface1_id, context)
|
||||||
n2 = self.get_node(session, request.node_two, context, CoreNode)
|
node2 = self.get_node(session, request.node2_id, context, CoreNode)
|
||||||
nem2 = grpcutils.get_nem_id(n2, request.interface_two_id, context)
|
nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context)
|
||||||
session.emane.publish_pathloss(nem1, nem2, request.rx_one, request.rx_two)
|
session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
|
||||||
return EmanePathlossesResponse()
|
return EmanePathlossesResponse()
|
||||||
|
|
|
@ -495,7 +495,7 @@ class CoreLinkTlv(CoreTlv):
|
||||||
LinkTlvs.N2_NUMBER.value: CoreTlvDataUint32,
|
LinkTlvs.N2_NUMBER.value: CoreTlvDataUint32,
|
||||||
LinkTlvs.DELAY.value: CoreTlvDataUint64,
|
LinkTlvs.DELAY.value: CoreTlvDataUint64,
|
||||||
LinkTlvs.BANDWIDTH.value: CoreTlvDataUint64,
|
LinkTlvs.BANDWIDTH.value: CoreTlvDataUint64,
|
||||||
LinkTlvs.PER.value: CoreTlvDataString,
|
LinkTlvs.LOSS.value: CoreTlvDataString,
|
||||||
LinkTlvs.DUP.value: CoreTlvDataString,
|
LinkTlvs.DUP.value: CoreTlvDataString,
|
||||||
LinkTlvs.JITTER.value: CoreTlvDataUint64,
|
LinkTlvs.JITTER.value: CoreTlvDataUint64,
|
||||||
LinkTlvs.MER.value: CoreTlvDataUint16,
|
LinkTlvs.MER.value: CoreTlvDataUint16,
|
||||||
|
@ -508,18 +508,18 @@ class CoreLinkTlv(CoreTlv):
|
||||||
LinkTlvs.EMULATION_ID.value: CoreTlvDataUint32,
|
LinkTlvs.EMULATION_ID.value: CoreTlvDataUint32,
|
||||||
LinkTlvs.NETWORK_ID.value: CoreTlvDataUint32,
|
LinkTlvs.NETWORK_ID.value: CoreTlvDataUint32,
|
||||||
LinkTlvs.KEY.value: CoreTlvDataUint32,
|
LinkTlvs.KEY.value: CoreTlvDataUint32,
|
||||||
LinkTlvs.INTERFACE1_NUMBER.value: CoreTlvDataUint16,
|
LinkTlvs.IFACE1_NUMBER.value: CoreTlvDataUint16,
|
||||||
LinkTlvs.INTERFACE1_IP4.value: CoreTlvDataIpv4Addr,
|
LinkTlvs.IFACE1_IP4.value: CoreTlvDataIpv4Addr,
|
||||||
LinkTlvs.INTERFACE1_IP4_MASK.value: CoreTlvDataUint16,
|
LinkTlvs.IFACE1_IP4_MASK.value: CoreTlvDataUint16,
|
||||||
LinkTlvs.INTERFACE1_MAC.value: CoreTlvDataMacAddr,
|
LinkTlvs.IFACE1_MAC.value: CoreTlvDataMacAddr,
|
||||||
LinkTlvs.INTERFACE1_IP6.value: CoreTlvDataIPv6Addr,
|
LinkTlvs.IFACE1_IP6.value: CoreTlvDataIPv6Addr,
|
||||||
LinkTlvs.INTERFACE1_IP6_MASK.value: CoreTlvDataUint16,
|
LinkTlvs.IFACE1_IP6_MASK.value: CoreTlvDataUint16,
|
||||||
LinkTlvs.INTERFACE2_NUMBER.value: CoreTlvDataUint16,
|
LinkTlvs.IFACE2_NUMBER.value: CoreTlvDataUint16,
|
||||||
LinkTlvs.INTERFACE2_IP4.value: CoreTlvDataIpv4Addr,
|
LinkTlvs.IFACE2_IP4.value: CoreTlvDataIpv4Addr,
|
||||||
LinkTlvs.INTERFACE2_IP4_MASK.value: CoreTlvDataUint16,
|
LinkTlvs.IFACE2_IP4_MASK.value: CoreTlvDataUint16,
|
||||||
LinkTlvs.INTERFACE2_MAC.value: CoreTlvDataMacAddr,
|
LinkTlvs.IFACE2_MAC.value: CoreTlvDataMacAddr,
|
||||||
LinkTlvs.INTERFACE2_IP6.value: CoreTlvDataIPv6Addr,
|
LinkTlvs.IFACE2_IP6.value: CoreTlvDataIPv6Addr,
|
||||||
LinkTlvs.INTERFACE2_IP6_MASK.value: CoreTlvDataUint16,
|
LinkTlvs.IFACE2_IP6_MASK.value: CoreTlvDataUint16,
|
||||||
LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString,
|
LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString,
|
||||||
LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString,
|
LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString,
|
||||||
LinkTlvs.OPAQUE.value: CoreTlvDataString,
|
LinkTlvs.OPAQUE.value: CoreTlvDataString,
|
||||||
|
@ -577,7 +577,7 @@ class CoreConfigTlv(CoreTlv):
|
||||||
ConfigTlvs.POSSIBLE_VALUES.value: CoreTlvDataString,
|
ConfigTlvs.POSSIBLE_VALUES.value: CoreTlvDataString,
|
||||||
ConfigTlvs.GROUPS.value: CoreTlvDataString,
|
ConfigTlvs.GROUPS.value: CoreTlvDataString,
|
||||||
ConfigTlvs.SESSION.value: CoreTlvDataString,
|
ConfigTlvs.SESSION.value: CoreTlvDataString,
|
||||||
ConfigTlvs.INTERFACE_NUMBER.value: CoreTlvDataUint16,
|
ConfigTlvs.IFACE_ID.value: CoreTlvDataUint16,
|
||||||
ConfigTlvs.NETWORK_ID.value: CoreTlvDataUint32,
|
ConfigTlvs.NETWORK_ID.value: CoreTlvDataUint32,
|
||||||
ConfigTlvs.OPAQUE.value: CoreTlvDataString,
|
ConfigTlvs.OPAQUE.value: CoreTlvDataString,
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import threading
|
||||||
import time
|
import time
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from queue import Empty, Queue
|
from queue import Empty, Queue
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
from core.api.tlv import coreapi, dataconversion, structutils
|
from core.api.tlv import coreapi, dataconversion, structutils
|
||||||
|
@ -28,8 +29,15 @@ from core.api.tlv.enumerations import (
|
||||||
NodeTlvs,
|
NodeTlvs,
|
||||||
SessionTlvs,
|
SessionTlvs,
|
||||||
)
|
)
|
||||||
from core.emulator.data import ConfigData, EventData, ExceptionData, FileData
|
from core.emulator.data import (
|
||||||
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
|
ConfigData,
|
||||||
|
EventData,
|
||||||
|
ExceptionData,
|
||||||
|
FileData,
|
||||||
|
InterfaceData,
|
||||||
|
LinkOptions,
|
||||||
|
NodeOptions,
|
||||||
|
)
|
||||||
from core.emulator.enumerations import (
|
from core.emulator.enumerations import (
|
||||||
ConfigDataTypes,
|
ConfigDataTypes,
|
||||||
EventTypes,
|
EventTypes,
|
||||||
|
@ -39,6 +47,7 @@ from core.emulator.enumerations import (
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
RegisterTlvs,
|
RegisterTlvs,
|
||||||
)
|
)
|
||||||
|
from core.emulator.session import Session
|
||||||
from core.errors import CoreCommandError, CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.location.mobility import BasicRangeModel
|
from core.location.mobility import BasicRangeModel
|
||||||
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
||||||
|
@ -69,7 +78,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
MessageTypes.REGISTER.value: self.handle_register_message,
|
MessageTypes.REGISTER.value: self.handle_register_message,
|
||||||
MessageTypes.CONFIG.value: self.handle_config_message,
|
MessageTypes.CONFIG.value: self.handle_config_message,
|
||||||
MessageTypes.FILE.value: self.handle_file_message,
|
MessageTypes.FILE.value: self.handle_file_message,
|
||||||
MessageTypes.INTERFACE.value: self.handle_interface_message,
|
MessageTypes.INTERFACE.value: self.handle_iface_message,
|
||||||
MessageTypes.EVENT.value: self.handle_event_message,
|
MessageTypes.EVENT.value: self.handle_event_message,
|
||||||
MessageTypes.SESSION.value: self.handle_session_message,
|
MessageTypes.SESSION.value: self.handle_session_message,
|
||||||
}
|
}
|
||||||
|
@ -83,7 +92,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
thread.start()
|
thread.start()
|
||||||
self.handler_threads.append(thread)
|
self.handler_threads.append(thread)
|
||||||
|
|
||||||
self.session = None
|
self.session: Optional[Session] = None
|
||||||
self.coreemu = server.coreemu
|
self.coreemu = server.coreemu
|
||||||
utils.close_onexec(request.fileno())
|
utils.close_onexec(request.fileno())
|
||||||
socketserver.BaseRequestHandler.__init__(self, request, client_address, server)
|
socketserver.BaseRequestHandler.__init__(self, request, client_address, server)
|
||||||
|
@ -176,7 +185,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
|
|
||||||
node_count_list.append(str(session.get_node_count()))
|
node_count_list.append(str(session.get_node_count()))
|
||||||
|
|
||||||
date_list.append(time.ctime(session._state_time))
|
date_list.append(time.ctime(session.state_time))
|
||||||
|
|
||||||
thumb = session.thumbnail
|
thumb = session.thumbnail
|
||||||
if not thumb:
|
if not thumb:
|
||||||
|
@ -320,7 +329,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
"""
|
"""
|
||||||
logging.debug("handling broadcast node: %s", node_data)
|
logging.debug("handling broadcast node: %s", node_data)
|
||||||
message = dataconversion.convert_node(node_data)
|
message = dataconversion.convert_node(node_data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.sendall(message)
|
self.sendall(message)
|
||||||
except IOError:
|
except IOError:
|
||||||
|
@ -334,46 +342,49 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
logging.debug("handling broadcast link: %s", link_data)
|
logging.debug("handling broadcast link: %s", link_data)
|
||||||
per = ""
|
options_data = link_data.options
|
||||||
if link_data.per is not None:
|
loss = ""
|
||||||
per = str(link_data.per)
|
if options_data.loss is not None:
|
||||||
|
loss = str(options_data.loss)
|
||||||
dup = ""
|
dup = ""
|
||||||
if link_data.dup is not None:
|
if options_data.dup is not None:
|
||||||
dup = str(link_data.dup)
|
dup = str(options_data.dup)
|
||||||
|
iface1 = link_data.iface1
|
||||||
|
if iface1 is None:
|
||||||
|
iface1 = InterfaceData()
|
||||||
|
iface2 = link_data.iface2
|
||||||
|
if iface2 is None:
|
||||||
|
iface2 = InterfaceData()
|
||||||
|
|
||||||
tlv_data = structutils.pack_values(
|
tlv_data = structutils.pack_values(
|
||||||
coreapi.CoreLinkTlv,
|
coreapi.CoreLinkTlv,
|
||||||
[
|
[
|
||||||
(LinkTlvs.N1_NUMBER, link_data.node1_id),
|
(LinkTlvs.N1_NUMBER, link_data.node1_id),
|
||||||
(LinkTlvs.N2_NUMBER, link_data.node2_id),
|
(LinkTlvs.N2_NUMBER, link_data.node2_id),
|
||||||
(LinkTlvs.DELAY, link_data.delay),
|
(LinkTlvs.DELAY, options_data.delay),
|
||||||
(LinkTlvs.BANDWIDTH, link_data.bandwidth),
|
(LinkTlvs.BANDWIDTH, options_data.bandwidth),
|
||||||
(LinkTlvs.PER, per),
|
(LinkTlvs.LOSS, loss),
|
||||||
(LinkTlvs.DUP, dup),
|
(LinkTlvs.DUP, dup),
|
||||||
(LinkTlvs.JITTER, link_data.jitter),
|
(LinkTlvs.JITTER, options_data.jitter),
|
||||||
(LinkTlvs.MER, link_data.mer),
|
(LinkTlvs.MER, options_data.mer),
|
||||||
(LinkTlvs.BURST, link_data.burst),
|
(LinkTlvs.BURST, options_data.burst),
|
||||||
(LinkTlvs.SESSION, link_data.session),
|
(LinkTlvs.MBURST, options_data.mburst),
|
||||||
(LinkTlvs.MBURST, link_data.mburst),
|
(LinkTlvs.TYPE, link_data.type.value),
|
||||||
(LinkTlvs.TYPE, link_data.link_type.value),
|
(LinkTlvs.UNIDIRECTIONAL, options_data.unidirectional),
|
||||||
(LinkTlvs.GUI_ATTRIBUTES, link_data.gui_attributes),
|
|
||||||
(LinkTlvs.UNIDIRECTIONAL, link_data.unidirectional),
|
|
||||||
(LinkTlvs.EMULATION_ID, link_data.emulation_id),
|
|
||||||
(LinkTlvs.NETWORK_ID, link_data.network_id),
|
(LinkTlvs.NETWORK_ID, link_data.network_id),
|
||||||
(LinkTlvs.KEY, link_data.key),
|
(LinkTlvs.KEY, options_data.key),
|
||||||
(LinkTlvs.INTERFACE1_NUMBER, link_data.interface1_id),
|
(LinkTlvs.IFACE1_NUMBER, iface1.id),
|
||||||
(LinkTlvs.INTERFACE1_IP4, link_data.interface1_ip4),
|
(LinkTlvs.IFACE1_IP4, iface1.ip4),
|
||||||
(LinkTlvs.INTERFACE1_IP4_MASK, link_data.interface1_ip4_mask),
|
(LinkTlvs.IFACE1_IP4_MASK, iface1.ip4_mask),
|
||||||
(LinkTlvs.INTERFACE1_MAC, link_data.interface1_mac),
|
(LinkTlvs.IFACE1_MAC, iface1.mac),
|
||||||
(LinkTlvs.INTERFACE1_IP6, link_data.interface1_ip6),
|
(LinkTlvs.IFACE1_IP6, iface1.ip6),
|
||||||
(LinkTlvs.INTERFACE1_IP6_MASK, link_data.interface1_ip6_mask),
|
(LinkTlvs.IFACE1_IP6_MASK, iface1.ip6_mask),
|
||||||
(LinkTlvs.INTERFACE2_NUMBER, link_data.interface2_id),
|
(LinkTlvs.IFACE2_NUMBER, iface2.id),
|
||||||
(LinkTlvs.INTERFACE2_IP4, link_data.interface2_ip4),
|
(LinkTlvs.IFACE2_IP4, iface2.ip4),
|
||||||
(LinkTlvs.INTERFACE2_IP4_MASK, link_data.interface2_ip4_mask),
|
(LinkTlvs.IFACE2_IP4_MASK, iface2.ip4_mask),
|
||||||
(LinkTlvs.INTERFACE2_MAC, link_data.interface2_mac),
|
(LinkTlvs.IFACE2_MAC, iface2.mac),
|
||||||
(LinkTlvs.INTERFACE2_IP6, link_data.interface2_ip6),
|
(LinkTlvs.IFACE2_IP6, iface2.ip6),
|
||||||
(LinkTlvs.INTERFACE2_IP6_MASK, link_data.interface2_ip6_mask),
|
(LinkTlvs.IFACE2_IP6_MASK, iface2.ip6_mask),
|
||||||
(LinkTlvs.OPAQUE, link_data.opaque),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -707,7 +718,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
|
|
||||||
options.icon = message.get_tlv(NodeTlvs.ICON.value)
|
options.icon = message.get_tlv(NodeTlvs.ICON.value)
|
||||||
options.canvas = message.get_tlv(NodeTlvs.CANVAS.value)
|
options.canvas = message.get_tlv(NodeTlvs.CANVAS.value)
|
||||||
options.opaque = message.get_tlv(NodeTlvs.OPAQUE.value)
|
|
||||||
options.server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value)
|
options.server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value)
|
||||||
|
|
||||||
services = message.get_tlv(NodeTlvs.SERVICES.value)
|
services = message.get_tlv(NodeTlvs.SERVICES.value)
|
||||||
|
@ -745,67 +755,54 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
:param core.api.tlv.coreapi.CoreLinkMessage message: link message to handle
|
:param core.api.tlv.coreapi.CoreLinkMessage message: link message to handle
|
||||||
:return: link message replies
|
:return: link message replies
|
||||||
"""
|
"""
|
||||||
node_one_id = message.get_tlv(LinkTlvs.N1_NUMBER.value)
|
node1_id = message.get_tlv(LinkTlvs.N1_NUMBER.value)
|
||||||
node_two_id = message.get_tlv(LinkTlvs.N2_NUMBER.value)
|
node2_id = message.get_tlv(LinkTlvs.N2_NUMBER.value)
|
||||||
|
iface1_data = InterfaceData(
|
||||||
interface_one = InterfaceData(
|
id=message.get_tlv(LinkTlvs.IFACE1_NUMBER.value),
|
||||||
id=message.get_tlv(LinkTlvs.INTERFACE1_NUMBER.value),
|
|
||||||
name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value),
|
name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value),
|
||||||
mac=message.get_tlv(LinkTlvs.INTERFACE1_MAC.value),
|
mac=message.get_tlv(LinkTlvs.IFACE1_MAC.value),
|
||||||
ip4=message.get_tlv(LinkTlvs.INTERFACE1_IP4.value),
|
ip4=message.get_tlv(LinkTlvs.IFACE1_IP4.value),
|
||||||
ip4_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP4_MASK.value),
|
ip4_mask=message.get_tlv(LinkTlvs.IFACE1_IP4_MASK.value),
|
||||||
ip6=message.get_tlv(LinkTlvs.INTERFACE1_IP6.value),
|
ip6=message.get_tlv(LinkTlvs.IFACE1_IP6.value),
|
||||||
ip6_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP6_MASK.value),
|
ip6_mask=message.get_tlv(LinkTlvs.IFACE1_IP6_MASK.value),
|
||||||
)
|
)
|
||||||
interface_two = InterfaceData(
|
iface2_data = InterfaceData(
|
||||||
id=message.get_tlv(LinkTlvs.INTERFACE2_NUMBER.value),
|
id=message.get_tlv(LinkTlvs.IFACE2_NUMBER.value),
|
||||||
name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value),
|
name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value),
|
||||||
mac=message.get_tlv(LinkTlvs.INTERFACE2_MAC.value),
|
mac=message.get_tlv(LinkTlvs.IFACE2_MAC.value),
|
||||||
ip4=message.get_tlv(LinkTlvs.INTERFACE2_IP4.value),
|
ip4=message.get_tlv(LinkTlvs.IFACE2_IP4.value),
|
||||||
ip4_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP4_MASK.value),
|
ip4_mask=message.get_tlv(LinkTlvs.IFACE2_IP4_MASK.value),
|
||||||
ip6=message.get_tlv(LinkTlvs.INTERFACE2_IP6.value),
|
ip6=message.get_tlv(LinkTlvs.IFACE2_IP6.value),
|
||||||
ip6_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP6_MASK.value),
|
ip6_mask=message.get_tlv(LinkTlvs.IFACE2_IP6_MASK.value),
|
||||||
)
|
)
|
||||||
|
link_type = LinkTypes.WIRED
|
||||||
link_type = None
|
|
||||||
link_type_value = message.get_tlv(LinkTlvs.TYPE.value)
|
link_type_value = message.get_tlv(LinkTlvs.TYPE.value)
|
||||||
if link_type_value is not None:
|
if link_type_value is not None:
|
||||||
link_type = LinkTypes(link_type_value)
|
link_type = LinkTypes(link_type_value)
|
||||||
|
options = LinkOptions()
|
||||||
link_options = LinkOptions(type=link_type)
|
options.delay = message.get_tlv(LinkTlvs.DELAY.value)
|
||||||
link_options.delay = message.get_tlv(LinkTlvs.DELAY.value)
|
options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value)
|
||||||
link_options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value)
|
options.loss = message.get_tlv(LinkTlvs.LOSS.value)
|
||||||
link_options.session = message.get_tlv(LinkTlvs.SESSION.value)
|
options.dup = message.get_tlv(LinkTlvs.DUP.value)
|
||||||
link_options.per = message.get_tlv(LinkTlvs.PER.value)
|
options.jitter = message.get_tlv(LinkTlvs.JITTER.value)
|
||||||
link_options.dup = message.get_tlv(LinkTlvs.DUP.value)
|
options.mer = message.get_tlv(LinkTlvs.MER.value)
|
||||||
link_options.jitter = message.get_tlv(LinkTlvs.JITTER.value)
|
options.burst = message.get_tlv(LinkTlvs.BURST.value)
|
||||||
link_options.mer = message.get_tlv(LinkTlvs.MER.value)
|
options.mburst = message.get_tlv(LinkTlvs.MBURST.value)
|
||||||
link_options.burst = message.get_tlv(LinkTlvs.BURST.value)
|
options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value)
|
||||||
link_options.mburst = message.get_tlv(LinkTlvs.MBURST.value)
|
options.key = message.get_tlv(LinkTlvs.KEY.value)
|
||||||
link_options.gui_attributes = message.get_tlv(LinkTlvs.GUI_ATTRIBUTES.value)
|
|
||||||
link_options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value)
|
|
||||||
link_options.emulation_id = message.get_tlv(LinkTlvs.EMULATION_ID.value)
|
|
||||||
link_options.network_id = message.get_tlv(LinkTlvs.NETWORK_ID.value)
|
|
||||||
link_options.key = message.get_tlv(LinkTlvs.KEY.value)
|
|
||||||
link_options.opaque = message.get_tlv(LinkTlvs.OPAQUE.value)
|
|
||||||
|
|
||||||
if message.flags & MessageFlags.ADD.value:
|
if message.flags & MessageFlags.ADD.value:
|
||||||
self.session.add_link(
|
self.session.add_link(
|
||||||
node_one_id, node_two_id, interface_one, interface_two, link_options
|
node1_id, node2_id, iface1_data, iface2_data, options, link_type
|
||||||
)
|
)
|
||||||
elif message.flags & MessageFlags.DELETE.value:
|
elif message.flags & MessageFlags.DELETE.value:
|
||||||
self.session.delete_link(
|
self.session.delete_link(
|
||||||
node_one_id, node_two_id, interface_one.id, interface_two.id
|
node1_id, node2_id, iface1_data.id, iface2_data.id, link_type
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.session.update_link(
|
self.session.update_link(
|
||||||
node_one_id,
|
node1_id, node2_id, iface1_data.id, iface2_data.id, options, link_type
|
||||||
node_two_id,
|
|
||||||
interface_one.id,
|
|
||||||
interface_two.id,
|
|
||||||
link_options,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def handle_execute_message(self, message):
|
def handle_execute_message(self, message):
|
||||||
|
@ -815,38 +812,38 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
:param core.api.tlv.coreapi.CoreExecMessage message: execute message to handle
|
:param core.api.tlv.coreapi.CoreExecMessage message: execute message to handle
|
||||||
:return: reply messages
|
:return: reply messages
|
||||||
"""
|
"""
|
||||||
node_num = message.get_tlv(ExecuteTlvs.NODE.value)
|
node_id = message.get_tlv(ExecuteTlvs.NODE.value)
|
||||||
execute_num = message.get_tlv(ExecuteTlvs.NUMBER.value)
|
execute_num = message.get_tlv(ExecuteTlvs.NUMBER.value)
|
||||||
execute_time = message.get_tlv(ExecuteTlvs.TIME.value)
|
execute_time = message.get_tlv(ExecuteTlvs.TIME.value)
|
||||||
command = message.get_tlv(ExecuteTlvs.COMMAND.value)
|
command = message.get_tlv(ExecuteTlvs.COMMAND.value)
|
||||||
|
|
||||||
# local flag indicates command executed locally, not on a node
|
# local flag indicates command executed locally, not on a node
|
||||||
if node_num is None and not message.flags & MessageFlags.LOCAL.value:
|
if node_id is None and not message.flags & MessageFlags.LOCAL.value:
|
||||||
raise ValueError("Execute Message is missing node number.")
|
raise ValueError("Execute Message is missing node number.")
|
||||||
|
|
||||||
if execute_num is None:
|
if execute_num is None:
|
||||||
raise ValueError("Execute Message is missing execution number.")
|
raise ValueError("Execute Message is missing execution number.")
|
||||||
|
|
||||||
if execute_time is not None:
|
if execute_time is not None:
|
||||||
self.session.add_event(execute_time, node=node_num, name=None, data=command)
|
self.session.add_event(
|
||||||
|
float(execute_time), node_id=node_id, name=None, data=command
|
||||||
|
)
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
node = self.session.get_node(node_num, CoreNodeBase)
|
node = self.session.get_node(node_id, CoreNodeBase)
|
||||||
|
|
||||||
# build common TLV items for reply
|
# build common TLV items for reply
|
||||||
tlv_data = b""
|
tlv_data = b""
|
||||||
if node_num is not None:
|
if node_id is not None:
|
||||||
tlv_data += coreapi.CoreExecuteTlv.pack(
|
tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.NODE.value, node_id)
|
||||||
ExecuteTlvs.NODE.value, node_num
|
|
||||||
)
|
|
||||||
tlv_data += coreapi.CoreExecuteTlv.pack(
|
tlv_data += coreapi.CoreExecuteTlv.pack(
|
||||||
ExecuteTlvs.NUMBER.value, execute_num
|
ExecuteTlvs.NUMBER.value, execute_num
|
||||||
)
|
)
|
||||||
tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.COMMAND.value, command)
|
tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.COMMAND.value, command)
|
||||||
|
|
||||||
if message.flags & MessageFlags.TTY.value:
|
if message.flags & MessageFlags.TTY.value:
|
||||||
if node_num is None:
|
if node_id is None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
# echo back exec message with cmd for spawning interactive terminal
|
# echo back exec message with cmd for spawning interactive terminal
|
||||||
if command == "bash":
|
if command == "bash":
|
||||||
|
@ -856,7 +853,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
reply = coreapi.CoreExecMessage.pack(MessageFlags.TTY.value, tlv_data)
|
reply = coreapi.CoreExecMessage.pack(MessageFlags.TTY.value, tlv_data)
|
||||||
return (reply,)
|
return (reply,)
|
||||||
else:
|
else:
|
||||||
logging.info("execute message with cmd=%s", command)
|
|
||||||
# execute command and send a response
|
# execute command and send a response
|
||||||
if (
|
if (
|
||||||
message.flags & MessageFlags.STRING.value
|
message.flags & MessageFlags.STRING.value
|
||||||
|
@ -876,7 +872,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
except CoreCommandError as e:
|
except CoreCommandError as e:
|
||||||
res = e.stderr
|
res = e.stderr
|
||||||
status = e.returncode
|
status = e.returncode
|
||||||
logging.info("done exec cmd=%s with status=%d", command, status)
|
|
||||||
if message.flags & MessageFlags.TEXT.value:
|
if message.flags & MessageFlags.TEXT.value:
|
||||||
tlv_data += coreapi.CoreExecuteTlv.pack(
|
tlv_data += coreapi.CoreExecuteTlv.pack(
|
||||||
ExecuteTlvs.RESULT.value, res
|
ExecuteTlvs.RESULT.value, res
|
||||||
|
@ -894,7 +889,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
else:
|
else:
|
||||||
node.cmd(command, wait=False)
|
node.cmd(command, wait=False)
|
||||||
except CoreError:
|
except CoreError:
|
||||||
logging.exception("error getting object: %s", node_num)
|
logging.exception("error getting object: %s", node_id)
|
||||||
# XXX wait and queue this message to try again later
|
# XXX wait and queue this message to try again later
|
||||||
# XXX maybe this should be done differently
|
# XXX maybe this should be done differently
|
||||||
if not message.flags & MessageFlags.LOCAL.value:
|
if not message.flags & MessageFlags.LOCAL.value:
|
||||||
|
@ -1016,7 +1011,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
possible_values=message.get_tlv(ConfigTlvs.POSSIBLE_VALUES.value),
|
possible_values=message.get_tlv(ConfigTlvs.POSSIBLE_VALUES.value),
|
||||||
groups=message.get_tlv(ConfigTlvs.GROUPS.value),
|
groups=message.get_tlv(ConfigTlvs.GROUPS.value),
|
||||||
session=message.get_tlv(ConfigTlvs.SESSION.value),
|
session=message.get_tlv(ConfigTlvs.SESSION.value),
|
||||||
interface_number=message.get_tlv(ConfigTlvs.INTERFACE_NUMBER.value),
|
iface_id=message.get_tlv(ConfigTlvs.IFACE_ID.value),
|
||||||
network_id=message.get_tlv(ConfigTlvs.NETWORK_ID.value),
|
network_id=message.get_tlv(ConfigTlvs.NETWORK_ID.value),
|
||||||
opaque=message.get_tlv(ConfigTlvs.OPAQUE.value),
|
opaque=message.get_tlv(ConfigTlvs.OPAQUE.value),
|
||||||
)
|
)
|
||||||
|
@ -1333,11 +1328,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
replies = []
|
replies = []
|
||||||
node_id = config_data.node
|
node_id = config_data.node
|
||||||
object_name = config_data.object
|
object_name = config_data.object
|
||||||
interface_id = config_data.interface_number
|
iface_id = config_data.iface_id
|
||||||
values_str = config_data.data_values
|
values_str = config_data.data_values
|
||||||
|
|
||||||
if interface_id is not None:
|
if iface_id is not None:
|
||||||
node_id = node_id * 1000 + interface_id
|
node_id = node_id * 1000 + iface_id
|
||||||
|
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"received configure message for %s nodenum: %s", object_name, node_id
|
"received configure message for %s nodenum: %s", object_name, node_id
|
||||||
|
@ -1383,11 +1378,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
replies = []
|
replies = []
|
||||||
node_id = config_data.node
|
node_id = config_data.node
|
||||||
object_name = config_data.object
|
object_name = config_data.object
|
||||||
interface_id = config_data.interface_number
|
iface_id = config_data.iface_id
|
||||||
values_str = config_data.data_values
|
values_str = config_data.data_values
|
||||||
|
|
||||||
if interface_id is not None:
|
if iface_id is not None:
|
||||||
node_id = node_id * 1000 + interface_id
|
node_id = node_id * 1000 + iface_id
|
||||||
|
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"received configure message for %s nodenum: %s", object_name, node_id
|
"received configure message for %s nodenum: %s", object_name, node_id
|
||||||
|
@ -1415,11 +1410,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
replies = []
|
replies = []
|
||||||
node_id = config_data.node
|
node_id = config_data.node
|
||||||
object_name = config_data.object
|
object_name = config_data.object
|
||||||
interface_id = config_data.interface_number
|
iface_id = config_data.iface_id
|
||||||
values_str = config_data.data_values
|
values_str = config_data.data_values
|
||||||
|
|
||||||
if interface_id is not None:
|
if iface_id is not None:
|
||||||
node_id = node_id * 1000 + interface_id
|
node_id = node_id * 1000 + iface_id
|
||||||
|
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"received configure message for %s nodenum: %s", object_name, node_id
|
"received configure message for %s nodenum: %s", object_name, node_id
|
||||||
|
@ -1513,7 +1508,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
|
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def handle_interface_message(self, message):
|
def handle_iface_message(self, message):
|
||||||
"""
|
"""
|
||||||
Interface Message handler.
|
Interface Message handler.
|
||||||
|
|
||||||
|
@ -1555,11 +1550,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
if event_type == EventTypes.INSTANTIATION_STATE and isinstance(
|
if event_type == EventTypes.INSTANTIATION_STATE and isinstance(
|
||||||
node, WlanNode
|
node, WlanNode
|
||||||
):
|
):
|
||||||
self.session.start_mobility(node_ids=(node.id,))
|
self.session.start_mobility(node_ids=[node.id])
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"dropping unhandled event message for node: %s", node_id
|
"dropping unhandled event message for node: %s", node.name
|
||||||
)
|
)
|
||||||
return ()
|
return ()
|
||||||
self.session.set_state(event_type)
|
self.session.set_state(event_type)
|
||||||
|
@ -1617,14 +1612,16 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
self.session.save_xml(filename)
|
self.session.save_xml(filename)
|
||||||
elif event_type == EventTypes.SCHEDULED:
|
elif event_type == EventTypes.SCHEDULED:
|
||||||
etime = event_data.time
|
etime = event_data.time
|
||||||
node = event_data.node
|
node_id = event_data.node
|
||||||
name = event_data.name
|
name = event_data.name
|
||||||
data = event_data.data
|
data = event_data.data
|
||||||
if etime is None:
|
if etime is None:
|
||||||
logging.warning("Event message scheduled event missing start time")
|
logging.warning("Event message scheduled event missing start time")
|
||||||
return ()
|
return ()
|
||||||
if message.flags & MessageFlags.ADD.value:
|
if message.flags & MessageFlags.ADD.value:
|
||||||
self.session.add_event(float(etime), node=node, name=name, data=data)
|
self.session.add_event(
|
||||||
|
float(etime), node_id=node_id, name=name, data=data
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -1827,16 +1824,16 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
Return API messages that describe the current session.
|
Return API messages that describe the current session.
|
||||||
"""
|
"""
|
||||||
# find all nodes and links
|
# find all nodes and links
|
||||||
links_data = []
|
all_links = []
|
||||||
with self.session._nodes_lock:
|
with self.session.nodes_lock:
|
||||||
for node_id in self.session.nodes:
|
for node_id in self.session.nodes:
|
||||||
node = self.session.nodes[node_id]
|
node = self.session.nodes[node_id]
|
||||||
self.session.broadcast_node(node, MessageFlags.ADD)
|
self.session.broadcast_node(node, MessageFlags.ADD)
|
||||||
node_links = node.all_link_data(flags=MessageFlags.ADD)
|
links = node.links(flags=MessageFlags.ADD)
|
||||||
links_data.extend(node_links)
|
all_links.extend(links)
|
||||||
|
|
||||||
for link_data in links_data:
|
for link in all_links:
|
||||||
self.session.broadcast_link(link_data)
|
self.session.broadcast_link(link)
|
||||||
|
|
||||||
# send mobility model info
|
# send mobility model info
|
||||||
for node_id in self.session.mobility.nodes():
|
for node_id in self.session.mobility.nodes():
|
||||||
|
@ -1906,8 +1903,8 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
# TODO: send location info
|
# TODO: send location info
|
||||||
|
|
||||||
# send hook scripts
|
# send hook scripts
|
||||||
for state in sorted(self.session._hooks.keys()):
|
for state in sorted(self.session.hooks.keys()):
|
||||||
for file_name, config_data in self.session._hooks[state]:
|
for file_name, config_data in self.session.hooks[state]:
|
||||||
file_data = FileData(
|
file_data = FileData(
|
||||||
message_type=MessageFlags.ADD,
|
message_type=MessageFlags.ADD,
|
||||||
name=str(file_name),
|
name=str(file_name),
|
||||||
|
@ -1943,7 +1940,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
|
|
||||||
node_count = self.session.get_node_count()
|
node_count = self.session.get_node_count()
|
||||||
logging.info(
|
logging.info(
|
||||||
"informed GUI about %d nodes and %d links", node_count, len(links_data)
|
"informed GUI about %d nodes and %d links", node_count, len(all_links)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1956,7 +1953,7 @@ class CoreUdpHandler(CoreHandler):
|
||||||
MessageTypes.REGISTER.value: self.handle_register_message,
|
MessageTypes.REGISTER.value: self.handle_register_message,
|
||||||
MessageTypes.CONFIG.value: self.handle_config_message,
|
MessageTypes.CONFIG.value: self.handle_config_message,
|
||||||
MessageTypes.FILE.value: self.handle_file_message,
|
MessageTypes.FILE.value: self.handle_file_message,
|
||||||
MessageTypes.INTERFACE.value: self.handle_interface_message,
|
MessageTypes.INTERFACE.value: self.handle_iface_message,
|
||||||
MessageTypes.EVENT.value: self.handle_event_message,
|
MessageTypes.EVENT.value: self.handle_event_message,
|
||||||
MessageTypes.SESSION.value: self.handle_session_message,
|
MessageTypes.SESSION.value: self.handle_session_message,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,45 +8,39 @@ from typing import Dict, List
|
||||||
from core.api.tlv import coreapi, structutils
|
from core.api.tlv import coreapi, structutils
|
||||||
from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs
|
from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs
|
||||||
from core.config import ConfigGroup, ConfigurableOptions
|
from core.config import ConfigGroup, ConfigurableOptions
|
||||||
from core.emulator.data import ConfigData
|
from core.emulator.data import ConfigData, NodeData
|
||||||
|
|
||||||
|
|
||||||
def convert_node(node_data):
|
def convert_node(node_data: NodeData):
|
||||||
"""
|
"""
|
||||||
Convenience method for converting NodeData to a packed TLV message.
|
Convenience method for converting NodeData to a packed TLV message.
|
||||||
|
|
||||||
:param core.emulator.data.NodeData node_data: node data to convert
|
:param core.emulator.data.NodeData node_data: node data to convert
|
||||||
:return: packed node message
|
:return: packed node message
|
||||||
"""
|
"""
|
||||||
session = None
|
node = node_data.node
|
||||||
if node_data.session is not None:
|
|
||||||
session = str(node_data.session)
|
|
||||||
services = None
|
services = None
|
||||||
if node_data.services is not None:
|
if node.services is not None:
|
||||||
services = "|".join([x for x in node_data.services])
|
services = "|".join([x.name for x in node.services])
|
||||||
|
server = None
|
||||||
|
if node.server is not None:
|
||||||
|
server = node.server.name
|
||||||
tlv_data = structutils.pack_values(
|
tlv_data = structutils.pack_values(
|
||||||
coreapi.CoreNodeTlv,
|
coreapi.CoreNodeTlv,
|
||||||
[
|
[
|
||||||
(NodeTlvs.NUMBER, node_data.id),
|
(NodeTlvs.NUMBER, node.id),
|
||||||
(NodeTlvs.TYPE, node_data.node_type.value),
|
(NodeTlvs.TYPE, node.apitype.value),
|
||||||
(NodeTlvs.NAME, node_data.name),
|
(NodeTlvs.NAME, node.name),
|
||||||
(NodeTlvs.IP_ADDRESS, node_data.ip_address),
|
(NodeTlvs.MODEL, node.type),
|
||||||
(NodeTlvs.MAC_ADDRESS, node_data.mac_address),
|
(NodeTlvs.EMULATION_SERVER, server),
|
||||||
(NodeTlvs.IP6_ADDRESS, node_data.ip6_address),
|
(NodeTlvs.X_POSITION, int(node.position.x)),
|
||||||
(NodeTlvs.MODEL, node_data.model),
|
(NodeTlvs.Y_POSITION, int(node.position.y)),
|
||||||
(NodeTlvs.EMULATION_ID, node_data.emulation_id),
|
(NodeTlvs.CANVAS, node.canvas),
|
||||||
(NodeTlvs.EMULATION_SERVER, node_data.server),
|
|
||||||
(NodeTlvs.SESSION, session),
|
|
||||||
(NodeTlvs.X_POSITION, int(node_data.x_position)),
|
|
||||||
(NodeTlvs.Y_POSITION, int(node_data.y_position)),
|
|
||||||
(NodeTlvs.CANVAS, node_data.canvas),
|
|
||||||
(NodeTlvs.NETWORK_ID, node_data.network_id),
|
|
||||||
(NodeTlvs.SERVICES, services),
|
(NodeTlvs.SERVICES, services),
|
||||||
(NodeTlvs.LATITUDE, str(node_data.latitude)),
|
(NodeTlvs.LATITUDE, str(node.position.lat)),
|
||||||
(NodeTlvs.LONGITUDE, str(node_data.longitude)),
|
(NodeTlvs.LONGITUDE, str(node.position.lon)),
|
||||||
(NodeTlvs.ALTITUDE, str(node_data.altitude)),
|
(NodeTlvs.ALTITUDE, str(node.position.alt)),
|
||||||
(NodeTlvs.ICON, node_data.icon),
|
(NodeTlvs.ICON, node.icon),
|
||||||
(NodeTlvs.OPAQUE, node_data.opaque),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data)
|
return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data)
|
||||||
|
@ -75,7 +69,7 @@ def convert_config(config_data):
|
||||||
(ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values),
|
(ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values),
|
||||||
(ConfigTlvs.GROUPS, config_data.groups),
|
(ConfigTlvs.GROUPS, config_data.groups),
|
||||||
(ConfigTlvs.SESSION, session),
|
(ConfigTlvs.SESSION, session),
|
||||||
(ConfigTlvs.INTERFACE_NUMBER, config_data.interface_number),
|
(ConfigTlvs.IFACE_ID, config_data.iface_id),
|
||||||
(ConfigTlvs.NETWORK_ID, config_data.network_id),
|
(ConfigTlvs.NETWORK_ID, config_data.network_id),
|
||||||
(ConfigTlvs.OPAQUE, config_data.opaque),
|
(ConfigTlvs.OPAQUE, config_data.opaque),
|
||||||
],
|
],
|
||||||
|
|
|
@ -59,7 +59,7 @@ class LinkTlvs(Enum):
|
||||||
N2_NUMBER = 0x02
|
N2_NUMBER = 0x02
|
||||||
DELAY = 0x03
|
DELAY = 0x03
|
||||||
BANDWIDTH = 0x04
|
BANDWIDTH = 0x04
|
||||||
PER = 0x05
|
LOSS = 0x05
|
||||||
DUP = 0x06
|
DUP = 0x06
|
||||||
JITTER = 0x07
|
JITTER = 0x07
|
||||||
MER = 0x08
|
MER = 0x08
|
||||||
|
@ -72,18 +72,18 @@ class LinkTlvs(Enum):
|
||||||
EMULATION_ID = 0x23
|
EMULATION_ID = 0x23
|
||||||
NETWORK_ID = 0x24
|
NETWORK_ID = 0x24
|
||||||
KEY = 0x25
|
KEY = 0x25
|
||||||
INTERFACE1_NUMBER = 0x30
|
IFACE1_NUMBER = 0x30
|
||||||
INTERFACE1_IP4 = 0x31
|
IFACE1_IP4 = 0x31
|
||||||
INTERFACE1_IP4_MASK = 0x32
|
IFACE1_IP4_MASK = 0x32
|
||||||
INTERFACE1_MAC = 0x33
|
IFACE1_MAC = 0x33
|
||||||
INTERFACE1_IP6 = 0x34
|
IFACE1_IP6 = 0x34
|
||||||
INTERFACE1_IP6_MASK = 0x35
|
IFACE1_IP6_MASK = 0x35
|
||||||
INTERFACE2_NUMBER = 0x36
|
IFACE2_NUMBER = 0x36
|
||||||
INTERFACE2_IP4 = 0x37
|
IFACE2_IP4 = 0x37
|
||||||
INTERFACE2_IP4_MASK = 0x38
|
IFACE2_IP4_MASK = 0x38
|
||||||
INTERFACE2_MAC = 0x39
|
IFACE2_MAC = 0x39
|
||||||
INTERFACE2_IP6 = 0x40
|
IFACE2_IP6 = 0x40
|
||||||
INTERFACE2_IP6_MASK = 0x41
|
IFACE2_IP6_MASK = 0x41
|
||||||
INTERFACE1_NAME = 0x42
|
INTERFACE1_NAME = 0x42
|
||||||
INTERFACE2_NAME = 0x43
|
INTERFACE2_NAME = 0x43
|
||||||
OPAQUE = 0x50
|
OPAQUE = 0x50
|
||||||
|
@ -118,7 +118,7 @@ class ConfigTlvs(Enum):
|
||||||
POSSIBLE_VALUES = 0x08
|
POSSIBLE_VALUES = 0x08
|
||||||
GROUPS = 0x09
|
GROUPS = 0x09
|
||||||
SESSION = 0x0A
|
SESSION = 0x0A
|
||||||
INTERFACE_NUMBER = 0x0B
|
IFACE_ID = 0x0B
|
||||||
NETWORK_ID = 0x24
|
NETWORK_ID = 0x24
|
||||||
OPAQUE = 0x50
|
OPAQUE = 0x50
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ Common support for configurable CORE objects.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Union
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.emulator.enumerations import ConfigDataTypes
|
from core.emulator.enumerations import ConfigDataTypes
|
||||||
|
@ -29,9 +29,9 @@ class ConfigGroup:
|
||||||
:param start: configurations start index for this group
|
:param start: configurations start index for this group
|
||||||
:param stop: configurations stop index for this group
|
:param stop: configurations stop index for this group
|
||||||
"""
|
"""
|
||||||
self.name = name
|
self.name: str = name
|
||||||
self.start = start
|
self.start: int = start
|
||||||
self.stop = stop
|
self.stop: int = stop
|
||||||
|
|
||||||
|
|
||||||
class Configuration:
|
class Configuration:
|
||||||
|
@ -56,18 +56,21 @@ class Configuration:
|
||||||
:param default: default value for configuration
|
:param default: default value for configuration
|
||||||
:param options: list options if this is a configuration with a combobox
|
:param options: list options if this is a configuration with a combobox
|
||||||
"""
|
"""
|
||||||
self.id = _id
|
self.id: str = _id
|
||||||
self.type = _type
|
self.type: ConfigDataTypes = _type
|
||||||
self.default = default
|
self.default: str = default
|
||||||
if not options:
|
if not options:
|
||||||
options = []
|
options = []
|
||||||
self.options = options
|
self.options: List[str] = options
|
||||||
if not label:
|
if not label:
|
||||||
label = _id
|
label = _id
|
||||||
self.label = label
|
self.label: str = label
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.__class__.__name__}(id={self.id}, type={self.type}, default={self.default}, options={self.options})"
|
return (
|
||||||
|
f"{self.__class__.__name__}(id={self.id}, type={self.type}, "
|
||||||
|
f"default={self.default}, options={self.options})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigurableOptions:
|
class ConfigurableOptions:
|
||||||
|
@ -75,9 +78,9 @@ class ConfigurableOptions:
|
||||||
Provides a base for defining configuration options within CORE.
|
Provides a base for defining configuration options within CORE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = None
|
name: Optional[str] = None
|
||||||
bitmap = None
|
bitmap: Optional[str] = None
|
||||||
options = []
|
options: List[Configuration] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def configurations(cls) -> List[Configuration]:
|
def configurations(cls) -> List[Configuration]:
|
||||||
|
@ -115,8 +118,8 @@ class ConfigurableManager:
|
||||||
nodes.
|
nodes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_default_node = -1
|
_default_node: int = -1
|
||||||
_default_type = _default_node
|
_default_type: int = _default_node
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -136,7 +139,8 @@ class ConfigurableManager:
|
||||||
"""
|
"""
|
||||||
Clears all configurations or configuration for a specific node.
|
Clears all configurations or configuration for a specific node.
|
||||||
|
|
||||||
:param node_id: node id to clear configurations for, default is None and clears all configurations
|
:param node_id: node id to clear configurations for, default is None and clears
|
||||||
|
all configurations
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
if not node_id:
|
if not node_id:
|
||||||
|
@ -222,7 +226,7 @@ class ConfigurableManager:
|
||||||
result = node_configs.get(config_type)
|
result = node_configs.get(config_type)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_all_configs(self, node_id: int = _default_node) -> List[Dict[str, str]]:
|
def get_all_configs(self, node_id: int = _default_node) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Retrieve all current configuration types for a node.
|
Retrieve all current configuration types for a node.
|
||||||
|
|
||||||
|
@ -242,8 +246,8 @@ class ModelManager(ConfigurableManager):
|
||||||
Creates a ModelManager object.
|
Creates a ModelManager object.
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.models = {}
|
self.models: Dict[str, Any] = {}
|
||||||
self.node_models = {}
|
self.node_models: Dict[int, str] = {}
|
||||||
|
|
||||||
def set_model_config(
|
def set_model_config(
|
||||||
self, node_id: int, model_name: str, config: Dict[str, str] = None
|
self, node_id: int, model_name: str, config: Dict[str, str] = None
|
||||||
|
|
|
@ -14,7 +14,7 @@ from core.config import Configuration
|
||||||
from core.errors import CoreCommandError, CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.nodes.base import CoreNode
|
from core.nodes.base import CoreNode
|
||||||
|
|
||||||
TEMPLATES_DIR = "templates"
|
TEMPLATES_DIR: str = "templates"
|
||||||
|
|
||||||
|
|
||||||
class ConfigServiceMode(enum.Enum):
|
class ConfigServiceMode(enum.Enum):
|
||||||
|
@ -33,10 +33,10 @@ class ConfigService(abc.ABC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# validation period in seconds, how frequent validation is attempted
|
# validation period in seconds, how frequent validation is attempted
|
||||||
validation_period = 0.5
|
validation_period: float = 0.5
|
||||||
|
|
||||||
# time to wait in seconds for determining if service started successfully
|
# time to wait in seconds for determining if service started successfully
|
||||||
validation_timer = 5
|
validation_timer: int = 5
|
||||||
|
|
||||||
def __init__(self, node: CoreNode) -> None:
|
def __init__(self, node: CoreNode) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -44,13 +44,13 @@ class ConfigService(abc.ABC):
|
||||||
|
|
||||||
:param node: node this service is assigned to
|
:param node: node this service is assigned to
|
||||||
"""
|
"""
|
||||||
self.node = node
|
self.node: CoreNode = node
|
||||||
class_file = inspect.getfile(self.__class__)
|
class_file = inspect.getfile(self.__class__)
|
||||||
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
|
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
|
||||||
self.templates = TemplateLookup(directories=templates_path)
|
self.templates: TemplateLookup = TemplateLookup(directories=templates_path)
|
||||||
self.config = {}
|
self.config: Dict[str, Configuration] = {}
|
||||||
self.custom_templates = {}
|
self.custom_templates: Dict[str, str] = {}
|
||||||
self.custom_config = {}
|
self.custom_config: Dict[str, str] = {}
|
||||||
configs = self.default_configs[:]
|
configs = self.default_configs[:]
|
||||||
self._define_config(configs)
|
self._define_config(configs)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Dict, List
|
from typing import TYPE_CHECKING, Dict, List, Set
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.configservice.base import ConfigService
|
from core.configservice.base import ConfigService
|
||||||
|
@ -17,9 +17,9 @@ class ConfigServiceDependencies:
|
||||||
:param services: services for determining dependency sets
|
:param services: services for determining dependency sets
|
||||||
"""
|
"""
|
||||||
# helpers to check validity
|
# helpers to check validity
|
||||||
self.dependents = {}
|
self.dependents: Dict[str, Set[str]] = {}
|
||||||
self.started = set()
|
self.started: Set[str] = set()
|
||||||
self.node_services = {}
|
self.node_services: Dict[str, "ConfigService"] = {}
|
||||||
for service in services.values():
|
for service in services.values():
|
||||||
self.node_services[service.name] = service
|
self.node_services[service.name] = service
|
||||||
for dependency in service.dependencies:
|
for dependency in service.dependencies:
|
||||||
|
@ -27,9 +27,9 @@ class ConfigServiceDependencies:
|
||||||
dependents.add(service.name)
|
dependents.add(service.name)
|
||||||
|
|
||||||
# used to find paths
|
# used to find paths
|
||||||
self.path = []
|
self.path: List["ConfigService"] = []
|
||||||
self.visited = set()
|
self.visited: Set[str] = set()
|
||||||
self.visiting = set()
|
self.visiting: Set[str] = set()
|
||||||
|
|
||||||
def startup_paths(self) -> List[List["ConfigService"]]:
|
def startup_paths(self) -> List[List["ConfigService"]]:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import List, Type
|
from typing import Dict, List, Type
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
from core.configservice.base import ConfigService
|
from core.configservice.base import ConfigService
|
||||||
|
@ -16,7 +16,7 @@ class ConfigServiceManager:
|
||||||
"""
|
"""
|
||||||
Create a ConfigServiceManager instance.
|
Create a ConfigServiceManager instance.
|
||||||
"""
|
"""
|
||||||
self.services = {}
|
self.services: Dict[str, Type[ConfigService]] = {}
|
||||||
|
|
||||||
def get_service(self, name: str) -> Type[ConfigService]:
|
def get_service(self, name: str) -> Type[ConfigService]:
|
||||||
"""
|
"""
|
||||||
|
@ -31,7 +31,7 @@ class ConfigServiceManager:
|
||||||
raise CoreError(f"service does not exit {name}")
|
raise CoreError(f"service does not exit {name}")
|
||||||
return service_class
|
return service_class
|
||||||
|
|
||||||
def add(self, service: ConfigService) -> None:
|
def add(self, service: Type[ConfigService]) -> None:
|
||||||
"""
|
"""
|
||||||
Add service to manager, checking service requirements have been met.
|
Add service to manager, checking service requirements have been met.
|
||||||
|
|
||||||
|
@ -40,7 +40,9 @@ class ConfigServiceManager:
|
||||||
:raises CoreError: when service is a duplicate or has unmet executables
|
:raises CoreError: when service is a duplicate or has unmet executables
|
||||||
"""
|
"""
|
||||||
name = service.name
|
name = service.name
|
||||||
logging.debug("loading service: class(%s) name(%s)", service.__class__, name)
|
logging.debug(
|
||||||
|
"loading service: class(%s) name(%s)", service.__class__.__name__, name
|
||||||
|
)
|
||||||
|
|
||||||
# avoid duplicate services
|
# avoid duplicate services
|
||||||
if name in self.services:
|
if name in self.services:
|
||||||
|
@ -50,10 +52,8 @@ class ConfigServiceManager:
|
||||||
for executable in service.executables:
|
for executable in service.executables:
|
||||||
try:
|
try:
|
||||||
utils.which(executable, required=True)
|
utils.which(executable, required=True)
|
||||||
except ValueError:
|
except CoreError as e:
|
||||||
raise CoreError(
|
raise CoreError(f"config service({service.name}): {e}")
|
||||||
f"service({service.name}) missing executable {executable}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# make service available
|
# make service available
|
||||||
self.services[name] = service
|
self.services[name] = service
|
||||||
|
@ -73,7 +73,6 @@ class ConfigServiceManager:
|
||||||
logging.debug("loading config services from: %s", subdir)
|
logging.debug("loading config services from: %s", subdir)
|
||||||
services = utils.load_classes(str(subdir), ConfigService)
|
services = utils.load_classes(str(subdir), ConfigService)
|
||||||
for service in services:
|
for service in services:
|
||||||
logging.debug("found service: %s", service)
|
|
||||||
try:
|
try:
|
||||||
self.add(service)
|
self.add(service)
|
||||||
except CoreError as e:
|
except CoreError as e:
|
||||||
|
|
|
@ -1,45 +1,44 @@
|
||||||
import abc
|
import abc
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import netaddr
|
from core.config import Configuration
|
||||||
|
|
||||||
from core import constants
|
|
||||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.nodes.base import CoreNodeBase
|
from core.nodes.base import CoreNodeBase
|
||||||
from core.nodes.interface import CoreInterface
|
from core.nodes.interface import CoreInterface
|
||||||
from core.nodes.network import WlanNode
|
from core.nodes.network import WlanNode
|
||||||
|
|
||||||
GROUP = "FRR"
|
GROUP: str = "FRR"
|
||||||
|
FRR_STATE_DIR: str = "/var/run/frr"
|
||||||
|
|
||||||
|
|
||||||
def has_mtu_mismatch(ifc: CoreInterface) -> bool:
|
def has_mtu_mismatch(iface: CoreInterface) -> bool:
|
||||||
"""
|
"""
|
||||||
Helper to detect MTU mismatch and add the appropriate FRR
|
Helper to detect MTU mismatch and add the appropriate FRR
|
||||||
mtu-ignore command. This is needed when e.g. a node is linked via a
|
mtu-ignore command. This is needed when e.g. a node is linked via a
|
||||||
GreTap device.
|
GreTap device.
|
||||||
"""
|
"""
|
||||||
if ifc.mtu != 1500:
|
if iface.mtu != 1500:
|
||||||
return True
|
return True
|
||||||
if not ifc.net:
|
if not iface.net:
|
||||||
return False
|
return False
|
||||||
for i in ifc.net.netifs():
|
for iface in iface.net.get_ifaces():
|
||||||
if i.mtu != ifc.mtu:
|
if iface.mtu != iface.mtu:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_min_mtu(ifc):
|
def get_min_mtu(iface: CoreInterface) -> int:
|
||||||
"""
|
"""
|
||||||
Helper to discover the minimum MTU of interfaces linked with the
|
Helper to discover the minimum MTU of interfaces linked with the
|
||||||
given interface.
|
given interface.
|
||||||
"""
|
"""
|
||||||
mtu = ifc.mtu
|
mtu = iface.mtu
|
||||||
if not ifc.net:
|
if not iface.net:
|
||||||
return mtu
|
return mtu
|
||||||
for i in ifc.net.netifs():
|
for iface in iface.net.get_ifaces():
|
||||||
if i.mtu < mtu:
|
if iface.mtu < mtu:
|
||||||
mtu = i.mtu
|
mtu = iface.mtu
|
||||||
return mtu
|
return mtu
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,34 +46,31 @@ def get_router_id(node: CoreNodeBase) -> str:
|
||||||
"""
|
"""
|
||||||
Helper to return the first IPv4 address of a node as its router ID.
|
Helper to return the first IPv4 address of a node as its router ID.
|
||||||
"""
|
"""
|
||||||
for ifc in node.netifs():
|
for iface in node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ip4 = iface.get_ip4()
|
||||||
continue
|
if ip4:
|
||||||
for a in ifc.addrlist:
|
return str(ip4.ip)
|
||||||
a = a.split("/")[0]
|
|
||||||
if netaddr.valid_ipv4(a):
|
|
||||||
return a
|
|
||||||
return "0.0.0.0"
|
return "0.0.0.0"
|
||||||
|
|
||||||
|
|
||||||
class FRRZebra(ConfigService):
|
class FRRZebra(ConfigService):
|
||||||
name = "FRRzebra"
|
name: str = "FRRzebra"
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
|
directories: List[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
|
||||||
files = [
|
files: List[str] = [
|
||||||
"/usr/local/etc/frr/frr.conf",
|
"/usr/local/etc/frr/frr.conf",
|
||||||
"frrboot.sh",
|
"frrboot.sh",
|
||||||
"/usr/local/etc/frr/vtysh.conf",
|
"/usr/local/etc/frr/vtysh.conf",
|
||||||
"/usr/local/etc/frr/daemons",
|
"/usr/local/etc/frr/daemons",
|
||||||
]
|
]
|
||||||
executables = ["zebra"]
|
executables: List[str] = ["zebra"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh frrboot.sh zebra"]
|
startup: List[str] = ["sh frrboot.sh zebra"]
|
||||||
validate = ["pidof zebra"]
|
validate: List[str] = ["pidof zebra"]
|
||||||
shutdown = ["killall zebra"]
|
shutdown: List[str] = ["killall zebra"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
frr_conf = self.files[0]
|
frr_conf = self.files[0]
|
||||||
|
@ -91,31 +87,31 @@ class FRRZebra(ConfigService):
|
||||||
for service in self.node.config_services.values():
|
for service in self.node.config_services.values():
|
||||||
if self.name not in service.dependencies:
|
if self.name not in service.dependencies:
|
||||||
continue
|
continue
|
||||||
|
if not isinstance(service, FrrService):
|
||||||
|
continue
|
||||||
if service.ipv4_routing:
|
if service.ipv4_routing:
|
||||||
want_ip4 = True
|
want_ip4 = True
|
||||||
if service.ipv6_routing:
|
if service.ipv6_routing:
|
||||||
want_ip6 = True
|
want_ip6 = True
|
||||||
services.append(service)
|
services.append(service)
|
||||||
|
|
||||||
interfaces = []
|
ifaces = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces():
|
||||||
ip4s = []
|
ip4s = []
|
||||||
ip6s = []
|
ip6s = []
|
||||||
for x in ifc.addrlist:
|
for ip4 in iface.ip4s:
|
||||||
addr = x.split("/")[0]
|
ip4s.append(str(ip4.ip))
|
||||||
if netaddr.valid_ipv4(addr):
|
for ip6 in iface.ip6s:
|
||||||
ip4s.append(x)
|
ip6s.append(str(ip6.ip))
|
||||||
else:
|
is_control = getattr(iface, "control", False)
|
||||||
ip6s.append(x)
|
ifaces.append((iface, ip4s, ip6s, is_control))
|
||||||
is_control = getattr(ifc, "control", False)
|
|
||||||
interfaces.append((ifc, ip4s, ip6s, is_control))
|
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
frr_conf=frr_conf,
|
frr_conf=frr_conf,
|
||||||
frr_sbin_search=frr_sbin_search,
|
frr_sbin_search=frr_sbin_search,
|
||||||
frr_bin_search=frr_bin_search,
|
frr_bin_search=frr_bin_search,
|
||||||
frr_state_dir=constants.FRR_STATE_DIR,
|
frr_state_dir=FRR_STATE_DIR,
|
||||||
interfaces=interfaces,
|
ifaces=ifaces,
|
||||||
want_ip4=want_ip4,
|
want_ip4=want_ip4,
|
||||||
want_ip6=want_ip6,
|
want_ip6=want_ip6,
|
||||||
services=services,
|
services=services,
|
||||||
|
@ -123,22 +119,22 @@ class FRRZebra(ConfigService):
|
||||||
|
|
||||||
|
|
||||||
class FrrService(abc.ABC):
|
class FrrService(abc.ABC):
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = []
|
files: List[str] = []
|
||||||
executables = []
|
executables: List[str] = []
|
||||||
dependencies = ["FRRzebra"]
|
dependencies: List[str] = ["FRRzebra"]
|
||||||
startup = []
|
startup: List[str] = []
|
||||||
validate = []
|
validate: List[str] = []
|
||||||
shutdown = []
|
shutdown: List[str] = []
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
ipv4_routing = False
|
ipv4_routing: bool = False
|
||||||
ipv6_routing = False
|
ipv6_routing: bool = False
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -153,22 +149,17 @@ class FRROspfv2(FrrService, ConfigService):
|
||||||
unified frr.conf file.
|
unified frr.conf file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "FRROSPFv2"
|
name: str = "FRROSPFv2"
|
||||||
startup = ()
|
shutdown: List[str] = ["killall ospfd"]
|
||||||
shutdown = ["killall ospfd"]
|
validate: List[str] = ["pidof ospfd"]
|
||||||
validate = ["pidof ospfd"]
|
ipv4_routing: bool = True
|
||||||
ipv4_routing = True
|
|
||||||
|
|
||||||
def frr_config(self) -> str:
|
def frr_config(self) -> str:
|
||||||
router_id = get_router_id(self.node)
|
router_id = get_router_id(self.node)
|
||||||
addresses = []
|
addresses = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
for ip4 in iface.ip4s:
|
||||||
continue
|
addresses.append(str(ip4.ip))
|
||||||
for a in ifc.addrlist:
|
|
||||||
addr = a.split("/")[0]
|
|
||||||
if netaddr.valid_ipv4(addr):
|
|
||||||
addresses.append(a)
|
|
||||||
data = dict(router_id=router_id, addresses=addresses)
|
data = dict(router_id=router_id, addresses=addresses)
|
||||||
text = """
|
text = """
|
||||||
router ospf
|
router ospf
|
||||||
|
@ -180,8 +171,8 @@ class FRROspfv2(FrrService, ConfigService):
|
||||||
"""
|
"""
|
||||||
return self.render_text(text, data)
|
return self.render_text(text, data)
|
||||||
|
|
||||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||||
if has_mtu_mismatch(ifc):
|
if has_mtu_mismatch(iface):
|
||||||
return "ip ospf mtu-ignore"
|
return "ip ospf mtu-ignore"
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
@ -194,19 +185,17 @@ class FRROspfv3(FrrService, ConfigService):
|
||||||
unified frr.conf file.
|
unified frr.conf file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "FRROSPFv3"
|
name: str = "FRROSPFv3"
|
||||||
shutdown = ["killall ospf6d"]
|
shutdown: List[str] = ["killall ospf6d"]
|
||||||
validate = ["pidof ospf6d"]
|
validate: List[str] = ["pidof ospf6d"]
|
||||||
ipv4_routing = True
|
ipv4_routing: bool = True
|
||||||
ipv6_routing = True
|
ipv6_routing: bool = True
|
||||||
|
|
||||||
def frr_config(self) -> str:
|
def frr_config(self) -> str:
|
||||||
router_id = get_router_id(self.node)
|
router_id = get_router_id(self.node)
|
||||||
ifnames = []
|
ifnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifnames.append(iface.name)
|
||||||
continue
|
|
||||||
ifnames.append(ifc.name)
|
|
||||||
data = dict(router_id=router_id, ifnames=ifnames)
|
data = dict(router_id=router_id, ifnames=ifnames)
|
||||||
text = """
|
text = """
|
||||||
router ospf6
|
router ospf6
|
||||||
|
@ -218,9 +207,9 @@ class FRROspfv3(FrrService, ConfigService):
|
||||||
"""
|
"""
|
||||||
return self.render_text(text, data)
|
return self.render_text(text, data)
|
||||||
|
|
||||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||||
mtu = get_min_mtu(ifc)
|
mtu = get_min_mtu(iface)
|
||||||
if mtu < ifc.mtu:
|
if mtu < iface.mtu:
|
||||||
return f"ipv6 ospf6 ifmtu {mtu}"
|
return f"ipv6 ospf6 ifmtu {mtu}"
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
@ -233,12 +222,12 @@ class FRRBgp(FrrService, ConfigService):
|
||||||
having the same AS number.
|
having the same AS number.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "FRRBGP"
|
name: str = "FRRBGP"
|
||||||
shutdown = ["killall bgpd"]
|
shutdown: List[str] = ["killall bgpd"]
|
||||||
validate = ["pidof bgpd"]
|
validate: List[str] = ["pidof bgpd"]
|
||||||
custom_needed = True
|
custom_needed: bool = True
|
||||||
ipv4_routing = True
|
ipv4_routing: bool = True
|
||||||
ipv6_routing = True
|
ipv6_routing: bool = True
|
||||||
|
|
||||||
def frr_config(self) -> str:
|
def frr_config(self) -> str:
|
||||||
router_id = get_router_id(self.node)
|
router_id = get_router_id(self.node)
|
||||||
|
@ -254,7 +243,7 @@ class FRRBgp(FrrService, ConfigService):
|
||||||
"""
|
"""
|
||||||
return self.clean_text(text)
|
return self.clean_text(text)
|
||||||
|
|
||||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@ -263,10 +252,10 @@ class FRRRip(FrrService, ConfigService):
|
||||||
The RIP service provides IPv4 routing for wired networks.
|
The RIP service provides IPv4 routing for wired networks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "FRRRIP"
|
name: str = "FRRRIP"
|
||||||
shutdown = ["killall ripd"]
|
shutdown: List[str] = ["killall ripd"]
|
||||||
validate = ["pidof ripd"]
|
validate: List[str] = ["pidof ripd"]
|
||||||
ipv4_routing = True
|
ipv4_routing: bool = True
|
||||||
|
|
||||||
def frr_config(self) -> str:
|
def frr_config(self) -> str:
|
||||||
text = """
|
text = """
|
||||||
|
@ -279,7 +268,7 @@ class FRRRip(FrrService, ConfigService):
|
||||||
"""
|
"""
|
||||||
return self.clean_text(text)
|
return self.clean_text(text)
|
||||||
|
|
||||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@ -288,10 +277,10 @@ class FRRRipng(FrrService, ConfigService):
|
||||||
The RIP NG service provides IPv6 routing for wired networks.
|
The RIP NG service provides IPv6 routing for wired networks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "FRRRIPNG"
|
name: str = "FRRRIPNG"
|
||||||
shutdown = ["killall ripngd"]
|
shutdown: List[str] = ["killall ripngd"]
|
||||||
validate = ["pidof ripngd"]
|
validate: List[str] = ["pidof ripngd"]
|
||||||
ipv6_routing = True
|
ipv6_routing: bool = True
|
||||||
|
|
||||||
def frr_config(self) -> str:
|
def frr_config(self) -> str:
|
||||||
text = """
|
text = """
|
||||||
|
@ -304,7 +293,7 @@ class FRRRipng(FrrService, ConfigService):
|
||||||
"""
|
"""
|
||||||
return self.clean_text(text)
|
return self.clean_text(text)
|
||||||
|
|
||||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@ -314,17 +303,15 @@ class FRRBabel(FrrService, ConfigService):
|
||||||
protocol for IPv6 and IPv4 with fast convergence properties.
|
protocol for IPv6 and IPv4 with fast convergence properties.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "FRRBabel"
|
name: str = "FRRBabel"
|
||||||
shutdown = ["killall babeld"]
|
shutdown: List[str] = ["killall babeld"]
|
||||||
validate = ["pidof babeld"]
|
validate: List[str] = ["pidof babeld"]
|
||||||
ipv6_routing = True
|
ipv6_routing: bool = True
|
||||||
|
|
||||||
def frr_config(self) -> str:
|
def frr_config(self) -> str:
|
||||||
ifnames = []
|
ifnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifnames.append(iface.name)
|
||||||
continue
|
|
||||||
ifnames.append(ifc.name)
|
|
||||||
text = """
|
text = """
|
||||||
router babel
|
router babel
|
||||||
% for ifname in ifnames:
|
% for ifname in ifnames:
|
||||||
|
@ -337,8 +324,8 @@ class FRRBabel(FrrService, ConfigService):
|
||||||
data = dict(ifnames=ifnames)
|
data = dict(ifnames=ifnames)
|
||||||
return self.render_text(text, data)
|
return self.render_text(text, data)
|
||||||
|
|
||||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||||
text = """
|
text = """
|
||||||
babel wireless
|
babel wireless
|
||||||
no babel split-horizon
|
no babel split-horizon
|
||||||
|
@ -356,16 +343,16 @@ class FRRpimd(FrrService, ConfigService):
|
||||||
PIM multicast routing based on XORP.
|
PIM multicast routing based on XORP.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "FRRpimd"
|
name: str = "FRRpimd"
|
||||||
shutdown = ["killall pimd"]
|
shutdown: List[str] = ["killall pimd"]
|
||||||
validate = ["pidof pimd"]
|
validate: List[str] = ["pidof pimd"]
|
||||||
ipv4_routing = True
|
ipv4_routing: bool = True
|
||||||
|
|
||||||
def frr_config(self) -> str:
|
def frr_config(self) -> str:
|
||||||
ifname = "eth0"
|
ifname = "eth0"
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces():
|
||||||
if ifc.name != "lo":
|
if iface.name != "lo":
|
||||||
ifname = ifc.name
|
ifname = iface.name
|
||||||
break
|
break
|
||||||
|
|
||||||
text = f"""
|
text = f"""
|
||||||
|
@ -382,7 +369,7 @@ class FRRpimd(FrrService, ConfigService):
|
||||||
"""
|
"""
|
||||||
return self.clean_text(text)
|
return self.clean_text(text)
|
||||||
|
|
||||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||||
text = """
|
text = """
|
||||||
ip mfea
|
ip mfea
|
||||||
ip igmp
|
ip igmp
|
||||||
|
|
|
@ -20,6 +20,7 @@ nhrpd=yes
|
||||||
eigrpd=yes
|
eigrpd=yes
|
||||||
babeld=yes
|
babeld=yes
|
||||||
sharpd=yes
|
sharpd=yes
|
||||||
|
staticd=yes
|
||||||
pbrd=yes
|
pbrd=yes
|
||||||
bfdd=yes
|
bfdd=yes
|
||||||
fabricd=yes
|
fabricd=yes
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
% for ifc, ip4s, ip6s, is_control in interfaces:
|
% for iface, ip4s, ip6s, is_control in ifaces:
|
||||||
interface ${ifc.name}
|
interface ${iface.name}
|
||||||
% if want_ip4:
|
% if want_ip4:
|
||||||
% for addr in ip4s:
|
% for addr in ip4s:
|
||||||
ip address ${addr}
|
ip address ${addr}
|
||||||
|
@ -12,7 +12,7 @@ interface ${ifc.name}
|
||||||
% endif
|
% endif
|
||||||
% if not is_control:
|
% if not is_control:
|
||||||
% for service in services:
|
% for service in services:
|
||||||
% for line in service.frr_interface_config(ifc).split("\n"):
|
% for line in service.frr_iface_config(iface).split("\n"):
|
||||||
${line}
|
${line}
|
||||||
% endfor
|
% endfor
|
||||||
% endfor
|
% endfor
|
||||||
|
|
|
@ -98,8 +98,8 @@ confcheck
|
||||||
bootfrr
|
bootfrr
|
||||||
|
|
||||||
# reset interfaces
|
# reset interfaces
|
||||||
% for ifc, _, _ , _ in interfaces:
|
% for iface, _, _ , _ in ifaces:
|
||||||
ip link set dev ${ifc.name} down
|
ip link set dev ${iface.name} down
|
||||||
sleep 1
|
sleep 1
|
||||||
ip link set dev ${ifc.name} up
|
ip link set dev ${iface.name} up
|
||||||
% endfor
|
% endfor
|
||||||
|
|
|
@ -1,72 +1,69 @@
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import netaddr
|
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
from core.config import Configuration
|
||||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||||
|
|
||||||
GROUP = "ProtoSvc"
|
GROUP: str = "ProtoSvc"
|
||||||
|
|
||||||
|
|
||||||
class MgenSinkService(ConfigService):
|
class MgenSinkService(ConfigService):
|
||||||
name = "MGEN_Sink"
|
name: str = "MGEN_Sink"
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["mgensink.sh", "sink.mgen"]
|
files: List[str] = ["mgensink.sh", "sink.mgen"]
|
||||||
executables = ["mgen"]
|
executables: List[str] = ["mgen"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh mgensink.sh"]
|
startup: List[str] = ["sh mgensink.sh"]
|
||||||
validate = ["pidof mgen"]
|
validate: List[str] = ["pidof mgen"]
|
||||||
shutdown = ["killall mgen"]
|
shutdown: List[str] = ["killall mgen"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
ifnames = []
|
ifnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces():
|
||||||
name = utils.sysctl_devname(ifc.name)
|
name = utils.sysctl_devname(iface.name)
|
||||||
ifnames.append(name)
|
ifnames.append(name)
|
||||||
return dict(ifnames=ifnames)
|
return dict(ifnames=ifnames)
|
||||||
|
|
||||||
|
|
||||||
class NrlNhdp(ConfigService):
|
class NrlNhdp(ConfigService):
|
||||||
name = "NHDP"
|
name: str = "NHDP"
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["nrlnhdp.sh"]
|
files: List[str] = ["nrlnhdp.sh"]
|
||||||
executables = ["nrlnhdp"]
|
executables: List[str] = ["nrlnhdp"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh nrlnhdp.sh"]
|
startup: List[str] = ["sh nrlnhdp.sh"]
|
||||||
validate = ["pidof nrlnhdp"]
|
validate: List[str] = ["pidof nrlnhdp"]
|
||||||
shutdown = ["killall nrlnhdp"]
|
shutdown: List[str] = ["killall nrlnhdp"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
has_smf = "SMF" in self.node.config_services
|
has_smf = "SMF" in self.node.config_services
|
||||||
ifnames = []
|
ifnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifnames.append(iface.name)
|
||||||
continue
|
|
||||||
ifnames.append(ifc.name)
|
|
||||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||||
|
|
||||||
|
|
||||||
class NrlSmf(ConfigService):
|
class NrlSmf(ConfigService):
|
||||||
name = "SMF"
|
name: str = "SMF"
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["startsmf.sh"]
|
files: List[str] = ["startsmf.sh"]
|
||||||
executables = ["nrlsmf", "killall"]
|
executables: List[str] = ["nrlsmf", "killall"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh startsmf.sh"]
|
startup: List[str] = ["sh startsmf.sh"]
|
||||||
validate = ["pidof nrlsmf"]
|
validate: List[str] = ["pidof nrlsmf"]
|
||||||
shutdown = ["killall nrlsmf"]
|
shutdown: List[str] = ["killall nrlsmf"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
has_arouted = "arouted" in self.node.config_services
|
has_arouted = "arouted" in self.node.config_services
|
||||||
|
@ -74,17 +71,12 @@ class NrlSmf(ConfigService):
|
||||||
has_olsr = "OLSR" in self.node.config_services
|
has_olsr = "OLSR" in self.node.config_services
|
||||||
ifnames = []
|
ifnames = []
|
||||||
ip4_prefix = None
|
ip4_prefix = None
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifnames.append(iface.name)
|
||||||
continue
|
ip4 = iface.get_ip4()
|
||||||
ifnames.append(ifc.name)
|
if ip4:
|
||||||
if ip4_prefix:
|
ip4_prefix = f"{ip4.ip}/{24}"
|
||||||
continue
|
break
|
||||||
for a in ifc.addrlist:
|
|
||||||
a = a.split("/")[0]
|
|
||||||
if netaddr.valid_ipv4(a):
|
|
||||||
ip4_prefix = f"{a}/{24}"
|
|
||||||
break
|
|
||||||
return dict(
|
return dict(
|
||||||
has_arouted=has_arouted,
|
has_arouted=has_arouted,
|
||||||
has_nhdp=has_nhdp,
|
has_nhdp=has_nhdp,
|
||||||
|
@ -95,118 +87,107 @@ class NrlSmf(ConfigService):
|
||||||
|
|
||||||
|
|
||||||
class NrlOlsr(ConfigService):
|
class NrlOlsr(ConfigService):
|
||||||
name = "OLSR"
|
name: str = "OLSR"
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["nrlolsrd.sh"]
|
files: List[str] = ["nrlolsrd.sh"]
|
||||||
executables = ["nrlolsrd"]
|
executables: List[str] = ["nrlolsrd"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh nrlolsrd.sh"]
|
startup: List[str] = ["sh nrlolsrd.sh"]
|
||||||
validate = ["pidof nrlolsrd"]
|
validate: List[str] = ["pidof nrlolsrd"]
|
||||||
shutdown = ["killall nrlolsrd"]
|
shutdown: List[str] = ["killall nrlolsrd"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
has_smf = "SMF" in self.node.config_services
|
has_smf = "SMF" in self.node.config_services
|
||||||
has_zebra = "zebra" in self.node.config_services
|
has_zebra = "zebra" in self.node.config_services
|
||||||
ifname = None
|
ifname = None
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifname = iface.name
|
||||||
continue
|
|
||||||
ifname = ifc.name
|
|
||||||
break
|
break
|
||||||
return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname)
|
return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname)
|
||||||
|
|
||||||
|
|
||||||
class NrlOlsrv2(ConfigService):
|
class NrlOlsrv2(ConfigService):
|
||||||
name = "OLSRv2"
|
name: str = "OLSRv2"
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["nrlolsrv2.sh"]
|
files: List[str] = ["nrlolsrv2.sh"]
|
||||||
executables = ["nrlolsrv2"]
|
executables: List[str] = ["nrlolsrv2"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh nrlolsrv2.sh"]
|
startup: List[str] = ["sh nrlolsrv2.sh"]
|
||||||
validate = ["pidof nrlolsrv2"]
|
validate: List[str] = ["pidof nrlolsrv2"]
|
||||||
shutdown = ["killall nrlolsrv2"]
|
shutdown: List[str] = ["killall nrlolsrv2"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
has_smf = "SMF" in self.node.config_services
|
has_smf = "SMF" in self.node.config_services
|
||||||
ifnames = []
|
ifnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifnames.append(iface.name)
|
||||||
continue
|
|
||||||
ifnames.append(ifc.name)
|
|
||||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||||
|
|
||||||
|
|
||||||
class OlsrOrg(ConfigService):
|
class OlsrOrg(ConfigService):
|
||||||
name = "OLSRORG"
|
name: str = "OLSRORG"
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = ["/etc/olsrd"]
|
directories: List[str] = ["/etc/olsrd"]
|
||||||
files = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
|
files: List[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
|
||||||
executables = ["olsrd"]
|
executables: List[str] = ["olsrd"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh olsrd.sh"]
|
startup: List[str] = ["sh olsrd.sh"]
|
||||||
validate = ["pidof olsrd"]
|
validate: List[str] = ["pidof olsrd"]
|
||||||
shutdown = ["killall olsrd"]
|
shutdown: List[str] = ["killall olsrd"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
has_smf = "SMF" in self.node.config_services
|
has_smf = "SMF" in self.node.config_services
|
||||||
ifnames = []
|
ifnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifnames.append(iface.name)
|
||||||
continue
|
|
||||||
ifnames.append(ifc.name)
|
|
||||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||||
|
|
||||||
|
|
||||||
class MgenActor(ConfigService):
|
class MgenActor(ConfigService):
|
||||||
name = "MgenActor"
|
name: str = "MgenActor"
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["start_mgen_actor.sh"]
|
files: List[str] = ["start_mgen_actor.sh"]
|
||||||
executables = ["mgen"]
|
executables: List[str] = ["mgen"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh start_mgen_actor.sh"]
|
startup: List[str] = ["sh start_mgen_actor.sh"]
|
||||||
validate = ["pidof mgen"]
|
validate: List[str] = ["pidof mgen"]
|
||||||
shutdown = ["killall mgen"]
|
shutdown: List[str] = ["killall mgen"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
|
||||||
class Arouted(ConfigService):
|
class Arouted(ConfigService):
|
||||||
name = "arouted"
|
name: str = "arouted"
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["startarouted.sh"]
|
files: List[str] = ["startarouted.sh"]
|
||||||
executables = ["arouted"]
|
executables: List[str] = ["arouted"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh startarouted.sh"]
|
startup: List[str] = ["sh startarouted.sh"]
|
||||||
validate = ["pidof arouted"]
|
validate: List[str] = ["pidof arouted"]
|
||||||
shutdown = ["pkill arouted"]
|
shutdown: List[str] = ["pkill arouted"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
ip4_prefix = None
|
ip4_prefix = None
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ip4 = iface.get_ip4()
|
||||||
continue
|
if ip4:
|
||||||
if ip4_prefix:
|
ip4_prefix = f"{ip4.ip}/{24}"
|
||||||
continue
|
break
|
||||||
for a in ifc.addrlist:
|
|
||||||
a = a.split("/")[0]
|
|
||||||
if netaddr.valid_ipv4(a):
|
|
||||||
ip4_prefix = f"{a}/{24}"
|
|
||||||
break
|
|
||||||
return dict(ip4_prefix=ip4_prefix)
|
return dict(ip4_prefix=ip4_prefix)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<%
|
<%
|
||||||
interfaces = "-i " + " -i ".join(ifnames)
|
ifaces = "-i " + " -i ".join(ifnames)
|
||||||
smf = ""
|
smf = ""
|
||||||
if has_smf:
|
if has_smf:
|
||||||
smf = "-flooding ecds -smfClient %s_smf" % node.name
|
smf = "-flooding ecds -smfClient %s_smf" % node.name
|
||||||
%>
|
%>
|
||||||
nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${interfaces}
|
nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${ifaces}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<%
|
<%
|
||||||
interfaces = "-i " + " -i ".join(ifnames)
|
ifaces = "-i " + " -i ".join(ifnames)
|
||||||
smf = ""
|
smf = ""
|
||||||
if has_smf:
|
if has_smf:
|
||||||
smf = "-flooding ecds -smfClient %s_smf" % node.name
|
smf = "-flooding ecds -smfClient %s_smf" % node.name
|
||||||
%>
|
%>
|
||||||
nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${interfaces}
|
nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${ifaces}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<%
|
<%
|
||||||
interfaces = "-i " + " -i ".join(ifnames)
|
ifaces = "-i " + " -i ".join(ifnames)
|
||||||
%>
|
%>
|
||||||
olsrd ${interfaces}
|
olsrd ${ifaces}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<%
|
<%
|
||||||
interfaces = ",".join(ifnames)
|
ifaces = ",".join(ifnames)
|
||||||
arouted = ""
|
arouted = ""
|
||||||
if has_arouted:
|
if has_arouted:
|
||||||
arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0])
|
arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0])
|
||||||
|
@ -12,4 +12,4 @@
|
||||||
%>
|
%>
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# auto-generated by NrlSmf service
|
# auto-generated by NrlSmf service
|
||||||
nrlsmf instance ${node.name}_smf ${interfaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
|
nrlsmf instance ${node.name}_smf ${ifaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
|
||||||
|
|
|
@ -1,46 +1,45 @@
|
||||||
import abc
|
import abc
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import netaddr
|
from core.config import Configuration
|
||||||
|
|
||||||
from core import constants
|
|
||||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.nodes.base import CoreNodeBase
|
from core.nodes.base import CoreNodeBase
|
||||||
from core.nodes.interface import CoreInterface
|
from core.nodes.interface import CoreInterface
|
||||||
from core.nodes.network import WlanNode
|
from core.nodes.network import WlanNode
|
||||||
|
|
||||||
GROUP = "Quagga"
|
GROUP: str = "Quagga"
|
||||||
|
QUAGGA_STATE_DIR: str = "/var/run/quagga"
|
||||||
|
|
||||||
|
|
||||||
def has_mtu_mismatch(ifc: CoreInterface) -> bool:
|
def has_mtu_mismatch(iface: CoreInterface) -> bool:
|
||||||
"""
|
"""
|
||||||
Helper to detect MTU mismatch and add the appropriate OSPF
|
Helper to detect MTU mismatch and add the appropriate OSPF
|
||||||
mtu-ignore command. This is needed when e.g. a node is linked via a
|
mtu-ignore command. This is needed when e.g. a node is linked via a
|
||||||
GreTap device.
|
GreTap device.
|
||||||
"""
|
"""
|
||||||
if ifc.mtu != 1500:
|
if iface.mtu != 1500:
|
||||||
return True
|
return True
|
||||||
if not ifc.net:
|
if not iface.net:
|
||||||
return False
|
return False
|
||||||
for i in ifc.net.netifs():
|
for iface in iface.net.get_ifaces():
|
||||||
if i.mtu != ifc.mtu:
|
if iface.mtu != iface.mtu:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_min_mtu(ifc):
|
def get_min_mtu(iface: CoreInterface):
|
||||||
"""
|
"""
|
||||||
Helper to discover the minimum MTU of interfaces linked with the
|
Helper to discover the minimum MTU of interfaces linked with the
|
||||||
given interface.
|
given interface.
|
||||||
"""
|
"""
|
||||||
mtu = ifc.mtu
|
mtu = iface.mtu
|
||||||
if not ifc.net:
|
if not iface.net:
|
||||||
return mtu
|
return mtu
|
||||||
for i in ifc.net.netifs():
|
for iface in iface.net.get_ifaces():
|
||||||
if i.mtu < mtu:
|
if iface.mtu < mtu:
|
||||||
mtu = i.mtu
|
mtu = iface.mtu
|
||||||
return mtu
|
return mtu
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,33 +47,30 @@ def get_router_id(node: CoreNodeBase) -> str:
|
||||||
"""
|
"""
|
||||||
Helper to return the first IPv4 address of a node as its router ID.
|
Helper to return the first IPv4 address of a node as its router ID.
|
||||||
"""
|
"""
|
||||||
for ifc in node.netifs():
|
for iface in node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ip4 = iface.get_ip4()
|
||||||
continue
|
if ip4:
|
||||||
for a in ifc.addrlist:
|
return str(ip4.ip)
|
||||||
a = a.split("/")[0]
|
|
||||||
if netaddr.valid_ipv4(a):
|
|
||||||
return a
|
|
||||||
return "0.0.0.0"
|
return "0.0.0.0"
|
||||||
|
|
||||||
|
|
||||||
class Zebra(ConfigService):
|
class Zebra(ConfigService):
|
||||||
name = "zebra"
|
name: str = "zebra"
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = ["/usr/local/etc/quagga", "/var/run/quagga"]
|
directories: List[str] = ["/usr/local/etc/quagga", "/var/run/quagga"]
|
||||||
files = [
|
files: List[str] = [
|
||||||
"/usr/local/etc/quagga/Quagga.conf",
|
"/usr/local/etc/quagga/Quagga.conf",
|
||||||
"quaggaboot.sh",
|
"quaggaboot.sh",
|
||||||
"/usr/local/etc/quagga/vtysh.conf",
|
"/usr/local/etc/quagga/vtysh.conf",
|
||||||
]
|
]
|
||||||
executables = ["zebra"]
|
executables: List[str] = ["zebra"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh quaggaboot.sh zebra"]
|
startup: List[str] = ["sh quaggaboot.sh zebra"]
|
||||||
validate = ["pidof zebra"]
|
validate: List[str] = ["pidof zebra"]
|
||||||
shutdown = ["killall zebra"]
|
shutdown: List[str] = ["killall zebra"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
quagga_bin_search = self.node.session.options.get_config(
|
quagga_bin_search = self.node.session.options.get_config(
|
||||||
|
@ -83,7 +79,7 @@ class Zebra(ConfigService):
|
||||||
quagga_sbin_search = self.node.session.options.get_config(
|
quagga_sbin_search = self.node.session.options.get_config(
|
||||||
"quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga"
|
"quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga"
|
||||||
).strip('"')
|
).strip('"')
|
||||||
quagga_state_dir = constants.QUAGGA_STATE_DIR
|
quagga_state_dir = QUAGGA_STATE_DIR
|
||||||
quagga_conf = self.files[0]
|
quagga_conf = self.files[0]
|
||||||
|
|
||||||
services = []
|
services = []
|
||||||
|
@ -92,31 +88,31 @@ class Zebra(ConfigService):
|
||||||
for service in self.node.config_services.values():
|
for service in self.node.config_services.values():
|
||||||
if self.name not in service.dependencies:
|
if self.name not in service.dependencies:
|
||||||
continue
|
continue
|
||||||
|
if not isinstance(service, QuaggaService):
|
||||||
|
continue
|
||||||
if service.ipv4_routing:
|
if service.ipv4_routing:
|
||||||
want_ip4 = True
|
want_ip4 = True
|
||||||
if service.ipv6_routing:
|
if service.ipv6_routing:
|
||||||
want_ip6 = True
|
want_ip6 = True
|
||||||
services.append(service)
|
services.append(service)
|
||||||
|
|
||||||
interfaces = []
|
ifaces = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces():
|
||||||
ip4s = []
|
ip4s = []
|
||||||
ip6s = []
|
ip6s = []
|
||||||
for x in ifc.addrlist:
|
for ip4 in iface.ip4s:
|
||||||
addr = x.split("/")[0]
|
ip4s.append(str(ip4.ip))
|
||||||
if netaddr.valid_ipv4(addr):
|
for ip6 in iface.ip6s:
|
||||||
ip4s.append(x)
|
ip6s.append(str(ip6.ip))
|
||||||
else:
|
is_control = getattr(iface, "control", False)
|
||||||
ip6s.append(x)
|
ifaces.append((iface, ip4s, ip6s, is_control))
|
||||||
is_control = getattr(ifc, "control", False)
|
|
||||||
interfaces.append((ifc, ip4s, ip6s, is_control))
|
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
quagga_bin_search=quagga_bin_search,
|
quagga_bin_search=quagga_bin_search,
|
||||||
quagga_sbin_search=quagga_sbin_search,
|
quagga_sbin_search=quagga_sbin_search,
|
||||||
quagga_state_dir=quagga_state_dir,
|
quagga_state_dir=quagga_state_dir,
|
||||||
quagga_conf=quagga_conf,
|
quagga_conf=quagga_conf,
|
||||||
interfaces=interfaces,
|
ifaces=ifaces,
|
||||||
want_ip4=want_ip4,
|
want_ip4=want_ip4,
|
||||||
want_ip6=want_ip6,
|
want_ip6=want_ip6,
|
||||||
services=services,
|
services=services,
|
||||||
|
@ -124,22 +120,22 @@ class Zebra(ConfigService):
|
||||||
|
|
||||||
|
|
||||||
class QuaggaService(abc.ABC):
|
class QuaggaService(abc.ABC):
|
||||||
group = GROUP
|
group: str = GROUP
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = []
|
files: List[str] = []
|
||||||
executables = []
|
executables: List[str] = []
|
||||||
dependencies = ["zebra"]
|
dependencies: List[str] = ["zebra"]
|
||||||
startup = []
|
startup: List[str] = []
|
||||||
validate = []
|
validate: List[str] = []
|
||||||
shutdown = []
|
shutdown: List[str] = []
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
ipv4_routing = False
|
ipv4_routing: bool = False
|
||||||
ipv6_routing = False
|
ipv6_routing: bool = False
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -154,13 +150,13 @@ class Ospfv2(QuaggaService, ConfigService):
|
||||||
unified Quagga.conf file.
|
unified Quagga.conf file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "OSPFv2"
|
name: str = "OSPFv2"
|
||||||
validate = ["pidof ospfd"]
|
validate: List[str] = ["pidof ospfd"]
|
||||||
shutdown = ["killall ospfd"]
|
shutdown: List[str] = ["killall ospfd"]
|
||||||
ipv4_routing = True
|
ipv4_routing: bool = True
|
||||||
|
|
||||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
if has_mtu_mismatch(ifc):
|
if has_mtu_mismatch(iface):
|
||||||
return "ip ospf mtu-ignore"
|
return "ip ospf mtu-ignore"
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
@ -168,13 +164,9 @@ class Ospfv2(QuaggaService, ConfigService):
|
||||||
def quagga_config(self) -> str:
|
def quagga_config(self) -> str:
|
||||||
router_id = get_router_id(self.node)
|
router_id = get_router_id(self.node)
|
||||||
addresses = []
|
addresses = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
for ip4 in iface.ip4s:
|
||||||
continue
|
addresses.append(str(ip4.ip))
|
||||||
for a in ifc.addrlist:
|
|
||||||
addr = a.split("/")[0]
|
|
||||||
if netaddr.valid_ipv4(addr):
|
|
||||||
addresses.append(a)
|
|
||||||
data = dict(router_id=router_id, addresses=addresses)
|
data = dict(router_id=router_id, addresses=addresses)
|
||||||
text = """
|
text = """
|
||||||
router ospf
|
router ospf
|
||||||
|
@ -194,15 +186,15 @@ class Ospfv3(QuaggaService, ConfigService):
|
||||||
unified Quagga.conf file.
|
unified Quagga.conf file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "OSPFv3"
|
name: str = "OSPFv3"
|
||||||
shutdown = ("killall ospf6d",)
|
shutdown: List[str] = ["killall ospf6d"]
|
||||||
validate = ("pidof ospf6d",)
|
validate: List[str] = ["pidof ospf6d"]
|
||||||
ipv4_routing = True
|
ipv4_routing: bool = True
|
||||||
ipv6_routing = True
|
ipv6_routing: bool = True
|
||||||
|
|
||||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
mtu = get_min_mtu(ifc)
|
mtu = get_min_mtu(iface)
|
||||||
if mtu < ifc.mtu:
|
if mtu < iface.mtu:
|
||||||
return f"ipv6 ospf6 ifmtu {mtu}"
|
return f"ipv6 ospf6 ifmtu {mtu}"
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
@ -210,10 +202,8 @@ class Ospfv3(QuaggaService, ConfigService):
|
||||||
def quagga_config(self) -> str:
|
def quagga_config(self) -> str:
|
||||||
router_id = get_router_id(self.node)
|
router_id = get_router_id(self.node)
|
||||||
ifnames = []
|
ifnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifnames.append(iface.name)
|
||||||
continue
|
|
||||||
ifnames.append(ifc.name)
|
|
||||||
data = dict(router_id=router_id, ifnames=ifnames)
|
data = dict(router_id=router_id, ifnames=ifnames)
|
||||||
text = """
|
text = """
|
||||||
router ospf6
|
router ospf6
|
||||||
|
@ -235,17 +225,17 @@ class Ospfv3mdr(Ospfv3):
|
||||||
unified Quagga.conf file.
|
unified Quagga.conf file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "OSPFv3MDR"
|
name: str = "OSPFv3MDR"
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces():
|
||||||
is_wireless = isinstance(ifc.net, (WlanNode, EmaneNet))
|
is_wireless = isinstance(iface.net, (WlanNode, EmaneNet))
|
||||||
logging.info("MDR wireless: %s", is_wireless)
|
logging.info("MDR wireless: %s", is_wireless)
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
config = super().quagga_interface_config(ifc)
|
config = super().quagga_iface_config(iface)
|
||||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||||
config = self.clean_text(
|
config = self.clean_text(
|
||||||
f"""
|
f"""
|
||||||
{config}
|
{config}
|
||||||
|
@ -268,16 +258,16 @@ class Bgp(QuaggaService, ConfigService):
|
||||||
having the same AS number.
|
having the same AS number.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "BGP"
|
name: str = "BGP"
|
||||||
shutdown = ["killall bgpd"]
|
shutdown: List[str] = ["killall bgpd"]
|
||||||
validate = ["pidof bgpd"]
|
validate: List[str] = ["pidof bgpd"]
|
||||||
ipv4_routing = True
|
ipv4_routing: bool = True
|
||||||
ipv6_routing = True
|
ipv6_routing: bool = True
|
||||||
|
|
||||||
def quagga_config(self) -> str:
|
def quagga_config(self) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
router_id = get_router_id(self.node)
|
router_id = get_router_id(self.node)
|
||||||
text = f"""
|
text = f"""
|
||||||
! BGP configuration
|
! BGP configuration
|
||||||
|
@ -297,10 +287,10 @@ class Rip(QuaggaService, ConfigService):
|
||||||
The RIP service provides IPv4 routing for wired networks.
|
The RIP service provides IPv4 routing for wired networks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "RIP"
|
name: str = "RIP"
|
||||||
shutdown = ["killall ripd"]
|
shutdown: List[str] = ["killall ripd"]
|
||||||
validate = ["pidof ripd"]
|
validate: List[str] = ["pidof ripd"]
|
||||||
ipv4_routing = True
|
ipv4_routing: bool = True
|
||||||
|
|
||||||
def quagga_config(self) -> str:
|
def quagga_config(self) -> str:
|
||||||
text = """
|
text = """
|
||||||
|
@ -313,7 +303,7 @@ class Rip(QuaggaService, ConfigService):
|
||||||
"""
|
"""
|
||||||
return self.clean_text(text)
|
return self.clean_text(text)
|
||||||
|
|
||||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@ -322,10 +312,10 @@ class Ripng(QuaggaService, ConfigService):
|
||||||
The RIP NG service provides IPv6 routing for wired networks.
|
The RIP NG service provides IPv6 routing for wired networks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "RIPNG"
|
name: str = "RIPNG"
|
||||||
shutdown = ["killall ripngd"]
|
shutdown: List[str] = ["killall ripngd"]
|
||||||
validate = ["pidof ripngd"]
|
validate: List[str] = ["pidof ripngd"]
|
||||||
ipv6_routing = True
|
ipv6_routing: bool = True
|
||||||
|
|
||||||
def quagga_config(self) -> str:
|
def quagga_config(self) -> str:
|
||||||
text = """
|
text = """
|
||||||
|
@ -338,7 +328,7 @@ class Ripng(QuaggaService, ConfigService):
|
||||||
"""
|
"""
|
||||||
return self.clean_text(text)
|
return self.clean_text(text)
|
||||||
|
|
||||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@ -348,17 +338,15 @@ class Babel(QuaggaService, ConfigService):
|
||||||
protocol for IPv6 and IPv4 with fast convergence properties.
|
protocol for IPv6 and IPv4 with fast convergence properties.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Babel"
|
name: str = "Babel"
|
||||||
shutdown = ["killall babeld"]
|
shutdown: List[str] = ["killall babeld"]
|
||||||
validate = ["pidof babeld"]
|
validate: List[str] = ["pidof babeld"]
|
||||||
ipv6_routing = True
|
ipv6_routing: bool = True
|
||||||
|
|
||||||
def quagga_config(self) -> str:
|
def quagga_config(self) -> str:
|
||||||
ifnames = []
|
ifnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifnames.append(iface.name)
|
||||||
continue
|
|
||||||
ifnames.append(ifc.name)
|
|
||||||
text = """
|
text = """
|
||||||
router babel
|
router babel
|
||||||
% for ifname in ifnames:
|
% for ifname in ifnames:
|
||||||
|
@ -371,8 +359,8 @@ class Babel(QuaggaService, ConfigService):
|
||||||
data = dict(ifnames=ifnames)
|
data = dict(ifnames=ifnames)
|
||||||
return self.render_text(text, data)
|
return self.render_text(text, data)
|
||||||
|
|
||||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||||
text = """
|
text = """
|
||||||
babel wireless
|
babel wireless
|
||||||
no babel split-horizon
|
no babel split-horizon
|
||||||
|
@ -390,16 +378,16 @@ class Xpimd(QuaggaService, ConfigService):
|
||||||
PIM multicast routing based on XORP.
|
PIM multicast routing based on XORP.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Xpimd"
|
name: str = "Xpimd"
|
||||||
shutdown = ["killall xpimd"]
|
shutdown: List[str] = ["killall xpimd"]
|
||||||
validate = ["pidof xpimd"]
|
validate: List[str] = ["pidof xpimd"]
|
||||||
ipv4_routing = True
|
ipv4_routing: bool = True
|
||||||
|
|
||||||
def quagga_config(self) -> str:
|
def quagga_config(self) -> str:
|
||||||
ifname = "eth0"
|
ifname = "eth0"
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces():
|
||||||
if ifc.name != "lo":
|
if iface.name != "lo":
|
||||||
ifname = ifc.name
|
ifname = iface.name
|
||||||
break
|
break
|
||||||
|
|
||||||
text = f"""
|
text = f"""
|
||||||
|
@ -416,7 +404,7 @@ class Xpimd(QuaggaService, ConfigService):
|
||||||
"""
|
"""
|
||||||
return self.clean_text(text)
|
return self.clean_text(text)
|
||||||
|
|
||||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||||
text = """
|
text = """
|
||||||
ip mfea
|
ip mfea
|
||||||
ip pim
|
ip pim
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
% for ifc, ip4s, ip6s, is_control in interfaces:
|
% for iface, ip4s, ip6s, is_control in ifaces:
|
||||||
interface ${ifc.name}
|
interface ${iface.name}
|
||||||
% if want_ip4:
|
% if want_ip4:
|
||||||
% for addr in ip4s:
|
% for addr in ip4s:
|
||||||
ip address ${addr}
|
ip address ${addr}
|
||||||
|
@ -12,7 +12,7 @@ interface ${ifc.name}
|
||||||
% endif
|
% endif
|
||||||
% if not is_control:
|
% if not is_control:
|
||||||
% for service in services:
|
% for service in services:
|
||||||
% for line in service.quagga_interface_config(ifc).split("\n"):
|
% for line in service.quagga_iface_config(iface).split("\n"):
|
||||||
${line}
|
${line}
|
||||||
% endfor
|
% endfor
|
||||||
% endfor
|
% endfor
|
||||||
|
|
135
daemon/core/configservices/securityservices/services.py
Normal file
135
daemon/core/configservices/securityservices/services.py
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from core.config import Configuration
|
||||||
|
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||||
|
from core.emulator.enumerations import ConfigDataTypes
|
||||||
|
|
||||||
|
GROUP_NAME: str = "Security"
|
||||||
|
|
||||||
|
|
||||||
|
class VpnClient(ConfigService):
|
||||||
|
name: str = "VPNClient"
|
||||||
|
group: str = GROUP_NAME
|
||||||
|
directories: List[str] = []
|
||||||
|
files: List[str] = ["vpnclient.sh"]
|
||||||
|
executables: List[str] = ["openvpn", "ip", "killall"]
|
||||||
|
dependencies: List[str] = []
|
||||||
|
startup: List[str] = ["sh vpnclient.sh"]
|
||||||
|
validate: List[str] = ["pidof openvpn"]
|
||||||
|
shutdown: List[str] = ["killall openvpn"]
|
||||||
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
|
default_configs: List[Configuration] = [
|
||||||
|
Configuration(
|
||||||
|
_id="keydir",
|
||||||
|
_type=ConfigDataTypes.STRING,
|
||||||
|
label="Key Dir",
|
||||||
|
default="/etc/core/keys",
|
||||||
|
),
|
||||||
|
Configuration(
|
||||||
|
_id="keyname",
|
||||||
|
_type=ConfigDataTypes.STRING,
|
||||||
|
label="Key Name",
|
||||||
|
default="client1",
|
||||||
|
),
|
||||||
|
Configuration(
|
||||||
|
_id="server",
|
||||||
|
_type=ConfigDataTypes.STRING,
|
||||||
|
label="Server",
|
||||||
|
default="10.0.2.10",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class VpnServer(ConfigService):
|
||||||
|
name: str = "VPNServer"
|
||||||
|
group: str = GROUP_NAME
|
||||||
|
directories: List[str] = []
|
||||||
|
files: List[str] = ["vpnserver.sh"]
|
||||||
|
executables: List[str] = ["openvpn", "ip", "killall"]
|
||||||
|
dependencies: List[str] = []
|
||||||
|
startup: List[str] = ["sh vpnserver.sh"]
|
||||||
|
validate: List[str] = ["pidof openvpn"]
|
||||||
|
shutdown: List[str] = ["killall openvpn"]
|
||||||
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
|
default_configs: List[Configuration] = [
|
||||||
|
Configuration(
|
||||||
|
_id="keydir",
|
||||||
|
_type=ConfigDataTypes.STRING,
|
||||||
|
label="Key Dir",
|
||||||
|
default="/etc/core/keys",
|
||||||
|
),
|
||||||
|
Configuration(
|
||||||
|
_id="keyname",
|
||||||
|
_type=ConfigDataTypes.STRING,
|
||||||
|
label="Key Name",
|
||||||
|
default="server",
|
||||||
|
),
|
||||||
|
Configuration(
|
||||||
|
_id="subnet",
|
||||||
|
_type=ConfigDataTypes.STRING,
|
||||||
|
label="Subnet",
|
||||||
|
default="10.0.200.0",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
def data(self) -> Dict[str, Any]:
|
||||||
|
address = None
|
||||||
|
for iface in self.node.get_ifaces(control=False):
|
||||||
|
ip4 = iface.get_ip4()
|
||||||
|
if ip4:
|
||||||
|
address = str(ip4.ip)
|
||||||
|
break
|
||||||
|
return dict(address=address)
|
||||||
|
|
||||||
|
|
||||||
|
class IPsec(ConfigService):
|
||||||
|
name: str = "IPsec"
|
||||||
|
group: str = GROUP_NAME
|
||||||
|
directories: List[str] = []
|
||||||
|
files: List[str] = ["ipsec.sh"]
|
||||||
|
executables: List[str] = ["racoon", "ip", "setkey", "killall"]
|
||||||
|
dependencies: List[str] = []
|
||||||
|
startup: List[str] = ["sh ipsec.sh"]
|
||||||
|
validate: List[str] = ["pidof racoon"]
|
||||||
|
shutdown: List[str] = ["killall racoon"]
|
||||||
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
|
default_configs: List[Configuration] = []
|
||||||
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Firewall(ConfigService):
|
||||||
|
name: str = "Firewall"
|
||||||
|
group: str = GROUP_NAME
|
||||||
|
directories: List[str] = []
|
||||||
|
files: List[str] = ["firewall.sh"]
|
||||||
|
executables: List[str] = ["iptables"]
|
||||||
|
dependencies: List[str] = []
|
||||||
|
startup: List[str] = ["sh firewall.sh"]
|
||||||
|
validate: List[str] = []
|
||||||
|
shutdown: List[str] = []
|
||||||
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
|
default_configs: List[Configuration] = []
|
||||||
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Nat(ConfigService):
|
||||||
|
name: str = "NAT"
|
||||||
|
group: str = GROUP_NAME
|
||||||
|
directories: List[str] = []
|
||||||
|
files: List[str] = ["nat.sh"]
|
||||||
|
executables: List[str] = ["iptables"]
|
||||||
|
dependencies: List[str] = []
|
||||||
|
startup: List[str] = ["sh nat.sh"]
|
||||||
|
validate: List[str] = []
|
||||||
|
shutdown: List[str] = []
|
||||||
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
|
default_configs: List[Configuration] = []
|
||||||
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
def data(self) -> Dict[str, Any]:
|
||||||
|
ifnames = []
|
||||||
|
for iface in self.node.get_ifaces(control=False):
|
||||||
|
ifnames.append(iface.name)
|
||||||
|
return dict(ifnames=ifnames)
|
|
@ -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)
|
|
|
@ -1,20 +1,22 @@
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
from core.config import Configuration
|
from core.config import Configuration
|
||||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||||
from core.emulator.enumerations import ConfigDataTypes
|
from core.emulator.enumerations import ConfigDataTypes
|
||||||
|
|
||||||
|
|
||||||
class SimpleService(ConfigService):
|
class SimpleService(ConfigService):
|
||||||
name = "Simple"
|
name: str = "Simple"
|
||||||
group = "SimpleGroup"
|
group: str = "SimpleGroup"
|
||||||
directories = ["/etc/quagga", "/usr/local/lib"]
|
directories: List[str] = ["/etc/quagga", "/usr/local/lib"]
|
||||||
files = ["test1.sh", "test2.sh"]
|
files: List[str] = ["test1.sh", "test2.sh"]
|
||||||
executables = []
|
executables: List[str] = []
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = []
|
startup: List[str] = []
|
||||||
validate = []
|
validate: List[str] = []
|
||||||
shutdown = []
|
shutdown: List[str] = []
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = [
|
default_configs: List[Configuration] = [
|
||||||
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
|
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
|
||||||
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
|
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
|
||||||
Configuration(
|
Configuration(
|
||||||
|
@ -24,7 +26,7 @@ class SimpleService(ConfigService):
|
||||||
options=["value1", "value2", "value3"],
|
options=["value1", "value2", "value3"],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
modes = {
|
modes: Dict[str, Dict[str, str]] = {
|
||||||
"mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
|
"mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
|
||||||
"mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
|
"mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
|
||||||
"mode3": {"value1": "value3", "value2": "0", "value3": "value1"},
|
"mode3": {"value1": "value3", "value2": "0", "value3": "value1"},
|
||||||
|
|
|
@ -1,35 +1,36 @@
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
from core.config import Configuration
|
||||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||||
|
|
||||||
GROUP_NAME = "Utility"
|
GROUP_NAME = "Utility"
|
||||||
|
|
||||||
|
|
||||||
class DefaultRouteService(ConfigService):
|
class DefaultRouteService(ConfigService):
|
||||||
name = "DefaultRoute"
|
name: str = "DefaultRoute"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["defaultroute.sh"]
|
files: List[str] = ["defaultroute.sh"]
|
||||||
executables = ["ip"]
|
executables: List[str] = ["ip"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh defaultroute.sh"]
|
startup: List[str] = ["sh defaultroute.sh"]
|
||||||
validate = []
|
validate: List[str] = []
|
||||||
shutdown = []
|
shutdown: List[str] = []
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
# only add default routes for linked routing nodes
|
# only add default routes for linked routing nodes
|
||||||
routes = []
|
routes = []
|
||||||
netifs = self.node.netifs(sort=True)
|
ifaces = self.node.get_ifaces()
|
||||||
if netifs:
|
if ifaces:
|
||||||
netif = netifs[0]
|
iface = ifaces[0]
|
||||||
for x in netif.addrlist:
|
for ip in iface.ips():
|
||||||
net = netaddr.IPNetwork(x).cidr
|
net = ip.cidr
|
||||||
if net.size > 1:
|
if net.size > 1:
|
||||||
router = net[1]
|
router = net[1]
|
||||||
routes.append(str(router))
|
routes.append(str(router))
|
||||||
|
@ -37,95 +38,90 @@ class DefaultRouteService(ConfigService):
|
||||||
|
|
||||||
|
|
||||||
class DefaultMulticastRouteService(ConfigService):
|
class DefaultMulticastRouteService(ConfigService):
|
||||||
name = "DefaultMulticastRoute"
|
name: str = "DefaultMulticastRoute"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["defaultmroute.sh"]
|
files: List[str] = ["defaultmroute.sh"]
|
||||||
executables = []
|
executables: List[str] = []
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh defaultmroute.sh"]
|
startup: List[str] = ["sh defaultmroute.sh"]
|
||||||
validate = []
|
validate: List[str] = []
|
||||||
shutdown = []
|
shutdown: List[str] = []
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
ifname = None
|
ifname = None
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifname = iface.name
|
||||||
continue
|
|
||||||
ifname = ifc.name
|
|
||||||
break
|
break
|
||||||
return dict(ifname=ifname)
|
return dict(ifname=ifname)
|
||||||
|
|
||||||
|
|
||||||
class StaticRouteService(ConfigService):
|
class StaticRouteService(ConfigService):
|
||||||
name = "StaticRoute"
|
name: str = "StaticRoute"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["staticroute.sh"]
|
files: List[str] = ["staticroute.sh"]
|
||||||
executables = []
|
executables: List[str] = []
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh staticroute.sh"]
|
startup: List[str] = ["sh staticroute.sh"]
|
||||||
validate = []
|
validate: List[str] = []
|
||||||
shutdown = []
|
shutdown: List[str] = []
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
routes = []
|
routes = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
for ip in iface.ips():
|
||||||
continue
|
address = str(ip.ip)
|
||||||
for x in ifc.addrlist:
|
if netaddr.valid_ipv6(address):
|
||||||
addr = x.split("/")[0]
|
|
||||||
if netaddr.valid_ipv6(addr):
|
|
||||||
dst = "3ffe:4::/64"
|
dst = "3ffe:4::/64"
|
||||||
else:
|
else:
|
||||||
dst = "10.9.8.0/24"
|
dst = "10.9.8.0/24"
|
||||||
net = netaddr.IPNetwork(x)
|
if ip[-2] != ip[1]:
|
||||||
if net[-2] != net[1]:
|
routes.append((dst, ip[1]))
|
||||||
routes.append((dst, net[1]))
|
|
||||||
return dict(routes=routes)
|
return dict(routes=routes)
|
||||||
|
|
||||||
|
|
||||||
class IpForwardService(ConfigService):
|
class IpForwardService(ConfigService):
|
||||||
name = "IPForward"
|
name: str = "IPForward"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["ipforward.sh"]
|
files: List[str] = ["ipforward.sh"]
|
||||||
executables = ["sysctl"]
|
executables: List[str] = ["sysctl"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh ipforward.sh"]
|
startup: List[str] = ["sh ipforward.sh"]
|
||||||
validate = []
|
validate: List[str] = []
|
||||||
shutdown = []
|
shutdown: List[str] = []
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
devnames = []
|
devnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces():
|
||||||
devname = utils.sysctl_devname(ifc.name)
|
devname = utils.sysctl_devname(iface.name)
|
||||||
devnames.append(devname)
|
devnames.append(devname)
|
||||||
return dict(devnames=devnames)
|
return dict(devnames=devnames)
|
||||||
|
|
||||||
|
|
||||||
class SshService(ConfigService):
|
class SshService(ConfigService):
|
||||||
name = "SSH"
|
name: str = "SSH"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = ["/etc/ssh", "/var/run/sshd"]
|
directories: List[str] = ["/etc/ssh", "/var/run/sshd"]
|
||||||
files = ["startsshd.sh", "/etc/ssh/sshd_config"]
|
files: List[str] = ["startsshd.sh", "/etc/ssh/sshd_config"]
|
||||||
executables = ["sshd"]
|
executables: List[str] = ["sshd"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh startsshd.sh"]
|
startup: List[str] = ["sh startsshd.sh"]
|
||||||
validate = []
|
validate: List[str] = []
|
||||||
shutdown = ["killall sshd"]
|
shutdown: List[str] = ["killall sshd"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
return dict(
|
return dict(
|
||||||
|
@ -136,146 +132,135 @@ class SshService(ConfigService):
|
||||||
|
|
||||||
|
|
||||||
class DhcpService(ConfigService):
|
class DhcpService(ConfigService):
|
||||||
name = "DHCP"
|
name: str = "DHCP"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = ["/etc/dhcp", "/var/lib/dhcp"]
|
directories: List[str] = ["/etc/dhcp", "/var/lib/dhcp"]
|
||||||
files = ["/etc/dhcp/dhcpd.conf"]
|
files: List[str] = ["/etc/dhcp/dhcpd.conf"]
|
||||||
executables = ["dhcpd"]
|
executables: List[str] = ["dhcpd"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
|
startup: List[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
|
||||||
validate = ["pidof dhcpd"]
|
validate: List[str] = ["pidof dhcpd"]
|
||||||
shutdown = ["killall dhcpd"]
|
shutdown: List[str] = ["killall dhcpd"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
subnets = []
|
subnets = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
for ip4 in iface.ip4s:
|
||||||
continue
|
# divide the address space in half
|
||||||
for x in ifc.addrlist:
|
index = (ip4.size - 2) / 2
|
||||||
addr = x.split("/")[0]
|
rangelow = ip4[index]
|
||||||
if netaddr.valid_ipv4(addr):
|
rangehigh = ip4[-2]
|
||||||
net = netaddr.IPNetwork(x)
|
subnets.append((ip4.ip, ip4.netmask, rangelow, rangehigh, str(ip4.ip)))
|
||||||
# divide the address space in half
|
|
||||||
index = (net.size - 2) / 2
|
|
||||||
rangelow = net[index]
|
|
||||||
rangehigh = net[-2]
|
|
||||||
subnets.append((net.ip, net.netmask, rangelow, rangehigh, addr))
|
|
||||||
return dict(subnets=subnets)
|
return dict(subnets=subnets)
|
||||||
|
|
||||||
|
|
||||||
class DhcpClientService(ConfigService):
|
class DhcpClientService(ConfigService):
|
||||||
name = "DHCPClient"
|
name: str = "DHCPClient"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["startdhcpclient.sh"]
|
files: List[str] = ["startdhcpclient.sh"]
|
||||||
executables = ["dhclient"]
|
executables: List[str] = ["dhclient"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh startdhcpclient.sh"]
|
startup: List[str] = ["sh startdhcpclient.sh"]
|
||||||
validate = ["pidof dhclient"]
|
validate: List[str] = ["pidof dhclient"]
|
||||||
shutdown = ["killall dhclient"]
|
shutdown: List[str] = ["killall dhclient"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
ifnames = []
|
ifnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifnames.append(iface.name)
|
||||||
continue
|
|
||||||
ifnames.append(ifc.name)
|
|
||||||
return dict(ifnames=ifnames)
|
return dict(ifnames=ifnames)
|
||||||
|
|
||||||
|
|
||||||
class FtpService(ConfigService):
|
class FtpService(ConfigService):
|
||||||
name = "FTP"
|
name: str = "FTP"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = ["/var/run/vsftpd/empty", "/var/ftp"]
|
directories: List[str] = ["/var/run/vsftpd/empty", "/var/ftp"]
|
||||||
files = ["vsftpd.conf"]
|
files: List[str] = ["vsftpd.conf"]
|
||||||
executables = ["vsftpd"]
|
executables: List[str] = ["vsftpd"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["vsftpd ./vsftpd.conf"]
|
startup: List[str] = ["vsftpd ./vsftpd.conf"]
|
||||||
validate = ["pidof vsftpd"]
|
validate: List[str] = ["pidof vsftpd"]
|
||||||
shutdown = ["killall vsftpd"]
|
shutdown: List[str] = ["killall vsftpd"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
|
||||||
class PcapService(ConfigService):
|
class PcapService(ConfigService):
|
||||||
name = "pcap"
|
name: str = "pcap"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = []
|
directories: List[str] = []
|
||||||
files = ["pcap.sh"]
|
files: List[str] = ["pcap.sh"]
|
||||||
executables = ["tcpdump"]
|
executables: List[str] = ["tcpdump"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh pcap.sh start"]
|
startup: List[str] = ["sh pcap.sh start"]
|
||||||
validate = ["pidof tcpdump"]
|
validate: List[str] = ["pidof tcpdump"]
|
||||||
shutdown = ["sh pcap.sh stop"]
|
shutdown: List[str] = ["sh pcap.sh stop"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
ifnames = []
|
ifnames = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifnames.append(iface.name)
|
||||||
continue
|
|
||||||
ifnames.append(ifc.name)
|
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
|
|
||||||
class RadvdService(ConfigService):
|
class RadvdService(ConfigService):
|
||||||
name = "radvd"
|
name: str = "radvd"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = ["/etc/radvd"]
|
directories: List[str] = ["/etc/radvd"]
|
||||||
files = ["/etc/radvd/radvd.conf"]
|
files: List[str] = ["/etc/radvd/radvd.conf"]
|
||||||
executables = ["radvd"]
|
executables: List[str] = ["radvd"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"]
|
startup: List[str] = [
|
||||||
validate = ["pidof radvd"]
|
"radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"
|
||||||
shutdown = ["pkill radvd"]
|
]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validate: List[str] = ["pidof radvd"]
|
||||||
default_configs = []
|
shutdown: List[str] = ["pkill radvd"]
|
||||||
modes = {}
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
|
default_configs: List[Configuration] = []
|
||||||
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
interfaces = []
|
ifaces = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
|
||||||
continue
|
|
||||||
prefixes = []
|
prefixes = []
|
||||||
for x in ifc.addrlist:
|
for ip6 in iface.ip6s:
|
||||||
addr = x.split("/")[0]
|
prefixes.append(str(ip6))
|
||||||
if netaddr.valid_ipv6(addr):
|
|
||||||
prefixes.append(x)
|
|
||||||
if not prefixes:
|
if not prefixes:
|
||||||
continue
|
continue
|
||||||
interfaces.append((ifc.name, prefixes))
|
ifaces.append((iface.name, prefixes))
|
||||||
return dict(interfaces=interfaces)
|
return dict(ifaces=ifaces)
|
||||||
|
|
||||||
|
|
||||||
class AtdService(ConfigService):
|
class AtdService(ConfigService):
|
||||||
name = "atd"
|
name: str = "atd"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
|
directories: List[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
|
||||||
files = ["startatd.sh"]
|
files: List[str] = ["startatd.sh"]
|
||||||
executables = ["atd"]
|
executables: List[str] = ["atd"]
|
||||||
dependencies = []
|
dependencies: List[str] = []
|
||||||
startup = ["sh startatd.sh"]
|
startup: List[str] = ["sh startatd.sh"]
|
||||||
validate = ["pidof atd"]
|
validate: List[str] = ["pidof atd"]
|
||||||
shutdown = ["pkill atd"]
|
shutdown: List[str] = ["pkill atd"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
default_configs = []
|
default_configs: List[Configuration] = []
|
||||||
modes = {}
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
|
||||||
class HttpService(ConfigService):
|
class HttpService(ConfigService):
|
||||||
name = "HTTP"
|
name: str = "HTTP"
|
||||||
group = GROUP_NAME
|
group: str = GROUP_NAME
|
||||||
directories = [
|
directories: List[str] = [
|
||||||
"/etc/apache2",
|
"/etc/apache2",
|
||||||
"/var/run/apache2",
|
"/var/run/apache2",
|
||||||
"/var/log/apache2",
|
"/var/log/apache2",
|
||||||
|
@ -283,20 +268,22 @@ class HttpService(ConfigService):
|
||||||
"/var/lock/apache2",
|
"/var/lock/apache2",
|
||||||
"/var/www",
|
"/var/www",
|
||||||
]
|
]
|
||||||
files = ["/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html"]
|
files: List[str] = [
|
||||||
executables = ["apache2ctl"]
|
"/etc/apache2/apache2.conf",
|
||||||
dependencies = []
|
"/etc/apache2/envvars",
|
||||||
startup = ["chown www-data /var/lock/apache2", "apache2ctl start"]
|
"/var/www/index.html",
|
||||||
validate = ["pidof apache2"]
|
]
|
||||||
shutdown = ["apache2ctl stop"]
|
executables: List[str] = ["apache2ctl"]
|
||||||
validation_mode = ConfigServiceMode.BLOCKING
|
dependencies: List[str] = []
|
||||||
default_configs = []
|
startup: List[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"]
|
||||||
modes = {}
|
validate: List[str] = ["pidof apache2"]
|
||||||
|
shutdown: List[str] = ["apache2ctl stop"]
|
||||||
|
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||||
|
default_configs: List[Configuration] = []
|
||||||
|
modes: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
def data(self) -> Dict[str, Any]:
|
def data(self) -> Dict[str, Any]:
|
||||||
interfaces = []
|
ifaces = []
|
||||||
for ifc in self.node.netifs():
|
for iface in self.node.get_ifaces(control=False):
|
||||||
if getattr(ifc, "control", False):
|
ifaces.append(iface)
|
||||||
continue
|
return dict(ifaces=ifaces)
|
||||||
interfaces.append(ifc)
|
|
||||||
return dict(interfaces=interfaces)
|
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
<p>This is the default web page for this server.</p>
|
<p>This is the default web page for this server.</p>
|
||||||
<p>The web server software is running but no content has been added, yet.</p>
|
<p>The web server software is running but no content has been added, yet.</p>
|
||||||
<ul>
|
<ul>
|
||||||
% for ifc in interfaces:
|
% for iface in ifaces:
|
||||||
<li>${ifc.name} - ${ifc.addrlist}</li>
|
<li>${iface.name} - ${iface.addrlist}</li>
|
||||||
% endfor
|
% endfor
|
||||||
</ul>
|
</ul>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,19 +1,3 @@
|
||||||
from core.utils import which
|
|
||||||
|
|
||||||
COREDPY_VERSION = "@PACKAGE_VERSION@"
|
COREDPY_VERSION = "@PACKAGE_VERSION@"
|
||||||
CORE_CONF_DIR = "@CORE_CONF_DIR@"
|
CORE_CONF_DIR = "@CORE_CONF_DIR@"
|
||||||
CORE_DATA_DIR = "@CORE_DATA_DIR@"
|
CORE_DATA_DIR = "@CORE_DATA_DIR@"
|
||||||
QUAGGA_STATE_DIR = "@CORE_STATE_DIR@/run/quagga"
|
|
||||||
FRR_STATE_DIR = "@CORE_STATE_DIR@/run/frr"
|
|
||||||
|
|
||||||
VNODED_BIN = which("vnoded", required=True)
|
|
||||||
VCMD_BIN = which("vcmd", required=True)
|
|
||||||
SYSCTL_BIN = which("sysctl", required=True)
|
|
||||||
IP_BIN = which("ip", required=True)
|
|
||||||
ETHTOOL_BIN = which("ethtool", required=True)
|
|
||||||
TC_BIN = which("tc", required=True)
|
|
||||||
EBTABLES_BIN = which("ebtables", required=True)
|
|
||||||
MOUNT_BIN = which("mount", required=True)
|
|
||||||
UMOUNT_BIN = which("umount", required=True)
|
|
||||||
OVS_BIN = which("ovs-vsctl", required=False)
|
|
||||||
OVS_FLOW_BIN = which("ovs-ofctl", required=False)
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""
|
"""
|
||||||
EMANE Bypass model for CORE
|
EMANE Bypass model for CORE
|
||||||
"""
|
"""
|
||||||
|
from typing import List, Set
|
||||||
|
|
||||||
from core.config import Configuration
|
from core.config import Configuration
|
||||||
from core.emane import emanemodel
|
from core.emane import emanemodel
|
||||||
|
@ -8,14 +9,14 @@ from core.emulator.enumerations import ConfigDataTypes
|
||||||
|
|
||||||
|
|
||||||
class EmaneBypassModel(emanemodel.EmaneModel):
|
class EmaneBypassModel(emanemodel.EmaneModel):
|
||||||
name = "emane_bypass"
|
name: str = "emane_bypass"
|
||||||
|
|
||||||
# values to ignore, when writing xml files
|
# values to ignore, when writing xml files
|
||||||
config_ignore = {"none"}
|
config_ignore: Set[str] = {"none"}
|
||||||
|
|
||||||
# mac definitions
|
# mac definitions
|
||||||
mac_library = "bypassmaclayer"
|
mac_library: str = "bypassmaclayer"
|
||||||
mac_config = [
|
mac_config: List[Configuration] = [
|
||||||
Configuration(
|
Configuration(
|
||||||
_id="none",
|
_id="none",
|
||||||
_type=ConfigDataTypes.BOOL,
|
_type=ConfigDataTypes.BOOL,
|
||||||
|
@ -25,8 +26,8 @@ class EmaneBypassModel(emanemodel.EmaneModel):
|
||||||
]
|
]
|
||||||
|
|
||||||
# phy definitions
|
# phy definitions
|
||||||
phy_library = "bypassphylayer"
|
phy_library: str = "bypassphylayer"
|
||||||
phy_config = []
|
phy_config: List[Configuration] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, emane_prefix: str) -> None:
|
def load(cls, emane_prefix: str) -> None:
|
||||||
|
|
|
@ -10,9 +10,7 @@ from lxml import etree
|
||||||
|
|
||||||
from core.config import ConfigGroup, Configuration
|
from core.config import ConfigGroup, Configuration
|
||||||
from core.emane import emanemanifest, emanemodel
|
from core.emane import emanemanifest, emanemodel
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emulator.data import LinkOptions
|
||||||
from core.emulator.emudata import LinkOptions
|
|
||||||
from core.emulator.enumerations import TransportType
|
|
||||||
from core.nodes.interface import CoreInterface
|
from core.nodes.interface import CoreInterface
|
||||||
from core.xml import emanexml
|
from core.xml import emanexml
|
||||||
|
|
||||||
|
@ -22,6 +20,7 @@ except ImportError:
|
||||||
try:
|
try:
|
||||||
from emanesh.events.commeffectevent import CommEffectEvent
|
from emanesh.events.commeffectevent import CommEffectEvent
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
CommEffectEvent = None
|
||||||
logging.debug("compatible emane python bindings not installed")
|
logging.debug("compatible emane python bindings not installed")
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,16 +37,15 @@ def convert_none(x: float) -> int:
|
||||||
|
|
||||||
|
|
||||||
class EmaneCommEffectModel(emanemodel.EmaneModel):
|
class EmaneCommEffectModel(emanemodel.EmaneModel):
|
||||||
name = "emane_commeffect"
|
name: str = "emane_commeffect"
|
||||||
|
shim_library: str = "commeffectshim"
|
||||||
shim_library = "commeffectshim"
|
shim_xml: str = "commeffectshim.xml"
|
||||||
shim_xml = "commeffectshim.xml"
|
shim_defaults: Dict[str, str] = {}
|
||||||
shim_defaults = {}
|
config_shim: List[Configuration] = []
|
||||||
config_shim = []
|
|
||||||
|
|
||||||
# comm effect does not need the default phy and external configurations
|
# comm effect does not need the default phy and external configurations
|
||||||
phy_config = []
|
phy_config: List[Configuration] = []
|
||||||
external_config = []
|
external_config: List[Configuration] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, emane_prefix: str) -> None:
|
def load(cls, emane_prefix: str) -> None:
|
||||||
|
@ -62,9 +60,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
||||||
def config_groups(cls) -> List[ConfigGroup]:
|
def config_groups(cls) -> List[ConfigGroup]:
|
||||||
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
|
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
|
||||||
|
|
||||||
def build_xml_files(
|
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
|
||||||
self, config: Dict[str, str], interface: CoreInterface = None
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Build the necessary nem and commeffect XMLs in the given path.
|
Build the necessary nem and commeffect XMLs in the given path.
|
||||||
If an individual NEM has a nonstandard config, we need to build
|
If an individual NEM has a nonstandard config, we need to build
|
||||||
|
@ -72,26 +68,19 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
||||||
nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used.
|
nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used.
|
||||||
|
|
||||||
:param config: emane model configuration for the node and interface
|
:param config: emane model configuration for the node and interface
|
||||||
:param interface: interface for the emane node
|
:param iface: interface for the emane node
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
# retrieve xml names
|
|
||||||
nem_name = emanexml.nem_file_name(self, interface)
|
|
||||||
shim_name = emanexml.shim_file_name(self, interface)
|
|
||||||
|
|
||||||
# create and write nem document
|
# create and write nem document
|
||||||
nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured")
|
nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured")
|
||||||
transport_type = TransportType.VIRTUAL
|
transport_name = emanexml.transport_file_name(iface)
|
||||||
if interface and interface.transport_type == TransportType.RAW:
|
etree.SubElement(nem_element, "transport", definition=transport_name)
|
||||||
transport_type = TransportType.RAW
|
|
||||||
transport_file = emanexml.transport_file_name(self.id, transport_type)
|
|
||||||
etree.SubElement(nem_element, "transport", definition=transport_file)
|
|
||||||
|
|
||||||
# set shim configuration
|
# set shim configuration
|
||||||
|
nem_name = emanexml.nem_file_name(iface)
|
||||||
|
shim_name = emanexml.shim_file_name(iface)
|
||||||
etree.SubElement(nem_element, "shim", definition=shim_name)
|
etree.SubElement(nem_element, "shim", definition=shim_name)
|
||||||
|
emanexml.create_iface_file(iface, nem_element, "nem", nem_name)
|
||||||
nem_file = os.path.join(self.session.session_dir, nem_name)
|
|
||||||
emanexml.create_file(nem_element, "nem", nem_file)
|
|
||||||
|
|
||||||
# create and write shim document
|
# create and write shim document
|
||||||
shim_element = etree.Element(
|
shim_element = etree.Element(
|
||||||
|
@ -110,12 +99,13 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
||||||
ff = config["filterfile"]
|
ff = config["filterfile"]
|
||||||
if ff.strip() != "":
|
if ff.strip() != "":
|
||||||
emanexml.add_param(shim_element, "filterfile", ff)
|
emanexml.add_param(shim_element, "filterfile", ff)
|
||||||
|
emanexml.create_iface_file(iface, shim_element, "shim", shim_name)
|
||||||
|
|
||||||
shim_file = os.path.join(self.session.session_dir, shim_name)
|
# create transport xml
|
||||||
emanexml.create_file(shim_element, "shim", shim_file)
|
emanexml.create_transport_xml(iface, config)
|
||||||
|
|
||||||
def linkconfig(
|
def linkconfig(
|
||||||
self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
|
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Generate CommEffect events when a Link Message is received having
|
Generate CommEffect events when a Link Message is received having
|
||||||
|
@ -126,24 +116,23 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
||||||
logging.warning("%s: EMANE event service unavailable", self.name)
|
logging.warning("%s: EMANE event service unavailable", self.name)
|
||||||
return
|
return
|
||||||
|
|
||||||
if netif is None or netif2 is None:
|
if iface is None or iface2 is None:
|
||||||
logging.warning("%s: missing NEM information", self.name)
|
logging.warning("%s: missing NEM information", self.name)
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO: batch these into multiple events per transmission
|
# TODO: batch these into multiple events per transmission
|
||||||
# TODO: may want to split out seconds portion of delay and jitter
|
# TODO: may want to split out seconds portion of delay and jitter
|
||||||
event = CommEffectEvent()
|
event = CommEffectEvent()
|
||||||
emane_node = self.session.get_node(self.id, EmaneNet)
|
nem1 = self.session.emane.get_nem_id(iface)
|
||||||
nemid = emane_node.getnemid(netif)
|
nem2 = self.session.emane.get_nem_id(iface2)
|
||||||
nemid2 = emane_node.getnemid(netif2)
|
|
||||||
logging.info("sending comm effect event")
|
logging.info("sending comm effect event")
|
||||||
event.append(
|
event.append(
|
||||||
nemid,
|
nem1,
|
||||||
latency=convert_none(options.delay),
|
latency=convert_none(options.delay),
|
||||||
jitter=convert_none(options.jitter),
|
jitter=convert_none(options.jitter),
|
||||||
loss=convert_none(options.per),
|
loss=convert_none(options.loss),
|
||||||
duplicate=convert_none(options.dup),
|
duplicate=convert_none(options.dup),
|
||||||
unicast=int(convert_none(options.bandwidth)),
|
unicast=int(convert_none(options.bandwidth)),
|
||||||
broadcast=int(convert_none(options.bandwidth)),
|
broadcast=int(convert_none(options.bandwidth)),
|
||||||
)
|
)
|
||||||
service.publish(nemid2, event)
|
service.publish(nem2, event)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import logging
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
@ -28,9 +29,7 @@ from core.emulator.enumerations import (
|
||||||
)
|
)
|
||||||
from core.errors import CoreCommandError, CoreError
|
from core.errors import CoreCommandError, CoreError
|
||||||
from core.nodes.base import CoreNode, NodeBase
|
from core.nodes.base import CoreNode, NodeBase
|
||||||
from core.nodes.interface import CoreInterface
|
from core.nodes.interface import CoreInterface, TunTap
|
||||||
from core.nodes.network import CtrlNet
|
|
||||||
from core.nodes.physical import Rj45Node
|
|
||||||
from core.xml import emanexml
|
from core.xml import emanexml
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -63,6 +62,12 @@ DEFAULT_EMANE_PREFIX = "/usr"
|
||||||
DEFAULT_DEV = "ctrl0"
|
DEFAULT_DEV = "ctrl0"
|
||||||
|
|
||||||
|
|
||||||
|
class EmaneState(Enum):
|
||||||
|
SUCCESS = 0
|
||||||
|
NOT_NEEDED = 1
|
||||||
|
NOT_READY = 2
|
||||||
|
|
||||||
|
|
||||||
class EmaneManager(ModelManager):
|
class EmaneManager(ModelManager):
|
||||||
"""
|
"""
|
||||||
EMANE controller object. Lives in a Session instance and is used for
|
EMANE controller object. Lives in a Session instance and is used for
|
||||||
|
@ -70,11 +75,11 @@ class EmaneManager(ModelManager):
|
||||||
controlling the EMANE daemons.
|
controlling the EMANE daemons.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "emane"
|
name: str = "emane"
|
||||||
config_type = RegisterTlvs.EMULATION_SERVER
|
config_type: RegisterTlvs = RegisterTlvs.EMULATION_SERVER
|
||||||
SUCCESS, NOT_NEEDED, NOT_READY = (0, 1, 2)
|
NOT_READY: int = 2
|
||||||
EVENTCFGVAR = "LIBEMANEEVENTSERVICECONFIG"
|
EVENTCFGVAR: str = "LIBEMANEEVENTSERVICECONFIG"
|
||||||
DEFAULT_LOG_LEVEL = 3
|
DEFAULT_LOG_LEVEL: int = 3
|
||||||
|
|
||||||
def __init__(self, session: "Session") -> None:
|
def __init__(self, session: "Session") -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -84,74 +89,71 @@ class EmaneManager(ModelManager):
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.session = session
|
self.session: "Session" = session
|
||||||
self._emane_nets = {}
|
self.nems_to_ifaces: Dict[int, CoreInterface] = {}
|
||||||
self._emane_node_lock = threading.Lock()
|
self.ifaces_to_nems: Dict[CoreInterface, int] = {}
|
||||||
|
self._emane_nets: Dict[int, EmaneNet] = {}
|
||||||
|
self._emane_node_lock: threading.Lock = threading.Lock()
|
||||||
# port numbers are allocated from these counters
|
# port numbers are allocated from these counters
|
||||||
self.platformport = self.session.options.get_config_int(
|
self.platformport: int = self.session.options.get_config_int(
|
||||||
"emane_platform_port", 8100
|
"emane_platform_port", 8100
|
||||||
)
|
)
|
||||||
self.transformport = self.session.options.get_config_int(
|
self.transformport: int = self.session.options.get_config_int(
|
||||||
"emane_transform_port", 8200
|
"emane_transform_port", 8200
|
||||||
)
|
)
|
||||||
self.doeventloop = False
|
self.doeventloop: bool = False
|
||||||
self.eventmonthread = None
|
self.eventmonthread: Optional[threading.Thread] = None
|
||||||
|
|
||||||
# model for global EMANE configuration options
|
# model for global EMANE configuration options
|
||||||
self.emane_config = EmaneGlobalModel(session)
|
self.emane_config: EmaneGlobalModel = EmaneGlobalModel(session)
|
||||||
self.set_configs(self.emane_config.default_values())
|
self.set_configs(self.emane_config.default_values())
|
||||||
|
|
||||||
# link monitor
|
# link monitor
|
||||||
self.link_monitor = EmaneLinkMonitor(self)
|
self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self)
|
||||||
|
|
||||||
self.service = None
|
self.service: Optional[EventService] = None
|
||||||
self.eventchannel = None
|
self.eventchannel: Optional[Tuple[str, int, str]] = None
|
||||||
self.event_device = None
|
self.event_device: Optional[str] = None
|
||||||
self.emane_check()
|
self.emane_check()
|
||||||
|
|
||||||
def getifcconfig(
|
def next_nem_id(self) -> int:
|
||||||
self, node_id: int, interface: CoreInterface, model_name: str
|
nem_id = int(self.get_config("nem_id_start"))
|
||||||
|
while nem_id in self.nems_to_ifaces:
|
||||||
|
nem_id += 1
|
||||||
|
return nem_id
|
||||||
|
|
||||||
|
def get_iface_config(
|
||||||
|
self, emane_net: EmaneNet, iface: CoreInterface
|
||||||
) -> Dict[str, str]:
|
) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Retrieve interface configuration or node configuration if not provided.
|
Retrieve configuration for a given interface.
|
||||||
|
|
||||||
:param node_id: node id
|
:param emane_net: emane network the interface is connected to
|
||||||
:param interface: node interface
|
:param iface: interface running emane
|
||||||
:param model_name: model to get configuration for
|
:return: net, node, or interface model configuration
|
||||||
:return: node/interface model configuration
|
|
||||||
"""
|
"""
|
||||||
# use the network-wide config values or interface(NEM)-specific values?
|
model_name = emane_net.model.name
|
||||||
if interface is None:
|
# don"t use default values when interface config is the same as net
|
||||||
return self.get_configs(node_id=node_id, config_type=model_name)
|
# note here that using iface.node.id as key allows for only one type
|
||||||
else:
|
# of each model per node;
|
||||||
# don"t use default values when interface config is the same as net
|
# TODO: use both node and interface as key
|
||||||
# note here that using ifc.node.id as key allows for only one type
|
# Adamson change: first check for iface config keyed by "node:iface.name"
|
||||||
# of each model per node;
|
# (so that nodes w/ multiple interfaces of same conftype can have
|
||||||
# TODO: use both node and interface as key
|
# different configs for each separate interface)
|
||||||
|
key = 1000 * iface.node.id
|
||||||
# Adamson change: first check for iface config keyed by "node:ifc.name"
|
if iface.node_id is not None:
|
||||||
# (so that nodes w/ multiple interfaces of same conftype can have
|
key += iface.node_id
|
||||||
# different configs for each separate interface)
|
# try retrieve interface specific configuration, avoid getting defaults
|
||||||
key = 1000 * interface.node.id
|
config = self.get_configs(node_id=key, config_type=model_name)
|
||||||
if interface.netindex is not None:
|
# otherwise retrieve the interfaces node configuration, avoid using defaults
|
||||||
key += interface.netindex
|
if not config:
|
||||||
|
config = self.get_configs(node_id=iface.node.id, config_type=model_name)
|
||||||
# try retrieve interface specific configuration, avoid getting defaults
|
# get non interface config, when none found
|
||||||
config = self.get_configs(node_id=key, config_type=model_name)
|
if not config:
|
||||||
|
# with EMANE 0.9.2+, we need an extra NEM XML from
|
||||||
# otherwise retrieve the interfaces node configuration, avoid using defaults
|
# model.buildnemxmlfiles(), so defaults are returned here
|
||||||
if not config:
|
config = self.get_configs(node_id=emane_net.id, config_type=model_name)
|
||||||
config = self.get_configs(
|
return config
|
||||||
node_id=interface.node.id, config_type=model_name
|
|
||||||
)
|
|
||||||
|
|
||||||
# get non interface config, when none found
|
|
||||||
if not config:
|
|
||||||
# with EMANE 0.9.2+, we need an extra NEM XML from
|
|
||||||
# model.buildnemxmlfiles(), so defaults are returned here
|
|
||||||
config = self.get_configs(node_id=node_id, config_type=model_name)
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
def config_reset(self, node_id: int = None) -> None:
|
def config_reset(self, node_id: int = None) -> None:
|
||||||
super().config_reset(node_id)
|
super().config_reset(node_id)
|
||||||
|
@ -163,23 +165,24 @@ class EmaneManager(ModelManager):
|
||||||
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
try:
|
# check for emane
|
||||||
# check for emane
|
path = utils.which("emane", required=False)
|
||||||
args = "emane --version"
|
if not path:
|
||||||
emane_version = utils.cmd(args)
|
|
||||||
logging.info("using EMANE: %s", emane_version)
|
|
||||||
self.session.distributed.execute(lambda x: x.remote_cmd(args))
|
|
||||||
|
|
||||||
# load default emane models
|
|
||||||
self.load_models(EMANE_MODELS)
|
|
||||||
|
|
||||||
# load custom models
|
|
||||||
custom_models_path = self.session.options.get_config("emane_models_dir")
|
|
||||||
if custom_models_path:
|
|
||||||
emane_models = utils.load_classes(custom_models_path, EmaneModel)
|
|
||||||
self.load_models(emane_models)
|
|
||||||
except CoreCommandError:
|
|
||||||
logging.info("emane is not installed")
|
logging.info("emane is not installed")
|
||||||
|
return
|
||||||
|
|
||||||
|
# get version
|
||||||
|
emane_version = utils.cmd("emane --version")
|
||||||
|
logging.info("using emane: %s", emane_version)
|
||||||
|
|
||||||
|
# load default emane models
|
||||||
|
self.load_models(EMANE_MODELS)
|
||||||
|
|
||||||
|
# load custom models
|
||||||
|
custom_models_path = self.session.options.get_config("emane_models_dir")
|
||||||
|
if custom_models_path:
|
||||||
|
emane_models = utils.load_classes(custom_models_path, EmaneModel)
|
||||||
|
self.load_models(emane_models)
|
||||||
|
|
||||||
def deleteeventservice(self) -> None:
|
def deleteeventservice(self) -> None:
|
||||||
if self.service:
|
if self.service:
|
||||||
|
@ -250,8 +253,8 @@ class EmaneManager(ModelManager):
|
||||||
"""
|
"""
|
||||||
with self._emane_node_lock:
|
with self._emane_node_lock:
|
||||||
if emane_net.id in self._emane_nets:
|
if emane_net.id in self._emane_nets:
|
||||||
raise KeyError(
|
raise CoreError(
|
||||||
f"non-unique EMANE object id {emane_net.id} for {emane_net}"
|
f"duplicate emane network({emane_net.id}): {emane_net.name}"
|
||||||
)
|
)
|
||||||
self._emane_nets[emane_net.id] = emane_net
|
self._emane_nets[emane_net.id] = emane_net
|
||||||
|
|
||||||
|
@ -260,14 +263,13 @@ class EmaneManager(ModelManager):
|
||||||
Return a set of CoreNodes that are linked to an EMANE network,
|
Return a set of CoreNodes that are linked to an EMANE network,
|
||||||
e.g. containers having one or more radio interfaces.
|
e.g. containers having one or more radio interfaces.
|
||||||
"""
|
"""
|
||||||
# assumes self._objslock already held
|
|
||||||
nodes = set()
|
nodes = set()
|
||||||
for emane_net in self._emane_nets.values():
|
for emane_net in self._emane_nets.values():
|
||||||
for netif in emane_net.netifs():
|
for iface in emane_net.get_ifaces():
|
||||||
nodes.add(netif.node)
|
nodes.add(iface.node)
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
def setup(self) -> int:
|
def setup(self) -> EmaneState:
|
||||||
"""
|
"""
|
||||||
Setup duties for EMANE manager.
|
Setup duties for EMANE manager.
|
||||||
|
|
||||||
|
@ -275,9 +277,7 @@ class EmaneManager(ModelManager):
|
||||||
instantiation
|
instantiation
|
||||||
"""
|
"""
|
||||||
logging.debug("emane setup")
|
logging.debug("emane setup")
|
||||||
|
with self.session.nodes_lock:
|
||||||
# TODO: drive this from the session object
|
|
||||||
with self.session._nodes_lock:
|
|
||||||
for node_id in self.session.nodes:
|
for node_id in self.session.nodes:
|
||||||
node = self.session.nodes[node_id]
|
node = self.session.nodes[node_id]
|
||||||
if isinstance(node, EmaneNet):
|
if isinstance(node, EmaneNet):
|
||||||
|
@ -285,10 +285,9 @@ class EmaneManager(ModelManager):
|
||||||
"adding emane node: id(%s) name(%s)", node.id, node.name
|
"adding emane node: id(%s) name(%s)", node.id, node.name
|
||||||
)
|
)
|
||||||
self.add_node(node)
|
self.add_node(node)
|
||||||
|
|
||||||
if not self._emane_nets:
|
if not self._emane_nets:
|
||||||
logging.debug("no emane nodes in session")
|
logging.debug("no emane nodes in session")
|
||||||
return EmaneManager.NOT_NEEDED
|
return EmaneState.NOT_NEEDED
|
||||||
|
|
||||||
# check if bindings were installed
|
# check if bindings were installed
|
||||||
if EventService is None:
|
if EventService is None:
|
||||||
|
@ -304,7 +303,7 @@ class EmaneManager(ModelManager):
|
||||||
"EMANE cannot start, check core config. invalid OTA device provided: %s",
|
"EMANE cannot start, check core config. invalid OTA device provided: %s",
|
||||||
otadev,
|
otadev,
|
||||||
)
|
)
|
||||||
return EmaneManager.NOT_READY
|
return EmaneState.NOT_READY
|
||||||
|
|
||||||
self.session.add_remove_control_net(
|
self.session.add_remove_control_net(
|
||||||
net_index=netidx, remove=False, conf_required=False
|
net_index=netidx, remove=False, conf_required=False
|
||||||
|
@ -316,19 +315,18 @@ class EmaneManager(ModelManager):
|
||||||
logging.debug("emane event service device index: %s", netidx)
|
logging.debug("emane event service device index: %s", netidx)
|
||||||
if netidx < 0:
|
if netidx < 0:
|
||||||
logging.error(
|
logging.error(
|
||||||
"EMANE cannot start, check core config. invalid event service device: %s",
|
"emane cannot start due to invalid event service device: %s",
|
||||||
eventdev,
|
eventdev,
|
||||||
)
|
)
|
||||||
return EmaneManager.NOT_READY
|
return EmaneState.NOT_READY
|
||||||
|
|
||||||
self.session.add_remove_control_net(
|
self.session.add_remove_control_net(
|
||||||
net_index=netidx, remove=False, conf_required=False
|
net_index=netidx, remove=False, conf_required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
self.check_node_models()
|
self.check_node_models()
|
||||||
return EmaneManager.SUCCESS
|
return EmaneState.SUCCESS
|
||||||
|
|
||||||
def startup(self) -> int:
|
def startup(self) -> EmaneState:
|
||||||
"""
|
"""
|
||||||
After all the EMANE networks have been added, build XML files
|
After all the EMANE networks have been added, build XML files
|
||||||
and start the daemons.
|
and start the daemons.
|
||||||
|
@ -337,39 +335,63 @@ class EmaneManager(ModelManager):
|
||||||
instantiation
|
instantiation
|
||||||
"""
|
"""
|
||||||
self.reset()
|
self.reset()
|
||||||
r = self.setup()
|
status = self.setup()
|
||||||
|
if status != EmaneState.SUCCESS:
|
||||||
# NOT_NEEDED or NOT_READY
|
return status
|
||||||
if r != EmaneManager.SUCCESS:
|
self.starteventmonitor()
|
||||||
return r
|
self.buildeventservicexml()
|
||||||
|
|
||||||
nems = []
|
|
||||||
with self._emane_node_lock:
|
with self._emane_node_lock:
|
||||||
self.buildxml()
|
logging.info("emane building xmls...")
|
||||||
self.starteventmonitor()
|
for node_id in sorted(self._emane_nets):
|
||||||
|
emane_net = self._emane_nets[node_id]
|
||||||
if self.numnems() > 0:
|
if not emane_net.model:
|
||||||
self.startdaemons()
|
logging.error("emane net(%s) has no model", emane_net.name)
|
||||||
self.installnetifs()
|
continue
|
||||||
|
for iface in emane_net.get_ifaces():
|
||||||
for node_id in self._emane_nets:
|
self.start_iface(emane_net, iface)
|
||||||
emane_node = self._emane_nets[node_id]
|
|
||||||
for netif in emane_node.netifs():
|
|
||||||
nems.append(
|
|
||||||
(netif.node.name, netif.name, emane_node.getnemid(netif))
|
|
||||||
)
|
|
||||||
|
|
||||||
if nems:
|
|
||||||
emane_nems_filename = os.path.join(self.session.session_dir, "emane_nems")
|
|
||||||
try:
|
|
||||||
with open(emane_nems_filename, "w") as f:
|
|
||||||
for nodename, ifname, nemid in nems:
|
|
||||||
f.write(f"{nodename} {ifname} {nemid}\n")
|
|
||||||
except IOError:
|
|
||||||
logging.exception("Error writing EMANE NEMs file: %s")
|
|
||||||
if self.links_enabled():
|
if self.links_enabled():
|
||||||
self.link_monitor.start()
|
self.link_monitor.start()
|
||||||
return EmaneManager.SUCCESS
|
return EmaneState.SUCCESS
|
||||||
|
|
||||||
|
def start_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
|
||||||
|
if not iface.node:
|
||||||
|
logging.error(
|
||||||
|
"emane net(%s) connected interface(%s) missing node",
|
||||||
|
emane_net.name,
|
||||||
|
iface.name,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
control_net = self.session.add_remove_control_net(
|
||||||
|
0, remove=False, conf_required=False
|
||||||
|
)
|
||||||
|
nem_id = self.next_nem_id()
|
||||||
|
self.set_nem(nem_id, iface)
|
||||||
|
self.write_nem(iface, nem_id)
|
||||||
|
emanexml.build_platform_xml(self, control_net, emane_net, iface, nem_id)
|
||||||
|
config = self.get_iface_config(emane_net, iface)
|
||||||
|
emane_net.model.build_xml_files(config, iface)
|
||||||
|
self.start_daemon(iface)
|
||||||
|
self.install_iface(emane_net, iface)
|
||||||
|
|
||||||
|
def set_nem(self, nem_id: int, iface: CoreInterface) -> None:
|
||||||
|
if nem_id in self.nems_to_ifaces:
|
||||||
|
raise CoreError(f"adding duplicate nem: {nem_id}")
|
||||||
|
self.nems_to_ifaces[nem_id] = iface
|
||||||
|
self.ifaces_to_nems[iface] = nem_id
|
||||||
|
|
||||||
|
def get_iface(self, nem_id: int) -> Optional[CoreInterface]:
|
||||||
|
return self.nems_to_ifaces.get(nem_id)
|
||||||
|
|
||||||
|
def get_nem_id(self, iface: CoreInterface) -> Optional[int]:
|
||||||
|
return self.ifaces_to_nems.get(iface)
|
||||||
|
|
||||||
|
def write_nem(self, iface: CoreInterface, nem_id: int) -> None:
|
||||||
|
path = os.path.join(self.session.session_dir, "emane_nems")
|
||||||
|
try:
|
||||||
|
with open(path, "a") as f:
|
||||||
|
f.write(f"{iface.node.name} {iface.name} {nem_id}\n")
|
||||||
|
except IOError:
|
||||||
|
logging.exception("error writing to emane nem file")
|
||||||
|
|
||||||
def links_enabled(self) -> bool:
|
def links_enabled(self) -> bool:
|
||||||
return self.get_config("link_enabled") == "1"
|
return self.get_config("link_enabled") == "1"
|
||||||
|
@ -380,18 +402,15 @@ class EmaneManager(ModelManager):
|
||||||
"""
|
"""
|
||||||
if not self.genlocationevents():
|
if not self.genlocationevents():
|
||||||
return
|
return
|
||||||
|
|
||||||
with self._emane_node_lock:
|
with self._emane_node_lock:
|
||||||
for key in sorted(self._emane_nets.keys()):
|
for node_id in sorted(self._emane_nets):
|
||||||
emane_node = self._emane_nets[key]
|
emane_net = self._emane_nets[node_id]
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"post startup for emane node: %s - %s",
|
"post startup for emane node: %s - %s", emane_net.id, emane_net.name
|
||||||
emane_node.id,
|
|
||||||
emane_node.name,
|
|
||||||
)
|
)
|
||||||
emane_node.model.post_startup()
|
emane_net.model.post_startup()
|
||||||
for netif in emane_node.netifs():
|
for iface in emane_net.get_ifaces():
|
||||||
netif.setposition()
|
iface.setposition()
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -400,13 +419,8 @@ class EmaneManager(ModelManager):
|
||||||
"""
|
"""
|
||||||
with self._emane_node_lock:
|
with self._emane_node_lock:
|
||||||
self._emane_nets.clear()
|
self._emane_nets.clear()
|
||||||
|
self.nems_to_ifaces.clear()
|
||||||
self.platformport = self.session.options.get_config_int(
|
self.ifaces_to_nems.clear()
|
||||||
"emane_platform_port", 8100
|
|
||||||
)
|
|
||||||
self.transformport = self.session.options.get_config_int(
|
|
||||||
"emane_transform_port", 8200
|
|
||||||
)
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -418,44 +432,27 @@ class EmaneManager(ModelManager):
|
||||||
logging.info("stopping EMANE daemons")
|
logging.info("stopping EMANE daemons")
|
||||||
if self.links_enabled():
|
if self.links_enabled():
|
||||||
self.link_monitor.stop()
|
self.link_monitor.stop()
|
||||||
self.deinstallnetifs()
|
self.deinstall_ifaces()
|
||||||
self.stopdaemons()
|
self.stopdaemons()
|
||||||
self.stopeventmonitor()
|
self.stopeventmonitor()
|
||||||
|
|
||||||
def buildxml(self) -> None:
|
|
||||||
"""
|
|
||||||
Build XML files required to run EMANE on each node.
|
|
||||||
NEMs run inside containers using the control network for passing
|
|
||||||
events and data.
|
|
||||||
"""
|
|
||||||
# assume self._objslock is already held here
|
|
||||||
logging.info("emane building xml...")
|
|
||||||
# on master, control network bridge added earlier in startup()
|
|
||||||
ctrlnet = self.session.add_remove_control_net(
|
|
||||||
net_index=0, remove=False, conf_required=False
|
|
||||||
)
|
|
||||||
self.buildplatformxml(ctrlnet)
|
|
||||||
self.buildnemxml()
|
|
||||||
self.buildeventservicexml()
|
|
||||||
|
|
||||||
def check_node_models(self) -> None:
|
def check_node_models(self) -> None:
|
||||||
"""
|
"""
|
||||||
Associate EMANE model classes with EMANE network nodes.
|
Associate EMANE model classes with EMANE network nodes.
|
||||||
"""
|
"""
|
||||||
for node_id in self._emane_nets:
|
for node_id in self._emane_nets:
|
||||||
emane_node = self._emane_nets[node_id]
|
emane_net = self._emane_nets[node_id]
|
||||||
logging.debug("checking emane model for node: %s", node_id)
|
logging.debug("checking emane model for node: %s", node_id)
|
||||||
|
|
||||||
# skip nodes that already have a model set
|
# skip nodes that already have a model set
|
||||||
if emane_node.model:
|
if emane_net.model:
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"node(%s) already has model(%s)",
|
"node(%s) already has model(%s)", emane_net.id, emane_net.model.name
|
||||||
emane_node.id,
|
|
||||||
emane_node.model.name,
|
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# set model configured for node, due to legacy messaging configuration before nodes exist
|
# set model configured for node, due to legacy messaging configuration
|
||||||
|
# before nodes exist
|
||||||
model_name = self.node_models.get(node_id)
|
model_name = self.node_models.get(node_id)
|
||||||
if not model_name:
|
if not model_name:
|
||||||
logging.error("emane node(%s) has no node model", node_id)
|
logging.error("emane node(%s) has no node model", node_id)
|
||||||
|
@ -464,81 +461,34 @@ class EmaneManager(ModelManager):
|
||||||
config = self.get_model_config(node_id=node_id, model_name=model_name)
|
config = self.get_model_config(node_id=node_id, model_name=model_name)
|
||||||
logging.debug("setting emane model(%s) config(%s)", model_name, config)
|
logging.debug("setting emane model(%s) config(%s)", model_name, config)
|
||||||
model_class = self.models[model_name]
|
model_class = self.models[model_name]
|
||||||
emane_node.setmodel(model_class, config)
|
emane_net.setmodel(model_class, config)
|
||||||
|
|
||||||
def nemlookup(self, nemid) -> Tuple[Optional[EmaneNet], Optional[CoreInterface]]:
|
|
||||||
"""
|
|
||||||
Look for the given numerical NEM ID and return the first matching
|
|
||||||
EMANE network and NEM interface.
|
|
||||||
"""
|
|
||||||
emane_node = None
|
|
||||||
netif = None
|
|
||||||
|
|
||||||
for node_id in self._emane_nets:
|
|
||||||
emane_node = self._emane_nets[node_id]
|
|
||||||
netif = emane_node.getnemnetif(nemid)
|
|
||||||
if netif is not None:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
emane_node = None
|
|
||||||
|
|
||||||
return emane_node, netif
|
|
||||||
|
|
||||||
def get_nem_link(
|
def get_nem_link(
|
||||||
self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE
|
self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE
|
||||||
) -> Optional[LinkData]:
|
) -> Optional[LinkData]:
|
||||||
emane1, netif = self.nemlookup(nem1)
|
iface1 = self.get_iface(nem1)
|
||||||
if not emane1 or not netif:
|
if not iface1:
|
||||||
logging.error("invalid nem: %s", nem1)
|
logging.error("invalid nem: %s", nem1)
|
||||||
return None
|
return None
|
||||||
node1 = netif.node
|
node1 = iface1.node
|
||||||
emane2, netif = self.nemlookup(nem2)
|
iface2 = self.get_iface(nem2)
|
||||||
if not emane2 or not netif:
|
if not iface2:
|
||||||
logging.error("invalid nem: %s", nem2)
|
logging.error("invalid nem: %s", nem2)
|
||||||
return None
|
return None
|
||||||
node2 = netif.node
|
node2 = iface2.node
|
||||||
color = self.session.get_link_color(emane1.id)
|
if iface1.net != iface2.net:
|
||||||
|
return None
|
||||||
|
emane_net = iface1.net
|
||||||
|
color = self.session.get_link_color(emane_net.id)
|
||||||
return LinkData(
|
return LinkData(
|
||||||
message_type=flags,
|
message_type=flags,
|
||||||
|
type=LinkTypes.WIRELESS,
|
||||||
node1_id=node1.id,
|
node1_id=node1.id,
|
||||||
node2_id=node2.id,
|
node2_id=node2.id,
|
||||||
network_id=emane1.id,
|
network_id=emane_net.id,
|
||||||
link_type=LinkTypes.WIRELESS,
|
|
||||||
color=color,
|
color=color,
|
||||||
)
|
)
|
||||||
|
|
||||||
def numnems(self) -> int:
|
|
||||||
"""
|
|
||||||
Return the number of NEMs emulated locally.
|
|
||||||
"""
|
|
||||||
count = 0
|
|
||||||
for node_id in self._emane_nets:
|
|
||||||
emane_node = self._emane_nets[node_id]
|
|
||||||
count += len(emane_node.netifs())
|
|
||||||
return count
|
|
||||||
|
|
||||||
def buildplatformxml(self, ctrlnet: CtrlNet) -> None:
|
|
||||||
"""
|
|
||||||
Build a platform.xml file now that all nodes are configured.
|
|
||||||
"""
|
|
||||||
nemid = int(self.get_config("nem_id_start"))
|
|
||||||
platform_xmls = {}
|
|
||||||
|
|
||||||
# assume self._objslock is already held here
|
|
||||||
for key in sorted(self._emane_nets.keys()):
|
|
||||||
emane_node = self._emane_nets[key]
|
|
||||||
nemid = emanexml.build_node_platform_xml(
|
|
||||||
self, ctrlnet, emane_node, nemid, platform_xmls
|
|
||||||
)
|
|
||||||
|
|
||||||
def buildnemxml(self) -> None:
|
|
||||||
"""
|
|
||||||
Builds the nem, mac, and phy xml files for each EMANE network.
|
|
||||||
"""
|
|
||||||
for key in sorted(self._emane_nets):
|
|
||||||
emane_net = self._emane_nets[key]
|
|
||||||
emanexml.build_xml_files(self, emane_net)
|
|
||||||
|
|
||||||
def buildeventservicexml(self) -> None:
|
def buildeventservicexml(self) -> None:
|
||||||
"""
|
"""
|
||||||
Build the libemaneeventservice.xml file if event service options
|
Build the libemaneeventservice.xml file if event service options
|
||||||
|
@ -571,7 +521,7 @@ class EmaneManager(ModelManager):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def startdaemons(self) -> None:
|
def start_daemon(self, iface: CoreInterface) -> None:
|
||||||
"""
|
"""
|
||||||
Start one EMANE daemon per node having a radio.
|
Start one EMANE daemon per node having a radio.
|
||||||
Add a control network even if the user has not configured one.
|
Add a control network even if the user has not configured one.
|
||||||
|
@ -581,116 +531,91 @@ class EmaneManager(ModelManager):
|
||||||
cfgloglevel = self.session.options.get_config_int("emane_log_level")
|
cfgloglevel = self.session.options.get_config_int("emane_log_level")
|
||||||
realtime = self.session.options.get_config_bool("emane_realtime", default=True)
|
realtime = self.session.options.get_config_bool("emane_realtime", default=True)
|
||||||
if cfgloglevel:
|
if cfgloglevel:
|
||||||
logging.info("setting user-defined EMANE log level: %d", cfgloglevel)
|
logging.info("setting user-defined emane log level: %d", cfgloglevel)
|
||||||
loglevel = str(cfgloglevel)
|
loglevel = str(cfgloglevel)
|
||||||
|
|
||||||
emanecmd = f"emane -d -l {loglevel}"
|
emanecmd = f"emane -d -l {loglevel}"
|
||||||
if realtime:
|
if realtime:
|
||||||
emanecmd += " -r"
|
emanecmd += " -r"
|
||||||
|
node = iface.node
|
||||||
otagroup, _otaport = self.get_config("otamanagergroup").split(":")
|
if iface.is_virtual():
|
||||||
otadev = self.get_config("otamanagerdevice")
|
otagroup, _otaport = self.get_config("otamanagergroup").split(":")
|
||||||
otanetidx = self.session.get_control_net_index(otadev)
|
otadev = self.get_config("otamanagerdevice")
|
||||||
|
otanetidx = self.session.get_control_net_index(otadev)
|
||||||
eventgroup, _eventport = self.get_config("eventservicegroup").split(":")
|
eventgroup, _eventport = self.get_config("eventservicegroup").split(":")
|
||||||
eventdev = self.get_config("eventservicedevice")
|
eventdev = self.get_config("eventservicedevice")
|
||||||
eventservicenetidx = self.session.get_control_net_index(eventdev)
|
eventservicenetidx = self.session.get_control_net_index(eventdev)
|
||||||
|
|
||||||
run_emane_on_host = False
|
|
||||||
for node in self.getnodes():
|
|
||||||
if isinstance(node, Rj45Node):
|
|
||||||
run_emane_on_host = True
|
|
||||||
continue
|
|
||||||
path = self.session.session_dir
|
|
||||||
n = node.id
|
|
||||||
|
|
||||||
# control network not yet started here
|
# control network not yet started here
|
||||||
self.session.add_remove_control_interface(
|
self.session.add_remove_control_iface(
|
||||||
node, 0, remove=False, conf_required=False
|
node, 0, remove=False, conf_required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
if otanetidx > 0:
|
if otanetidx > 0:
|
||||||
logging.info("adding ota device ctrl%d", otanetidx)
|
logging.info("adding ota device ctrl%d", otanetidx)
|
||||||
self.session.add_remove_control_interface(
|
self.session.add_remove_control_iface(
|
||||||
node, otanetidx, remove=False, conf_required=False
|
node, otanetidx, remove=False, conf_required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
if eventservicenetidx >= 0:
|
if eventservicenetidx >= 0:
|
||||||
logging.info("adding event service device ctrl%d", eventservicenetidx)
|
logging.info("adding event service device ctrl%d", eventservicenetidx)
|
||||||
self.session.add_remove_control_interface(
|
self.session.add_remove_control_iface(
|
||||||
node, eventservicenetidx, remove=False, conf_required=False
|
node, eventservicenetidx, remove=False, conf_required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# multicast route is needed for OTA data
|
# multicast route is needed for OTA data
|
||||||
|
logging.info("OTA GROUP(%s) OTA DEV(%s)", otagroup, otadev)
|
||||||
node.node_net_client.create_route(otagroup, otadev)
|
node.node_net_client.create_route(otagroup, otadev)
|
||||||
|
|
||||||
# multicast route is also needed for event data if on control network
|
# multicast route is also needed for event data if on control network
|
||||||
if eventservicenetidx >= 0 and eventgroup != otagroup:
|
if eventservicenetidx >= 0 and eventgroup != otagroup:
|
||||||
node.node_net_client.create_route(eventgroup, eventdev)
|
node.node_net_client.create_route(eventgroup, eventdev)
|
||||||
|
|
||||||
# start emane
|
# start emane
|
||||||
log_file = os.path.join(path, f"emane{n}.log")
|
log_file = os.path.join(node.nodedir, f"{iface.name}-emane.log")
|
||||||
platform_xml = os.path.join(path, f"platform{n}.xml")
|
platform_xml = os.path.join(node.nodedir, f"{iface.name}-platform.xml")
|
||||||
args = f"{emanecmd} -f {log_file} {platform_xml}"
|
args = f"{emanecmd} -f {log_file} {platform_xml}"
|
||||||
output = node.cmd(args)
|
node.cmd(args)
|
||||||
logging.info("node(%s) emane daemon running: %s", node.name, args)
|
logging.info("node(%s) emane daemon running: %s", node.name, args)
|
||||||
logging.debug("node(%s) emane daemon output: %s", node.name, output)
|
else:
|
||||||
|
path = self.session.session_dir
|
||||||
if not run_emane_on_host:
|
log_file = os.path.join(path, f"{iface.name}-emane.log")
|
||||||
return
|
platform_xml = os.path.join(path, f"{iface.name}-platform.xml")
|
||||||
|
emanecmd += f" -f {log_file} {platform_xml}"
|
||||||
path = self.session.session_dir
|
node.host_cmd(emanecmd, cwd=path)
|
||||||
log_file = os.path.join(path, "emane.log")
|
logging.info("node(%s) host emane daemon running: %s", node.name, emanecmd)
|
||||||
platform_xml = os.path.join(path, "platform.xml")
|
|
||||||
emanecmd += f" -f {log_file} {platform_xml}"
|
|
||||||
utils.cmd(emanecmd, cwd=path)
|
|
||||||
self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path))
|
|
||||||
logging.info("host emane daemon running: %s", emanecmd)
|
|
||||||
|
|
||||||
def stopdaemons(self) -> None:
|
def stopdaemons(self) -> None:
|
||||||
"""
|
"""
|
||||||
Kill the appropriate EMANE daemons.
|
Kill the appropriate EMANE daemons.
|
||||||
"""
|
"""
|
||||||
# TODO: we may want to improve this if we had the PIDs from the specific EMANE
|
|
||||||
# daemons that we"ve started
|
|
||||||
kill_emaned = "killall -q emane"
|
kill_emaned = "killall -q emane"
|
||||||
kill_transortd = "killall -q emanetransportd"
|
for node_id in sorted(self._emane_nets):
|
||||||
stop_emane_on_host = False
|
emane_net = self._emane_nets[node_id]
|
||||||
for node in self.getnodes():
|
for iface in emane_net.get_ifaces():
|
||||||
if isinstance(node, Rj45Node):
|
node = iface.node
|
||||||
stop_emane_on_host = True
|
if not node.up:
|
||||||
continue
|
continue
|
||||||
|
if iface.is_raw():
|
||||||
|
node.host_cmd(kill_emaned, wait=False)
|
||||||
|
else:
|
||||||
|
node.cmd(kill_emaned, wait=False)
|
||||||
|
|
||||||
if node.up:
|
def install_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
|
||||||
node.cmd(kill_emaned, wait=False)
|
config = self.get_iface_config(emane_net, iface)
|
||||||
# TODO: RJ45 node
|
external = config.get("external", "0")
|
||||||
|
if isinstance(iface, TunTap) and external == "0":
|
||||||
|
iface.set_ips()
|
||||||
|
# at this point we register location handlers for generating
|
||||||
|
# EMANE location events
|
||||||
|
if self.genlocationevents():
|
||||||
|
iface.poshook = emane_net.setnemposition
|
||||||
|
iface.setposition()
|
||||||
|
|
||||||
if stop_emane_on_host:
|
def deinstall_ifaces(self) -> None:
|
||||||
try:
|
|
||||||
utils.cmd(kill_emaned)
|
|
||||||
utils.cmd(kill_transortd)
|
|
||||||
self.session.distributed.execute(lambda x: x.remote_cmd(kill_emaned))
|
|
||||||
self.session.distributed.execute(lambda x: x.remote_cmd(kill_transortd))
|
|
||||||
except CoreCommandError:
|
|
||||||
logging.exception("error shutting down emane daemons")
|
|
||||||
|
|
||||||
def installnetifs(self) -> None:
|
|
||||||
"""
|
|
||||||
Install TUN/TAP virtual interfaces into their proper namespaces
|
|
||||||
now that the EMANE daemons are running.
|
|
||||||
"""
|
|
||||||
for key in sorted(self._emane_nets.keys()):
|
|
||||||
emane_node = self._emane_nets[key]
|
|
||||||
logging.info("emane install netifs for node: %d", key)
|
|
||||||
emane_node.installnetifs()
|
|
||||||
|
|
||||||
def deinstallnetifs(self) -> None:
|
|
||||||
"""
|
"""
|
||||||
Uninstall TUN/TAP virtual interfaces.
|
Uninstall TUN/TAP virtual interfaces.
|
||||||
"""
|
"""
|
||||||
for key in sorted(self._emane_nets.keys()):
|
for key in sorted(self._emane_nets):
|
||||||
emane_node = self._emane_nets[key]
|
emane_net = self._emane_nets[key]
|
||||||
emane_node.deinstallnetifs()
|
for iface in emane_net.get_ifaces():
|
||||||
|
if iface.is_virtual():
|
||||||
|
iface.shutdown()
|
||||||
|
iface.poshook = None
|
||||||
|
|
||||||
def doeventmonitor(self) -> bool:
|
def doeventmonitor(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -718,7 +643,6 @@ class EmaneManager(ModelManager):
|
||||||
logging.info("emane start event monitor")
|
logging.info("emane start event monitor")
|
||||||
if not self.doeventmonitor():
|
if not self.doeventmonitor():
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.service is None:
|
if self.service is None:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Warning: EMANE events will not be generated "
|
"Warning: EMANE events will not be generated "
|
||||||
|
@ -806,12 +730,12 @@ class EmaneManager(ModelManager):
|
||||||
Returns True if successfully parsed and a Node Message was sent.
|
Returns True if successfully parsed and a Node Message was sent.
|
||||||
"""
|
"""
|
||||||
# convert nemid to node number
|
# convert nemid to node number
|
||||||
_emanenode, netif = self.nemlookup(nemid)
|
iface = self.get_iface(nemid)
|
||||||
if netif is None:
|
if iface is None:
|
||||||
logging.info("location event for unknown NEM %s", nemid)
|
logging.info("location event for unknown NEM %s", nemid)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
n = netif.node.id
|
n = iface.node.id
|
||||||
# convert from lat/long/alt to x,y,z coordinates
|
# convert from lat/long/alt to x,y,z coordinates
|
||||||
x, y, z = self.session.location.getxyz(lat, lon, alt)
|
x, y, z = self.session.location.getxyz(lat, lon, alt)
|
||||||
x = int(x)
|
x = int(x)
|
||||||
|
@ -890,12 +814,12 @@ class EmaneGlobalModel:
|
||||||
Global EMANE configuration options.
|
Global EMANE configuration options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "emane"
|
name: str = "emane"
|
||||||
bitmap = None
|
bitmap: Optional[str] = None
|
||||||
|
|
||||||
def __init__(self, session: "Session") -> None:
|
def __init__(self, session: "Session") -> None:
|
||||||
self.session = session
|
self.session: "Session" = session
|
||||||
self.core_config = [
|
self.core_config: List[Configuration] = [
|
||||||
Configuration(
|
Configuration(
|
||||||
_id="platform_id_start",
|
_id="platform_id_start",
|
||||||
_type=ConfigDataTypes.INT32,
|
_type=ConfigDataTypes.INT32,
|
||||||
|
|
|
@ -11,6 +11,7 @@ except ImportError:
|
||||||
try:
|
try:
|
||||||
from emanesh import manifest
|
from emanesh import manifest
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
manifest = None
|
||||||
logging.debug("compatible emane python bindings not installed")
|
logging.debug("compatible emane python bindings not installed")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,13 @@ Defines Emane Models used within CORE.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Optional, Set
|
||||||
|
|
||||||
from core.config import ConfigGroup, Configuration
|
from core.config import ConfigGroup, Configuration
|
||||||
from core.emane import emanemanifest
|
from core.emane import emanemanifest
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.emulator.emudata import LinkOptions
|
from core.emulator.data import LinkOptions
|
||||||
from core.emulator.enumerations import ConfigDataTypes, TransportType
|
from core.emulator.enumerations import ConfigDataTypes
|
||||||
from core.errors import CoreError
|
from core.errors import CoreError
|
||||||
from core.location.mobility import WirelessModel
|
from core.location.mobility import WirelessModel
|
||||||
from core.nodes.base import CoreNode
|
from core.nodes.base import CoreNode
|
||||||
|
@ -25,19 +25,23 @@ class EmaneModel(WirelessModel):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# default mac configuration settings
|
# default mac configuration settings
|
||||||
mac_library = None
|
mac_library: Optional[str] = None
|
||||||
mac_xml = None
|
mac_xml: Optional[str] = None
|
||||||
mac_defaults = {}
|
mac_defaults: Dict[str, str] = {}
|
||||||
mac_config = []
|
mac_config: List[Configuration] = []
|
||||||
|
|
||||||
# default phy configuration settings, using the universal model
|
# default phy configuration settings, using the universal model
|
||||||
phy_library = None
|
phy_library: Optional[str] = None
|
||||||
phy_xml = "emanephy.xml"
|
phy_xml: str = "emanephy.xml"
|
||||||
phy_defaults = {"subid": "1", "propagationmodel": "2ray", "noisemode": "none"}
|
phy_defaults: Dict[str, str] = {
|
||||||
phy_config = []
|
"subid": "1",
|
||||||
|
"propagationmodel": "2ray",
|
||||||
|
"noisemode": "none",
|
||||||
|
}
|
||||||
|
phy_config: List[Configuration] = []
|
||||||
|
|
||||||
# support for external configurations
|
# support for external configurations
|
||||||
external_config = [
|
external_config: List[Configuration] = [
|
||||||
Configuration("external", ConfigDataTypes.BOOL, default="0"),
|
Configuration("external", ConfigDataTypes.BOOL, default="0"),
|
||||||
Configuration(
|
Configuration(
|
||||||
"platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001"
|
"platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001"
|
||||||
|
@ -47,7 +51,7 @@ class EmaneModel(WirelessModel):
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
config_ignore = set()
|
config_ignore: Set[str] = set()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, emane_prefix: str) -> None:
|
def load(cls, emane_prefix: str) -> None:
|
||||||
|
@ -92,45 +96,20 @@ class EmaneModel(WirelessModel):
|
||||||
ConfigGroup("External Parameters", phy_len + 1, config_len),
|
ConfigGroup("External Parameters", phy_len + 1, config_len),
|
||||||
]
|
]
|
||||||
|
|
||||||
def build_xml_files(
|
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
|
||||||
self, config: Dict[str, str], interface: CoreInterface = None
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Builds xml files for this emane model. Creates a nem.xml file that points to
|
Builds xml files for this emane model. Creates a nem.xml file that points to
|
||||||
both mac.xml and phy.xml definitions.
|
both mac.xml and phy.xml definitions.
|
||||||
|
|
||||||
:param config: emane model configuration for the node and interface
|
:param config: emane model configuration for the node and interface
|
||||||
:param interface: interface for the emane node
|
:param iface: interface to run emane for
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
nem_name = emanexml.nem_file_name(self, interface)
|
# create nem, mac, and phy xml files
|
||||||
mac_name = emanexml.mac_file_name(self, interface)
|
emanexml.create_nem_xml(self, iface, config)
|
||||||
phy_name = emanexml.phy_file_name(self, interface)
|
emanexml.create_mac_xml(self, iface, config)
|
||||||
|
emanexml.create_phy_xml(self, iface, config)
|
||||||
# remote server for file
|
emanexml.create_transport_xml(iface, config)
|
||||||
server = None
|
|
||||||
if interface is not None:
|
|
||||||
server = interface.node.server
|
|
||||||
|
|
||||||
# check if this is external
|
|
||||||
transport_type = TransportType.VIRTUAL
|
|
||||||
if interface and interface.transport_type == TransportType.RAW:
|
|
||||||
transport_type = TransportType.RAW
|
|
||||||
transport_name = emanexml.transport_file_name(self.id, transport_type)
|
|
||||||
|
|
||||||
# create nem xml file
|
|
||||||
nem_file = os.path.join(self.session.session_dir, nem_name)
|
|
||||||
emanexml.create_nem_xml(
|
|
||||||
self, config, nem_file, transport_name, mac_name, phy_name, server
|
|
||||||
)
|
|
||||||
|
|
||||||
# create mac xml file
|
|
||||||
mac_file = os.path.join(self.session.session_dir, mac_name)
|
|
||||||
emanexml.create_mac_xml(self, config, mac_file, server)
|
|
||||||
|
|
||||||
# create phy xml file
|
|
||||||
phy_file = os.path.join(self.session.session_dir, phy_name)
|
|
||||||
emanexml.create_phy_xml(self, config, phy_file, server)
|
|
||||||
|
|
||||||
def post_startup(self) -> None:
|
def post_startup(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -140,31 +119,31 @@ class EmaneModel(WirelessModel):
|
||||||
"""
|
"""
|
||||||
logging.debug("emane model(%s) has no post setup tasks", self.name)
|
logging.debug("emane model(%s) has no post setup tasks", self.name)
|
||||||
|
|
||||||
def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None:
|
def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None:
|
||||||
"""
|
"""
|
||||||
Invoked from MobilityModel when nodes are moved; this causes
|
Invoked from MobilityModel when nodes are moved; this causes
|
||||||
emane location events to be generated for the nodes in the moved
|
emane location events to be generated for the nodes in the moved
|
||||||
list, making EmaneModels compatible with Ns2ScriptedMobility.
|
list, making EmaneModels compatible with Ns2ScriptedMobility.
|
||||||
|
|
||||||
:param moved: moved nodes
|
:param moved: moved nodes
|
||||||
:param moved_netifs: interfaces that were moved
|
:param moved_ifaces: interfaces that were moved
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
wlan = self.session.get_node(self.id, EmaneNet)
|
wlan = self.session.get_node(self.id, EmaneNet)
|
||||||
wlan.setnempositions(moved_netifs)
|
wlan.setnempositions(moved_ifaces)
|
||||||
except CoreError:
|
except CoreError:
|
||||||
logging.exception("error during update")
|
logging.exception("error during update")
|
||||||
|
|
||||||
def linkconfig(
|
def linkconfig(
|
||||||
self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
|
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Invoked when a Link Message is received. Default is unimplemented.
|
Invoked when a Link Message is received. Default is unimplemented.
|
||||||
|
|
||||||
:param netif: interface one
|
:param iface: interface one
|
||||||
:param options: options for configuring link
|
:param options: options for configuring link
|
||||||
:param netif2: interface two
|
:param iface2: interface two
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
logging.warning("emane model(%s) does not support link config", self.name)
|
logging.warning("emane model(%s) does not support link config", self.name)
|
||||||
|
|
|
@ -8,11 +8,11 @@ from core.emane import emanemodel
|
||||||
|
|
||||||
class EmaneIeee80211abgModel(emanemodel.EmaneModel):
|
class EmaneIeee80211abgModel(emanemodel.EmaneModel):
|
||||||
# model name
|
# model name
|
||||||
name = "emane_ieee80211abg"
|
name: str = "emane_ieee80211abg"
|
||||||
|
|
||||||
# mac configuration
|
# mac configuration
|
||||||
mac_library = "ieee80211abgmaclayer"
|
mac_library: str = "ieee80211abgmaclayer"
|
||||||
mac_xml = "ieee80211abgmaclayer.xml"
|
mac_xml: str = "ieee80211abgmaclayer.xml"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, emane_prefix: str) -> None:
|
def load(cls, emane_prefix: str) -> None:
|
||||||
|
|
|
@ -2,9 +2,8 @@ import logging
|
||||||
import sched
|
import sched
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Dict, List, Tuple
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
import netaddr
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from core.emulator.data import LinkData
|
from core.emulator.data import LinkData
|
||||||
|
@ -17,28 +16,29 @@ except ImportError:
|
||||||
try:
|
try:
|
||||||
from emanesh import shell
|
from emanesh import shell
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
shell = None
|
||||||
logging.debug("compatible emane python bindings not installed")
|
logging.debug("compatible emane python bindings not installed")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.emane.emanemanager import EmaneManager
|
from core.emane.emanemanager import EmaneManager
|
||||||
|
|
||||||
DEFAULT_PORT = 47_000
|
DEFAULT_PORT: int = 47_000
|
||||||
MAC_COMPONENT_INDEX = 1
|
MAC_COMPONENT_INDEX: int = 1
|
||||||
EMANE_RFPIPE = "rfpipemaclayer"
|
EMANE_RFPIPE: str = "rfpipemaclayer"
|
||||||
EMANE_80211 = "ieee80211abgmaclayer"
|
EMANE_80211: str = "ieee80211abgmaclayer"
|
||||||
EMANE_TDMA = "tdmaeventschedulerradiomodel"
|
EMANE_TDMA: str = "tdmaeventschedulerradiomodel"
|
||||||
SINR_TABLE = "NeighborStatusTable"
|
SINR_TABLE: str = "NeighborStatusTable"
|
||||||
NEM_SELF = 65535
|
NEM_SELF: int = 65535
|
||||||
|
|
||||||
|
|
||||||
class LossTable:
|
class LossTable:
|
||||||
def __init__(self, losses: Dict[float, float]) -> None:
|
def __init__(self, losses: Dict[float, float]) -> None:
|
||||||
self.losses = losses
|
self.losses: Dict[float, float] = losses
|
||||||
self.sinrs = sorted(self.losses.keys())
|
self.sinrs: List[float] = sorted(self.losses.keys())
|
||||||
self.loss_lookup = {}
|
self.loss_lookup: Dict[int, float] = {}
|
||||||
for index, value in enumerate(self.sinrs):
|
for index, value in enumerate(self.sinrs):
|
||||||
self.loss_lookup[index] = self.losses[value]
|
self.loss_lookup[index] = self.losses[value]
|
||||||
self.mac_id = None
|
self.mac_id: Optional[str] = None
|
||||||
|
|
||||||
def get_loss(self, sinr: float) -> float:
|
def get_loss(self, sinr: float) -> float:
|
||||||
index = self._get_index(sinr)
|
index = self._get_index(sinr)
|
||||||
|
@ -54,11 +54,11 @@ class LossTable:
|
||||||
|
|
||||||
class EmaneLink:
|
class EmaneLink:
|
||||||
def __init__(self, from_nem: int, to_nem: int, sinr: float) -> None:
|
def __init__(self, from_nem: int, to_nem: int, sinr: float) -> None:
|
||||||
self.from_nem = from_nem
|
self.from_nem: int = from_nem
|
||||||
self.to_nem = to_nem
|
self.to_nem: int = to_nem
|
||||||
self.sinr = sinr
|
self.sinr: float = sinr
|
||||||
self.last_seen = None
|
self.last_seen: Optional[float] = None
|
||||||
self.updated = False
|
self.updated: bool = False
|
||||||
self.touch()
|
self.touch()
|
||||||
|
|
||||||
def update(self, sinr: float) -> None:
|
def update(self, sinr: float) -> None:
|
||||||
|
@ -78,9 +78,11 @@ class EmaneLink:
|
||||||
|
|
||||||
class EmaneClient:
|
class EmaneClient:
|
||||||
def __init__(self, address: str) -> None:
|
def __init__(self, address: str) -> None:
|
||||||
self.address = address
|
self.address: str = address
|
||||||
self.client = shell.ControlPortClient(self.address, DEFAULT_PORT)
|
self.client: shell.ControlPortClient = shell.ControlPortClient(
|
||||||
self.nems = {}
|
self.address, DEFAULT_PORT
|
||||||
|
)
|
||||||
|
self.nems: Dict[int, LossTable] = {}
|
||||||
self.setup()
|
self.setup()
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
|
@ -174,15 +176,15 @@ class EmaneClient:
|
||||||
|
|
||||||
class EmaneLinkMonitor:
|
class EmaneLinkMonitor:
|
||||||
def __init__(self, emane_manager: "EmaneManager") -> None:
|
def __init__(self, emane_manager: "EmaneManager") -> None:
|
||||||
self.emane_manager = emane_manager
|
self.emane_manager: "EmaneManager" = emane_manager
|
||||||
self.clients = []
|
self.clients: List[EmaneClient] = []
|
||||||
self.links = {}
|
self.links: Dict[Tuple[int, int], EmaneLink] = {}
|
||||||
self.complete_links = set()
|
self.complete_links: Set[Tuple[int, int]] = set()
|
||||||
self.loss_threshold = None
|
self.loss_threshold: Optional[int] = None
|
||||||
self.link_interval = None
|
self.link_interval: Optional[int] = None
|
||||||
self.link_timeout = None
|
self.link_timeout: Optional[int] = None
|
||||||
self.scheduler = None
|
self.scheduler: Optional[sched.scheduler] = None
|
||||||
self.running = False
|
self.running: bool = False
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
self.loss_threshold = int(self.emane_manager.get_config("loss_threshold"))
|
self.loss_threshold = int(self.emane_manager.get_config("loss_threshold"))
|
||||||
|
@ -209,15 +211,12 @@ class EmaneLinkMonitor:
|
||||||
addresses = []
|
addresses = []
|
||||||
nodes = self.emane_manager.getnodes()
|
nodes = self.emane_manager.getnodes()
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
for netif in node.netifs():
|
for iface in node.get_ifaces():
|
||||||
if isinstance(netif.net, CtrlNet):
|
if isinstance(iface.net, CtrlNet):
|
||||||
ip4 = None
|
ip4 = iface.get_ip4()
|
||||||
for x in netif.addrlist:
|
|
||||||
address, prefix = x.split("/")
|
|
||||||
if netaddr.valid_ipv4(address):
|
|
||||||
ip4 = address
|
|
||||||
if ip4:
|
if ip4:
|
||||||
addresses.append(ip4)
|
address = str(ip4.ip)
|
||||||
|
addresses.append(address)
|
||||||
break
|
break
|
||||||
return addresses
|
return addresses
|
||||||
|
|
||||||
|
@ -266,11 +265,11 @@ class EmaneLinkMonitor:
|
||||||
self.scheduler.enter(self.link_interval, 0, self.check_links)
|
self.scheduler.enter(self.link_interval, 0, self.check_links)
|
||||||
|
|
||||||
def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]:
|
def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]:
|
||||||
value_one, value_two = link_id
|
value1, value2 = link_id
|
||||||
if value_one < value_two:
|
if value1 < value2:
|
||||||
return value_one, value_two
|
return value1, value2
|
||||||
else:
|
else:
|
||||||
return value_two, value_one
|
return value2, value1
|
||||||
|
|
||||||
def is_complete_link(self, link_id: Tuple[int, int]) -> bool:
|
def is_complete_link(self, link_id: Tuple[int, int]) -> bool:
|
||||||
reverse_id = link_id[1], link_id[0]
|
reverse_id = link_id[1], link_id[0]
|
||||||
|
@ -284,8 +283,8 @@ class EmaneLinkMonitor:
|
||||||
return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}"
|
return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}"
|
||||||
|
|
||||||
def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None:
|
def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None:
|
||||||
nem_one, nem_two = link_id
|
nem1, nem2 = link_id
|
||||||
link = self.emane_manager.get_nem_link(nem_one, nem_two, message_type)
|
link = self.emane_manager.get_nem_link(nem1, nem2, message_type)
|
||||||
if link:
|
if link:
|
||||||
label = self.get_link_label(link_id)
|
label = self.get_link_label(link_id)
|
||||||
link.label = label
|
link.label = label
|
||||||
|
@ -295,18 +294,18 @@ class EmaneLinkMonitor:
|
||||||
self,
|
self,
|
||||||
message_type: MessageFlags,
|
message_type: MessageFlags,
|
||||||
label: str,
|
label: str,
|
||||||
node_one: int,
|
node1: int,
|
||||||
node_two: int,
|
node2: int,
|
||||||
emane_id: int,
|
emane_id: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
color = self.emane_manager.session.get_link_color(emane_id)
|
color = self.emane_manager.session.get_link_color(emane_id)
|
||||||
link_data = LinkData(
|
link_data = LinkData(
|
||||||
message_type=message_type,
|
message_type=message_type,
|
||||||
|
type=LinkTypes.WIRELESS,
|
||||||
label=label,
|
label=label,
|
||||||
node1_id=node_one,
|
node1_id=node1,
|
||||||
node2_id=node_two,
|
node2_id=node2,
|
||||||
network_id=emane_id,
|
network_id=emane_id,
|
||||||
link_type=LinkTypes.WIRELESS,
|
|
||||||
color=color,
|
color=color,
|
||||||
)
|
)
|
||||||
self.emane_manager.session.broadcast_link(link_data)
|
self.emane_manager.session.broadcast_link(link_data)
|
||||||
|
|
|
@ -6,23 +6,25 @@ share the same MAC+PHY model.
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
from core.emulator.data import LinkData
|
from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
||||||
from core.emulator.distributed import DistributedServer
|
from core.emulator.distributed import DistributedServer
|
||||||
from core.emulator.emudata import LinkOptions
|
|
||||||
from core.emulator.enumerations import (
|
from core.emulator.enumerations import (
|
||||||
|
EventTypes,
|
||||||
LinkTypes,
|
LinkTypes,
|
||||||
MessageFlags,
|
MessageFlags,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
RegisterTlvs,
|
RegisterTlvs,
|
||||||
TransportType,
|
|
||||||
)
|
)
|
||||||
from core.nodes.base import CoreNetworkBase
|
from core.errors import CoreError
|
||||||
|
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||||
from core.nodes.interface import CoreInterface
|
from core.nodes.interface import CoreInterface
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from core.emane.emanemodel import EmaneModel
|
||||||
from core.emulator.session import Session
|
from core.emulator.session import Session
|
||||||
from core.location.mobility import WirelessModel
|
from core.location.mobility import WirelessModel, WayPointMobility
|
||||||
|
|
||||||
|
OptionalEmaneModel = Optional[EmaneModel]
|
||||||
WirelessModelType = Type[WirelessModel]
|
WirelessModelType = Type[WirelessModel]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -31,6 +33,7 @@ except ImportError:
|
||||||
try:
|
try:
|
||||||
from emanesh.events import LocationEvent
|
from emanesh.events import LocationEvent
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
LocationEvent = None
|
||||||
logging.debug("compatible emane python bindings not installed")
|
logging.debug("compatible emane python bindings not installed")
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,60 +44,63 @@ class EmaneNet(CoreNetworkBase):
|
||||||
Emane controller object that exists in a session.
|
Emane controller object that exists in a session.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
apitype = NodeTypes.EMANE
|
apitype: NodeTypes = NodeTypes.EMANE
|
||||||
linktype = LinkTypes.WIRED
|
linktype: LinkTypes = LinkTypes.WIRED
|
||||||
type = "wlan"
|
type: str = "wlan"
|
||||||
is_emane = True
|
has_custom_iface: bool = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: "Session",
|
session: "Session",
|
||||||
_id: int = None,
|
_id: int = None,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
start: bool = True,
|
|
||||||
server: DistributedServer = None,
|
server: DistributedServer = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(session, _id, name, start, server)
|
super().__init__(session, _id, name, server)
|
||||||
self.conf = ""
|
self.conf: str = ""
|
||||||
self.nemidmap = {}
|
self.model: "OptionalEmaneModel" = None
|
||||||
self.model = None
|
self.mobility: Optional[WayPointMobility] = None
|
||||||
self.mobility = None
|
|
||||||
|
|
||||||
def linkconfig(
|
def linkconfig(
|
||||||
self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
|
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
The CommEffect model supports link configuration.
|
The CommEffect model supports link configuration.
|
||||||
"""
|
"""
|
||||||
if not self.model:
|
if not self.model:
|
||||||
return
|
return
|
||||||
self.model.linkconfig(netif, options, netif2)
|
self.model.linkconfig(iface, options, iface2)
|
||||||
|
|
||||||
def config(self, conf: str) -> None:
|
def config(self, conf: str) -> None:
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
|
|
||||||
|
def startup(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
|
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
|
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
|
||||||
|
raise CoreError("emane networks cannot be linked to other networks")
|
||||||
|
|
||||||
def updatemodel(self, config: Dict[str, str]) -> None:
|
def updatemodel(self, config: Dict[str, str]) -> None:
|
||||||
if not self.model:
|
if not self.model:
|
||||||
raise ValueError("no model set to update for node(%s)", self.id)
|
raise CoreError(f"no model set to update for node({self.name})")
|
||||||
logging.info(
|
logging.info(
|
||||||
"node(%s) updating model(%s): %s", self.id, self.model.name, config
|
"node(%s) updating model(%s): %s", self.id, self.model.name, config
|
||||||
)
|
)
|
||||||
self.model.set_configs(config, node_id=self.id)
|
self.model.update_config(config)
|
||||||
|
|
||||||
def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None:
|
def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None:
|
||||||
"""
|
"""
|
||||||
set the EmaneModel associated with this node
|
set the EmaneModel associated with this node
|
||||||
"""
|
"""
|
||||||
logging.info("adding model: %s", model.name)
|
|
||||||
if model.config_type == RegisterTlvs.WIRELESS:
|
if model.config_type == RegisterTlvs.WIRELESS:
|
||||||
# EmaneModel really uses values from ConfigurableManager
|
# EmaneModel really uses values from ConfigurableManager
|
||||||
# when buildnemxml() is called, not during init()
|
# when buildnemxml() is called, not during init()
|
||||||
|
@ -104,94 +110,21 @@ class EmaneNet(CoreNetworkBase):
|
||||||
self.mobility = model(session=self.session, _id=self.id)
|
self.mobility = model(session=self.session, _id=self.id)
|
||||||
self.mobility.update_config(config)
|
self.mobility.update_config(config)
|
||||||
|
|
||||||
def setnemid(self, netif: CoreInterface, nemid: int) -> None:
|
|
||||||
"""
|
|
||||||
Record an interface to numerical ID mapping. The Emane controller
|
|
||||||
object manages and assigns these IDs for all NEMs.
|
|
||||||
"""
|
|
||||||
self.nemidmap[netif] = nemid
|
|
||||||
|
|
||||||
def getnemid(self, netif: CoreInterface) -> Optional[int]:
|
|
||||||
"""
|
|
||||||
Given an interface, return its numerical ID.
|
|
||||||
"""
|
|
||||||
if netif not in self.nemidmap:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return self.nemidmap[netif]
|
|
||||||
|
|
||||||
def getnemnetif(self, nemid: int) -> Optional[CoreInterface]:
|
|
||||||
"""
|
|
||||||
Given a numerical NEM ID, return its interface. This returns the
|
|
||||||
first interface that matches the given NEM ID.
|
|
||||||
"""
|
|
||||||
for netif in self.nemidmap:
|
|
||||||
if self.nemidmap[netif] == nemid:
|
|
||||||
return netif
|
|
||||||
return None
|
|
||||||
|
|
||||||
def netifs(self, sort: bool = True) -> List[CoreInterface]:
|
|
||||||
"""
|
|
||||||
Retrieve list of linked interfaces sorted by node number.
|
|
||||||
"""
|
|
||||||
return sorted(self._netif.values(), key=lambda ifc: ifc.node.id)
|
|
||||||
|
|
||||||
def installnetifs(self) -> None:
|
|
||||||
"""
|
|
||||||
Install TAP devices into their namespaces. This is done after
|
|
||||||
EMANE daemons have been started, because that is their only chance
|
|
||||||
to bind to the TAPs.
|
|
||||||
"""
|
|
||||||
if (
|
|
||||||
self.session.emane.genlocationevents()
|
|
||||||
and self.session.emane.service is None
|
|
||||||
):
|
|
||||||
warntxt = "unable to publish EMANE events because the eventservice "
|
|
||||||
warntxt += "Python bindings failed to load"
|
|
||||||
logging.error(warntxt)
|
|
||||||
|
|
||||||
for netif in self.netifs():
|
|
||||||
external = self.session.emane.get_config(
|
|
||||||
"external", self.id, self.model.name
|
|
||||||
)
|
|
||||||
if external == "0":
|
|
||||||
netif.setaddrs()
|
|
||||||
|
|
||||||
if not self.session.emane.genlocationevents():
|
|
||||||
netif.poshook = None
|
|
||||||
continue
|
|
||||||
|
|
||||||
# at this point we register location handlers for generating
|
|
||||||
# EMANE location events
|
|
||||||
netif.poshook = self.setnemposition
|
|
||||||
netif.setposition()
|
|
||||||
|
|
||||||
def deinstallnetifs(self) -> None:
|
|
||||||
"""
|
|
||||||
Uninstall TAP devices. This invokes their shutdown method for
|
|
||||||
any required cleanup; the device may be actually removed when
|
|
||||||
emanetransportd terminates.
|
|
||||||
"""
|
|
||||||
for netif in self.netifs():
|
|
||||||
if netif.transport_type == TransportType.VIRTUAL:
|
|
||||||
netif.shutdown()
|
|
||||||
netif.poshook = None
|
|
||||||
|
|
||||||
def _nem_position(
|
def _nem_position(
|
||||||
self, netif: CoreInterface
|
self, iface: CoreInterface
|
||||||
) -> Optional[Tuple[int, float, float, float]]:
|
) -> Optional[Tuple[int, float, float, float]]:
|
||||||
"""
|
"""
|
||||||
Creates nem position for emane event for a given interface.
|
Creates nem position for emane event for a given interface.
|
||||||
|
|
||||||
:param netif: interface to get nem emane position for
|
:param iface: interface to get nem emane position for
|
||||||
:return: nem position tuple, None otherwise
|
:return: nem position tuple, None otherwise
|
||||||
"""
|
"""
|
||||||
nemid = self.getnemid(netif)
|
nem_id = self.session.emane.get_nem_id(iface)
|
||||||
ifname = netif.localname
|
ifname = iface.localname
|
||||||
if nemid is None:
|
if nem_id is None:
|
||||||
logging.info("nemid for %s is unknown", ifname)
|
logging.info("nemid for %s is unknown", ifname)
|
||||||
return
|
return
|
||||||
node = netif.node
|
node = iface.node
|
||||||
x, y, z = node.getposition()
|
x, y, z = node.getposition()
|
||||||
lat, lon, alt = self.session.location.getgeo(x, y, z)
|
lat, lon, alt = self.session.location.getgeo(x, y, z)
|
||||||
if node.position.alt is not None:
|
if node.position.alt is not None:
|
||||||
|
@ -199,32 +132,31 @@ class EmaneNet(CoreNetworkBase):
|
||||||
node.position.set_geo(lon, lat, alt)
|
node.position.set_geo(lon, lat, alt)
|
||||||
# altitude must be an integer or warning is printed
|
# altitude must be an integer or warning is printed
|
||||||
alt = int(round(alt))
|
alt = int(round(alt))
|
||||||
return nemid, lon, lat, alt
|
return nem_id, lon, lat, alt
|
||||||
|
|
||||||
def setnemposition(self, netif: CoreInterface) -> None:
|
def setnemposition(self, iface: CoreInterface) -> None:
|
||||||
"""
|
"""
|
||||||
Publish a NEM location change event using the EMANE event service.
|
Publish a NEM location change event using the EMANE event service.
|
||||||
|
|
||||||
:param netif: interface to set nem position for
|
:param iface: interface to set nem position for
|
||||||
"""
|
"""
|
||||||
if self.session.emane.service is None:
|
if self.session.emane.service is None:
|
||||||
logging.info("position service not available")
|
logging.info("position service not available")
|
||||||
return
|
return
|
||||||
|
position = self._nem_position(iface)
|
||||||
position = self._nem_position(netif)
|
|
||||||
if position:
|
if position:
|
||||||
nemid, lon, lat, alt = position
|
nemid, lon, lat, alt = position
|
||||||
event = LocationEvent()
|
event = LocationEvent()
|
||||||
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
||||||
self.session.emane.service.publish(0, event)
|
self.session.emane.service.publish(0, event)
|
||||||
|
|
||||||
def setnempositions(self, moved_netifs: List[CoreInterface]) -> None:
|
def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None:
|
||||||
"""
|
"""
|
||||||
Several NEMs have moved, from e.g. a WaypointMobilityModel
|
Several NEMs have moved, from e.g. a WaypointMobilityModel
|
||||||
calculation. Generate an EMANE Location Event having several
|
calculation. Generate an EMANE Location Event having several
|
||||||
entries for each netif that has moved.
|
entries for each interface that has moved.
|
||||||
"""
|
"""
|
||||||
if len(moved_netifs) == 0:
|
if len(moved_ifaces) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.session.emane.service is None:
|
if self.session.emane.service is None:
|
||||||
|
@ -232,18 +164,21 @@ class EmaneNet(CoreNetworkBase):
|
||||||
return
|
return
|
||||||
|
|
||||||
event = LocationEvent()
|
event = LocationEvent()
|
||||||
for netif in moved_netifs:
|
for iface in moved_ifaces:
|
||||||
position = self._nem_position(netif)
|
position = self._nem_position(iface)
|
||||||
if position:
|
if position:
|
||||||
nemid, lon, lat, alt = position
|
nemid, lon, lat, alt = position
|
||||||
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
||||||
self.session.emane.service.publish(0, event)
|
self.session.emane.service.publish(0, event)
|
||||||
|
|
||||||
def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
||||||
links = super().all_link_data(flags)
|
links = super().links(flags)
|
||||||
# gather current emane links
|
|
||||||
nem_ids = set(self.nemidmap.values())
|
|
||||||
emane_manager = self.session.emane
|
emane_manager = self.session.emane
|
||||||
|
# gather current emane links
|
||||||
|
nem_ids = set()
|
||||||
|
for iface in self.get_ifaces():
|
||||||
|
nem_id = emane_manager.get_nem_id(iface)
|
||||||
|
nem_ids.add(nem_id)
|
||||||
emane_links = emane_manager.link_monitor.links
|
emane_links = emane_manager.link_monitor.links
|
||||||
considered = set()
|
considered = set()
|
||||||
for link_key in emane_links:
|
for link_key in emane_links:
|
||||||
|
@ -262,3 +197,18 @@ class EmaneNet(CoreNetworkBase):
|
||||||
if link:
|
if link:
|
||||||
links.append(link)
|
links.append(link)
|
||||||
return links
|
return links
|
||||||
|
|
||||||
|
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
||||||
|
# TUN/TAP is not ready for addressing yet; the device may
|
||||||
|
# take some time to appear, and installing it into a
|
||||||
|
# namespace after it has been bound removes addressing;
|
||||||
|
# save addresses with the interface now
|
||||||
|
iface_id = node.newtuntap(iface_data.id, iface_data.name)
|
||||||
|
node.attachnet(iface_id, self)
|
||||||
|
iface = node.get_iface(iface_id)
|
||||||
|
iface.set_mac(iface_data.mac)
|
||||||
|
for ip in iface_data.get_ips():
|
||||||
|
iface.add_ip(ip)
|
||||||
|
if self.session.state == EventTypes.RUNTIME_STATE:
|
||||||
|
self.session.emane.start_iface(self, iface)
|
||||||
|
return iface
|
||||||
|
|
|
@ -8,11 +8,11 @@ from core.emane import emanemodel
|
||||||
|
|
||||||
class EmaneRfPipeModel(emanemodel.EmaneModel):
|
class EmaneRfPipeModel(emanemodel.EmaneModel):
|
||||||
# model name
|
# model name
|
||||||
name = "emane_rfpipe"
|
name: str = "emane_rfpipe"
|
||||||
|
|
||||||
# mac configuration
|
# mac configuration
|
||||||
mac_library = "rfpipemaclayer"
|
mac_library: str = "rfpipemaclayer"
|
||||||
mac_xml = "rfpipemaclayer.xml"
|
mac_xml: str = "rfpipemaclayer.xml"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, emane_prefix: str) -> None:
|
def load(cls, emane_prefix: str) -> None:
|
||||||
|
|
|
@ -4,6 +4,7 @@ tdma.py: EMANE TDMA model bindings for CORE
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
from core import constants, utils
|
from core import constants, utils
|
||||||
from core.config import Configuration
|
from core.config import Configuration
|
||||||
|
@ -13,18 +14,18 @@ from core.emulator.enumerations import ConfigDataTypes
|
||||||
|
|
||||||
class EmaneTdmaModel(emanemodel.EmaneModel):
|
class EmaneTdmaModel(emanemodel.EmaneModel):
|
||||||
# model name
|
# model name
|
||||||
name = "emane_tdma"
|
name: str = "emane_tdma"
|
||||||
|
|
||||||
# mac configuration
|
# mac configuration
|
||||||
mac_library = "tdmaeventschedulerradiomodel"
|
mac_library: str = "tdmaeventschedulerradiomodel"
|
||||||
mac_xml = "tdmaeventschedulerradiomodel.xml"
|
mac_xml: str = "tdmaeventschedulerradiomodel.xml"
|
||||||
|
|
||||||
# add custom schedule options and ignore it when writing emane xml
|
# add custom schedule options and ignore it when writing emane xml
|
||||||
schedule_name = "schedule"
|
schedule_name: str = "schedule"
|
||||||
default_schedule = os.path.join(
|
default_schedule: str = os.path.join(
|
||||||
constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml"
|
constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml"
|
||||||
)
|
)
|
||||||
config_ignore = {schedule_name}
|
config_ignore: Set[str] = {schedule_name}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, emane_prefix: str) -> None:
|
def load(cls, emane_prefix: str) -> None:
|
||||||
|
|
|
@ -6,9 +6,10 @@ import sys
|
||||||
from typing import Dict, List, Type
|
from typing import Dict, List, Type
|
||||||
|
|
||||||
import core.services
|
import core.services
|
||||||
from core import configservices
|
from core import configservices, utils
|
||||||
from core.configservice.manager import ConfigServiceManager
|
from core.configservice.manager import ConfigServiceManager
|
||||||
from core.emulator.session import Session
|
from core.emulator.session import Session
|
||||||
|
from core.executables import get_requirements
|
||||||
from core.services.coreservices import ServiceManager
|
from core.services.coreservices import ServiceManager
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,10 +66,29 @@ class CoreEmu:
|
||||||
if custom_dir:
|
if custom_dir:
|
||||||
self.service_manager.load(custom_dir)
|
self.service_manager.load(custom_dir)
|
||||||
|
|
||||||
|
# check executables exist on path
|
||||||
|
self._validate_env()
|
||||||
|
|
||||||
# catch exit event
|
# catch exit event
|
||||||
atexit.register(self.shutdown)
|
atexit.register(self.shutdown)
|
||||||
|
|
||||||
|
def _validate_env(self) -> None:
|
||||||
|
"""
|
||||||
|
Validates executables CORE depends on exist on path.
|
||||||
|
|
||||||
|
:return: nothing
|
||||||
|
:raises core.errors.CoreError: when an executable does not exist on path
|
||||||
|
"""
|
||||||
|
use_ovs = self.config.get("ovs") == "1"
|
||||||
|
for requirement in get_requirements(use_ovs):
|
||||||
|
utils.which(requirement, required=True)
|
||||||
|
|
||||||
def load_services(self) -> None:
|
def load_services(self) -> None:
|
||||||
|
"""
|
||||||
|
Loads default and custom services for use within CORE.
|
||||||
|
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
# load default services
|
# load default services
|
||||||
self.service_errors = core.services.load()
|
self.service_errors = core.services.load()
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
"""
|
"""
|
||||||
CORE data objects.
|
CORE data objects.
|
||||||
"""
|
"""
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||||
|
|
||||||
from dataclasses import dataclass
|
import netaddr
|
||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
|
from core import utils
|
||||||
from core.emulator.enumerations import (
|
from core.emulator.enumerations import (
|
||||||
EventTypes,
|
EventTypes,
|
||||||
ExceptionLevels,
|
ExceptionLevels,
|
||||||
LinkTypes,
|
LinkTypes,
|
||||||
MessageFlags,
|
MessageFlags,
|
||||||
NodeTypes,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from core.nodes.base import CoreNode, NodeBase
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ConfigData:
|
class ConfigData:
|
||||||
|
@ -27,7 +31,7 @@ class ConfigData:
|
||||||
possible_values: str = None
|
possible_values: str = None
|
||||||
groups: str = None
|
groups: str = None
|
||||||
session: int = None
|
session: int = None
|
||||||
interface_number: int = None
|
iface_id: int = None
|
||||||
network_id: int = None
|
network_id: int = None
|
||||||
opaque: str = None
|
opaque: str = None
|
||||||
|
|
||||||
|
@ -68,65 +72,218 @@ class FileData:
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NodeData:
|
class NodeOptions:
|
||||||
message_type: MessageFlags = None
|
"""
|
||||||
id: int = None
|
Options for creating and updating nodes within core.
|
||||||
node_type: NodeTypes = None
|
"""
|
||||||
|
|
||||||
name: str = None
|
name: str = None
|
||||||
ip_address: str = None
|
model: Optional[str] = "PC"
|
||||||
mac_address: str = None
|
|
||||||
ip6_address: str = None
|
|
||||||
model: str = None
|
|
||||||
emulation_id: int = None
|
|
||||||
server: str = None
|
|
||||||
session: int = None
|
|
||||||
x_position: float = None
|
|
||||||
y_position: float = None
|
|
||||||
canvas: int = None
|
canvas: int = None
|
||||||
network_id: int = None
|
|
||||||
services: List[str] = None
|
|
||||||
latitude: float = None
|
|
||||||
longitude: float = None
|
|
||||||
altitude: float = None
|
|
||||||
icon: str = None
|
icon: str = None
|
||||||
opaque: str = None
|
services: List[str] = field(default_factory=list)
|
||||||
|
config_services: List[str] = field(default_factory=list)
|
||||||
|
x: float = None
|
||||||
|
y: float = None
|
||||||
|
lat: float = None
|
||||||
|
lon: float = None
|
||||||
|
alt: float = None
|
||||||
|
server: str = None
|
||||||
|
image: str = None
|
||||||
|
emane: str = None
|
||||||
|
|
||||||
|
def set_position(self, x: float, y: float) -> None:
|
||||||
|
"""
|
||||||
|
Convenience method for setting position.
|
||||||
|
|
||||||
|
:param x: x position
|
||||||
|
:param y: y position
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
|
||||||
|
def set_location(self, lat: float, lon: float, alt: float) -> None:
|
||||||
|
"""
|
||||||
|
Convenience method for setting location.
|
||||||
|
|
||||||
|
:param lat: latitude
|
||||||
|
:param lon: longitude
|
||||||
|
:param alt: altitude
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
self.lat = lat
|
||||||
|
self.lon = lon
|
||||||
|
self.alt = alt
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NodeData:
|
||||||
|
"""
|
||||||
|
Node to broadcast.
|
||||||
|
"""
|
||||||
|
|
||||||
|
node: "NodeBase"
|
||||||
|
message_type: MessageFlags = None
|
||||||
source: str = None
|
source: str = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InterfaceData:
|
||||||
|
"""
|
||||||
|
Convenience class for storing interface data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: int = None
|
||||||
|
name: str = None
|
||||||
|
mac: str = None
|
||||||
|
ip4: str = None
|
||||||
|
ip4_mask: int = None
|
||||||
|
ip6: str = None
|
||||||
|
ip6_mask: int = None
|
||||||
|
|
||||||
|
def get_ips(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Returns a list of ip4 and ip6 addresses when present.
|
||||||
|
|
||||||
|
:return: list of ip addresses
|
||||||
|
"""
|
||||||
|
ips = []
|
||||||
|
if self.ip4 and self.ip4_mask:
|
||||||
|
ips.append(f"{self.ip4}/{self.ip4_mask}")
|
||||||
|
if self.ip6 and self.ip6_mask:
|
||||||
|
ips.append(f"{self.ip6}/{self.ip6_mask}")
|
||||||
|
return ips
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LinkOptions:
|
||||||
|
"""
|
||||||
|
Options for creating and updating links within core.
|
||||||
|
"""
|
||||||
|
|
||||||
|
delay: int = None
|
||||||
|
bandwidth: int = None
|
||||||
|
loss: float = None
|
||||||
|
dup: int = None
|
||||||
|
jitter: int = None
|
||||||
|
mer: int = None
|
||||||
|
burst: int = None
|
||||||
|
mburst: int = None
|
||||||
|
unidirectional: int = None
|
||||||
|
key: int = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LinkData:
|
class LinkData:
|
||||||
|
"""
|
||||||
|
Represents all data associated with a link.
|
||||||
|
"""
|
||||||
|
|
||||||
message_type: MessageFlags = None
|
message_type: MessageFlags = None
|
||||||
|
type: LinkTypes = LinkTypes.WIRED
|
||||||
label: str = None
|
label: str = None
|
||||||
node1_id: int = None
|
node1_id: int = None
|
||||||
node2_id: int = None
|
node2_id: int = None
|
||||||
delay: float = None
|
|
||||||
bandwidth: float = None
|
|
||||||
per: float = None
|
|
||||||
dup: float = None
|
|
||||||
jitter: float = None
|
|
||||||
mer: float = None
|
|
||||||
burst: float = None
|
|
||||||
session: int = None
|
|
||||||
mburst: float = None
|
|
||||||
link_type: LinkTypes = None
|
|
||||||
gui_attributes: str = None
|
|
||||||
unidirectional: int = None
|
|
||||||
emulation_id: int = None
|
|
||||||
network_id: int = None
|
network_id: int = None
|
||||||
key: int = None
|
iface1: InterfaceData = None
|
||||||
interface1_id: int = None
|
iface2: InterfaceData = None
|
||||||
interface1_name: str = None
|
options: LinkOptions = LinkOptions()
|
||||||
interface1_ip4: str = None
|
|
||||||
interface1_ip4_mask: int = None
|
|
||||||
interface1_mac: str = None
|
|
||||||
interface1_ip6: str = None
|
|
||||||
interface1_ip6_mask: int = None
|
|
||||||
interface2_id: int = None
|
|
||||||
interface2_name: str = None
|
|
||||||
interface2_ip4: str = None
|
|
||||||
interface2_ip4_mask: int = None
|
|
||||||
interface2_mac: str = None
|
|
||||||
interface2_ip6: str = None
|
|
||||||
interface2_ip6_mask: int = None
|
|
||||||
opaque: str = None
|
|
||||||
color: str = None
|
color: str = None
|
||||||
|
source: str = None
|
||||||
|
|
||||||
|
|
||||||
|
class IpPrefixes:
|
||||||
|
"""
|
||||||
|
Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
|
||||||
|
"""
|
||||||
|
Creates an IpPrefixes object.
|
||||||
|
|
||||||
|
:param ip4_prefix: ip4 prefix to use for generation
|
||||||
|
:param ip6_prefix: ip6 prefix to use for generation
|
||||||
|
:raises ValueError: when both ip4 and ip6 prefixes have not been provided
|
||||||
|
"""
|
||||||
|
if not ip4_prefix and not ip6_prefix:
|
||||||
|
raise ValueError("ip4 or ip6 must be provided")
|
||||||
|
|
||||||
|
self.ip4 = None
|
||||||
|
if ip4_prefix:
|
||||||
|
self.ip4 = netaddr.IPNetwork(ip4_prefix)
|
||||||
|
self.ip6 = None
|
||||||
|
if ip6_prefix:
|
||||||
|
self.ip6 = netaddr.IPNetwork(ip6_prefix)
|
||||||
|
|
||||||
|
def ip4_address(self, node_id: int) -> str:
|
||||||
|
"""
|
||||||
|
Convenience method to return the IP4 address for a node.
|
||||||
|
|
||||||
|
:param node_id: node id to get IP4 address for
|
||||||
|
:return: IP4 address or None
|
||||||
|
"""
|
||||||
|
if not self.ip4:
|
||||||
|
raise ValueError("ip4 prefixes have not been set")
|
||||||
|
return str(self.ip4[node_id])
|
||||||
|
|
||||||
|
def ip6_address(self, node_id: int) -> str:
|
||||||
|
"""
|
||||||
|
Convenience method to return the IP6 address for a node.
|
||||||
|
|
||||||
|
:param node_id: node id to get IP6 address for
|
||||||
|
:return: IP4 address or None
|
||||||
|
"""
|
||||||
|
if not self.ip6:
|
||||||
|
raise ValueError("ip6 prefixes have not been set")
|
||||||
|
return str(self.ip6[node_id])
|
||||||
|
|
||||||
|
def gen_iface(self, node_id: int, name: str = None, mac: str = None):
|
||||||
|
"""
|
||||||
|
Creates interface data for linking nodes, using the nodes unique id for
|
||||||
|
generation, along with a random mac address, unless provided.
|
||||||
|
|
||||||
|
:param node_id: node id to create an interface for
|
||||||
|
:param name: name to set for interface, default is eth{id}
|
||||||
|
:param mac: mac address to use for this interface, default is random
|
||||||
|
generation
|
||||||
|
:return: new interface data for the provided node
|
||||||
|
"""
|
||||||
|
# generate ip4 data
|
||||||
|
ip4 = None
|
||||||
|
ip4_mask = None
|
||||||
|
if self.ip4:
|
||||||
|
ip4 = self.ip4_address(node_id)
|
||||||
|
ip4_mask = self.ip4.prefixlen
|
||||||
|
|
||||||
|
# generate ip6 data
|
||||||
|
ip6 = None
|
||||||
|
ip6_mask = None
|
||||||
|
if self.ip6:
|
||||||
|
ip6 = self.ip6_address(node_id)
|
||||||
|
ip6_mask = self.ip6.prefixlen
|
||||||
|
|
||||||
|
# random mac
|
||||||
|
if not mac:
|
||||||
|
mac = utils.random_mac()
|
||||||
|
|
||||||
|
return InterfaceData(
|
||||||
|
name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_iface(
|
||||||
|
self, node: "CoreNode", name: str = None, mac: str = None
|
||||||
|
) -> InterfaceData:
|
||||||
|
"""
|
||||||
|
Creates interface data for linking nodes, using the nodes unique id for
|
||||||
|
generation, along with a random mac address, unless provided.
|
||||||
|
|
||||||
|
:param node: node to create interface for
|
||||||
|
:param name: name to set for interface, default is eth{id}
|
||||||
|
:param mac: mac address to use for this interface, default is random
|
||||||
|
generation
|
||||||
|
:return: new interface data for the provided node
|
||||||
|
"""
|
||||||
|
iface_data = self.gen_iface(node.id, name, mac)
|
||||||
|
iface_data.id = node.next_iface_id()
|
||||||
|
return iface_data
|
||||||
|
|
|
@ -14,7 +14,8 @@ from fabric import Connection
|
||||||
from invoke import UnexpectedExit
|
from invoke import UnexpectedExit
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
from core.errors import CoreCommandError
|
from core.errors import CoreCommandError, CoreError
|
||||||
|
from core.executables import get_requirements
|
||||||
from core.nodes.interface import GreTap
|
from core.nodes.interface import GreTap
|
||||||
from core.nodes.network import CoreNetwork, CtrlNet
|
from core.nodes.network import CoreNetwork, CtrlNet
|
||||||
|
|
||||||
|
@ -131,8 +132,17 @@ class DistributedController:
|
||||||
:param name: distributed server name
|
:param name: distributed server name
|
||||||
:param host: distributed server host address
|
:param host: distributed server host address
|
||||||
:return: nothing
|
:return: nothing
|
||||||
|
:raises CoreError: when there is an error validating server
|
||||||
"""
|
"""
|
||||||
server = DistributedServer(name, host)
|
server = DistributedServer(name, host)
|
||||||
|
for requirement in get_requirements(self.session.use_ovs()):
|
||||||
|
try:
|
||||||
|
server.remote_cmd(f"which {requirement}")
|
||||||
|
except CoreCommandError:
|
||||||
|
raise CoreError(
|
||||||
|
f"server({server.name}) failed validation for "
|
||||||
|
f"command({requirement})"
|
||||||
|
)
|
||||||
self.servers[name] = server
|
self.servers[name] = server
|
||||||
cmd = f"mkdir -p {self.session.session_dir}"
|
cmd = f"mkdir -p {self.session.session_dir}"
|
||||||
server.remote_cmd(cmd)
|
server.remote_cmd(cmd)
|
||||||
|
@ -208,7 +218,7 @@ class DistributedController:
|
||||||
"local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key
|
"local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key
|
||||||
)
|
)
|
||||||
local_tap = GreTap(session=self.session, remoteip=host, key=key)
|
local_tap = GreTap(session=self.session, remoteip=host, key=key)
|
||||||
local_tap.net_client.set_interface_master(node.brname, local_tap.localname)
|
local_tap.net_client.set_iface_master(node.brname, local_tap.localname)
|
||||||
|
|
||||||
# server to local
|
# server to local
|
||||||
logging.info(
|
logging.info(
|
||||||
|
@ -217,25 +227,27 @@ class DistributedController:
|
||||||
remote_tap = GreTap(
|
remote_tap = GreTap(
|
||||||
session=self.session, remoteip=self.address, key=key, server=server
|
session=self.session, remoteip=self.address, key=key, server=server
|
||||||
)
|
)
|
||||||
remote_tap.net_client.set_interface_master(node.brname, remote_tap.localname)
|
remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
|
||||||
|
|
||||||
# save tunnels for shutdown
|
# save tunnels for shutdown
|
||||||
tunnel = (local_tap, remote_tap)
|
tunnel = (local_tap, remote_tap)
|
||||||
self.tunnels[key] = tunnel
|
self.tunnels[key] = tunnel
|
||||||
return tunnel
|
return tunnel
|
||||||
|
|
||||||
def tunnel_key(self, n1_id: int, n2_id: int) -> int:
|
def tunnel_key(self, node1_id: int, node2_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
Compute a 32-bit key used to uniquely identify a GRE tunnel.
|
Compute a 32-bit key used to uniquely identify a GRE tunnel.
|
||||||
The hash(n1num), hash(n2num) values are used, so node numbers may be
|
The hash(n1num), hash(n2num) values are used, so node numbers may be
|
||||||
None or string values (used for e.g. "ctrlnet").
|
None or string values (used for e.g. "ctrlnet").
|
||||||
|
|
||||||
:param n1_id: node one id
|
:param node1_id: node one id
|
||||||
:param n2_id: node two id
|
:param node2_id: node two id
|
||||||
:return: tunnel key for the node pair
|
:return: tunnel key for the node pair
|
||||||
"""
|
"""
|
||||||
logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id)
|
logging.debug("creating tunnel key for: %s, %s", node1_id, node2_id)
|
||||||
key = (
|
key = (
|
||||||
(self.session.id << 16) ^ utils.hashkey(n1_id) ^ (utils.hashkey(n2_id) << 8)
|
(self.session.id << 16)
|
||||||
|
^ utils.hashkey(node1_id)
|
||||||
|
^ (utils.hashkey(node2_id) << 8)
|
||||||
)
|
)
|
||||||
return key & 0xFFFFFFFF
|
return key & 0xFFFFFFFF
|
||||||
|
|
|
@ -1,206 +0,0 @@
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import TYPE_CHECKING, List, Optional
|
|
||||||
|
|
||||||
import netaddr
|
|
||||||
|
|
||||||
from core import utils
|
|
||||||
from core.emulator.enumerations import LinkTypes
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from core.nodes.base import CoreNode
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NodeOptions:
|
|
||||||
"""
|
|
||||||
Options for creating and updating nodes within core.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str = None
|
|
||||||
model: Optional[str] = "PC"
|
|
||||||
canvas: int = None
|
|
||||||
icon: str = None
|
|
||||||
opaque: str = None
|
|
||||||
services: List[str] = field(default_factory=list)
|
|
||||||
config_services: List[str] = field(default_factory=list)
|
|
||||||
x: float = None
|
|
||||||
y: float = None
|
|
||||||
lat: float = None
|
|
||||||
lon: float = None
|
|
||||||
alt: float = None
|
|
||||||
emulation_id: int = None
|
|
||||||
server: str = None
|
|
||||||
image: str = None
|
|
||||||
emane: str = None
|
|
||||||
|
|
||||||
def set_position(self, x: float, y: float) -> None:
|
|
||||||
"""
|
|
||||||
Convenience method for setting position.
|
|
||||||
|
|
||||||
:param x: x position
|
|
||||||
:param y: y position
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
|
|
||||||
def set_location(self, lat: float, lon: float, alt: float) -> None:
|
|
||||||
"""
|
|
||||||
Convenience method for setting location.
|
|
||||||
|
|
||||||
:param lat: latitude
|
|
||||||
:param lon: longitude
|
|
||||||
:param alt: altitude
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
self.lat = lat
|
|
||||||
self.lon = lon
|
|
||||||
self.alt = alt
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LinkOptions:
|
|
||||||
"""
|
|
||||||
Options for creating and updating links within core.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: LinkTypes = LinkTypes.WIRED
|
|
||||||
session: int = None
|
|
||||||
delay: int = None
|
|
||||||
bandwidth: int = None
|
|
||||||
per: float = None
|
|
||||||
dup: int = None
|
|
||||||
jitter: int = None
|
|
||||||
mer: int = None
|
|
||||||
burst: int = None
|
|
||||||
mburst: int = None
|
|
||||||
gui_attributes: str = None
|
|
||||||
unidirectional: bool = None
|
|
||||||
emulation_id: int = None
|
|
||||||
network_id: int = None
|
|
||||||
key: int = None
|
|
||||||
opaque: str = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class InterfaceData:
|
|
||||||
"""
|
|
||||||
Convenience class for storing interface data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: int = None
|
|
||||||
name: str = None
|
|
||||||
mac: str = None
|
|
||||||
ip4: str = None
|
|
||||||
ip4_mask: int = None
|
|
||||||
ip6: str = None
|
|
||||||
ip6_mask: int = None
|
|
||||||
|
|
||||||
def get_addresses(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Returns a list of ip4 and ip6 addresses when present.
|
|
||||||
|
|
||||||
:return: list of addresses
|
|
||||||
"""
|
|
||||||
addresses = []
|
|
||||||
if self.ip4 and self.ip4_mask:
|
|
||||||
addresses.append(f"{self.ip4}/{self.ip4_mask}")
|
|
||||||
if self.ip6 and self.ip6_mask:
|
|
||||||
addresses.append(f"{self.ip6}/{self.ip6_mask}")
|
|
||||||
return addresses
|
|
||||||
|
|
||||||
|
|
||||||
class IpPrefixes:
|
|
||||||
"""
|
|
||||||
Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
|
|
||||||
"""
|
|
||||||
Creates an IpPrefixes object.
|
|
||||||
|
|
||||||
:param ip4_prefix: ip4 prefix to use for generation
|
|
||||||
:param ip6_prefix: ip6 prefix to use for generation
|
|
||||||
:raises ValueError: when both ip4 and ip6 prefixes have not been provided
|
|
||||||
"""
|
|
||||||
if not ip4_prefix and not ip6_prefix:
|
|
||||||
raise ValueError("ip4 or ip6 must be provided")
|
|
||||||
|
|
||||||
self.ip4 = None
|
|
||||||
if ip4_prefix:
|
|
||||||
self.ip4 = netaddr.IPNetwork(ip4_prefix)
|
|
||||||
self.ip6 = None
|
|
||||||
if ip6_prefix:
|
|
||||||
self.ip6 = netaddr.IPNetwork(ip6_prefix)
|
|
||||||
|
|
||||||
def ip4_address(self, node_id: int) -> str:
|
|
||||||
"""
|
|
||||||
Convenience method to return the IP4 address for a node.
|
|
||||||
|
|
||||||
:param node_id: node id to get IP4 address for
|
|
||||||
:return: IP4 address or None
|
|
||||||
"""
|
|
||||||
if not self.ip4:
|
|
||||||
raise ValueError("ip4 prefixes have not been set")
|
|
||||||
return str(self.ip4[node_id])
|
|
||||||
|
|
||||||
def ip6_address(self, node_id: int) -> str:
|
|
||||||
"""
|
|
||||||
Convenience method to return the IP6 address for a node.
|
|
||||||
|
|
||||||
:param node_id: node id to get IP6 address for
|
|
||||||
:return: IP4 address or None
|
|
||||||
"""
|
|
||||||
if not self.ip6:
|
|
||||||
raise ValueError("ip6 prefixes have not been set")
|
|
||||||
return str(self.ip6[node_id])
|
|
||||||
|
|
||||||
def gen_interface(self, node_id: int, name: str = None, mac: str = None):
|
|
||||||
"""
|
|
||||||
Creates interface data for linking nodes, using the nodes unique id for
|
|
||||||
generation, along with a random mac address, unless provided.
|
|
||||||
|
|
||||||
:param node_id: node id to create an interface for
|
|
||||||
:param name: name to set for interface, default is eth{id}
|
|
||||||
:param mac: mac address to use for this interface, default is random
|
|
||||||
generation
|
|
||||||
:return: new interface data for the provided node
|
|
||||||
"""
|
|
||||||
# generate ip4 data
|
|
||||||
ip4 = None
|
|
||||||
ip4_mask = None
|
|
||||||
if self.ip4:
|
|
||||||
ip4 = self.ip4_address(node_id)
|
|
||||||
ip4_mask = self.ip4.prefixlen
|
|
||||||
|
|
||||||
# generate ip6 data
|
|
||||||
ip6 = None
|
|
||||||
ip6_mask = None
|
|
||||||
if self.ip6:
|
|
||||||
ip6 = self.ip6_address(node_id)
|
|
||||||
ip6_mask = self.ip6.prefixlen
|
|
||||||
|
|
||||||
# random mac
|
|
||||||
if not mac:
|
|
||||||
mac = utils.random_mac()
|
|
||||||
|
|
||||||
return InterfaceData(
|
|
||||||
name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_interface(
|
|
||||||
self, node: "CoreNode", name: str = None, mac: str = None
|
|
||||||
) -> InterfaceData:
|
|
||||||
"""
|
|
||||||
Creates interface data for linking nodes, using the nodes unique id for
|
|
||||||
generation, along with a random mac address, unless provided.
|
|
||||||
|
|
||||||
:param node: node to create interface for
|
|
||||||
:param name: name to set for interface, default is eth{id}
|
|
||||||
:param mac: mac address to use for this interface, default is random
|
|
||||||
generation
|
|
||||||
:return: new interface data for the provided node
|
|
||||||
"""
|
|
||||||
interface = self.gen_interface(node.id, name, mac)
|
|
||||||
interface.id = node.newifindex()
|
|
||||||
return interface
|
|
File diff suppressed because it is too large
Load diff
|
@ -56,6 +56,9 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
||||||
default=Sdt.DEFAULT_SDT_URL,
|
default=Sdt.DEFAULT_SDT_URL,
|
||||||
label="SDT3D URL",
|
label="SDT3D URL",
|
||||||
),
|
),
|
||||||
|
Configuration(
|
||||||
|
_id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS"
|
||||||
|
),
|
||||||
]
|
]
|
||||||
config_type: RegisterTlvs = RegisterTlvs.UTILITY
|
config_type: RegisterTlvs = RegisterTlvs.UTILITY
|
||||||
|
|
||||||
|
|
31
daemon/core/executables.py
Normal file
31
daemon/core/executables.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
VNODED: str = "vnoded"
|
||||||
|
VCMD: str = "vcmd"
|
||||||
|
SYSCTL: str = "sysctl"
|
||||||
|
IP: str = "ip"
|
||||||
|
ETHTOOL: str = "ethtool"
|
||||||
|
TC: str = "tc"
|
||||||
|
EBTABLES: str = "ebtables"
|
||||||
|
MOUNT: str = "mount"
|
||||||
|
UMOUNT: str = "umount"
|
||||||
|
OVS_VSCTL: str = "ovs-vsctl"
|
||||||
|
|
||||||
|
COMMON_REQUIREMENTS: List[str] = [SYSCTL, IP, ETHTOOL, TC, EBTABLES, MOUNT, UMOUNT]
|
||||||
|
VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD]
|
||||||
|
OVS_REQUIREMENTS: List[str] = [OVS_VSCTL]
|
||||||
|
|
||||||
|
|
||||||
|
def get_requirements(use_ovs: bool) -> List[str]:
|
||||||
|
"""
|
||||||
|
Retrieve executable requirements needed to run CORE.
|
||||||
|
|
||||||
|
:param use_ovs: True if OVS is being used, False otherwise
|
||||||
|
:return: list of executable requirements
|
||||||
|
"""
|
||||||
|
requirements = COMMON_REQUIREMENTS
|
||||||
|
if use_ovs:
|
||||||
|
requirements += OVS_REQUIREMENTS
|
||||||
|
else:
|
||||||
|
requirements += VCMD_REQUIREMENTS
|
||||||
|
return requirements
|
|
@ -3,52 +3,60 @@ import math
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import PhotoImage, font, ttk
|
from tkinter import PhotoImage, font, ttk
|
||||||
from tkinter.ttk import Progressbar
|
from tkinter.ttk import Progressbar
|
||||||
|
from typing import Any, Dict, Optional, Type
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
from core.gui import appconfig, themes
|
from core.gui import appconfig, themes
|
||||||
|
from core.gui.appconfig import GuiConfig
|
||||||
from core.gui.coreclient import CoreClient
|
from core.gui.coreclient import CoreClient
|
||||||
from core.gui.dialogs.error import ErrorDialog
|
from core.gui.dialogs.error import ErrorDialog
|
||||||
|
from core.gui.frames.base import InfoFrameBase
|
||||||
|
from core.gui.frames.default import DefaultInfoFrame
|
||||||
from core.gui.graph.graph import CanvasGraph
|
from core.gui.graph.graph import CanvasGraph
|
||||||
from core.gui.images import ImageEnum, Images
|
from core.gui.images import ImageEnum, Images
|
||||||
from core.gui.menubar import Menubar
|
from core.gui.menubar import Menubar
|
||||||
from core.gui.nodeutils import NodeUtils
|
from core.gui.nodeutils import NodeUtils
|
||||||
from core.gui.statusbar import StatusBar
|
from core.gui.statusbar import StatusBar
|
||||||
|
from core.gui.themes import PADY
|
||||||
from core.gui.toolbar import Toolbar
|
from core.gui.toolbar import Toolbar
|
||||||
|
|
||||||
WIDTH = 1000
|
WIDTH: int = 1000
|
||||||
HEIGHT = 800
|
HEIGHT: int = 800
|
||||||
|
|
||||||
|
|
||||||
class Application(ttk.Frame):
|
class Application(ttk.Frame):
|
||||||
def __init__(self, proxy: bool) -> None:
|
def __init__(self, proxy: bool, session_id: int = None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# load node icons
|
# load node icons
|
||||||
NodeUtils.setup()
|
NodeUtils.setup()
|
||||||
|
|
||||||
# widgets
|
# widgets
|
||||||
self.menubar = None
|
self.menubar: Optional[Menubar] = None
|
||||||
self.toolbar = None
|
self.toolbar: Optional[Toolbar] = None
|
||||||
self.right_frame = None
|
self.right_frame: Optional[ttk.Frame] = None
|
||||||
self.canvas = None
|
self.canvas: Optional[CanvasGraph] = None
|
||||||
self.statusbar = None
|
self.statusbar: Optional[StatusBar] = None
|
||||||
self.progress = None
|
self.progress: Optional[Progressbar] = None
|
||||||
|
self.infobar: Optional[ttk.Frame] = None
|
||||||
|
self.info_frame: Optional[InfoFrameBase] = None
|
||||||
|
self.show_infobar: tk.BooleanVar = tk.BooleanVar(value=False)
|
||||||
|
|
||||||
# fonts
|
# fonts
|
||||||
self.fonts_size = None
|
self.fonts_size: Dict[str, int] = {}
|
||||||
self.icon_text_font = None
|
self.icon_text_font: Optional[font.Font] = None
|
||||||
self.edge_font = None
|
self.edge_font: Optional[font.Font] = None
|
||||||
|
|
||||||
# setup
|
# setup
|
||||||
self.guiconfig = appconfig.read()
|
self.guiconfig: GuiConfig = appconfig.read()
|
||||||
self.app_scale = self.guiconfig.scale
|
self.app_scale: float = self.guiconfig.scale
|
||||||
self.setup_scaling()
|
self.setup_scaling()
|
||||||
self.style = ttk.Style()
|
self.style: ttk.Style = ttk.Style()
|
||||||
self.setup_theme()
|
self.setup_theme()
|
||||||
self.core = CoreClient(self, proxy)
|
self.core: CoreClient = CoreClient(self, proxy)
|
||||||
self.setup_app()
|
self.setup_app()
|
||||||
self.draw()
|
self.draw()
|
||||||
self.core.setup()
|
self.core.setup(session_id)
|
||||||
|
|
||||||
def setup_scaling(self) -> None:
|
def setup_scaling(self) -> None:
|
||||||
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
|
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
|
||||||
|
@ -111,16 +119,27 @@ class Application(ttk.Frame):
|
||||||
self.right_frame.rowconfigure(0, weight=1)
|
self.right_frame.rowconfigure(0, weight=1)
|
||||||
self.right_frame.grid(row=0, column=1, sticky="nsew")
|
self.right_frame.grid(row=0, column=1, sticky="nsew")
|
||||||
self.draw_canvas()
|
self.draw_canvas()
|
||||||
|
self.draw_infobar()
|
||||||
self.draw_status()
|
self.draw_status()
|
||||||
self.progress = Progressbar(self.right_frame, mode="indeterminate")
|
self.progress = Progressbar(self.right_frame, mode="indeterminate")
|
||||||
self.menubar = Menubar(self)
|
self.menubar = Menubar(self)
|
||||||
self.master.config(menu=self.menubar)
|
self.master.config(menu=self.menubar)
|
||||||
|
|
||||||
|
def draw_infobar(self) -> None:
|
||||||
|
self.infobar = ttk.Frame(self.right_frame, padding=5, relief=tk.RAISED)
|
||||||
|
self.infobar.columnconfigure(0, weight=1)
|
||||||
|
self.infobar.rowconfigure(1, weight=1)
|
||||||
|
label_font = font.Font(weight=font.BOLD, underline=tk.TRUE)
|
||||||
|
label = ttk.Label(
|
||||||
|
self.infobar, text="Details", anchor=tk.CENTER, font=label_font
|
||||||
|
)
|
||||||
|
label.grid(sticky=tk.EW, pady=PADY)
|
||||||
|
|
||||||
def draw_canvas(self) -> None:
|
def draw_canvas(self) -> None:
|
||||||
canvas_frame = ttk.Frame(self.right_frame)
|
canvas_frame = ttk.Frame(self.right_frame)
|
||||||
canvas_frame.rowconfigure(0, weight=1)
|
canvas_frame.rowconfigure(0, weight=1)
|
||||||
canvas_frame.columnconfigure(0, weight=1)
|
canvas_frame.columnconfigure(0, weight=1)
|
||||||
canvas_frame.grid(sticky="nsew", pady=1)
|
canvas_frame.grid(row=0, column=0, sticky="nsew", pady=1)
|
||||||
self.canvas = CanvasGraph(canvas_frame, self, self.core)
|
self.canvas = CanvasGraph(canvas_frame, self, self.core)
|
||||||
self.canvas.grid(sticky="nsew")
|
self.canvas.grid(sticky="nsew")
|
||||||
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
|
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
|
||||||
|
@ -134,7 +153,31 @@ class Application(ttk.Frame):
|
||||||
|
|
||||||
def draw_status(self) -> None:
|
def draw_status(self) -> None:
|
||||||
self.statusbar = StatusBar(self.right_frame, self)
|
self.statusbar = StatusBar(self.right_frame, self)
|
||||||
self.statusbar.grid(sticky="ew")
|
self.statusbar.grid(sticky="ew", columnspan=2)
|
||||||
|
|
||||||
|
def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None:
|
||||||
|
if not self.show_infobar.get():
|
||||||
|
return
|
||||||
|
self.clear_info()
|
||||||
|
self.info_frame = frame_class(self.infobar, **kwargs)
|
||||||
|
self.info_frame.draw()
|
||||||
|
self.info_frame.grid(sticky="nsew")
|
||||||
|
|
||||||
|
def clear_info(self) -> None:
|
||||||
|
if self.info_frame:
|
||||||
|
self.info_frame.destroy()
|
||||||
|
self.info_frame = None
|
||||||
|
|
||||||
|
def default_info(self) -> None:
|
||||||
|
self.clear_info()
|
||||||
|
self.display_info(DefaultInfoFrame, app=self)
|
||||||
|
|
||||||
|
def show_info(self) -> None:
|
||||||
|
self.default_info()
|
||||||
|
self.infobar.grid(row=0, column=1, sticky="nsew")
|
||||||
|
|
||||||
|
def hide_info(self) -> None:
|
||||||
|
self.infobar.grid_forget()
|
||||||
|
|
||||||
def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
|
def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
|
||||||
logging.exception("app grpc exception", exc_info=e)
|
logging.exception("app grpc exception", exc_info=e)
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import Dict, List, Optional, Type
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from core.gui import themes
|
from core.gui import themes
|
||||||
|
|
||||||
HOME_PATH = Path.home().joinpath(".coregui")
|
HOME_PATH: Path = Path.home().joinpath(".coregui")
|
||||||
BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds")
|
BACKGROUNDS_PATH: Path = HOME_PATH.joinpath("backgrounds")
|
||||||
CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane")
|
CUSTOM_EMANE_PATH: Path = HOME_PATH.joinpath("custom_emane")
|
||||||
CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services")
|
CUSTOM_SERVICE_PATH: Path = HOME_PATH.joinpath("custom_services")
|
||||||
ICONS_PATH = HOME_PATH.joinpath("icons")
|
ICONS_PATH: Path = HOME_PATH.joinpath("icons")
|
||||||
MOBILITY_PATH = HOME_PATH.joinpath("mobility")
|
MOBILITY_PATH: Path = HOME_PATH.joinpath("mobility")
|
||||||
XMLS_PATH = HOME_PATH.joinpath("xmls")
|
XMLS_PATH: Path = HOME_PATH.joinpath("xmls")
|
||||||
CONFIG_PATH = HOME_PATH.joinpath("config.yaml")
|
CONFIG_PATH: Path = HOME_PATH.joinpath("config.yaml")
|
||||||
LOG_PATH = HOME_PATH.joinpath("gui.log")
|
LOG_PATH: Path = HOME_PATH.joinpath("gui.log")
|
||||||
SCRIPT_PATH = HOME_PATH.joinpath("scripts")
|
SCRIPT_PATH: Path = HOME_PATH.joinpath("scripts")
|
||||||
|
|
||||||
# local paths
|
# local paths
|
||||||
DATA_PATH = Path(__file__).parent.joinpath("data")
|
DATA_PATH: Path = Path(__file__).parent.joinpath("data")
|
||||||
LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute()
|
LOCAL_ICONS_PATH: Path = DATA_PATH.joinpath("icons").absolute()
|
||||||
LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute()
|
LOCAL_BACKGROUND_PATH: Path = DATA_PATH.joinpath("backgrounds").absolute()
|
||||||
LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute()
|
LOCAL_XMLS_PATH: Path = DATA_PATH.joinpath("xmls").absolute()
|
||||||
LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute()
|
LOCAL_MOBILITY_PATH: Path = DATA_PATH.joinpath("mobility").absolute()
|
||||||
|
|
||||||
# configuration data
|
# configuration data
|
||||||
TERMINALS = {
|
TERMINALS: Dict[str, str] = {
|
||||||
"xterm": "xterm -e",
|
"xterm": "xterm -e",
|
||||||
"aterm": "aterm -e",
|
"aterm": "aterm -e",
|
||||||
"eterm": "eterm -e",
|
"eterm": "eterm -e",
|
||||||
|
@ -36,45 +36,45 @@ TERMINALS = {
|
||||||
"xfce4-terminal": "xfce4-terminal -x",
|
"xfce4-terminal": "xfce4-terminal -x",
|
||||||
"gnome-terminal": "gnome-terminal --window --",
|
"gnome-terminal": "gnome-terminal --window --",
|
||||||
}
|
}
|
||||||
EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
EDITORS: List[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||||
|
|
||||||
|
|
||||||
class IndentDumper(yaml.Dumper):
|
class IndentDumper(yaml.Dumper):
|
||||||
def increase_indent(self, flow=False, indentless=False):
|
def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
|
||||||
return super().increase_indent(flow, False)
|
super().increase_indent(flow, False)
|
||||||
|
|
||||||
|
|
||||||
class CustomNode(yaml.YAMLObject):
|
class CustomNode(yaml.YAMLObject):
|
||||||
yaml_tag = "!CustomNode"
|
yaml_tag: str = "!CustomNode"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(self, name: str, image: str, services: List[str]) -> None:
|
def __init__(self, name: str, image: str, services: List[str]) -> None:
|
||||||
self.name = name
|
self.name: str = name
|
||||||
self.image = image
|
self.image: str = image
|
||||||
self.services = services
|
self.services: List[str] = services
|
||||||
|
|
||||||
|
|
||||||
class CoreServer(yaml.YAMLObject):
|
class CoreServer(yaml.YAMLObject):
|
||||||
yaml_tag = "!CoreServer"
|
yaml_tag: str = "!CoreServer"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(self, name: str, address: str) -> None:
|
def __init__(self, name: str, address: str) -> None:
|
||||||
self.name = name
|
self.name: str = name
|
||||||
self.address = address
|
self.address: str = address
|
||||||
|
|
||||||
|
|
||||||
class Observer(yaml.YAMLObject):
|
class Observer(yaml.YAMLObject):
|
||||||
yaml_tag = "!Observer"
|
yaml_tag: str = "!Observer"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(self, name: str, cmd: str) -> None:
|
def __init__(self, name: str, cmd: str) -> None:
|
||||||
self.name = name
|
self.name: str = name
|
||||||
self.cmd = cmd
|
self.cmd: str = cmd
|
||||||
|
|
||||||
|
|
||||||
class PreferencesConfig(yaml.YAMLObject):
|
class PreferencesConfig(yaml.YAMLObject):
|
||||||
yaml_tag = "!PreferencesConfig"
|
yaml_tag: str = "!PreferencesConfig"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -85,17 +85,17 @@ class PreferencesConfig(yaml.YAMLObject):
|
||||||
width: int = 1000,
|
width: int = 1000,
|
||||||
height: int = 750,
|
height: int = 750,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.theme = theme
|
self.theme: str = theme
|
||||||
self.editor = editor
|
self.editor: str = editor
|
||||||
self.terminal = terminal
|
self.terminal: str = terminal
|
||||||
self.gui3d = gui3d
|
self.gui3d: str = gui3d
|
||||||
self.width = width
|
self.width: int = width
|
||||||
self.height = height
|
self.height: int = height
|
||||||
|
|
||||||
|
|
||||||
class LocationConfig(yaml.YAMLObject):
|
class LocationConfig(yaml.YAMLObject):
|
||||||
yaml_tag = "!LocationConfig"
|
yaml_tag: str = "!LocationConfig"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -107,18 +107,18 @@ class LocationConfig(yaml.YAMLObject):
|
||||||
alt: float = 2.0,
|
alt: float = 2.0,
|
||||||
scale: float = 150.0,
|
scale: float = 150.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.x = x
|
self.x: float = x
|
||||||
self.y = y
|
self.y: float = y
|
||||||
self.z = z
|
self.z: float = z
|
||||||
self.lat = lat
|
self.lat: float = lat
|
||||||
self.lon = lon
|
self.lon: float = lon
|
||||||
self.alt = alt
|
self.alt: float = alt
|
||||||
self.scale = scale
|
self.scale: float = scale
|
||||||
|
|
||||||
|
|
||||||
class IpConfigs(yaml.YAMLObject):
|
class IpConfigs(yaml.YAMLObject):
|
||||||
yaml_tag = "!IpConfigs"
|
yaml_tag: str = "!IpConfigs"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -129,21 +129,21 @@ class IpConfigs(yaml.YAMLObject):
|
||||||
) -> None:
|
) -> None:
|
||||||
if ip4s is None:
|
if ip4s is None:
|
||||||
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
|
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
|
||||||
self.ip4s = ip4s
|
self.ip4s: List[str] = ip4s
|
||||||
if ip6s is None:
|
if ip6s is None:
|
||||||
ip6s = ["2001::", "2002::", "a::"]
|
ip6s = ["2001::", "2002::", "a::"]
|
||||||
self.ip6s = ip6s
|
self.ip6s: List[str] = ip6s
|
||||||
if ip4 is None:
|
if ip4 is None:
|
||||||
ip4 = self.ip4s[0]
|
ip4 = self.ip4s[0]
|
||||||
self.ip4 = ip4
|
self.ip4: str = ip4
|
||||||
if ip6 is None:
|
if ip6 is None:
|
||||||
ip6 = self.ip6s[0]
|
ip6 = self.ip6s[0]
|
||||||
self.ip6 = ip6
|
self.ip6: str = ip6
|
||||||
|
|
||||||
|
|
||||||
class GuiConfig(yaml.YAMLObject):
|
class GuiConfig(yaml.YAMLObject):
|
||||||
yaml_tag = "!GuiConfig"
|
yaml_tag: str = "!GuiConfig"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -159,30 +159,30 @@ class GuiConfig(yaml.YAMLObject):
|
||||||
) -> None:
|
) -> None:
|
||||||
if preferences is None:
|
if preferences is None:
|
||||||
preferences = PreferencesConfig()
|
preferences = PreferencesConfig()
|
||||||
self.preferences = preferences
|
self.preferences: PreferencesConfig = preferences
|
||||||
if location is None:
|
if location is None:
|
||||||
location = LocationConfig()
|
location = LocationConfig()
|
||||||
self.location = location
|
self.location: LocationConfig = location
|
||||||
if servers is None:
|
if servers is None:
|
||||||
servers = []
|
servers = []
|
||||||
self.servers = servers
|
self.servers: List[CoreServer] = servers
|
||||||
if nodes is None:
|
if nodes is None:
|
||||||
nodes = []
|
nodes = []
|
||||||
self.nodes = nodes
|
self.nodes: List[CustomNode] = nodes
|
||||||
if recentfiles is None:
|
if recentfiles is None:
|
||||||
recentfiles = []
|
recentfiles = []
|
||||||
self.recentfiles = recentfiles
|
self.recentfiles: List[str] = recentfiles
|
||||||
if observers is None:
|
if observers is None:
|
||||||
observers = []
|
observers = []
|
||||||
self.observers = observers
|
self.observers: List[Observer] = observers
|
||||||
self.scale = scale
|
self.scale: float = scale
|
||||||
if ips is None:
|
if ips is None:
|
||||||
ips = IpConfigs()
|
ips = IpConfigs()
|
||||||
self.ips = ips
|
self.ips: IpConfigs = ips
|
||||||
self.mac = mac
|
self.mac: str = mac
|
||||||
|
|
||||||
|
|
||||||
def copy_files(current_path, new_path) -> None:
|
def copy_files(current_path: Path, new_path: Path) -> None:
|
||||||
for current_file in current_path.glob("*"):
|
for current_file in current_path.glob("*"):
|
||||||
new_file = new_path.joinpath(current_file.name)
|
new_file = new_path.joinpath(current_file.name)
|
||||||
shutil.copy(current_file, new_file)
|
shutil.copy(current_file, new_file)
|
||||||
|
|
|
@ -1,21 +1,46 @@
|
||||||
"""
|
"""
|
||||||
Incorporate grpc into python tkinter GUI
|
Incorporate grpc into python tkinter GUI
|
||||||
"""
|
"""
|
||||||
|
import getpass
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import tkinter as tk
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional
|
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
from core.api.grpc import client, common_pb2, configservices_pb2, core_pb2
|
from core.api.grpc import client
|
||||||
|
from core.api.grpc.common_pb2 import ConfigOption
|
||||||
|
from core.api.grpc.configservices_pb2 import ConfigService, ConfigServiceConfig
|
||||||
|
from core.api.grpc.core_pb2 import (
|
||||||
|
CpuUsageEvent,
|
||||||
|
Event,
|
||||||
|
ExceptionEvent,
|
||||||
|
Hook,
|
||||||
|
Interface,
|
||||||
|
Link,
|
||||||
|
LinkEvent,
|
||||||
|
LinkType,
|
||||||
|
MessageType,
|
||||||
|
Node,
|
||||||
|
NodeEvent,
|
||||||
|
NodeType,
|
||||||
|
Position,
|
||||||
|
SessionLocation,
|
||||||
|
SessionState,
|
||||||
|
StartSessionResponse,
|
||||||
|
StopSessionResponse,
|
||||||
|
ThroughputsEvent,
|
||||||
|
)
|
||||||
from core.api.grpc.emane_pb2 import EmaneModelConfig
|
from core.api.grpc.emane_pb2 import EmaneModelConfig
|
||||||
from core.api.grpc.mobility_pb2 import MobilityConfig
|
from core.api.grpc.mobility_pb2 import MobilityConfig
|
||||||
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig
|
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig
|
||||||
from core.api.grpc.wlan_pb2 import WlanConfig
|
from core.api.grpc.wlan_pb2 import WlanConfig
|
||||||
from core.gui import appconfig
|
from core.gui import appconfig
|
||||||
|
from core.gui.appconfig import CoreServer, Observer
|
||||||
from core.gui.dialogs.emaneinstall import EmaneInstallDialog
|
from core.gui.dialogs.emaneinstall import EmaneInstallDialog
|
||||||
from core.gui.dialogs.error import ErrorDialog
|
from core.gui.dialogs.error import ErrorDialog
|
||||||
from core.gui.dialogs.mobilityplayer import MobilityPlayer
|
from core.gui.dialogs.mobilityplayer import MobilityPlayer
|
||||||
|
@ -31,50 +56,52 @@ if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
|
||||||
GUI_SOURCE = "gui"
|
GUI_SOURCE = "gui"
|
||||||
|
CPU_USAGE_DELAY = 3
|
||||||
|
|
||||||
|
|
||||||
class CoreClient:
|
class CoreClient:
|
||||||
def __init__(self, app: "Application", proxy: bool):
|
def __init__(self, app: "Application", proxy: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Create a CoreGrpc instance
|
Create a CoreGrpc instance
|
||||||
"""
|
"""
|
||||||
self._client = client.CoreGrpcClient(proxy=proxy)
|
self.app: "Application" = app
|
||||||
self.session_id = None
|
self.master: tk.Tk = app.master
|
||||||
self.node_ids = []
|
self._client: client.CoreGrpcClient = client.CoreGrpcClient(proxy=proxy)
|
||||||
self.app = app
|
self.session_id: Optional[int] = None
|
||||||
self.master = app.master
|
self.services: Dict[str, Set[str]] = {}
|
||||||
self.services = {}
|
self.config_services_groups: Dict[str, Set[str]] = {}
|
||||||
self.config_services_groups = {}
|
self.config_services: Dict[str, ConfigService] = {}
|
||||||
self.config_services = {}
|
self.default_services: Dict[NodeType, Set[str]] = {}
|
||||||
self.default_services = {}
|
self.emane_models: List[str] = []
|
||||||
self.emane_models = []
|
self.observer: Optional[str] = None
|
||||||
self.observer = None
|
self.user = getpass.getuser()
|
||||||
|
|
||||||
# loaded configuration data
|
# loaded configuration data
|
||||||
self.servers = {}
|
self.servers: Dict[str, CoreServer] = {}
|
||||||
self.custom_nodes = {}
|
self.custom_nodes: Dict[str, NodeDraw] = {}
|
||||||
self.custom_observers = {}
|
self.custom_observers: Dict[str, Observer] = {}
|
||||||
self.read_config()
|
self.read_config()
|
||||||
|
|
||||||
# helpers
|
# helpers
|
||||||
self.interface_to_edge = {}
|
self.iface_to_edge: Dict[Tuple[int, ...], Tuple[int, ...]] = {}
|
||||||
self.interfaces_manager = InterfaceManager(self.app)
|
self.ifaces_manager: InterfaceManager = InterfaceManager(self.app)
|
||||||
|
|
||||||
# session data
|
# session data
|
||||||
self.state = None
|
self.state: Optional[SessionState] = None
|
||||||
self.canvas_nodes = {}
|
self.canvas_nodes: Dict[int, CanvasNode] = {}
|
||||||
self.location = None
|
self.location: Optional[SessionLocation] = None
|
||||||
self.links = {}
|
self.links: Dict[Tuple[int, int], CanvasEdge] = {}
|
||||||
self.hooks = {}
|
self.hooks: Dict[str, Hook] = {}
|
||||||
self.emane_config = None
|
self.emane_config: Dict[str, ConfigOption] = {}
|
||||||
self.mobility_players = {}
|
self.mobility_players: Dict[int, MobilityPlayer] = {}
|
||||||
self.handling_throughputs = None
|
self.handling_throughputs: Optional[grpc.Future] = None
|
||||||
self.handling_events = None
|
self.handling_cpu_usage: Optional[grpc.Future] = None
|
||||||
self.xml_dir = None
|
self.handling_events: Optional[grpc.Future] = None
|
||||||
self.xml_file = None
|
self.xml_dir: Optional[str] = None
|
||||||
|
self.xml_file: Optional[str] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client(self):
|
def client(self) -> client.CoreGrpcClient:
|
||||||
if self.session_id:
|
if self.session_id:
|
||||||
response = self._client.check_session(self.session_id)
|
response = self._client.check_session(self.session_id)
|
||||||
if not response.result:
|
if not response.result:
|
||||||
|
@ -87,12 +114,13 @@ class CoreClient:
|
||||||
)
|
)
|
||||||
if throughputs_enabled:
|
if throughputs_enabled:
|
||||||
self.enable_throughputs()
|
self.enable_throughputs()
|
||||||
|
self.setup_cpu_usage()
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
def reset(self):
|
def reset(self) -> None:
|
||||||
# helpers
|
# helpers
|
||||||
self.interfaces_manager.reset()
|
self.ifaces_manager.reset()
|
||||||
self.interface_to_edge.clear()
|
self.iface_to_edge.clear()
|
||||||
# session data
|
# session data
|
||||||
self.canvas_nodes.clear()
|
self.canvas_nodes.clear()
|
||||||
self.links.clear()
|
self.links.clear()
|
||||||
|
@ -104,14 +132,14 @@ class CoreClient:
|
||||||
self.cancel_throughputs()
|
self.cancel_throughputs()
|
||||||
self.cancel_events()
|
self.cancel_events()
|
||||||
|
|
||||||
def close_mobility_players(self):
|
def close_mobility_players(self) -> None:
|
||||||
for mobility_player in self.mobility_players.values():
|
for mobility_player in self.mobility_players.values():
|
||||||
mobility_player.close()
|
mobility_player.close()
|
||||||
|
|
||||||
def set_observer(self, value: str):
|
def set_observer(self, value: Optional[str]) -> None:
|
||||||
self.observer = value
|
self.observer = value
|
||||||
|
|
||||||
def read_config(self):
|
def read_config(self) -> None:
|
||||||
# read distributed servers
|
# read distributed servers
|
||||||
for server in self.app.guiconfig.servers:
|
for server in self.app.guiconfig.servers:
|
||||||
self.servers[server.name] = server
|
self.servers[server.name] = server
|
||||||
|
@ -125,7 +153,9 @@ class CoreClient:
|
||||||
for observer in self.app.guiconfig.observers:
|
for observer in self.app.guiconfig.observers:
|
||||||
self.custom_observers[observer.name] = observer
|
self.custom_observers[observer.name] = observer
|
||||||
|
|
||||||
def handle_events(self, event: core_pb2.Event):
|
def handle_events(self, event: Event) -> None:
|
||||||
|
if event.source == GUI_SOURCE:
|
||||||
|
return
|
||||||
if event.session_id != self.session_id:
|
if event.session_id != self.session_id:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"ignoring event session(%s) current(%s)",
|
"ignoring event session(%s) current(%s)",
|
||||||
|
@ -139,7 +169,7 @@ class CoreClient:
|
||||||
elif event.HasField("session_event"):
|
elif event.HasField("session_event"):
|
||||||
logging.info("session event: %s", event)
|
logging.info("session event: %s", event)
|
||||||
session_event = event.session_event
|
session_event = event.session_event
|
||||||
if session_event.event <= core_pb2.SessionState.SHUTDOWN:
|
if session_event.event <= SessionState.SHUTDOWN:
|
||||||
self.state = event.session_event.event
|
self.state = event.session_event.event
|
||||||
elif session_event.event in {7, 8, 9}:
|
elif session_event.event in {7, 8, 9}:
|
||||||
node_id = session_event.node_id
|
node_id = session_event.node_id
|
||||||
|
@ -162,56 +192,91 @@ class CoreClient:
|
||||||
else:
|
else:
|
||||||
logging.info("unhandled event: %s", event)
|
logging.info("unhandled event: %s", event)
|
||||||
|
|
||||||
def handle_link_event(self, event: core_pb2.LinkEvent):
|
def handle_link_event(self, event: LinkEvent) -> None:
|
||||||
logging.debug("Link event: %s", event)
|
logging.debug("Link event: %s", event)
|
||||||
node_one_id = event.link.node_one_id
|
node1_id = event.link.node1_id
|
||||||
node_two_id = event.link.node_two_id
|
node2_id = event.link.node2_id
|
||||||
if node_one_id == node_two_id:
|
if node1_id == node2_id:
|
||||||
logging.warning("ignoring links with loops: %s", event)
|
logging.warning("ignoring links with loops: %s", event)
|
||||||
return
|
return
|
||||||
canvas_node_one = self.canvas_nodes[node_one_id]
|
canvas_node1 = self.canvas_nodes[node1_id]
|
||||||
canvas_node_two = self.canvas_nodes[node_two_id]
|
canvas_node2 = self.canvas_nodes[node2_id]
|
||||||
if event.message_type == core_pb2.MessageType.ADD:
|
if event.link.type == LinkType.WIRELESS:
|
||||||
self.app.canvas.add_wireless_edge(
|
if event.message_type == MessageType.ADD:
|
||||||
canvas_node_one, canvas_node_two, event.link
|
self.app.canvas.add_wireless_edge(
|
||||||
)
|
canvas_node1, canvas_node2, event.link
|
||||||
elif event.message_type == core_pb2.MessageType.DELETE:
|
)
|
||||||
self.app.canvas.delete_wireless_edge(
|
elif event.message_type == MessageType.DELETE:
|
||||||
canvas_node_one, canvas_node_two, event.link
|
self.app.canvas.delete_wireless_edge(
|
||||||
)
|
canvas_node1, canvas_node2, event.link
|
||||||
elif event.message_type == core_pb2.MessageType.NONE:
|
)
|
||||||
self.app.canvas.update_wireless_edge(
|
elif event.message_type == MessageType.NONE:
|
||||||
canvas_node_one, canvas_node_two, event.link
|
self.app.canvas.update_wireless_edge(
|
||||||
)
|
canvas_node1, canvas_node2, event.link
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.warning("unknown link event: %s", event)
|
||||||
else:
|
else:
|
||||||
logging.warning("unknown link event: %s", event)
|
if event.message_type == MessageType.ADD:
|
||||||
|
self.app.canvas.add_wired_edge(canvas_node1, canvas_node2, event.link)
|
||||||
|
self.app.canvas.organize()
|
||||||
|
elif event.message_type == MessageType.DELETE:
|
||||||
|
self.app.canvas.delete_wired_edge(canvas_node1, canvas_node2)
|
||||||
|
elif event.message_type == MessageType.NONE:
|
||||||
|
self.app.canvas.update_wired_edge(
|
||||||
|
canvas_node1, canvas_node2, event.link
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.warning("unknown link event: %s", event)
|
||||||
|
|
||||||
def handle_node_event(self, event: core_pb2.NodeEvent):
|
def handle_node_event(self, event: NodeEvent) -> None:
|
||||||
logging.debug("node event: %s", event)
|
logging.debug("node event: %s", event)
|
||||||
if event.source == GUI_SOURCE:
|
if event.message_type == MessageType.NONE:
|
||||||
return
|
canvas_node = self.canvas_nodes[event.node.id]
|
||||||
node_id = event.node.id
|
x = event.node.position.x
|
||||||
x = event.node.position.x
|
y = event.node.position.y
|
||||||
y = event.node.position.y
|
canvas_node.move(x, y)
|
||||||
canvas_node = self.canvas_nodes[node_id]
|
elif event.message_type == MessageType.DELETE:
|
||||||
canvas_node.move(x, y)
|
canvas_node = self.canvas_nodes[event.node.id]
|
||||||
|
self.app.canvas.clear_selection()
|
||||||
|
self.app.canvas.select_object(canvas_node.id)
|
||||||
|
self.app.canvas.delete_selected_objects()
|
||||||
|
elif event.message_type == MessageType.ADD:
|
||||||
|
self.app.canvas.add_core_node(event.node)
|
||||||
|
else:
|
||||||
|
logging.warning("unknown node event: %s", event)
|
||||||
|
|
||||||
def enable_throughputs(self):
|
def enable_throughputs(self) -> None:
|
||||||
self.handling_throughputs = self.client.throughputs(
|
self.handling_throughputs = self.client.throughputs(
|
||||||
self.session_id, self.handle_throughputs
|
self.session_id, self.handle_throughputs
|
||||||
)
|
)
|
||||||
|
|
||||||
def cancel_throughputs(self):
|
def cancel_throughputs(self) -> None:
|
||||||
if self.handling_throughputs:
|
if self.handling_throughputs:
|
||||||
self.handling_throughputs.cancel()
|
self.handling_throughputs.cancel()
|
||||||
self.handling_throughputs = None
|
self.handling_throughputs = None
|
||||||
|
self.app.canvas.clear_throughputs()
|
||||||
|
|
||||||
def cancel_events(self):
|
def cancel_events(self) -> None:
|
||||||
if self.handling_events:
|
if self.handling_events:
|
||||||
self.handling_events.cancel()
|
self.handling_events.cancel()
|
||||||
self.handling_events = None
|
self.handling_events = None
|
||||||
|
|
||||||
def handle_throughputs(self, event: core_pb2.ThroughputsEvent):
|
def cancel_cpu_usage(self) -> None:
|
||||||
|
if self.handling_cpu_usage:
|
||||||
|
self.handling_cpu_usage.cancel()
|
||||||
|
self.handling_cpu_usage = None
|
||||||
|
|
||||||
|
def setup_cpu_usage(self) -> None:
|
||||||
|
if self.handling_cpu_usage and self.handling_cpu_usage.running():
|
||||||
|
return
|
||||||
|
if self.handling_cpu_usage:
|
||||||
|
self.handling_cpu_usage.cancel()
|
||||||
|
self.handling_cpu_usage = self._client.cpu_usage(
|
||||||
|
CPU_USAGE_DELAY, self.handle_cpu_event
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_throughputs(self, event: ThroughputsEvent) -> None:
|
||||||
if event.session_id != self.session_id:
|
if event.session_id != self.session_id:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"ignoring throughput event session(%s) current(%s)",
|
"ignoring throughput event session(%s) current(%s)",
|
||||||
|
@ -222,11 +287,14 @@ class CoreClient:
|
||||||
logging.debug("handling throughputs event: %s", event)
|
logging.debug("handling throughputs event: %s", event)
|
||||||
self.app.after(0, self.app.canvas.set_throughputs, event)
|
self.app.after(0, self.app.canvas.set_throughputs, event)
|
||||||
|
|
||||||
def handle_exception_event(self, event: core_pb2.ExceptionEvent):
|
def handle_cpu_event(self, event: CpuUsageEvent) -> None:
|
||||||
logging.info("exception event: %s", event)
|
self.app.after(0, self.app.statusbar.set_cpu, event.usage)
|
||||||
self.app.statusbar.core_alarms.append(event)
|
|
||||||
|
|
||||||
def join_session(self, session_id: int, query_location: bool = True):
|
def handle_exception_event(self, event: ExceptionEvent) -> None:
|
||||||
|
logging.info("exception event: %s", event)
|
||||||
|
self.app.statusbar.add_alert(event)
|
||||||
|
|
||||||
|
def join_session(self, session_id: int, query_location: bool = True) -> None:
|
||||||
logging.info("join session(%s)", session_id)
|
logging.info("join session(%s)", session_id)
|
||||||
# update session and title
|
# update session and title
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
|
@ -244,6 +312,9 @@ class CoreClient:
|
||||||
self.session_id, self.handle_events
|
self.session_id, self.handle_events
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# set session user
|
||||||
|
self.client.set_session_user(self.session_id, self.user)
|
||||||
|
|
||||||
# get session service defaults
|
# get session service defaults
|
||||||
response = self.client.get_service_defaults(self.session_id)
|
response = self.client.get_service_defaults(self.session_id)
|
||||||
self.default_services = {
|
self.default_services = {
|
||||||
|
@ -269,7 +340,7 @@ class CoreClient:
|
||||||
self.emane_config = response.config
|
self.emane_config = response.config
|
||||||
|
|
||||||
# update interface manager
|
# update interface manager
|
||||||
self.interfaces_manager.joined(session.links)
|
self.ifaces_manager.joined(session.links)
|
||||||
|
|
||||||
# draw session
|
# draw session
|
||||||
self.app.canvas.reset_and_redraw(session)
|
self.app.canvas.reset_and_redraw(session)
|
||||||
|
@ -284,11 +355,11 @@ class CoreClient:
|
||||||
# get emane model config
|
# get emane model config
|
||||||
response = self.client.get_emane_model_configs(self.session_id)
|
response = self.client.get_emane_model_configs(self.session_id)
|
||||||
for config in response.configs:
|
for config in response.configs:
|
||||||
interface = None
|
iface_id = None
|
||||||
if config.interface != -1:
|
if config.iface_id != -1:
|
||||||
interface = config.interface
|
iface_id = config.iface_id
|
||||||
canvas_node = self.canvas_nodes[config.node_id]
|
canvas_node = self.canvas_nodes[config.node_id]
|
||||||
canvas_node.emane_model_configs[(config.model, interface)] = dict(
|
canvas_node.emane_model_configs[(config.model, iface_id)] = dict(
|
||||||
config.config
|
config.config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -332,14 +403,15 @@ class CoreClient:
|
||||||
|
|
||||||
# organize canvas
|
# organize canvas
|
||||||
self.app.canvas.organize()
|
self.app.canvas.organize()
|
||||||
|
if self.is_runtime():
|
||||||
|
self.show_mobility_players()
|
||||||
# update ui to represent current state
|
# update ui to represent current state
|
||||||
self.app.after(0, self.app.joined_session_update)
|
self.app.after(0, self.app.joined_session_update)
|
||||||
|
|
||||||
def is_runtime(self) -> bool:
|
def is_runtime(self) -> bool:
|
||||||
return self.state == core_pb2.SessionState.RUNTIME
|
return self.state == SessionState.RUNTIME
|
||||||
|
|
||||||
def parse_metadata(self, config: Dict[str, str]):
|
def parse_metadata(self, config: Dict[str, str]) -> None:
|
||||||
# canvas setting
|
# canvas setting
|
||||||
canvas_config = config.get("canvas")
|
canvas_config = config.get("canvas")
|
||||||
logging.debug("canvas metadata: %s", canvas_config)
|
logging.debug("canvas metadata: %s", canvas_config)
|
||||||
|
@ -392,7 +464,7 @@ class CoreClient:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logging.exception("unknown shape: %s", shape_type)
|
logging.exception("unknown shape: %s", shape_type)
|
||||||
|
|
||||||
def create_new_session(self):
|
def create_new_session(self) -> None:
|
||||||
"""
|
"""
|
||||||
Create a new session
|
Create a new session
|
||||||
"""
|
"""
|
||||||
|
@ -400,7 +472,7 @@ class CoreClient:
|
||||||
response = self.client.create_session()
|
response = self.client.create_session()
|
||||||
logging.info("created session: %s", response)
|
logging.info("created session: %s", response)
|
||||||
location_config = self.app.guiconfig.location
|
location_config = self.app.guiconfig.location
|
||||||
self.location = core_pb2.SessionLocation(
|
self.location = SessionLocation(
|
||||||
x=location_config.x,
|
x=location_config.x,
|
||||||
y=location_config.y,
|
y=location_config.y,
|
||||||
z=location_config.z,
|
z=location_config.z,
|
||||||
|
@ -413,7 +485,7 @@ class CoreClient:
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("New Session Error", e)
|
self.app.show_grpc_exception("New Session Error", e)
|
||||||
|
|
||||||
def delete_session(self, session_id: int = None):
|
def delete_session(self, session_id: int = None) -> None:
|
||||||
if session_id is None:
|
if session_id is None:
|
||||||
session_id = self.session_id
|
session_id = self.session_id
|
||||||
try:
|
try:
|
||||||
|
@ -422,12 +494,14 @@ class CoreClient:
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Delete Session Error", e)
|
self.app.show_grpc_exception("Delete Session Error", e)
|
||||||
|
|
||||||
def setup(self):
|
def setup(self, session_id: int = None) -> None:
|
||||||
"""
|
"""
|
||||||
Query sessions, if there exist any, prompt whether to join one
|
Query sessions, if there exist any, prompt whether to join one
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.client.connect()
|
self.client.connect()
|
||||||
|
self.setup_cpu_usage()
|
||||||
|
|
||||||
# get service information
|
# get service information
|
||||||
response = self.client.get_services()
|
response = self.client.get_services()
|
||||||
for service in response.services:
|
for service in response.services:
|
||||||
|
@ -443,21 +517,33 @@ class CoreClient:
|
||||||
)
|
)
|
||||||
group_services.add(service.name)
|
group_services.add(service.name)
|
||||||
|
|
||||||
# if there are no sessions, create a new session, else join a session
|
# join provided session, create new session, or show dialog to select an
|
||||||
|
# existing session
|
||||||
response = self.client.get_sessions()
|
response = self.client.get_sessions()
|
||||||
sessions = response.sessions
|
sessions = response.sessions
|
||||||
if len(sessions) == 0:
|
if session_id:
|
||||||
self.create_new_session()
|
session_ids = set(x.id for x in sessions)
|
||||||
|
if session_id not in session_ids:
|
||||||
|
dialog = ErrorDialog(
|
||||||
|
self.app, "Join Session Error", f"{session_id} does not exist"
|
||||||
|
)
|
||||||
|
dialog.show()
|
||||||
|
self.app.close()
|
||||||
|
else:
|
||||||
|
self.join_session(session_id)
|
||||||
else:
|
else:
|
||||||
dialog = SessionsDialog(self.app, True)
|
if not sessions:
|
||||||
dialog.show()
|
self.create_new_session()
|
||||||
|
else:
|
||||||
|
dialog = SessionsDialog(self.app, True)
|
||||||
|
dialog.show()
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
logging.exception("core setup error")
|
logging.exception("core setup error")
|
||||||
dialog = ErrorDialog(self.app, "Setup Error", e.details())
|
dialog = ErrorDialog(self.app, "Setup Error", e.details())
|
||||||
dialog.show()
|
dialog.show()
|
||||||
self.app.close()
|
self.app.close()
|
||||||
|
|
||||||
def edit_node(self, core_node: core_pb2.Node):
|
def edit_node(self, core_node: Node) -> None:
|
||||||
try:
|
try:
|
||||||
self.client.edit_node(
|
self.client.edit_node(
|
||||||
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
|
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
|
||||||
|
@ -465,17 +551,21 @@ class CoreClient:
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Edit Node Error", e)
|
self.app.show_grpc_exception("Edit Node Error", e)
|
||||||
|
|
||||||
def start_session(self) -> core_pb2.StartSessionResponse:
|
def send_servers(self) -> None:
|
||||||
self.interfaces_manager.reset_mac()
|
for server in self.servers.values():
|
||||||
|
self.client.add_session_server(self.session_id, server.name, server.address)
|
||||||
|
|
||||||
|
def start_session(self) -> StartSessionResponse:
|
||||||
|
self.ifaces_manager.reset_mac()
|
||||||
nodes = [x.core_node for x in self.canvas_nodes.values()]
|
nodes = [x.core_node for x in self.canvas_nodes.values()]
|
||||||
links = []
|
links = []
|
||||||
for edge in self.links.values():
|
for edge in self.links.values():
|
||||||
link = core_pb2.Link()
|
link = Link()
|
||||||
link.CopyFrom(edge.link)
|
link.CopyFrom(edge.link)
|
||||||
if link.HasField("interface_one") and not link.interface_one.mac:
|
if link.HasField("iface1") and not link.iface1.mac:
|
||||||
link.interface_one.mac = self.interfaces_manager.next_mac()
|
link.iface1.mac = self.ifaces_manager.next_mac()
|
||||||
if link.HasField("interface_two") and not link.interface_two.mac:
|
if link.HasField("iface2") and not link.iface2.mac:
|
||||||
link.interface_two.mac = self.interfaces_manager.next_mac()
|
link.iface2.mac = self.ifaces_manager.next_mac()
|
||||||
links.append(link)
|
links.append(link)
|
||||||
wlan_configs = self.get_wlan_configs_proto()
|
wlan_configs = self.get_wlan_configs_proto()
|
||||||
mobility_configs = self.get_mobility_configs_proto()
|
mobility_configs = self.get_mobility_configs_proto()
|
||||||
|
@ -491,8 +581,9 @@ class CoreClient:
|
||||||
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
|
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||||
else:
|
else:
|
||||||
emane_config = None
|
emane_config = None
|
||||||
response = core_pb2.StartSessionResponse(result=False)
|
response = StartSessionResponse(result=False)
|
||||||
try:
|
try:
|
||||||
|
self.send_servers()
|
||||||
response = self.client.start_session(
|
response = self.client.start_session(
|
||||||
self.session_id,
|
self.session_id,
|
||||||
nodes,
|
nodes,
|
||||||
|
@ -517,10 +608,10 @@ class CoreClient:
|
||||||
self.app.show_grpc_exception("Start Session Error", e)
|
self.app.show_grpc_exception("Start Session Error", e)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse:
|
def stop_session(self, session_id: int = None) -> StopSessionResponse:
|
||||||
if not session_id:
|
if not session_id:
|
||||||
session_id = self.session_id
|
session_id = self.session_id
|
||||||
response = core_pb2.StopSessionResponse(result=False)
|
response = StopSessionResponse(result=False)
|
||||||
try:
|
try:
|
||||||
response = self.client.stop_session(session_id)
|
response = self.client.stop_session(session_id)
|
||||||
logging.info("stopped session(%s), result: %s", session_id, response)
|
logging.info("stopped session(%s), result: %s", session_id, response)
|
||||||
|
@ -528,9 +619,9 @@ class CoreClient:
|
||||||
self.app.show_grpc_exception("Stop Session Error", e)
|
self.app.show_grpc_exception("Stop Session Error", e)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def show_mobility_players(self):
|
def show_mobility_players(self) -> None:
|
||||||
for canvas_node in self.canvas_nodes.values():
|
for canvas_node in self.canvas_nodes.values():
|
||||||
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN:
|
if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
|
||||||
continue
|
continue
|
||||||
if canvas_node.mobility_config:
|
if canvas_node.mobility_config:
|
||||||
mobility_player = MobilityPlayer(
|
mobility_player = MobilityPlayer(
|
||||||
|
@ -540,7 +631,7 @@ class CoreClient:
|
||||||
self.mobility_players[node_id] = mobility_player
|
self.mobility_players[node_id] = mobility_player
|
||||||
mobility_player.show()
|
mobility_player.show()
|
||||||
|
|
||||||
def set_metadata(self):
|
def set_metadata(self) -> None:
|
||||||
# create canvas data
|
# create canvas data
|
||||||
wallpaper = None
|
wallpaper = None
|
||||||
if self.app.canvas.wallpaper_file:
|
if self.app.canvas.wallpaper_file:
|
||||||
|
@ -564,7 +655,7 @@ class CoreClient:
|
||||||
response = self.client.set_session_metadata(self.session_id, metadata)
|
response = self.client.set_session_metadata(self.session_id, metadata)
|
||||||
logging.info("set session metadata %s, result: %s", metadata, response)
|
logging.info("set session metadata %s, result: %s", metadata, response)
|
||||||
|
|
||||||
def launch_terminal(self, node_id: int):
|
def launch_terminal(self, node_id: int) -> None:
|
||||||
try:
|
try:
|
||||||
terminal = self.app.guiconfig.preferences.terminal
|
terminal = self.app.guiconfig.preferences.terminal
|
||||||
if not terminal:
|
if not terminal:
|
||||||
|
@ -581,12 +672,12 @@ class CoreClient:
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Node Terminal Error", e)
|
self.app.show_grpc_exception("Node Terminal Error", e)
|
||||||
|
|
||||||
def save_xml(self, file_path: str):
|
def save_xml(self, file_path: str) -> None:
|
||||||
"""
|
"""
|
||||||
Save core session as to an xml file
|
Save core session as to an xml file
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if self.state != core_pb2.SessionState.RUNTIME:
|
if self.state != SessionState.RUNTIME:
|
||||||
logging.debug("Send session data to the daemon")
|
logging.debug("Send session data to the daemon")
|
||||||
self.send_data()
|
self.send_data()
|
||||||
response = self.client.save_xml(self.session_id, file_path)
|
response = self.client.save_xml(self.session_id, file_path)
|
||||||
|
@ -594,7 +685,7 @@ class CoreClient:
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Save XML Error", e)
|
self.app.show_grpc_exception("Save XML Error", e)
|
||||||
|
|
||||||
def open_xml(self, file_path: str):
|
def open_xml(self, file_path: str) -> None:
|
||||||
"""
|
"""
|
||||||
Open core xml
|
Open core xml
|
||||||
"""
|
"""
|
||||||
|
@ -633,7 +724,8 @@ class CoreClient:
|
||||||
shutdown=shutdowns,
|
shutdown=shutdowns,
|
||||||
)
|
)
|
||||||
logging.info(
|
logging.info(
|
||||||
"Set %s service for node(%s), files: %s, Startup: %s, Validation: %s, Shutdown: %s, Result: %s",
|
"Set %s service for node(%s), files: %s, Startup: %s, "
|
||||||
|
"Validation: %s, Shutdown: %s, Result: %s",
|
||||||
service_name,
|
service_name,
|
||||||
node_id,
|
node_id,
|
||||||
files,
|
files,
|
||||||
|
@ -662,7 +754,7 @@ class CoreClient:
|
||||||
|
|
||||||
def set_node_service_file(
|
def set_node_service_file(
|
||||||
self, node_id: int, service_name: str, file_name: str, data: str
|
self, node_id: int, service_name: str, file_name: str, data: str
|
||||||
):
|
) -> None:
|
||||||
response = self.client.set_node_service_file(
|
response = self.client.set_node_service_file(
|
||||||
self.session_id, node_id, service_name, file_name, data
|
self.session_id, node_id, service_name, file_name, data
|
||||||
)
|
)
|
||||||
|
@ -675,36 +767,35 @@ class CoreClient:
|
||||||
response,
|
response,
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_nodes_and_links(self):
|
def create_nodes_and_links(self) -> None:
|
||||||
"""
|
"""
|
||||||
create nodes and links that have not been created yet
|
create nodes and links that have not been created yet
|
||||||
"""
|
"""
|
||||||
node_protos = [x.core_node for x in self.canvas_nodes.values()]
|
node_protos = [x.core_node for x in self.canvas_nodes.values()]
|
||||||
link_protos = [x.link for x in self.links.values()]
|
link_protos = [x.link for x in self.links.values()]
|
||||||
if self.state != core_pb2.SessionState.DEFINITION:
|
if self.state != SessionState.DEFINITION:
|
||||||
self.client.set_session_state(
|
self.client.set_session_state(self.session_id, SessionState.DEFINITION)
|
||||||
self.session_id, core_pb2.SessionState.DEFINITION
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION)
|
self.client.set_session_state(self.session_id, SessionState.DEFINITION)
|
||||||
for node_proto in node_protos:
|
for node_proto in node_protos:
|
||||||
response = self.client.add_node(self.session_id, node_proto)
|
response = self.client.add_node(self.session_id, node_proto)
|
||||||
logging.debug("create node: %s", response)
|
logging.debug("create node: %s", response)
|
||||||
for link_proto in link_protos:
|
for link_proto in link_protos:
|
||||||
response = self.client.add_link(
|
response = self.client.add_link(
|
||||||
self.session_id,
|
self.session_id,
|
||||||
link_proto.node_one_id,
|
link_proto.node1_id,
|
||||||
link_proto.node_two_id,
|
link_proto.node2_id,
|
||||||
link_proto.interface_one,
|
link_proto.iface1,
|
||||||
link_proto.interface_two,
|
link_proto.iface2,
|
||||||
link_proto.options,
|
link_proto.options,
|
||||||
)
|
)
|
||||||
logging.debug("create link: %s", response)
|
logging.debug("create link: %s", response)
|
||||||
|
|
||||||
def send_data(self):
|
def send_data(self) -> None:
|
||||||
"""
|
"""
|
||||||
send to daemon all session info, but don't start the session
|
Send to daemon all session info, but don't start the session
|
||||||
"""
|
"""
|
||||||
|
self.send_servers()
|
||||||
self.create_nodes_and_links()
|
self.create_nodes_and_links()
|
||||||
for config_proto in self.get_wlan_configs_proto():
|
for config_proto in self.get_wlan_configs_proto():
|
||||||
self.client.set_wlan_config(
|
self.client.set_wlan_config(
|
||||||
|
@ -739,15 +830,25 @@ class CoreClient:
|
||||||
config_proto.node_id,
|
config_proto.node_id,
|
||||||
config_proto.model,
|
config_proto.model,
|
||||||
config_proto.config,
|
config_proto.config,
|
||||||
config_proto.interface_id,
|
config_proto.iface_id,
|
||||||
)
|
)
|
||||||
if self.emane_config:
|
if self.emane_config:
|
||||||
config = {x: self.emane_config[x].value for x in self.emane_config}
|
config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||||
self.client.set_emane_config(self.session_id, config)
|
self.client.set_emane_config(self.session_id, config)
|
||||||
|
if self.location:
|
||||||
|
self.client.set_session_location(
|
||||||
|
self.session_id,
|
||||||
|
self.location.x,
|
||||||
|
self.location.y,
|
||||||
|
self.location.z,
|
||||||
|
self.location.lat,
|
||||||
|
self.location.lon,
|
||||||
|
self.location.alt,
|
||||||
|
self.location.scale,
|
||||||
|
)
|
||||||
self.set_metadata()
|
self.set_metadata()
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
"""
|
"""
|
||||||
Clean ups when done using grpc
|
Clean ups when done using grpc
|
||||||
"""
|
"""
|
||||||
|
@ -766,31 +867,31 @@ class CoreClient:
|
||||||
return i
|
return i
|
||||||
|
|
||||||
def create_node(
|
def create_node(
|
||||||
self, x: float, y: float, node_type: core_pb2.NodeType, model: str
|
self, x: float, y: float, node_type: NodeType, model: str
|
||||||
) -> Optional[core_pb2.Node]:
|
) -> Optional[Node]:
|
||||||
"""
|
"""
|
||||||
Add node, with information filled in, to grpc manager
|
Add node, with information filled in, to grpc manager
|
||||||
"""
|
"""
|
||||||
node_id = self.next_node_id()
|
node_id = self.next_node_id()
|
||||||
position = core_pb2.Position(x=x, y=y)
|
position = Position(x=x, y=y)
|
||||||
image = None
|
image = None
|
||||||
if NodeUtils.is_image_node(node_type):
|
if NodeUtils.is_image_node(node_type):
|
||||||
image = "ubuntu:latest"
|
image = "ubuntu:latest"
|
||||||
emane = None
|
emane = None
|
||||||
if node_type == core_pb2.NodeType.EMANE:
|
if node_type == NodeType.EMANE:
|
||||||
if not self.emane_models:
|
if not self.emane_models:
|
||||||
dialog = EmaneInstallDialog(self.app)
|
dialog = EmaneInstallDialog(self.app)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
return
|
return
|
||||||
emane = self.emane_models[0]
|
emane = self.emane_models[0]
|
||||||
name = f"EMANE{node_id}"
|
name = f"EMANE{node_id}"
|
||||||
elif node_type == core_pb2.NodeType.WIRELESS_LAN:
|
elif node_type == NodeType.WIRELESS_LAN:
|
||||||
name = f"WLAN{node_id}"
|
name = f"WLAN{node_id}"
|
||||||
elif node_type in [core_pb2.NodeType.RJ45, core_pb2.NodeType.TUNNEL]:
|
elif node_type in [NodeType.RJ45, NodeType.TUNNEL]:
|
||||||
name = "UNASSIGNED"
|
name = "UNASSIGNED"
|
||||||
else:
|
else:
|
||||||
name = f"n{node_id}"
|
name = f"n{node_id}"
|
||||||
node = core_pb2.Node(
|
node = Node(
|
||||||
id=node_id,
|
id=node_id,
|
||||||
type=node_type,
|
type=node_type,
|
||||||
name=name,
|
name=name,
|
||||||
|
@ -816,7 +917,7 @@ class CoreClient:
|
||||||
)
|
)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
def deleted_graph_nodes(self, canvas_nodes: List[core_pb2.Node]):
|
def deleted_graph_nodes(self, canvas_nodes: List[Node]) -> None:
|
||||||
"""
|
"""
|
||||||
remove the nodes selected by the user and anything related to that node
|
remove the nodes selected by the user and anything related to that node
|
||||||
such as link, configurations, interfaces
|
such as link, configurations, interfaces
|
||||||
|
@ -830,35 +931,35 @@ class CoreClient:
|
||||||
for edge in edges:
|
for edge in edges:
|
||||||
del self.links[edge.token]
|
del self.links[edge.token]
|
||||||
links.append(edge.link)
|
links.append(edge.link)
|
||||||
self.interfaces_manager.removed(links)
|
self.ifaces_manager.removed(links)
|
||||||
|
|
||||||
def create_interface(self, canvas_node: CanvasNode) -> core_pb2.Interface:
|
def create_iface(self, canvas_node: CanvasNode) -> Interface:
|
||||||
node = canvas_node.core_node
|
node = canvas_node.core_node
|
||||||
ip4, ip6 = self.interfaces_manager.get_ips(node)
|
ip4, ip6 = self.ifaces_manager.get_ips(node)
|
||||||
ip4_mask = self.interfaces_manager.ip4_mask
|
ip4_mask = self.ifaces_manager.ip4_mask
|
||||||
ip6_mask = self.interfaces_manager.ip6_mask
|
ip6_mask = self.ifaces_manager.ip6_mask
|
||||||
interface_id = canvas_node.next_interface_id()
|
iface_id = canvas_node.next_iface_id()
|
||||||
name = f"eth{interface_id}"
|
name = f"eth{iface_id}"
|
||||||
interface = core_pb2.Interface(
|
iface = Interface(
|
||||||
id=interface_id,
|
id=iface_id,
|
||||||
name=name,
|
name=name,
|
||||||
ip4=ip4,
|
ip4=ip4,
|
||||||
ip4mask=ip4_mask,
|
ip4_mask=ip4_mask,
|
||||||
ip6=ip6,
|
ip6=ip6,
|
||||||
ip6mask=ip6_mask,
|
ip6_mask=ip6_mask,
|
||||||
)
|
)
|
||||||
logging.info(
|
logging.info(
|
||||||
"create node(%s) interface(%s) IPv4(%s) IPv6(%s)",
|
"create node(%s) interface(%s) IPv4(%s) IPv6(%s)",
|
||||||
node.name,
|
node.name,
|
||||||
interface.name,
|
iface.name,
|
||||||
interface.ip4,
|
iface.ip4,
|
||||||
interface.ip6,
|
iface.ip6,
|
||||||
)
|
)
|
||||||
return interface
|
return iface
|
||||||
|
|
||||||
def create_link(
|
def create_link(
|
||||||
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
|
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Create core link for a pair of canvas nodes, with token referencing
|
Create core link for a pair of canvas nodes, with token referencing
|
||||||
the canvas edge.
|
the canvas edge.
|
||||||
|
@ -867,34 +968,34 @@ class CoreClient:
|
||||||
dst_node = canvas_dst_node.core_node
|
dst_node = canvas_dst_node.core_node
|
||||||
|
|
||||||
# determine subnet
|
# determine subnet
|
||||||
self.interfaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)
|
self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)
|
||||||
|
|
||||||
src_interface = None
|
src_iface = None
|
||||||
if NodeUtils.is_container_node(src_node.type):
|
if NodeUtils.is_container_node(src_node.type):
|
||||||
src_interface = self.create_interface(canvas_src_node)
|
src_iface = self.create_iface(canvas_src_node)
|
||||||
self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token
|
self.iface_to_edge[(src_node.id, src_iface.id)] = edge.token
|
||||||
|
|
||||||
dst_interface = None
|
dst_iface = None
|
||||||
if NodeUtils.is_container_node(dst_node.type):
|
if NodeUtils.is_container_node(dst_node.type):
|
||||||
dst_interface = self.create_interface(canvas_dst_node)
|
dst_iface = self.create_iface(canvas_dst_node)
|
||||||
self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token
|
self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token
|
||||||
|
|
||||||
link = core_pb2.Link(
|
link = Link(
|
||||||
type=core_pb2.LinkType.WIRED,
|
type=LinkType.WIRED,
|
||||||
node_one_id=src_node.id,
|
node1_id=src_node.id,
|
||||||
node_two_id=dst_node.id,
|
node2_id=dst_node.id,
|
||||||
interface_one=src_interface,
|
iface1=src_iface,
|
||||||
interface_two=dst_interface,
|
iface2=dst_iface,
|
||||||
)
|
)
|
||||||
# assign after creating link proto, since interfaces are copied
|
# assign after creating link proto, since interfaces are copied
|
||||||
if src_interface:
|
if src_iface:
|
||||||
interface_one = link.interface_one
|
iface1 = link.iface1
|
||||||
edge.src_interface = interface_one
|
edge.src_iface = iface1
|
||||||
canvas_src_node.interfaces[interface_one.id] = interface_one
|
canvas_src_node.ifaces[iface1.id] = iface1
|
||||||
if dst_interface:
|
if dst_iface:
|
||||||
interface_two = link.interface_two
|
iface2 = link.iface2
|
||||||
edge.dst_interface = interface_two
|
edge.dst_iface = iface2
|
||||||
canvas_dst_node.interfaces[interface_two.id] = interface_two
|
canvas_dst_node.ifaces[iface2.id] = iface2
|
||||||
edge.set_link(link)
|
edge.set_link(link)
|
||||||
self.links[edge.token] = edge
|
self.links[edge.token] = edge
|
||||||
logging.info("Add link between %s and %s", src_node.name, dst_node.name)
|
logging.info("Add link between %s and %s", src_node.name, dst_node.name)
|
||||||
|
@ -902,7 +1003,7 @@ class CoreClient:
|
||||||
def get_wlan_configs_proto(self) -> List[WlanConfig]:
|
def get_wlan_configs_proto(self) -> List[WlanConfig]:
|
||||||
configs = []
|
configs = []
|
||||||
for canvas_node in self.canvas_nodes.values():
|
for canvas_node in self.canvas_nodes.values():
|
||||||
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN:
|
if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
|
||||||
continue
|
continue
|
||||||
if not canvas_node.wlan_config:
|
if not canvas_node.wlan_config:
|
||||||
continue
|
continue
|
||||||
|
@ -916,7 +1017,7 @@ class CoreClient:
|
||||||
def get_mobility_configs_proto(self) -> List[MobilityConfig]:
|
def get_mobility_configs_proto(self) -> List[MobilityConfig]:
|
||||||
configs = []
|
configs = []
|
||||||
for canvas_node in self.canvas_nodes.values():
|
for canvas_node in self.canvas_nodes.values():
|
||||||
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN:
|
if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
|
||||||
continue
|
continue
|
||||||
if not canvas_node.mobility_config:
|
if not canvas_node.mobility_config:
|
||||||
continue
|
continue
|
||||||
|
@ -930,16 +1031,16 @@ class CoreClient:
|
||||||
def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]:
|
def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]:
|
||||||
configs = []
|
configs = []
|
||||||
for canvas_node in self.canvas_nodes.values():
|
for canvas_node in self.canvas_nodes.values():
|
||||||
if canvas_node.core_node.type != core_pb2.NodeType.EMANE:
|
if canvas_node.core_node.type != NodeType.EMANE:
|
||||||
continue
|
continue
|
||||||
node_id = canvas_node.core_node.id
|
node_id = canvas_node.core_node.id
|
||||||
for key, config in canvas_node.emane_model_configs.items():
|
for key, config in canvas_node.emane_model_configs.items():
|
||||||
model, interface = key
|
model, iface_id = key
|
||||||
config = {x: config[x].value for x in config}
|
config = {x: config[x].value for x in config}
|
||||||
if interface is None:
|
if iface_id is None:
|
||||||
interface = -1
|
iface_id = -1
|
||||||
config_proto = EmaneModelConfig(
|
config_proto = EmaneModelConfig(
|
||||||
node_id=node_id, interface_id=interface, model=model, config=config
|
node_id=node_id, iface_id=iface_id, model=model, config=config
|
||||||
)
|
)
|
||||||
configs.append(config_proto)
|
configs.append(config_proto)
|
||||||
return configs
|
return configs
|
||||||
|
@ -981,9 +1082,7 @@ class CoreClient:
|
||||||
configs.append(config_proto)
|
configs.append(config_proto)
|
||||||
return configs
|
return configs
|
||||||
|
|
||||||
def get_config_service_configs_proto(
|
def get_config_service_configs_proto(self) -> List[ConfigServiceConfig]:
|
||||||
self
|
|
||||||
) -> List[configservices_pb2.ConfigServiceConfig]:
|
|
||||||
config_service_protos = []
|
config_service_protos = []
|
||||||
for canvas_node in self.canvas_nodes.values():
|
for canvas_node in self.canvas_nodes.values():
|
||||||
if not NodeUtils.is_container_node(canvas_node.core_node.type):
|
if not NodeUtils.is_container_node(canvas_node.core_node.type):
|
||||||
|
@ -993,7 +1092,7 @@ class CoreClient:
|
||||||
node_id = canvas_node.core_node.id
|
node_id = canvas_node.core_node.id
|
||||||
for name, service_config in canvas_node.config_service_configs.items():
|
for name, service_config in canvas_node.config_service_configs.items():
|
||||||
config = service_config.get("config", {})
|
config = service_config.get("config", {})
|
||||||
config_proto = configservices_pb2.ConfigServiceConfig(
|
config_proto = ConfigServiceConfig(
|
||||||
node_id=node_id,
|
node_id=node_id,
|
||||||
name=name,
|
name=name,
|
||||||
templates=service_config["templates"],
|
templates=service_config["templates"],
|
||||||
|
@ -1006,7 +1105,7 @@ class CoreClient:
|
||||||
logging.info("running node(%s) cmd: %s", node_id, self.observer)
|
logging.info("running node(%s) cmd: %s", node_id, self.observer)
|
||||||
return self.client.node_command(self.session_id, node_id, self.observer).output
|
return self.client.node_command(self.session_id, node_id, self.observer).output
|
||||||
|
|
||||||
def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
|
def get_wlan_config(self, node_id: int) -> Dict[str, ConfigOption]:
|
||||||
response = self.client.get_wlan_config(self.session_id, node_id)
|
response = self.client.get_wlan_config(self.session_id, node_id)
|
||||||
config = response.config
|
config = response.config
|
||||||
logging.debug(
|
logging.debug(
|
||||||
|
@ -1016,7 +1115,7 @@ class CoreClient:
|
||||||
)
|
)
|
||||||
return dict(config)
|
return dict(config)
|
||||||
|
|
||||||
def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
|
def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]:
|
||||||
response = self.client.get_mobility_config(self.session_id, node_id)
|
response = self.client.get_mobility_config(self.session_id, node_id)
|
||||||
config = response.config
|
config = response.config
|
||||||
logging.debug(
|
logging.debug(
|
||||||
|
@ -1027,24 +1126,25 @@ class CoreClient:
|
||||||
return dict(config)
|
return dict(config)
|
||||||
|
|
||||||
def get_emane_model_config(
|
def get_emane_model_config(
|
||||||
self, node_id: int, model: str, interface: int = None
|
self, node_id: int, model: str, iface_id: int = None
|
||||||
) -> Dict[str, common_pb2.ConfigOption]:
|
) -> Dict[str, ConfigOption]:
|
||||||
if interface is None:
|
if iface_id is None:
|
||||||
interface = -1
|
iface_id = -1
|
||||||
response = self.client.get_emane_model_config(
|
response = self.client.get_emane_model_config(
|
||||||
self.session_id, node_id, model, interface
|
self.session_id, node_id, model, iface_id
|
||||||
)
|
)
|
||||||
config = response.config
|
config = response.config
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"get emane model config: node id: %s, EMANE model: %s, interface: %s, config: %s",
|
"get emane model config: node id: %s, EMANE model: %s, "
|
||||||
|
"interface: %s, config: %s",
|
||||||
node_id,
|
node_id,
|
||||||
model,
|
model,
|
||||||
interface,
|
iface_id,
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
return dict(config)
|
return dict(config)
|
||||||
|
|
||||||
def execute_script(self, script):
|
def execute_script(self, script) -> None:
|
||||||
response = self.client.execute_script(script)
|
response = self.client.execute_script(script)
|
||||||
logging.info("execute python script %s", response)
|
logging.info("execute python script %s", response)
|
||||||
if response.session_id != -1:
|
if response.session_id != -1:
|
||||||
|
|
|
@ -188,7 +188,7 @@
|
||||||
<configuration name="error" value="0"/>
|
<configuration name="error" value="0"/>
|
||||||
</mobility_configuration>
|
</mobility_configuration>
|
||||||
<mobility_configuration node="10" model="ns2script">
|
<mobility_configuration node="10" model="ns2script">
|
||||||
<configuration name="file" value="/home/developer/.coretk/mobility/sample1.scen"/>
|
<configuration name="file" value="sample1.scen"/>
|
||||||
<configuration name="refresh_ms" value="50"/>
|
<configuration name="refresh_ms" value="50"/>
|
||||||
<configuration name="loop" value="1"/>
|
<configuration name="loop" value="1"/>
|
||||||
<configuration name="autostart" value="5"/>
|
<configuration name="autostart" value="5"/>
|
||||||
|
|
|
@ -35,11 +35,11 @@ THE POSSIBILITY OF SUCH DAMAGE.\
|
||||||
|
|
||||||
|
|
||||||
class AboutDialog(Dialog):
|
class AboutDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "About CORE")
|
super().__init__(app, "About CORE")
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ check engine light
|
||||||
"""
|
"""
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict, Optional
|
||||||
|
|
||||||
from core.api.grpc.core_pb2 import ExceptionLevel
|
from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.themes import PADX, PADY
|
from core.gui.themes import PADX, PADY
|
||||||
from core.gui.widgets import CodeText
|
from core.gui.widgets import CodeText
|
||||||
|
@ -15,14 +15,14 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class AlertsDialog(Dialog):
|
class AlertsDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "Alerts")
|
super().__init__(app, "Alerts")
|
||||||
self.tree = None
|
self.tree: Optional[ttk.Treeview] = None
|
||||||
self.codetext = None
|
self.codetext: Optional[CodeText] = None
|
||||||
self.alarm_map = {}
|
self.alarm_map: Dict[int, ExceptionEvent] = {}
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(1, weight=1)
|
self.top.rowconfigure(1, weight=1)
|
||||||
|
@ -52,6 +52,7 @@ class AlertsDialog(Dialog):
|
||||||
for alarm in self.app.statusbar.core_alarms:
|
for alarm in self.app.statusbar.core_alarms:
|
||||||
exception = alarm.exception_event
|
exception = alarm.exception_event
|
||||||
level_name = ExceptionLevel.Enum.Name(exception.level)
|
level_name = ExceptionLevel.Enum.Name(exception.level)
|
||||||
|
node_id = exception.node_id if exception.node_id else ""
|
||||||
insert_id = self.tree.insert(
|
insert_id = self.tree.insert(
|
||||||
"",
|
"",
|
||||||
tk.END,
|
tk.END,
|
||||||
|
@ -60,7 +61,7 @@ class AlertsDialog(Dialog):
|
||||||
exception.date,
|
exception.date,
|
||||||
level_name,
|
level_name,
|
||||||
alarm.session_id,
|
alarm.session_id,
|
||||||
exception.node_id,
|
node_id,
|
||||||
exception.source,
|
exception.source,
|
||||||
),
|
),
|
||||||
tags=(level_name,),
|
tags=(level_name,),
|
||||||
|
@ -97,16 +98,18 @@ class AlertsDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Close", command=self.destroy)
|
button = ttk.Button(frame, text="Close", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def reset_alerts(self):
|
def reset_alerts(self) -> None:
|
||||||
self.codetext.text.delete("1.0", tk.END)
|
self.codetext.text.config(state=tk.NORMAL)
|
||||||
|
self.codetext.text.delete(1.0, tk.END)
|
||||||
|
self.codetext.text.config(state=tk.DISABLED)
|
||||||
for item in self.tree.get_children():
|
for item in self.tree.get_children():
|
||||||
self.tree.delete(item)
|
self.tree.delete(item)
|
||||||
self.app.statusbar.core_alarms.clear()
|
self.app.statusbar.clear_alerts()
|
||||||
|
|
||||||
def click_select(self, event: tk.Event):
|
def click_select(self, event: tk.Event) -> None:
|
||||||
current = self.tree.selection()[0]
|
current = self.tree.selection()[0]
|
||||||
alarm = self.alarm_map[current]
|
alarm = self.alarm_map[current]
|
||||||
self.codetext.text.config(state=tk.NORMAL)
|
self.codetext.text.config(state=tk.NORMAL)
|
||||||
self.codetext.text.delete("1.0", "end")
|
self.codetext.text.delete(1.0, tk.END)
|
||||||
self.codetext.text.insert("1.0", alarm.exception_event.text)
|
self.codetext.text.insert(1.0, alarm.exception_event.text)
|
||||||
self.codetext.text.config(state=tk.DISABLED)
|
self.codetext.text.config(state=tk.DISABLED)
|
||||||
|
|
|
@ -7,38 +7,43 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from core.gui import validation
|
from core.gui import validation
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
from core.gui.graph.graph import CanvasGraph
|
||||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
|
||||||
PIXEL_SCALE = 100
|
PIXEL_SCALE: int = 100
|
||||||
|
|
||||||
|
|
||||||
class SizeAndScaleDialog(Dialog):
|
class SizeAndScaleDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
"""
|
"""
|
||||||
create an instance for size and scale object
|
create an instance for size and scale object
|
||||||
"""
|
"""
|
||||||
super().__init__(app, "Canvas Size and Scale")
|
super().__init__(app, "Canvas Size and Scale")
|
||||||
self.canvas = self.app.canvas
|
self.canvas: CanvasGraph = self.app.canvas
|
||||||
self.section_font = font.Font(weight="bold")
|
self.section_font: font.Font = font.Font(weight="bold")
|
||||||
width, height = self.canvas.current_dimensions
|
width, height = self.canvas.current_dimensions
|
||||||
self.pixel_width = tk.IntVar(value=width)
|
self.pixel_width: tk.IntVar = tk.IntVar(value=width)
|
||||||
self.pixel_height = tk.IntVar(value=height)
|
self.pixel_height: tk.IntVar = tk.IntVar(value=height)
|
||||||
location = self.app.core.location
|
location = self.app.core.location
|
||||||
self.x = tk.DoubleVar(value=location.x)
|
self.x: tk.DoubleVar = tk.DoubleVar(value=location.x)
|
||||||
self.y = tk.DoubleVar(value=location.y)
|
self.y: tk.DoubleVar = tk.DoubleVar(value=location.y)
|
||||||
self.lat = tk.DoubleVar(value=location.lat)
|
self.lat: tk.DoubleVar = tk.DoubleVar(value=location.lat)
|
||||||
self.lon = tk.DoubleVar(value=location.lon)
|
self.lon: tk.DoubleVar = tk.DoubleVar(value=location.lon)
|
||||||
self.alt = tk.DoubleVar(value=location.alt)
|
self.alt: tk.DoubleVar = tk.DoubleVar(value=location.alt)
|
||||||
self.scale = tk.DoubleVar(value=location.scale)
|
self.scale: tk.DoubleVar = tk.DoubleVar(value=location.scale)
|
||||||
self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale)
|
self.meters_width: tk.IntVar = tk.IntVar(
|
||||||
self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale)
|
value=width / PIXEL_SCALE * location.scale
|
||||||
self.save_default = tk.BooleanVar(value=False)
|
)
|
||||||
|
self.meters_height: tk.IntVar = tk.IntVar(
|
||||||
|
value=height / PIXEL_SCALE * location.scale
|
||||||
|
)
|
||||||
|
self.save_default: tk.BooleanVar = tk.BooleanVar(value=False)
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.draw_size()
|
self.draw_size()
|
||||||
self.draw_scale()
|
self.draw_scale()
|
||||||
|
@ -47,7 +52,7 @@ class SizeAndScaleDialog(Dialog):
|
||||||
self.draw_spacer()
|
self.draw_spacer()
|
||||||
self.draw_buttons()
|
self.draw_buttons()
|
||||||
|
|
||||||
def draw_size(self):
|
def draw_size(self) -> None:
|
||||||
label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD)
|
label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD)
|
||||||
label_frame.grid(sticky="ew")
|
label_frame.grid(sticky="ew")
|
||||||
label_frame.columnconfigure(0, weight=1)
|
label_frame.columnconfigure(0, weight=1)
|
||||||
|
@ -61,10 +66,12 @@ class SizeAndScaleDialog(Dialog):
|
||||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||||
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
|
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
|
||||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||||
|
entry.bind("<KeyRelease>", self.size_scale_keyup)
|
||||||
label = ttk.Label(frame, text="x Height")
|
label = ttk.Label(frame, text="x Height")
|
||||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||||
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
|
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
|
||||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||||
|
entry.bind("<KeyRelease>", self.size_scale_keyup)
|
||||||
label = ttk.Label(frame, text="Pixels")
|
label = ttk.Label(frame, text="Pixels")
|
||||||
label.grid(row=0, column=4, sticky="w")
|
label.grid(row=0, column=4, sticky="w")
|
||||||
|
|
||||||
|
@ -75,16 +82,20 @@ class SizeAndScaleDialog(Dialog):
|
||||||
frame.columnconfigure(3, weight=1)
|
frame.columnconfigure(3, weight=1)
|
||||||
label = ttk.Label(frame, text="Width")
|
label = ttk.Label(frame, text="Width")
|
||||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||||
entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_width)
|
entry = validation.PositiveFloatEntry(
|
||||||
|
frame, textvariable=self.meters_width, state=tk.DISABLED
|
||||||
|
)
|
||||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||||
label = ttk.Label(frame, text="x Height")
|
label = ttk.Label(frame, text="x Height")
|
||||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||||
entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_height)
|
entry = validation.PositiveFloatEntry(
|
||||||
|
frame, textvariable=self.meters_height, state=tk.DISABLED
|
||||||
|
)
|
||||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||||
label = ttk.Label(frame, text="Meters")
|
label = ttk.Label(frame, text="Meters")
|
||||||
label.grid(row=0, column=4, sticky="w")
|
label.grid(row=0, column=4, sticky="w")
|
||||||
|
|
||||||
def draw_scale(self):
|
def draw_scale(self) -> None:
|
||||||
label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD)
|
label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD)
|
||||||
label_frame.grid(sticky="ew")
|
label_frame.grid(sticky="ew")
|
||||||
label_frame.columnconfigure(0, weight=1)
|
label_frame.columnconfigure(0, weight=1)
|
||||||
|
@ -96,10 +107,11 @@ class SizeAndScaleDialog(Dialog):
|
||||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||||
entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
|
entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
|
||||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||||
|
entry.bind("<KeyRelease>", self.size_scale_keyup)
|
||||||
label = ttk.Label(frame, text="Meters")
|
label = ttk.Label(frame, text="Meters")
|
||||||
label.grid(row=0, column=2, sticky="w")
|
label.grid(row=0, column=2, sticky="w")
|
||||||
|
|
||||||
def draw_reference_point(self):
|
def draw_reference_point(self) -> None:
|
||||||
label_frame = ttk.Labelframe(
|
label_frame = ttk.Labelframe(
|
||||||
self.top, text="Reference Point", padding=FRAME_PAD
|
self.top, text="Reference Point", padding=FRAME_PAD
|
||||||
)
|
)
|
||||||
|
@ -150,13 +162,13 @@ class SizeAndScaleDialog(Dialog):
|
||||||
entry = validation.FloatEntry(frame, textvariable=self.alt)
|
entry = validation.FloatEntry(frame, textvariable=self.alt)
|
||||||
entry.grid(row=0, column=5, sticky="ew")
|
entry.grid(row=0, column=5, sticky="ew")
|
||||||
|
|
||||||
def draw_save_as_default(self):
|
def draw_save_as_default(self) -> None:
|
||||||
button = ttk.Checkbutton(
|
button = ttk.Checkbutton(
|
||||||
self.top, text="Save as default?", variable=self.save_default
|
self.top, text="Save as default?", variable=self.save_default
|
||||||
)
|
)
|
||||||
button.grid(sticky="w", pady=PADY)
|
button.grid(sticky="w", pady=PADY)
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.columnconfigure(0, weight=1)
|
frame.columnconfigure(0, weight=1)
|
||||||
frame.columnconfigure(1, weight=1)
|
frame.columnconfigure(1, weight=1)
|
||||||
|
@ -168,7 +180,14 @@ class SizeAndScaleDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def click_apply(self):
|
def size_scale_keyup(self, _event: tk.Event) -> None:
|
||||||
|
scale = self.scale.get()
|
||||||
|
width = self.pixel_width.get()
|
||||||
|
height = self.pixel_height.get()
|
||||||
|
self.meters_width.set(width / PIXEL_SCALE * scale)
|
||||||
|
self.meters_height.set(height / PIXEL_SCALE * scale)
|
||||||
|
|
||||||
|
def click_apply(self) -> None:
|
||||||
width, height = self.pixel_width.get(), self.pixel_height.get()
|
width, height = self.pixel_width.get(), self.pixel_height.get()
|
||||||
self.canvas.redraw_canvas((width, height))
|
self.canvas.redraw_canvas((width, height))
|
||||||
if self.canvas.wallpaper:
|
if self.canvas.wallpaper:
|
||||||
|
|
|
@ -4,10 +4,11 @@ set wallpaper
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
from core.gui.appconfig import BACKGROUNDS_PATH
|
from core.gui.appconfig import BACKGROUNDS_PATH
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
from core.gui.graph.graph import CanvasGraph
|
||||||
from core.gui.images import Images
|
from core.gui.images import Images
|
||||||
from core.gui.themes import PADX, PADY
|
from core.gui.themes import PADX, PADY
|
||||||
from core.gui.widgets import image_chooser
|
from core.gui.widgets import image_chooser
|
||||||
|
@ -17,20 +18,22 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class CanvasWallpaperDialog(Dialog):
|
class CanvasWallpaperDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
"""
|
"""
|
||||||
create an instance of CanvasWallpaper object
|
create an instance of CanvasWallpaper object
|
||||||
"""
|
"""
|
||||||
super().__init__(app, "Canvas Background")
|
super().__init__(app, "Canvas Background")
|
||||||
self.canvas = self.app.canvas
|
self.canvas: CanvasGraph = self.app.canvas
|
||||||
self.scale_option = tk.IntVar(value=self.canvas.scale_option.get())
|
self.scale_option: tk.IntVar = tk.IntVar(value=self.canvas.scale_option.get())
|
||||||
self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get())
|
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(
|
||||||
self.filename = tk.StringVar(value=self.canvas.wallpaper_file)
|
value=self.canvas.adjust_to_dim.get()
|
||||||
self.image_label = None
|
)
|
||||||
self.options = []
|
self.filename: tk.StringVar = tk.StringVar(value=self.canvas.wallpaper_file)
|
||||||
|
self.image_label: Optional[ttk.Label] = None
|
||||||
|
self.options: List[ttk.Radiobutton] = []
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.draw_image()
|
self.draw_image()
|
||||||
self.draw_image_label()
|
self.draw_image_label()
|
||||||
|
@ -40,19 +43,19 @@ class CanvasWallpaperDialog(Dialog):
|
||||||
self.draw_spacer()
|
self.draw_spacer()
|
||||||
self.draw_buttons()
|
self.draw_buttons()
|
||||||
|
|
||||||
def draw_image(self):
|
def draw_image(self) -> None:
|
||||||
self.image_label = ttk.Label(
|
self.image_label = ttk.Label(
|
||||||
self.top, text="(image preview)", width=32, anchor=tk.CENTER
|
self.top, text="(image preview)", width=32, anchor=tk.CENTER
|
||||||
)
|
)
|
||||||
self.image_label.grid(pady=PADY)
|
self.image_label.grid(pady=PADY)
|
||||||
|
|
||||||
def draw_image_label(self):
|
def draw_image_label(self) -> None:
|
||||||
label = ttk.Label(self.top, text="Image filename: ")
|
label = ttk.Label(self.top, text="Image filename: ")
|
||||||
label.grid(sticky="ew")
|
label.grid(sticky="ew")
|
||||||
if self.filename.get():
|
if self.filename.get():
|
||||||
self.draw_preview()
|
self.draw_preview()
|
||||||
|
|
||||||
def draw_image_selection(self):
|
def draw_image_selection(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.columnconfigure(0, weight=2)
|
frame.columnconfigure(0, weight=2)
|
||||||
frame.columnconfigure(1, weight=1)
|
frame.columnconfigure(1, weight=1)
|
||||||
|
@ -69,7 +72,7 @@ class CanvasWallpaperDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Clear", command=self.click_clear)
|
button = ttk.Button(frame, text="Clear", command=self.click_clear)
|
||||||
button.grid(row=0, column=2, sticky="ew")
|
button.grid(row=0, column=2, sticky="ew")
|
||||||
|
|
||||||
def draw_options(self):
|
def draw_options(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.columnconfigure(0, weight=1)
|
frame.columnconfigure(0, weight=1)
|
||||||
frame.columnconfigure(1, weight=1)
|
frame.columnconfigure(1, weight=1)
|
||||||
|
@ -101,7 +104,7 @@ class CanvasWallpaperDialog(Dialog):
|
||||||
button.grid(row=0, column=3, sticky="ew")
|
button.grid(row=0, column=3, sticky="ew")
|
||||||
self.options.append(button)
|
self.options.append(button)
|
||||||
|
|
||||||
def draw_additional_options(self):
|
def draw_additional_options(self) -> None:
|
||||||
checkbutton = ttk.Checkbutton(
|
checkbutton = ttk.Checkbutton(
|
||||||
self.top,
|
self.top,
|
||||||
text="Adjust canvas size to image dimensions",
|
text="Adjust canvas size to image dimensions",
|
||||||
|
@ -110,7 +113,7 @@ class CanvasWallpaperDialog(Dialog):
|
||||||
)
|
)
|
||||||
checkbutton.grid(sticky="ew", padx=PADX)
|
checkbutton.grid(sticky="ew", padx=PADX)
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(pady=PADY, sticky="ew")
|
frame.grid(pady=PADY, sticky="ew")
|
||||||
frame.columnconfigure(0, weight=1)
|
frame.columnconfigure(0, weight=1)
|
||||||
|
@ -122,18 +125,18 @@ class CanvasWallpaperDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def click_open_image(self):
|
def click_open_image(self) -> None:
|
||||||
filename = image_chooser(self, BACKGROUNDS_PATH)
|
filename = image_chooser(self, BACKGROUNDS_PATH)
|
||||||
if filename:
|
if filename:
|
||||||
self.filename.set(filename)
|
self.filename.set(filename)
|
||||||
self.draw_preview()
|
self.draw_preview()
|
||||||
|
|
||||||
def draw_preview(self):
|
def draw_preview(self) -> None:
|
||||||
image = Images.create(self.filename.get(), 250, 135)
|
image = Images.create(self.filename.get(), 250, 135)
|
||||||
self.image_label.config(image=image)
|
self.image_label.config(image=image)
|
||||||
self.image_label.image = image
|
self.image_label.image = image
|
||||||
|
|
||||||
def click_clear(self):
|
def click_clear(self) -> None:
|
||||||
"""
|
"""
|
||||||
delete like shown in image link entry if there is any
|
delete like shown in image link entry if there is any
|
||||||
"""
|
"""
|
||||||
|
@ -143,7 +146,7 @@ class CanvasWallpaperDialog(Dialog):
|
||||||
self.image_label.config(image="", width=32)
|
self.image_label.config(image="", width=32)
|
||||||
self.image_label.image = None
|
self.image_label.image = None
|
||||||
|
|
||||||
def click_adjust_canvas(self):
|
def click_adjust_canvas(self) -> None:
|
||||||
# deselect all radio buttons and grey them out
|
# deselect all radio buttons and grey them out
|
||||||
if self.adjust_to_dim.get():
|
if self.adjust_to_dim.get():
|
||||||
self.scale_option.set(0)
|
self.scale_option.set(0)
|
||||||
|
@ -155,7 +158,7 @@ class CanvasWallpaperDialog(Dialog):
|
||||||
for option in self.options:
|
for option in self.options:
|
||||||
option.config(state=tk.NORMAL)
|
option.config(state=tk.NORMAL)
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self) -> None:
|
||||||
self.canvas.scale_option.set(self.scale_option.get())
|
self.canvas.scale_option.set(self.scale_option.get())
|
||||||
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
|
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
|
||||||
self.canvas.show_grid.click_handler()
|
self.canvas.show_grid.click_handler()
|
||||||
|
|
|
@ -3,7 +3,7 @@ custom color picker
|
||||||
"""
|
"""
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
|
|
||||||
from core.gui import validation
|
from core.gui import validation
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
@ -18,23 +18,23 @@ class ColorPickerDialog(Dialog):
|
||||||
self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000"
|
self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000"
|
||||||
):
|
):
|
||||||
super().__init__(app, "Color Picker", master=master)
|
super().__init__(app, "Color Picker", master=master)
|
||||||
self.red_entry = None
|
self.red_entry: Optional[validation.RgbEntry] = None
|
||||||
self.blue_entry = None
|
self.blue_entry: Optional[validation.RgbEntry] = None
|
||||||
self.green_entry = None
|
self.green_entry: Optional[validation.RgbEntry] = None
|
||||||
self.hex_entry = None
|
self.hex_entry: Optional[validation.HexEntry] = None
|
||||||
self.red_label = None
|
self.red_label: Optional[ttk.Label] = None
|
||||||
self.green_label = None
|
self.green_label: Optional[ttk.Label] = None
|
||||||
self.blue_label = None
|
self.blue_label: Optional[ttk.Label] = None
|
||||||
self.display = None
|
self.display: Optional[tk.Frame] = None
|
||||||
self.color = initcolor
|
self.color: str = initcolor
|
||||||
red, green, blue = self.get_rgb(initcolor)
|
red, green, blue = self.get_rgb(initcolor)
|
||||||
self.red = tk.IntVar(value=red)
|
self.red: tk.IntVar = tk.IntVar(value=red)
|
||||||
self.blue = tk.IntVar(value=blue)
|
self.blue: tk.IntVar = tk.IntVar(value=blue)
|
||||||
self.green = tk.IntVar(value=green)
|
self.green: tk.IntVar = tk.IntVar(value=green)
|
||||||
self.hex = tk.StringVar(value=initcolor)
|
self.hex: tk.StringVar = tk.StringVar(value=initcolor)
|
||||||
self.red_scale = tk.IntVar(value=red)
|
self.red_scale: tk.IntVar = tk.IntVar(value=red)
|
||||||
self.green_scale = tk.IntVar(value=green)
|
self.green_scale: tk.IntVar = tk.IntVar(value=green)
|
||||||
self.blue_scale = tk.IntVar(value=blue)
|
self.blue_scale: tk.IntVar = tk.IntVar(value=blue)
|
||||||
self.draw()
|
self.draw()
|
||||||
self.set_bindings()
|
self.set_bindings()
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class ColorPickerDialog(Dialog):
|
||||||
self.show()
|
self.show()
|
||||||
return self.color
|
return self.color
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(3, weight=1)
|
self.top.rowconfigure(3, weight=1)
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ class ColorPickerDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def set_bindings(self):
|
def set_bindings(self) -> None:
|
||||||
self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
||||||
self.green_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
self.green_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
||||||
self.blue_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
self.blue_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
||||||
|
@ -146,7 +146,7 @@ class ColorPickerDialog(Dialog):
|
||||||
self.blue.trace_add("write", self.update_color)
|
self.blue.trace_add("write", self.update_color)
|
||||||
self.hex.trace_add("write", self.update_color)
|
self.hex.trace_add("write", self.update_color)
|
||||||
|
|
||||||
def button_ok(self):
|
def button_ok(self) -> None:
|
||||||
self.color = self.hex.get()
|
self.color = self.hex.get()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
@ -159,10 +159,10 @@ class ColorPickerDialog(Dialog):
|
||||||
green = self.green_entry.get()
|
green = self.green_entry.get()
|
||||||
return "#%02x%02x%02x" % (int(red), int(green), int(blue))
|
return "#%02x%02x%02x" % (int(red), int(green), int(blue))
|
||||||
|
|
||||||
def current_focus(self, focus: str):
|
def current_focus(self, focus: str) -> None:
|
||||||
self.focus = focus
|
self.focus = focus
|
||||||
|
|
||||||
def update_color(self, arg1=None, arg2=None, arg3=None):
|
def update_color(self, arg1=None, arg2=None, arg3=None) -> None:
|
||||||
if self.focus == "rgb":
|
if self.focus == "rgb":
|
||||||
red = self.red_entry.get()
|
red = self.red_entry.get()
|
||||||
blue = self.blue_entry.get()
|
blue = self.blue_entry.get()
|
||||||
|
@ -184,7 +184,7 @@ class ColorPickerDialog(Dialog):
|
||||||
self.display.config(background=hex_code)
|
self.display.config(background=hex_code)
|
||||||
self.set_label(str(red), str(green), str(blue))
|
self.set_label(str(red), str(green), str(blue))
|
||||||
|
|
||||||
def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar):
|
def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar) -> None:
|
||||||
color_var.set(var.get())
|
color_var.set(var.get())
|
||||||
self.focus = "rgb"
|
self.focus = "rgb"
|
||||||
self.update_color()
|
self.update_color()
|
||||||
|
@ -194,17 +194,17 @@ class ColorPickerDialog(Dialog):
|
||||||
self.green_scale.set(green)
|
self.green_scale.set(green)
|
||||||
self.blue_scale.set(blue)
|
self.blue_scale.set(blue)
|
||||||
|
|
||||||
def set_entry(self, red: int, green: int, blue: int):
|
def set_entry(self, red: int, green: int, blue: int) -> None:
|
||||||
self.red.set(red)
|
self.red.set(red)
|
||||||
self.green.set(green)
|
self.green.set(green)
|
||||||
self.blue.set(blue)
|
self.blue.set(blue)
|
||||||
|
|
||||||
def set_label(self, red: str, green: str, blue: str):
|
def set_label(self, red: str, green: str, blue: str) -> None:
|
||||||
self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0))
|
self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0))
|
||||||
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
|
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
|
||||||
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue)))
|
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue)))
|
||||||
|
|
||||||
def get_rgb(self, hex_code: str) -> [int, int, int]:
|
def get_rgb(self, hex_code: str) -> Tuple[int, int, int]:
|
||||||
"""
|
"""
|
||||||
convert a valid hex code to RGB values
|
convert a valid hex code to RGB values
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -4,10 +4,11 @@ Service configuration dialog
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
|
from core.api.grpc.common_pb2 import ConfigOption
|
||||||
from core.api.grpc.services_pb2 import ServiceValidationMode
|
from core.api.grpc.services_pb2 import ServiceValidationMode
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||||
|
@ -16,6 +17,7 @@ from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
from core.gui.graph.node import CanvasNode
|
from core.gui.graph.node import CanvasNode
|
||||||
|
from core.gui.coreclient import CoreClient
|
||||||
|
|
||||||
|
|
||||||
class ConfigServiceConfigDialog(Dialog):
|
class ConfigServiceConfigDialog(Dialog):
|
||||||
|
@ -26,56 +28,53 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
service_name: str,
|
service_name: str,
|
||||||
canvas_node: "CanvasNode",
|
canvas_node: "CanvasNode",
|
||||||
node_id: int,
|
node_id: int,
|
||||||
):
|
) -> None:
|
||||||
title = f"{service_name} Config Service"
|
title = f"{service_name} Config Service"
|
||||||
super().__init__(app, title, master=master)
|
super().__init__(app, title, master=master)
|
||||||
self.core = app.core
|
self.core: "CoreClient" = app.core
|
||||||
self.canvas_node = canvas_node
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
self.node_id = node_id
|
self.node_id: int = node_id
|
||||||
self.service_name = service_name
|
self.service_name: str = service_name
|
||||||
self.radiovar = tk.IntVar()
|
self.radiovar: tk.IntVar = tk.IntVar()
|
||||||
self.radiovar.set(2)
|
self.radiovar.set(2)
|
||||||
self.directories = []
|
self.directories: List[str] = []
|
||||||
self.templates = []
|
self.templates: List[str] = []
|
||||||
self.dependencies = []
|
self.dependencies: List[str] = []
|
||||||
self.executables = []
|
self.executables: List[str] = []
|
||||||
self.startup_commands = []
|
self.startup_commands: List[str] = []
|
||||||
self.validation_commands = []
|
self.validation_commands: List[str] = []
|
||||||
self.shutdown_commands = []
|
self.shutdown_commands: List[str] = []
|
||||||
self.default_startup = []
|
self.default_startup: List[str] = []
|
||||||
self.default_validate = []
|
self.default_validate: List[str] = []
|
||||||
self.default_shutdown = []
|
self.default_shutdown: List[str] = []
|
||||||
self.validation_mode = None
|
self.validation_mode: Optional[ServiceValidationMode] = None
|
||||||
self.validation_time = None
|
self.validation_time: Optional[int] = None
|
||||||
self.validation_period = tk.StringVar()
|
self.validation_period: tk.StringVar = tk.StringVar()
|
||||||
self.modes = []
|
self.modes: List[str] = []
|
||||||
self.mode_configs = {}
|
self.mode_configs: Dict[str, str] = {}
|
||||||
|
|
||||||
self.notebook = None
|
|
||||||
self.templates_combobox = None
|
|
||||||
self.modes_combobox = None
|
|
||||||
self.startup_commands_listbox = None
|
|
||||||
self.shutdown_commands_listbox = None
|
|
||||||
self.validate_commands_listbox = None
|
|
||||||
self.validation_time_entry = None
|
|
||||||
self.validation_mode_entry = None
|
|
||||||
self.template_text = None
|
|
||||||
self.validation_period_entry = None
|
|
||||||
self.original_service_files = {}
|
|
||||||
self.temp_service_files = {}
|
|
||||||
self.modified_files = set()
|
|
||||||
self.config_frame = None
|
|
||||||
self.default_config = None
|
|
||||||
self.config = None
|
|
||||||
|
|
||||||
self.has_error = False
|
|
||||||
|
|
||||||
|
self.notebook: Optional[ttk.Notebook] = None
|
||||||
|
self.templates_combobox: Optional[ttk.Combobox] = None
|
||||||
|
self.modes_combobox: Optional[ttk.Combobox] = None
|
||||||
|
self.startup_commands_listbox: Optional[tk.Listbox] = None
|
||||||
|
self.shutdown_commands_listbox: Optional[tk.Listbox] = None
|
||||||
|
self.validate_commands_listbox: Optional[tk.Listbox] = None
|
||||||
|
self.validation_time_entry: Optional[ttk.Entry] = None
|
||||||
|
self.validation_mode_entry: Optional[ttk.Entry] = None
|
||||||
|
self.template_text: Optional[CodeText] = None
|
||||||
|
self.validation_period_entry: Optional[ttk.Entry] = None
|
||||||
|
self.original_service_files: Dict[str, str] = {}
|
||||||
|
self.temp_service_files: Dict[str, str] = {}
|
||||||
|
self.modified_files: Set[str] = set()
|
||||||
|
self.config_frame: Optional[ConfigFrame] = None
|
||||||
|
self.default_config: Dict[str, str] = {}
|
||||||
|
self.config: Dict[str, ConfigOption] = {}
|
||||||
|
self.has_error: bool = False
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
if not self.has_error:
|
if not self.has_error:
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def load(self):
|
def load(self) -> None:
|
||||||
try:
|
try:
|
||||||
self.core.create_nodes_and_links()
|
self.core.create_nodes_and_links()
|
||||||
service = self.core.config_services[self.service_name]
|
service = self.core.config_services[self.service_name]
|
||||||
|
@ -116,7 +115,7 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
self.app.show_grpc_exception("Get Config Service Error", e)
|
self.app.show_grpc_exception("Get Config Service Error", e)
|
||||||
self.has_error = True
|
self.has_error = True
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
@ -130,7 +129,7 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
self.draw_tab_validation()
|
self.draw_tab_validation()
|
||||||
self.draw_buttons()
|
self.draw_buttons()
|
||||||
|
|
||||||
def draw_tab_files(self):
|
def draw_tab_files(self) -> None:
|
||||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||||
tab.grid(sticky="nsew")
|
tab.grid(sticky="nsew")
|
||||||
tab.columnconfigure(0, weight=1)
|
tab.columnconfigure(0, weight=1)
|
||||||
|
@ -174,7 +173,7 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
)
|
)
|
||||||
self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
|
self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||||
|
|
||||||
def draw_tab_config(self):
|
def draw_tab_config(self) -> None:
|
||||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||||
tab.grid(sticky="nsew")
|
tab.grid(sticky="nsew")
|
||||||
tab.columnconfigure(0, weight=1)
|
tab.columnconfigure(0, weight=1)
|
||||||
|
@ -198,7 +197,7 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||||
tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1)
|
tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1)
|
||||||
|
|
||||||
def draw_tab_startstop(self):
|
def draw_tab_startstop(self) -> None:
|
||||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||||
tab.grid(sticky="nsew")
|
tab.grid(sticky="nsew")
|
||||||
tab.columnconfigure(0, weight=1)
|
tab.columnconfigure(0, weight=1)
|
||||||
|
@ -239,7 +238,7 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
elif i == 2:
|
elif i == 2:
|
||||||
self.validate_commands_listbox = listbox_scroll.listbox
|
self.validate_commands_listbox = listbox_scroll.listbox
|
||||||
|
|
||||||
def draw_tab_validation(self):
|
def draw_tab_validation(self) -> None:
|
||||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||||
tab.grid(sticky="ew")
|
tab.grid(sticky="ew")
|
||||||
tab.columnconfigure(0, weight=1)
|
tab.columnconfigure(0, weight=1)
|
||||||
|
@ -298,7 +297,7 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
for dependency in self.dependencies:
|
for dependency in self.dependencies:
|
||||||
listbox_scroll.listbox.insert("end", dependency)
|
listbox_scroll.listbox.insert("end", dependency)
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
|
@ -312,7 +311,7 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=3, sticky="ew")
|
button.grid(row=0, column=3, sticky="ew")
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self) -> None:
|
||||||
current_listbox = self.master.current.listbox
|
current_listbox = self.master.current.listbox
|
||||||
if not self.is_custom():
|
if not self.is_custom():
|
||||||
self.canvas_node.config_service_configs.pop(self.service_name, None)
|
self.canvas_node.config_service_configs.pop(self.service_name, None)
|
||||||
|
@ -333,18 +332,18 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
|
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def handle_template_changed(self, event: tk.Event):
|
def handle_template_changed(self, event: tk.Event) -> None:
|
||||||
template = self.templates_combobox.get()
|
template = self.templates_combobox.get()
|
||||||
self.template_text.text.delete(1.0, "end")
|
self.template_text.text.delete(1.0, "end")
|
||||||
self.template_text.text.insert("end", self.temp_service_files[template])
|
self.template_text.text.insert("end", self.temp_service_files[template])
|
||||||
|
|
||||||
def handle_mode_changed(self, event: tk.Event):
|
def handle_mode_changed(self, event: tk.Event) -> None:
|
||||||
mode = self.modes_combobox.get()
|
mode = self.modes_combobox.get()
|
||||||
config = self.mode_configs[mode]
|
config = self.mode_configs[mode]
|
||||||
logging.info("mode config: %s", config)
|
logging.info("mode config: %s", config)
|
||||||
self.config_frame.set_values(config)
|
self.config_frame.set_values(config)
|
||||||
|
|
||||||
def update_template_file_data(self, event: tk.Event):
|
def update_template_file_data(self, event: tk.Event) -> None:
|
||||||
scrolledtext = event.widget
|
scrolledtext = event.widget
|
||||||
template = self.templates_combobox.get()
|
template = self.templates_combobox.get()
|
||||||
self.temp_service_files[template] = scrolledtext.get(1.0, "end")
|
self.temp_service_files[template] = scrolledtext.get(1.0, "end")
|
||||||
|
@ -353,7 +352,7 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
else:
|
else:
|
||||||
self.modified_files.discard(template)
|
self.modified_files.discard(template)
|
||||||
|
|
||||||
def is_custom(self):
|
def is_custom(self) -> bool:
|
||||||
has_custom_templates = len(self.modified_files) > 0
|
has_custom_templates = len(self.modified_files) > 0
|
||||||
has_custom_config = False
|
has_custom_config = False
|
||||||
if self.config_frame:
|
if self.config_frame:
|
||||||
|
@ -361,7 +360,7 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
has_custom_config = self.default_config != current
|
has_custom_config = self.default_config != current
|
||||||
return has_custom_templates or has_custom_config
|
return has_custom_templates or has_custom_config
|
||||||
|
|
||||||
def click_defaults(self):
|
def click_defaults(self) -> None:
|
||||||
self.canvas_node.config_service_configs.pop(self.service_name, None)
|
self.canvas_node.config_service_configs.pop(self.service_name, None)
|
||||||
logging.info(
|
logging.info(
|
||||||
"cleared config service config: %s", self.canvas_node.config_service_configs
|
"cleared config service config: %s", self.canvas_node.config_service_configs
|
||||||
|
@ -374,12 +373,12 @@ class ConfigServiceConfigDialog(Dialog):
|
||||||
logging.info("resetting defaults: %s", self.default_config)
|
logging.info("resetting defaults: %s", self.default_config)
|
||||||
self.config_frame.set_values(self.default_config)
|
self.config_frame.set_values(self.default_config)
|
||||||
|
|
||||||
def click_copy(self):
|
def click_copy(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def append_commands(
|
def append_commands(
|
||||||
self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
||||||
):
|
) -> None:
|
||||||
for cmd in to_add:
|
for cmd in to_add:
|
||||||
commands.append(cmd)
|
commands.append(cmd)
|
||||||
listbox.insert(tk.END, cmd)
|
listbox.insert(tk.END, cmd)
|
||||||
|
|
|
@ -4,81 +4,58 @@ copy service config dialog
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING, Tuple
|
from typing import TYPE_CHECKING, Dict, Optional
|
||||||
|
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.themes import FRAME_PAD, PADX
|
from core.gui.themes import PADX, PADY
|
||||||
from core.gui.widgets import CodeText
|
from core.gui.widgets import CodeText, ListboxScroll
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
from core.gui.dialogs.serviceconfig import ServiceConfigDialog
|
||||||
|
|
||||||
|
|
||||||
class CopyServiceConfigDialog(Dialog):
|
class CopyServiceConfigDialog(Dialog):
|
||||||
def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int):
|
def __init__(
|
||||||
super().__init__(app, f"Copy services to node {node_id}", master=master)
|
self,
|
||||||
self.parent = master
|
app: "Application",
|
||||||
self.node_id = node_id
|
dialog: "ServiceConfigDialog",
|
||||||
self.service_configs = app.core.service_configs
|
name: str,
|
||||||
self.file_configs = app.core.file_configs
|
service: str,
|
||||||
|
file_name: str,
|
||||||
self.tree = None
|
) -> None:
|
||||||
|
super().__init__(app, f"Copy Custom File to {name}", master=dialog)
|
||||||
|
self.dialog: "ServiceConfigDialog" = dialog
|
||||||
|
self.service: str = service
|
||||||
|
self.file_name: str = file_name
|
||||||
|
self.listbox: Optional[tk.Listbox] = None
|
||||||
|
self.nodes: Dict[str, int] = {}
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.tree = ttk.Treeview(self.top)
|
self.top.rowconfigure(1, weight=1)
|
||||||
self.tree.grid(row=0, column=0, sticky="ew", padx=PADX)
|
label = ttk.Label(
|
||||||
self.tree["columns"] = ()
|
self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER
|
||||||
self.tree.column("#0", width=270, minwidth=270, stretch=tk.YES)
|
)
|
||||||
self.tree.heading("#0", text="Service configuration items", anchor=tk.CENTER)
|
label.grid(sticky="ew", pady=PADY)
|
||||||
custom_nodes = set(self.service_configs).union(set(self.file_configs))
|
|
||||||
for nid in custom_nodes:
|
listbox_scroll = ListboxScroll(self.top)
|
||||||
treeid = self.tree.insert("", "end", text=f"n{nid}", tags="node")
|
listbox_scroll.grid(sticky="nsew", pady=PADY)
|
||||||
services = self.service_configs.get(nid, None)
|
self.listbox = listbox_scroll.listbox
|
||||||
files = self.file_configs.get(nid, None)
|
for canvas_node in self.app.canvas.nodes.values():
|
||||||
tree_ids = {}
|
file_configs = canvas_node.service_file_configs.get(self.service)
|
||||||
if services:
|
if not file_configs:
|
||||||
for service, config in services.items():
|
continue
|
||||||
serviceid = self.tree.insert(
|
data = file_configs.get(self.file_name)
|
||||||
treeid, "end", text=service, tags="service"
|
if not data:
|
||||||
)
|
continue
|
||||||
tree_ids[service] = serviceid
|
name = canvas_node.core_node.name
|
||||||
cmdup = config.startup[:]
|
self.nodes[name] = canvas_node.id
|
||||||
cmddown = config.shutdown[:]
|
self.listbox.insert(tk.END, name)
|
||||||
cmdval = config.validate[:]
|
|
||||||
self.tree.insert(
|
|
||||||
serviceid,
|
|
||||||
"end",
|
|
||||||
text=f"cmdup=({str(cmdup)[1:-1]})",
|
|
||||||
tags=("cmd", "up"),
|
|
||||||
)
|
|
||||||
self.tree.insert(
|
|
||||||
serviceid,
|
|
||||||
"end",
|
|
||||||
text=f"cmddown=({str(cmddown)[1:-1]})",
|
|
||||||
tags=("cmd", "down"),
|
|
||||||
)
|
|
||||||
self.tree.insert(
|
|
||||||
serviceid,
|
|
||||||
"end",
|
|
||||||
text=f"cmdval=({str(cmdval)[1:-1]})",
|
|
||||||
tags=("cmd", "val"),
|
|
||||||
)
|
|
||||||
if files:
|
|
||||||
for service, configs in files.items():
|
|
||||||
if service in tree_ids:
|
|
||||||
serviceid = tree_ids[service]
|
|
||||||
else:
|
|
||||||
serviceid = self.tree.insert(
|
|
||||||
treeid, "end", text=service, tags="service"
|
|
||||||
)
|
|
||||||
tree_ids[service] = serviceid
|
|
||||||
for filename, data in configs.items():
|
|
||||||
self.tree.insert(serviceid, "end", text=filename, tags="file")
|
|
||||||
|
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(row=1, column=0)
|
frame.grid(sticky="ew")
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
frame.columnconfigure(i, weight=1)
|
frame.columnconfigure(i, weight=1)
|
||||||
button = ttk.Button(frame, text="Copy", command=self.click_copy)
|
button = ttk.Button(frame, text="Copy", command=self.click_copy)
|
||||||
|
@ -86,118 +63,58 @@ class CopyServiceConfigDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="View", command=self.click_view)
|
button = ttk.Button(frame, text="View", command=self.click_view)
|
||||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
button.grid(row=0, column=2, sticky="ew")
|
||||||
|
|
||||||
def click_copy(self):
|
def click_copy(self) -> None:
|
||||||
selected = self.tree.selection()
|
selection = self.listbox.curselection()
|
||||||
if selected:
|
if not selection:
|
||||||
item = self.tree.item(selected[0])
|
return
|
||||||
if "file" in item["tags"]:
|
name = self.listbox.get(selection)
|
||||||
filename = item["text"]
|
canvas_node_id = self.nodes[name]
|
||||||
nid, service = self.get_node_service(selected)
|
canvas_node = self.app.canvas.nodes[canvas_node_id]
|
||||||
data = self.file_configs[nid][service][filename]
|
data = canvas_node.service_file_configs[self.service][self.file_name]
|
||||||
if service == self.parent.service_name:
|
self.dialog.temp_service_files[self.file_name] = data
|
||||||
self.parent.temp_service_files[filename] = data
|
self.dialog.modified_files.add(self.file_name)
|
||||||
self.parent.modified_files.add(filename)
|
self.dialog.service_file_data.text.delete(1.0, tk.END)
|
||||||
if self.parent.filename_combobox.get() == filename:
|
self.dialog.service_file_data.text.insert(tk.END, data)
|
||||||
self.parent.service_file_data.text.delete(1.0, "end")
|
|
||||||
self.parent.service_file_data.text.insert("end", data)
|
|
||||||
if "cmd" in item["tags"]:
|
|
||||||
nid, service = self.get_node_service(selected)
|
|
||||||
if service == self.master.service_name:
|
|
||||||
cmds = self.service_configs[nid][service]
|
|
||||||
if "up" in item["tags"]:
|
|
||||||
self.master.append_commands(
|
|
||||||
self.master.startup_commands,
|
|
||||||
self.master.startup_commands_listbox,
|
|
||||||
cmds.startup,
|
|
||||||
)
|
|
||||||
elif "down" in item["tags"]:
|
|
||||||
self.master.append_commands(
|
|
||||||
self.master.shutdown_commands,
|
|
||||||
self.master.shutdown_commands_listbox,
|
|
||||||
cmds.shutdown,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif "val" in item["tags"]:
|
|
||||||
self.master.append_commands(
|
|
||||||
self.master.validate_commands,
|
|
||||||
self.master.validate_commands_listbox,
|
|
||||||
cmds.validate,
|
|
||||||
)
|
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def click_view(self):
|
def click_view(self) -> None:
|
||||||
selected = self.tree.selection()
|
selection = self.listbox.curselection()
|
||||||
data = ""
|
if not selection:
|
||||||
if selected:
|
return
|
||||||
item = self.tree.item(selected[0])
|
name = self.listbox.get(selection)
|
||||||
if "file" in item["tags"]:
|
canvas_node_id = self.nodes[name]
|
||||||
nid, service = self.get_node_service(selected)
|
canvas_node = self.app.canvas.nodes[canvas_node_id]
|
||||||
data = self.file_configs[nid][service][item["text"]]
|
data = canvas_node.service_file_configs[self.service][self.file_name]
|
||||||
dialog = ViewConfigDialog(
|
dialog = ViewConfigDialog(
|
||||||
self, self.app, nid, data, item["text"].split("/")[-1]
|
self.app, self, name, self.service, self.file_name, data
|
||||||
)
|
)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
if "cmd" in item["tags"]:
|
|
||||||
nid, service = self.get_node_service(selected)
|
|
||||||
cmds = self.service_configs[nid][service]
|
|
||||||
if "up" in item["tags"]:
|
|
||||||
data = f"({str(cmds.startup[:])[1:-1]})"
|
|
||||||
dialog = ViewConfigDialog(
|
|
||||||
self, self.app, self.node_id, data, "cmdup"
|
|
||||||
)
|
|
||||||
elif "down" in item["tags"]:
|
|
||||||
data = f"({str(cmds.shutdown[:])[1:-1]})"
|
|
||||||
dialog = ViewConfigDialog(
|
|
||||||
self, self.app, self.node_id, data, "cmdup"
|
|
||||||
)
|
|
||||||
elif "val" in item["tags"]:
|
|
||||||
data = f"({str(cmds.validate[:])[1:-1]})"
|
|
||||||
dialog = ViewConfigDialog(
|
|
||||||
self, self.app, self.node_id, data, "cmdup"
|
|
||||||
)
|
|
||||||
dialog.show()
|
|
||||||
|
|
||||||
def get_node_service(self, selected: Tuple[str]) -> [int, str]:
|
|
||||||
service_tree_id = self.tree.parent(selected[0])
|
|
||||||
service_name = self.tree.item(service_tree_id)["text"]
|
|
||||||
node_tree_id = self.tree.parent(service_tree_id)
|
|
||||||
node_id = int(self.tree.item(node_tree_id)["text"][1:])
|
|
||||||
return node_id, service_name
|
|
||||||
|
|
||||||
|
|
||||||
class ViewConfigDialog(Dialog):
|
class ViewConfigDialog(Dialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
master: tk.BaseWidget,
|
|
||||||
app: "Application",
|
app: "Application",
|
||||||
node_id: int,
|
master: tk.BaseWidget,
|
||||||
|
name: str,
|
||||||
|
service: str,
|
||||||
|
file_name: str,
|
||||||
data: str,
|
data: str,
|
||||||
filename: str = None,
|
) -> None:
|
||||||
):
|
title = f"{name} Service({service}) File({file_name})"
|
||||||
super().__init__(app, f"n{node_id} config data", master=master)
|
super().__init__(app, title, master=master)
|
||||||
self.data = data
|
self.data = data
|
||||||
self.service_data = None
|
self.service_data = None
|
||||||
self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}")
|
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
self.top.rowconfigure(0, weight=1)
|
||||||
frame.columnconfigure(0, weight=1)
|
|
||||||
frame.columnconfigure(1, weight=10)
|
|
||||||
frame.grid(row=0, column=0, sticky="ew")
|
|
||||||
label = ttk.Label(frame, text="File: ")
|
|
||||||
label.grid(row=0, column=0, sticky="ew", padx=PADX)
|
|
||||||
entry = ttk.Entry(frame, textvariable=self.filepath)
|
|
||||||
entry.config(state="disabled")
|
|
||||||
entry.grid(row=0, column=1, sticky="ew")
|
|
||||||
|
|
||||||
self.service_data = CodeText(self.top)
|
self.service_data = CodeText(self.top)
|
||||||
self.service_data.grid(row=1, column=0, sticky="nsew")
|
self.service_data.grid(sticky="nsew", pady=PADY)
|
||||||
self.service_data.text.insert("end", self.data)
|
self.service_data.text.insert(tk.END, self.data)
|
||||||
self.service_data.text.config(state="disabled")
|
self.service_data.text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
button = ttk.Button(self.top, text="Close", command=self.destroy)
|
button = ttk.Button(self.top, text="Close", command=self.destroy)
|
||||||
button.grid(row=2, column=0, sticky="ew", padx=PADX)
|
button.grid(sticky="ew")
|
||||||
|
|
|
@ -2,7 +2,9 @@ import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING, Set
|
from typing import TYPE_CHECKING, Optional, Set
|
||||||
|
|
||||||
|
from PIL.ImageTk import PhotoImage
|
||||||
|
|
||||||
from core.gui import nodeutils
|
from core.gui import nodeutils
|
||||||
from core.gui.appconfig import ICONS_PATH, CustomNode
|
from core.gui.appconfig import ICONS_PATH, CustomNode
|
||||||
|
@ -19,15 +21,15 @@ if TYPE_CHECKING:
|
||||||
class ServicesSelectDialog(Dialog):
|
class ServicesSelectDialog(Dialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
|
self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
|
||||||
):
|
) -> None:
|
||||||
super().__init__(app, "Node Services", master=master)
|
super().__init__(app, "Node Services", master=master)
|
||||||
self.groups = None
|
self.groups: Optional[ListboxScroll] = None
|
||||||
self.services = None
|
self.services: Optional[CheckboxList] = None
|
||||||
self.current = None
|
self.current: Optional[ListboxScroll] = None
|
||||||
self.current_services = set(current_services)
|
self.current_services: Set[str] = current_services
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
@ -77,7 +79,7 @@ class ServicesSelectDialog(Dialog):
|
||||||
# trigger group change
|
# trigger group change
|
||||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
||||||
|
|
||||||
def handle_group_change(self, event: tk.Event):
|
def handle_group_change(self, event: tk.Event) -> None:
|
||||||
selection = self.groups.listbox.curselection()
|
selection = self.groups.listbox.curselection()
|
||||||
if selection:
|
if selection:
|
||||||
index = selection[0]
|
index = selection[0]
|
||||||
|
@ -87,7 +89,7 @@ class ServicesSelectDialog(Dialog):
|
||||||
checked = name in self.current_services
|
checked = name in self.current_services
|
||||||
self.services.add(name, checked)
|
self.services.add(name, checked)
|
||||||
|
|
||||||
def service_clicked(self, name: str, var: tk.BooleanVar):
|
def service_clicked(self, name: str, var: tk.BooleanVar) -> None:
|
||||||
if var.get() and name not in self.current_services:
|
if var.get() and name not in self.current_services:
|
||||||
self.current_services.add(name)
|
self.current_services.add(name)
|
||||||
elif not var.get() and name in self.current_services:
|
elif not var.get() and name in self.current_services:
|
||||||
|
@ -96,34 +98,34 @@ class ServicesSelectDialog(Dialog):
|
||||||
for name in sorted(self.current_services):
|
for name in sorted(self.current_services):
|
||||||
self.current.listbox.insert(tk.END, name)
|
self.current.listbox.insert(tk.END, name)
|
||||||
|
|
||||||
def click_cancel(self):
|
def click_cancel(self) -> None:
|
||||||
self.current_services = None
|
self.current_services = None
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
class CustomNodesDialog(Dialog):
|
class CustomNodesDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "Custom Nodes")
|
super().__init__(app, "Custom Nodes")
|
||||||
self.edit_button = None
|
self.edit_button: Optional[ttk.Button] = None
|
||||||
self.delete_button = None
|
self.delete_button: Optional[ttk.Button] = None
|
||||||
self.nodes_list = None
|
self.nodes_list: Optional[ListboxScroll] = None
|
||||||
self.name = tk.StringVar()
|
self.name: tk.StringVar = tk.StringVar()
|
||||||
self.image_button = None
|
self.image_button: Optional[ttk.Button] = None
|
||||||
self.image = None
|
self.image: Optional[PhotoImage] = None
|
||||||
self.image_file = None
|
self.image_file: Optional[str] = None
|
||||||
self.services = set()
|
self.services: Set[str] = set()
|
||||||
self.selected = None
|
self.selected: Optional[str] = None
|
||||||
self.selected_index = None
|
self.selected_index: Optional[int] = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
self.draw_node_config()
|
self.draw_node_config()
|
||||||
self.draw_node_buttons()
|
self.draw_node_buttons()
|
||||||
self.draw_buttons()
|
self.draw_buttons()
|
||||||
|
|
||||||
def draw_node_config(self):
|
def draw_node_config(self) -> None:
|
||||||
frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
|
frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
|
||||||
frame.grid(sticky="nsew", pady=PADY)
|
frame.grid(sticky="nsew", pady=PADY)
|
||||||
frame.columnconfigure(0, weight=1)
|
frame.columnconfigure(0, weight=1)
|
||||||
|
@ -147,7 +149,7 @@ class CustomNodesDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Services", command=self.click_services)
|
button = ttk.Button(frame, text="Services", command=self.click_services)
|
||||||
button.grid(sticky="ew")
|
button.grid(sticky="ew")
|
||||||
|
|
||||||
def draw_node_buttons(self):
|
def draw_node_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew", pady=PADY)
|
frame.grid(sticky="ew", pady=PADY)
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
|
@ -166,7 +168,7 @@ class CustomNodesDialog(Dialog):
|
||||||
)
|
)
|
||||||
self.delete_button.grid(row=0, column=2, sticky="ew")
|
self.delete_button.grid(row=0, column=2, sticky="ew")
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
|
@ -178,14 +180,14 @@ class CustomNodesDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def reset_values(self):
|
def reset_values(self) -> None:
|
||||||
self.name.set("")
|
self.name.set("")
|
||||||
self.image = None
|
self.image = None
|
||||||
self.image_file = None
|
self.image_file = None
|
||||||
self.services = set()
|
self.services = set()
|
||||||
self.image_button.config(image="")
|
self.image_button.config(image="")
|
||||||
|
|
||||||
def click_icon(self):
|
def click_icon(self) -> None:
|
||||||
file_path = image_chooser(self, ICONS_PATH)
|
file_path = image_chooser(self, ICONS_PATH)
|
||||||
if file_path:
|
if file_path:
|
||||||
image = Images.create(file_path, nodeutils.ICON_SIZE)
|
image = Images.create(file_path, nodeutils.ICON_SIZE)
|
||||||
|
@ -193,24 +195,26 @@ class CustomNodesDialog(Dialog):
|
||||||
self.image_file = file_path
|
self.image_file = file_path
|
||||||
self.image_button.config(image=self.image)
|
self.image_button.config(image=self.image)
|
||||||
|
|
||||||
def click_services(self):
|
def click_services(self) -> None:
|
||||||
dialog = ServicesSelectDialog(self, self.app, self.services)
|
dialog = ServicesSelectDialog(self, self.app, self.services)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
if dialog.current_services is not None:
|
if dialog.current_services is not None:
|
||||||
self.services.clear()
|
self.services.clear()
|
||||||
self.services.update(dialog.current_services)
|
self.services.update(dialog.current_services)
|
||||||
|
|
||||||
def click_save(self):
|
def click_save(self) -> None:
|
||||||
self.app.guiconfig.nodes.clear()
|
self.app.guiconfig.nodes.clear()
|
||||||
for name in self.app.core.custom_nodes:
|
for name in self.app.core.custom_nodes:
|
||||||
node_draw = self.app.core.custom_nodes[name]
|
node_draw = self.app.core.custom_nodes[name]
|
||||||
custom_node = CustomNode(name, node_draw.image_file, node_draw.services)
|
custom_node = CustomNode(
|
||||||
|
name, node_draw.image_file, list(node_draw.services)
|
||||||
|
)
|
||||||
self.app.guiconfig.nodes.append(custom_node)
|
self.app.guiconfig.nodes.append(custom_node)
|
||||||
logging.info("saving custom nodes: %s", self.app.guiconfig.nodes)
|
logging.info("saving custom nodes: %s", self.app.guiconfig.nodes)
|
||||||
self.app.save_config()
|
self.app.save_config()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def click_create(self):
|
def click_create(self) -> None:
|
||||||
name = self.name.get()
|
name = self.name.get()
|
||||||
if name not in self.app.core.custom_nodes:
|
if name not in self.app.core.custom_nodes:
|
||||||
image_file = Path(self.image_file).stem
|
image_file = Path(self.image_file).stem
|
||||||
|
@ -226,7 +230,7 @@ class CustomNodesDialog(Dialog):
|
||||||
self.nodes_list.listbox.insert(tk.END, name)
|
self.nodes_list.listbox.insert(tk.END, name)
|
||||||
self.reset_values()
|
self.reset_values()
|
||||||
|
|
||||||
def click_edit(self):
|
def click_edit(self) -> None:
|
||||||
name = self.name.get()
|
name = self.name.get()
|
||||||
if self.selected:
|
if self.selected:
|
||||||
previous_name = self.selected
|
previous_name = self.selected
|
||||||
|
@ -247,7 +251,7 @@ class CustomNodesDialog(Dialog):
|
||||||
self.nodes_list.listbox.insert(self.selected_index, name)
|
self.nodes_list.listbox.insert(self.selected_index, name)
|
||||||
self.nodes_list.listbox.selection_set(self.selected_index)
|
self.nodes_list.listbox.selection_set(self.selected_index)
|
||||||
|
|
||||||
def click_delete(self):
|
def click_delete(self) -> None:
|
||||||
if self.selected and self.selected in self.app.core.custom_nodes:
|
if self.selected and self.selected in self.app.core.custom_nodes:
|
||||||
self.nodes_list.listbox.delete(self.selected_index)
|
self.nodes_list.listbox.delete(self.selected_index)
|
||||||
del self.app.core.custom_nodes[self.selected]
|
del self.app.core.custom_nodes[self.selected]
|
||||||
|
@ -255,7 +259,7 @@ class CustomNodesDialog(Dialog):
|
||||||
self.nodes_list.listbox.selection_clear(0, tk.END)
|
self.nodes_list.listbox.selection_clear(0, tk.END)
|
||||||
self.nodes_list.listbox.event_generate("<<ListboxSelect>>")
|
self.nodes_list.listbox.event_generate("<<ListboxSelect>>")
|
||||||
|
|
||||||
def handle_node_select(self, event: tk.Event):
|
def handle_node_select(self, event: tk.Event) -> None:
|
||||||
selection = self.nodes_list.listbox.curselection()
|
selection = self.nodes_list.listbox.curselection()
|
||||||
if selection:
|
if selection:
|
||||||
self.selected_index = selection[0]
|
self.selected_index = selection[0]
|
||||||
|
|
|
@ -16,23 +16,23 @@ class Dialog(tk.Toplevel):
|
||||||
title: str,
|
title: str,
|
||||||
modal: bool = True,
|
modal: bool = True,
|
||||||
master: tk.BaseWidget = None,
|
master: tk.BaseWidget = None,
|
||||||
):
|
) -> None:
|
||||||
if master is None:
|
if master is None:
|
||||||
master = app
|
master = app
|
||||||
super().__init__(master)
|
super().__init__(master)
|
||||||
self.withdraw()
|
self.withdraw()
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
self.modal = modal
|
self.modal: bool = modal
|
||||||
self.title(title)
|
self.title(title)
|
||||||
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||||
image = Images.get(ImageEnum.CORE, 16)
|
image = Images.get(ImageEnum.CORE, 16)
|
||||||
self.tk.call("wm", "iconphoto", self._w, image)
|
self.tk.call("wm", "iconphoto", self._w, image)
|
||||||
self.columnconfigure(0, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
self.rowconfigure(0, weight=1)
|
self.rowconfigure(0, weight=1)
|
||||||
self.top = ttk.Frame(self, padding=DIALOG_PAD)
|
self.top: ttk.Frame = ttk.Frame(self, padding=DIALOG_PAD)
|
||||||
self.top.grid(sticky="nsew")
|
self.top.grid(sticky="nsew")
|
||||||
|
|
||||||
def show(self):
|
def show(self) -> None:
|
||||||
self.transient(self.master)
|
self.transient(self.master)
|
||||||
self.focus_force()
|
self.focus_force()
|
||||||
self.update()
|
self.update()
|
||||||
|
@ -42,7 +42,7 @@ class Dialog(tk.Toplevel):
|
||||||
self.grab_set()
|
self.grab_set()
|
||||||
self.wait_window()
|
self.wait_window()
|
||||||
|
|
||||||
def draw_spacer(self, row: int = None):
|
def draw_spacer(self, row: int = None) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(row=row, sticky="nsew")
|
frame.grid(row=row, sticky="nsew")
|
||||||
frame.rowconfigure(0, weight=1)
|
frame.rowconfigure(0, weight=1)
|
||||||
|
|
|
@ -4,10 +4,12 @@ emane configuration
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
|
from core.api.grpc.common_pb2 import ConfigOption
|
||||||
|
from core.api.grpc.core_pb2 import Node
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.images import ImageEnum, Images
|
from core.gui.images import ImageEnum, Images
|
||||||
from core.gui.themes import PADX, PADY
|
from core.gui.themes import PADX, PADY
|
||||||
|
@ -19,32 +21,35 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class GlobalEmaneDialog(Dialog):
|
class GlobalEmaneDialog(Dialog):
|
||||||
def __init__(self, master: tk.BaseWidget, app: "Application"):
|
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
|
||||||
super().__init__(app, "EMANE Configuration", master=master)
|
super().__init__(app, "EMANE Configuration", master=master)
|
||||||
self.config_frame = None
|
self.config_frame: Optional[ConfigFrame] = None
|
||||||
|
self.enabled: bool = not self.app.core.is_runtime()
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config)
|
self.config_frame = ConfigFrame(
|
||||||
|
self.top, self.app, self.app.core.emane_config, self.enabled
|
||||||
|
)
|
||||||
self.config_frame.draw_config()
|
self.config_frame.draw_config()
|
||||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||||
self.draw_spacer()
|
self.draw_spacer()
|
||||||
self.draw_buttons()
|
self.draw_buttons()
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
frame.columnconfigure(i, weight=1)
|
frame.columnconfigure(i, weight=1)
|
||||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||||
|
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
|
||||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||||
|
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self) -> None:
|
||||||
self.config_frame.parse_config()
|
self.config_frame.parse_config()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
@ -56,71 +61,77 @@ class EmaneModelDialog(Dialog):
|
||||||
app: "Application",
|
app: "Application",
|
||||||
canvas_node: "CanvasNode",
|
canvas_node: "CanvasNode",
|
||||||
model: str,
|
model: str,
|
||||||
interface: int = None,
|
iface_id: int = None,
|
||||||
):
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
app, f"{canvas_node.core_node.name} {model} Configuration", master=master
|
app, f"{canvas_node.core_node.name} {model} Configuration", master=master
|
||||||
)
|
)
|
||||||
self.canvas_node = canvas_node
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
self.node = canvas_node.core_node
|
self.node: Node = canvas_node.core_node
|
||||||
self.model = f"emane_{model}"
|
self.model: str = f"emane_{model}"
|
||||||
self.interface = interface
|
self.iface_id: int = iface_id
|
||||||
self.config_frame = None
|
self.config_frame: Optional[ConfigFrame] = None
|
||||||
self.has_error = False
|
self.enabled: bool = not self.app.core.is_runtime()
|
||||||
|
self.has_error: bool = False
|
||||||
try:
|
try:
|
||||||
self.config = self.canvas_node.emane_model_configs.get(
|
config = self.canvas_node.emane_model_configs.get(
|
||||||
(self.model, self.interface)
|
(self.model, self.iface_id)
|
||||||
)
|
)
|
||||||
if not self.config:
|
if not config:
|
||||||
self.config = self.app.core.get_emane_model_config(
|
config = self.app.core.get_emane_model_config(
|
||||||
self.node.id, self.model, self.interface
|
self.node.id, self.model, self.iface_id
|
||||||
)
|
)
|
||||||
|
self.config: Dict[str, ConfigOption] = config
|
||||||
self.draw()
|
self.draw()
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Get EMANE Config Error", e)
|
self.app.show_grpc_exception("Get EMANE Config Error", e)
|
||||||
self.has_error = True
|
self.has_error: bool = True
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
self.config_frame = ConfigFrame(self.top, self.app, self.config)
|
self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled)
|
||||||
self.config_frame.draw_config()
|
self.config_frame.draw_config()
|
||||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||||
self.draw_spacer()
|
self.draw_spacer()
|
||||||
self.draw_buttons()
|
self.draw_buttons()
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
frame.columnconfigure(i, weight=1)
|
frame.columnconfigure(i, weight=1)
|
||||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||||
|
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
|
||||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||||
|
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self) -> None:
|
||||||
self.config_frame.parse_config()
|
self.config_frame.parse_config()
|
||||||
key = (self.model, self.interface)
|
key = (self.model, self.iface_id)
|
||||||
self.canvas_node.emane_model_configs[key] = self.config
|
self.canvas_node.emane_model_configs[key] = self.config
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
class EmaneConfigDialog(Dialog):
|
class EmaneConfigDialog(Dialog):
|
||||||
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
|
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
|
||||||
super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration")
|
super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration")
|
||||||
self.canvas_node = canvas_node
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
self.node = canvas_node.core_node
|
self.node: Node = canvas_node.core_node
|
||||||
self.radiovar = tk.IntVar()
|
self.radiovar: tk.IntVar = tk.IntVar()
|
||||||
self.radiovar.set(1)
|
self.radiovar.set(1)
|
||||||
self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models]
|
self.emane_models: List[str] = [
|
||||||
self.emane_model = tk.StringVar(value=self.node.emane.split("_")[1])
|
x.split("_")[1] for x in self.app.core.emane_models
|
||||||
self.emane_model_button = None
|
]
|
||||||
|
model = self.node.emane.split("_")[1]
|
||||||
|
self.emane_model: tk.StringVar = tk.StringVar(value=model)
|
||||||
|
self.emane_model_button: Optional[ttk.Button] = None
|
||||||
|
self.enabled: bool = not self.app.core.is_runtime()
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.draw_emane_configuration()
|
self.draw_emane_configuration()
|
||||||
self.draw_emane_models()
|
self.draw_emane_models()
|
||||||
|
@ -128,14 +139,15 @@ class EmaneConfigDialog(Dialog):
|
||||||
self.draw_spacer()
|
self.draw_spacer()
|
||||||
self.draw_apply_and_cancel()
|
self.draw_apply_and_cancel()
|
||||||
|
|
||||||
def draw_emane_configuration(self):
|
def draw_emane_configuration(self) -> None:
|
||||||
"""
|
"""
|
||||||
draw the main frame for emane configuration
|
draw the main frame for emane configuration
|
||||||
"""
|
"""
|
||||||
label = ttk.Label(
|
label = ttk.Label(
|
||||||
self.top,
|
self.top,
|
||||||
text="The EMANE emulation system provides more complex wireless radio emulation "
|
text="The EMANE emulation system provides more complex wireless radio "
|
||||||
"\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details",
|
"emulation \nusing pluggable MAC and PHY modules. Refer to the wiki "
|
||||||
|
"for configuration option details",
|
||||||
justify=tk.CENTER,
|
justify=tk.CENTER,
|
||||||
)
|
)
|
||||||
label.grid(pady=PADY)
|
label.grid(pady=PADY)
|
||||||
|
@ -153,7 +165,7 @@ class EmaneConfigDialog(Dialog):
|
||||||
button.image = image
|
button.image = image
|
||||||
button.grid(sticky="ew", pady=PADY)
|
button.grid(sticky="ew", pady=PADY)
|
||||||
|
|
||||||
def draw_emane_models(self):
|
def draw_emane_models(self) -> None:
|
||||||
"""
|
"""
|
||||||
create a combobox that has all the known emane models
|
create a combobox that has all the known emane models
|
||||||
"""
|
"""
|
||||||
|
@ -165,16 +177,14 @@ class EmaneConfigDialog(Dialog):
|
||||||
label.grid(row=0, column=0, sticky="w")
|
label.grid(row=0, column=0, sticky="w")
|
||||||
|
|
||||||
# create combo box and its binding
|
# create combo box and its binding
|
||||||
|
state = "readonly" if self.enabled else tk.DISABLED
|
||||||
combobox = ttk.Combobox(
|
combobox = ttk.Combobox(
|
||||||
frame,
|
frame, textvariable=self.emane_model, values=self.emane_models, state=state
|
||||||
textvariable=self.emane_model,
|
|
||||||
values=self.emane_models,
|
|
||||||
state="readonly",
|
|
||||||
)
|
)
|
||||||
combobox.grid(row=0, column=1, sticky="ew")
|
combobox.grid(row=0, column=1, sticky="ew")
|
||||||
combobox.bind("<<ComboboxSelected>>", self.emane_model_change)
|
combobox.bind("<<ComboboxSelected>>", self.emane_model_change)
|
||||||
|
|
||||||
def draw_emane_buttons(self):
|
def draw_emane_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew", pady=PADY)
|
frame.grid(sticky="ew", pady=PADY)
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
|
@ -202,23 +212,22 @@ class EmaneConfigDialog(Dialog):
|
||||||
button.image = image
|
button.image = image
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def draw_apply_and_cancel(self):
|
def draw_apply_and_cancel(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
frame.columnconfigure(i, weight=1)
|
frame.columnconfigure(i, weight=1)
|
||||||
|
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
|
||||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||||
|
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def click_emane_config(self):
|
def click_emane_config(self) -> None:
|
||||||
dialog = GlobalEmaneDialog(self, self.app)
|
dialog = GlobalEmaneDialog(self, self.app)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
def click_model_config(self):
|
def click_model_config(self) -> None:
|
||||||
"""
|
"""
|
||||||
draw emane model configuration
|
draw emane model configuration
|
||||||
"""
|
"""
|
||||||
|
@ -227,13 +236,13 @@ class EmaneConfigDialog(Dialog):
|
||||||
if not dialog.has_error:
|
if not dialog.has_error:
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
def emane_model_change(self, event: tk.Event):
|
def emane_model_change(self, event: tk.Event) -> None:
|
||||||
"""
|
"""
|
||||||
update emane model options button
|
update emane model options button
|
||||||
"""
|
"""
|
||||||
model_name = self.emane_model.get()
|
model_name = self.emane_model.get()
|
||||||
self.emane_model_button.config(text=f"{model_name} options")
|
self.emane_model_button.config(text=f"{model_name} options")
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self) -> None:
|
||||||
self.node.emane = f"emane_{self.emane_model.get()}"
|
self.node.emane = f"emane_{self.emane_model.get()}"
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
|
@ -10,7 +10,7 @@ class EmaneInstallDialog(Dialog):
|
||||||
super().__init__(app, "EMANE Error")
|
super().__init__(app, "EMANE Error")
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
label = ttk.Label(self.top, text="EMANE needs to be installed!")
|
label = ttk.Label(self.top, text="EMANE needs to be installed!")
|
||||||
label.grid(sticky="ew", pady=PADY)
|
label.grid(sticky="ew", pady=PADY)
|
||||||
|
@ -21,5 +21,5 @@ class EmaneInstallDialog(Dialog):
|
||||||
button = ttk.Button(self.top, text="Close", command=self.destroy)
|
button = ttk.Button(self.top, text="Close", command=self.destroy)
|
||||||
button.grid(sticky="ew")
|
button.grid(sticky="ew")
|
||||||
|
|
||||||
def click_doc(self):
|
def click_doc(self) -> None:
|
||||||
webbrowser.open_new("https://coreemu.github.io/core/emane.html")
|
webbrowser.open_new("https://coreemu.github.io/core/emane.html")
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.images import ImageEnum, Images
|
from core.gui.images import ImageEnum, Images
|
||||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
from core.gui.themes import PADY
|
||||||
from core.gui.widgets import CodeText
|
from core.gui.widgets import CodeText
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -13,29 +14,23 @@ if TYPE_CHECKING:
|
||||||
class ErrorDialog(Dialog):
|
class ErrorDialog(Dialog):
|
||||||
def __init__(self, app: "Application", title: str, details: str) -> None:
|
def __init__(self, app: "Application", title: str, details: str) -> None:
|
||||||
super().__init__(app, "CORE Exception")
|
super().__init__(app, "CORE Exception")
|
||||||
self.title = title
|
self.title: str = title
|
||||||
self.details = details
|
self.details: str = details
|
||||||
self.error_message = None
|
self.error_message: Optional[CodeText] = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self) -> None:
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(1, weight=1)
|
self.top.rowconfigure(1, weight=1)
|
||||||
|
image = Images.get(ImageEnum.ERROR, 24)
|
||||||
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
label = ttk.Label(
|
||||||
frame.grid(pady=PADY, sticky="ew")
|
self.top, text=self.title, image=image, compound=tk.LEFT, anchor=tk.CENTER
|
||||||
frame.columnconfigure(1, weight=1)
|
)
|
||||||
image = Images.get(ImageEnum.ERROR, 36)
|
|
||||||
label = ttk.Label(frame, image=image)
|
|
||||||
label.image = image
|
label.image = image
|
||||||
label.grid(row=0, column=0, padx=PADX)
|
label.grid(sticky=tk.EW, pady=PADY)
|
||||||
label = ttk.Label(frame, text=self.title)
|
|
||||||
label.grid(row=0, column=1, sticky="ew")
|
|
||||||
|
|
||||||
self.error_message = CodeText(self.top)
|
self.error_message = CodeText(self.top)
|
||||||
self.error_message.text.insert("1.0", self.details)
|
self.error_message.text.insert("1.0", self.details)
|
||||||
self.error_message.text.config(state="disabled")
|
self.error_message.text.config(state=tk.DISABLED)
|
||||||
self.error_message.grid(sticky="nsew", pady=PADY)
|
self.error_message.grid(sticky=tk.NSEW, pady=PADY)
|
||||||
|
|
||||||
button = ttk.Button(self.top, text="Close", command=lambda: self.destroy())
|
button = ttk.Button(self.top, text="Close", command=lambda: self.destroy())
|
||||||
button.grid(sticky="ew")
|
button.grid(sticky=tk.EW)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import filedialog, ttk
|
from tkinter import filedialog, ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from core.gui.appconfig import SCRIPT_PATH
|
from core.gui.appconfig import SCRIPT_PATH
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
@ -12,15 +12,15 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class ExecutePythonDialog(Dialog):
|
class ExecutePythonDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "Execute Python Script")
|
super().__init__(app, "Execute Python Script")
|
||||||
self.with_options = tk.IntVar(value=0)
|
self.with_options: tk.IntVar = tk.IntVar(value=0)
|
||||||
self.options = tk.StringVar(value="")
|
self.options: tk.StringVar = tk.StringVar(value="")
|
||||||
self.option_entry = None
|
self.option_entry: Optional[ttk.Entry] = None
|
||||||
self.file_entry = None
|
self.file_entry: Optional[ttk.Entry] = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
i = 0
|
i = 0
|
||||||
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
||||||
frame.columnconfigure(0, weight=1)
|
frame.columnconfigure(0, weight=1)
|
||||||
|
@ -63,13 +63,13 @@ class ExecutePythonDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||||
|
|
||||||
def add_options(self):
|
def add_options(self) -> None:
|
||||||
if self.with_options.get():
|
if self.with_options.get():
|
||||||
self.option_entry.configure(state="normal")
|
self.option_entry.configure(state="normal")
|
||||||
else:
|
else:
|
||||||
self.option_entry.configure(state="disabled")
|
self.option_entry.configure(state="disabled")
|
||||||
|
|
||||||
def select_file(self):
|
def select_file(self) -> None:
|
||||||
file = filedialog.askopenfilename(
|
file = filedialog.askopenfilename(
|
||||||
parent=self.top,
|
parent=self.top,
|
||||||
initialdir=str(SCRIPT_PATH),
|
initialdir=str(SCRIPT_PATH),
|
||||||
|
@ -80,7 +80,7 @@ class ExecutePythonDialog(Dialog):
|
||||||
self.file_entry.delete(0, "end")
|
self.file_entry.delete(0, "end")
|
||||||
self.file_entry.insert("end", file)
|
self.file_entry.insert("end", file)
|
||||||
|
|
||||||
def script_execute(self):
|
def script_execute(self) -> None:
|
||||||
file = self.file_entry.get()
|
file = self.file_entry.get()
|
||||||
options = self.option_entry.get()
|
options = self.option_entry.get()
|
||||||
logging.info("Execute %s with options %s", file, options)
|
logging.info("Execute %s with options %s", file, options)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||||
|
@ -13,8 +13,8 @@ if TYPE_CHECKING:
|
||||||
class FindDialog(Dialog):
|
class FindDialog(Dialog):
|
||||||
def __init__(self, app: "Application") -> None:
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "Find", modal=False)
|
super().__init__(app, "Find", modal=False)
|
||||||
self.find_text = tk.StringVar(value="")
|
self.find_text: tk.StringVar = tk.StringVar(value="")
|
||||||
self.tree = None
|
self.tree: Optional[ttk.Treeview] = None
|
||||||
self.draw()
|
self.draw()
|
||||||
self.protocol("WM_DELETE_WINDOW", self.close_dialog)
|
self.protocol("WM_DELETE_WINDOW", self.close_dialog)
|
||||||
self.bind("<Return>", self.find_node)
|
self.bind("<Return>", self.find_node)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
@ -12,15 +12,15 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class HookDialog(Dialog):
|
class HookDialog(Dialog):
|
||||||
def __init__(self, master: tk.BaseWidget, app: "Application"):
|
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
|
||||||
super().__init__(app, "Hook", master=master)
|
super().__init__(app, "Hook", master=master)
|
||||||
self.name = tk.StringVar()
|
self.name: tk.StringVar = tk.StringVar()
|
||||||
self.codetext = None
|
self.codetext: Optional[CodeText] = None
|
||||||
self.hook = core_pb2.Hook()
|
self.hook: core_pb2.Hook = core_pb2.Hook()
|
||||||
self.state = tk.StringVar()
|
self.state: tk.StringVar = tk.StringVar()
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(1, weight=1)
|
self.top.rowconfigure(1, weight=1)
|
||||||
|
|
||||||
|
@ -66,11 +66,11 @@ class HookDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
|
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def state_change(self, event: tk.Event):
|
def state_change(self, event: tk.Event) -> None:
|
||||||
state_name = self.state.get()
|
state_name = self.state.get()
|
||||||
self.name.set(f"{state_name.lower()}_hook.sh")
|
self.name.set(f"{state_name.lower()}_hook.sh")
|
||||||
|
|
||||||
def set(self, hook: core_pb2.Hook):
|
def set(self, hook: core_pb2.Hook) -> None:
|
||||||
self.hook = hook
|
self.hook = hook
|
||||||
self.name.set(hook.file)
|
self.name.set(hook.file)
|
||||||
self.codetext.text.delete(1.0, tk.END)
|
self.codetext.text.delete(1.0, tk.END)
|
||||||
|
@ -78,7 +78,7 @@ class HookDialog(Dialog):
|
||||||
state_name = core_pb2.SessionState.Enum.Name(hook.state)
|
state_name = core_pb2.SessionState.Enum.Name(hook.state)
|
||||||
self.state.set(state_name)
|
self.state.set(state_name)
|
||||||
|
|
||||||
def save(self):
|
def save(self) -> None:
|
||||||
data = self.codetext.text.get("1.0", tk.END).strip()
|
data = self.codetext.text.get("1.0", tk.END).strip()
|
||||||
state_value = core_pb2.SessionState.Enum.Value(self.state.get())
|
state_value = core_pb2.SessionState.Enum.Value(self.state.get())
|
||||||
self.hook.file = self.name.get()
|
self.hook.file = self.name.get()
|
||||||
|
@ -88,15 +88,15 @@ class HookDialog(Dialog):
|
||||||
|
|
||||||
|
|
||||||
class HooksDialog(Dialog):
|
class HooksDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "Hooks")
|
super().__init__(app, "Hooks")
|
||||||
self.listbox = None
|
self.listbox: Optional[tk.Listbox] = None
|
||||||
self.edit_button = None
|
self.edit_button: Optional[ttk.Button] = None
|
||||||
self.delete_button = None
|
self.delete_button: Optional[ttk.Button] = None
|
||||||
self.selected = None
|
self.selected: Optional[str] = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ class HooksDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
|
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
|
||||||
button.grid(row=0, column=3, sticky="ew")
|
button.grid(row=0, column=3, sticky="ew")
|
||||||
|
|
||||||
def click_create(self):
|
def click_create(self) -> None:
|
||||||
dialog = HookDialog(self, self.app)
|
dialog = HookDialog(self, self.app)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
hook = dialog.hook
|
hook = dialog.hook
|
||||||
|
@ -132,19 +132,19 @@ class HooksDialog(Dialog):
|
||||||
self.app.core.hooks[hook.file] = hook
|
self.app.core.hooks[hook.file] = hook
|
||||||
self.listbox.insert(tk.END, hook.file)
|
self.listbox.insert(tk.END, hook.file)
|
||||||
|
|
||||||
def click_edit(self):
|
def click_edit(self) -> None:
|
||||||
hook = self.app.core.hooks[self.selected]
|
hook = self.app.core.hooks[self.selected]
|
||||||
dialog = HookDialog(self, self.app)
|
dialog = HookDialog(self, self.app)
|
||||||
dialog.set(hook)
|
dialog.set(hook)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
def click_delete(self):
|
def click_delete(self) -> None:
|
||||||
del self.app.core.hooks[self.selected]
|
del self.app.core.hooks[self.selected]
|
||||||
self.listbox.delete(tk.ANCHOR)
|
self.listbox.delete(tk.ANCHOR)
|
||||||
self.edit_button.config(state=tk.DISABLED)
|
self.edit_button.config(state=tk.DISABLED)
|
||||||
self.delete_button.config(state=tk.DISABLED)
|
self.delete_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
def select(self, event: tk.Event):
|
def select(self, event: tk.Event) -> None:
|
||||||
if self.listbox.curselection():
|
if self.listbox.curselection():
|
||||||
index = self.listbox.curselection()[0]
|
index = self.listbox.curselection()[0]
|
||||||
self.selected = self.listbox.get(index)
|
self.selected = self.listbox.get(index)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox, ttk
|
from tkinter import messagebox, ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
|
@ -15,14 +15,14 @@ if TYPE_CHECKING:
|
||||||
class IpConfigDialog(Dialog):
|
class IpConfigDialog(Dialog):
|
||||||
def __init__(self, app: "Application") -> None:
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "IP Configuration")
|
super().__init__(app, "IP Configuration")
|
||||||
self.ip4 = self.app.guiconfig.ips.ip4
|
self.ip4: str = self.app.guiconfig.ips.ip4
|
||||||
self.ip6 = self.app.guiconfig.ips.ip6
|
self.ip6: str = self.app.guiconfig.ips.ip6
|
||||||
self.ip4s = self.app.guiconfig.ips.ip4s
|
self.ip4s: List[str] = self.app.guiconfig.ips.ip4s
|
||||||
self.ip6s = self.app.guiconfig.ips.ip6s
|
self.ip6s: List[str] = self.app.guiconfig.ips.ip6s
|
||||||
self.ip4_entry = None
|
self.ip4_entry: Optional[ttk.Entry] = None
|
||||||
self.ip4_listbox = None
|
self.ip4_listbox: Optional[ListboxScroll] = None
|
||||||
self.ip6_entry = None
|
self.ip6_entry: Optional[ttk.Entry] = None
|
||||||
self.ip6_listbox = None
|
self.ip6_listbox: Optional[ListboxScroll] = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self) -> None:
|
def draw(self) -> None:
|
||||||
|
@ -146,6 +146,6 @@ class IpConfigDialog(Dialog):
|
||||||
ip_config.ip6 = self.ip6
|
ip_config.ip6 = self.ip6
|
||||||
ip_config.ip4s = ip4s
|
ip_config.ip4s = ip4s
|
||||||
ip_config.ip6s = ip6s
|
ip_config.ip6s = ip6s
|
||||||
self.app.core.interfaces_manager.update_ips(self.ip4, self.ip6)
|
self.app.core.ifaces_manager.update_ips(self.ip4, self.ip6)
|
||||||
self.app.save_config()
|
self.app.save_config()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
|
@ -3,7 +3,7 @@ link configuration
|
||||||
"""
|
"""
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING, Union
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
from core.gui import validation
|
from core.gui import validation
|
||||||
|
@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
||||||
from core.gui.graph.graph import CanvasEdge
|
from core.gui.graph.graph import CanvasEdge
|
||||||
|
|
||||||
|
|
||||||
def get_int(var: tk.StringVar) -> Union[int, None]:
|
def get_int(var: tk.StringVar) -> Optional[int]:
|
||||||
value = var.get()
|
value = var.get()
|
||||||
if value != "":
|
if value != "":
|
||||||
return int(value)
|
return int(value)
|
||||||
|
@ -24,7 +24,7 @@ def get_int(var: tk.StringVar) -> Union[int, None]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_float(var: tk.StringVar) -> Union[float, None]:
|
def get_float(var: tk.StringVar) -> Optional[float]:
|
||||||
value = var.get()
|
value = var.get()
|
||||||
if value != "":
|
if value != "":
|
||||||
return float(value)
|
return float(value)
|
||||||
|
@ -33,38 +33,39 @@ def get_float(var: tk.StringVar) -> Union[float, None]:
|
||||||
|
|
||||||
|
|
||||||
class LinkConfigurationDialog(Dialog):
|
class LinkConfigurationDialog(Dialog):
|
||||||
def __init__(self, app: "Application", edge: "CanvasEdge"):
|
def __init__(self, app: "Application", edge: "CanvasEdge") -> None:
|
||||||
super().__init__(app, "Link Configuration")
|
super().__init__(app, "Link Configuration")
|
||||||
self.edge = edge
|
self.edge: "CanvasEdge" = edge
|
||||||
self.is_symmetric = edge.link.options.unidirectional is False
|
self.is_symmetric: bool = edge.link.options.unidirectional is False
|
||||||
if self.is_symmetric:
|
if self.is_symmetric:
|
||||||
self.symmetry_var = tk.StringVar(value=">>")
|
symmetry_var = tk.StringVar(value=">>")
|
||||||
else:
|
else:
|
||||||
self.symmetry_var = tk.StringVar(value="<<")
|
symmetry_var = tk.StringVar(value="<<")
|
||||||
|
self.symmetry_var: tk.StringVar = symmetry_var
|
||||||
|
|
||||||
self.bandwidth = tk.StringVar()
|
self.bandwidth: tk.StringVar = tk.StringVar()
|
||||||
self.delay = tk.StringVar()
|
self.delay: tk.StringVar = tk.StringVar()
|
||||||
self.jitter = tk.StringVar()
|
self.jitter: tk.StringVar = tk.StringVar()
|
||||||
self.loss = tk.StringVar()
|
self.loss: tk.StringVar = tk.StringVar()
|
||||||
self.duplicate = tk.StringVar()
|
self.duplicate: tk.StringVar = tk.StringVar()
|
||||||
|
|
||||||
self.down_bandwidth = tk.StringVar()
|
self.down_bandwidth: tk.StringVar = tk.StringVar()
|
||||||
self.down_delay = tk.StringVar()
|
self.down_delay: tk.StringVar = tk.StringVar()
|
||||||
self.down_jitter = tk.StringVar()
|
self.down_jitter: tk.StringVar = tk.StringVar()
|
||||||
self.down_loss = tk.StringVar()
|
self.down_loss: tk.StringVar = tk.StringVar()
|
||||||
self.down_duplicate = tk.StringVar()
|
self.down_duplicate: tk.StringVar = tk.StringVar()
|
||||||
|
|
||||||
self.color = tk.StringVar(value="#000000")
|
self.color: tk.StringVar = tk.StringVar(value="#000000")
|
||||||
self.color_button = None
|
self.color_button: Optional[tk.Button] = None
|
||||||
self.width = tk.DoubleVar()
|
self.width: tk.DoubleVar = tk.DoubleVar()
|
||||||
|
|
||||||
self.load_link_config()
|
self.load_link_config()
|
||||||
self.symmetric_frame = None
|
self.symmetric_frame: Optional[ttk.Frame] = None
|
||||||
self.asymmetric_frame = None
|
self.asymmetric_frame: Optional[ttk.Frame] = None
|
||||||
|
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
source_name = self.app.canvas.nodes[self.edge.src].core_node.name
|
source_name = self.app.canvas.nodes[self.edge.src].core_node.name
|
||||||
dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name
|
dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name
|
||||||
|
@ -207,13 +208,13 @@ class LinkConfigurationDialog(Dialog):
|
||||||
|
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def click_color(self):
|
def click_color(self) -> None:
|
||||||
dialog = ColorPickerDialog(self, self.app, self.color.get())
|
dialog = ColorPickerDialog(self, self.app, self.color.get())
|
||||||
color = dialog.askcolor()
|
color = dialog.askcolor()
|
||||||
self.color.set(color)
|
self.color.set(color)
|
||||||
self.color_button.config(background=color)
|
self.color_button.config(background=color)
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self) -> None:
|
||||||
self.app.canvas.itemconfigure(self.edge.id, width=self.width.get())
|
self.app.canvas.itemconfigure(self.edge.id, width=self.width.get())
|
||||||
self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get())
|
self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get())
|
||||||
link = self.edge.link
|
link = self.edge.link
|
||||||
|
@ -223,25 +224,25 @@ class LinkConfigurationDialog(Dialog):
|
||||||
duplicate = get_int(self.duplicate)
|
duplicate = get_int(self.duplicate)
|
||||||
loss = get_float(self.loss)
|
loss = get_float(self.loss)
|
||||||
options = core_pb2.LinkOptions(
|
options = core_pb2.LinkOptions(
|
||||||
bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, per=loss
|
bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, loss=loss
|
||||||
)
|
)
|
||||||
link.options.CopyFrom(options)
|
link.options.CopyFrom(options)
|
||||||
|
|
||||||
interface_one = None
|
iface1_id = None
|
||||||
if link.HasField("interface_one"):
|
if link.HasField("iface1"):
|
||||||
interface_one = link.interface_one.id
|
iface1_id = link.iface1.id
|
||||||
interface_two = None
|
iface2_id = None
|
||||||
if link.HasField("interface_two"):
|
if link.HasField("iface2"):
|
||||||
interface_two = link.interface_two.id
|
iface2_id = link.iface2.id
|
||||||
|
|
||||||
if not self.is_symmetric:
|
if not self.is_symmetric:
|
||||||
link.options.unidirectional = True
|
link.options.unidirectional = True
|
||||||
asym_interface_one = None
|
asym_iface1 = None
|
||||||
if interface_one:
|
if iface1_id:
|
||||||
asym_interface_one = core_pb2.Interface(id=interface_one)
|
asym_iface1 = core_pb2.Interface(id=iface1_id)
|
||||||
asym_interface_two = None
|
asym_iface2 = None
|
||||||
if interface_two:
|
if iface2_id:
|
||||||
asym_interface_two = core_pb2.Interface(id=interface_two)
|
asym_iface2 = core_pb2.Interface(id=iface2_id)
|
||||||
down_bandwidth = get_int(self.down_bandwidth)
|
down_bandwidth = get_int(self.down_bandwidth)
|
||||||
down_jitter = get_int(self.down_jitter)
|
down_jitter = get_int(self.down_jitter)
|
||||||
down_delay = get_int(self.down_delay)
|
down_delay = get_int(self.down_delay)
|
||||||
|
@ -252,14 +253,14 @@ class LinkConfigurationDialog(Dialog):
|
||||||
jitter=down_jitter,
|
jitter=down_jitter,
|
||||||
delay=down_delay,
|
delay=down_delay,
|
||||||
dup=down_duplicate,
|
dup=down_duplicate,
|
||||||
per=down_loss,
|
loss=down_loss,
|
||||||
unidirectional=True,
|
unidirectional=True,
|
||||||
)
|
)
|
||||||
self.edge.asymmetric_link = core_pb2.Link(
|
self.edge.asymmetric_link = core_pb2.Link(
|
||||||
node_one_id=link.node_two_id,
|
node1_id=link.node2_id,
|
||||||
node_two_id=link.node_one_id,
|
node2_id=link.node1_id,
|
||||||
interface_one=asym_interface_one,
|
iface1=asym_iface1,
|
||||||
interface_two=asym_interface_two,
|
iface2=asym_iface2,
|
||||||
options=options,
|
options=options,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -270,25 +271,27 @@ class LinkConfigurationDialog(Dialog):
|
||||||
session_id = self.app.core.session_id
|
session_id = self.app.core.session_id
|
||||||
self.app.core.client.edit_link(
|
self.app.core.client.edit_link(
|
||||||
session_id,
|
session_id,
|
||||||
link.node_one_id,
|
link.node1_id,
|
||||||
link.node_two_id,
|
link.node2_id,
|
||||||
link.options,
|
link.options,
|
||||||
interface_one,
|
iface1_id,
|
||||||
interface_two,
|
iface2_id,
|
||||||
)
|
)
|
||||||
if self.edge.asymmetric_link:
|
if self.edge.asymmetric_link:
|
||||||
self.app.core.client.edit_link(
|
self.app.core.client.edit_link(
|
||||||
session_id,
|
session_id,
|
||||||
link.node_two_id,
|
link.node2_id,
|
||||||
link.node_one_id,
|
link.node1_id,
|
||||||
self.edge.asymmetric_link.options,
|
self.edge.asymmetric_link.options,
|
||||||
interface_one,
|
iface1_id,
|
||||||
interface_two,
|
iface2_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# update edge label
|
||||||
|
self.edge.draw_link_options()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def change_symmetry(self):
|
def change_symmetry(self) -> None:
|
||||||
if self.is_symmetric:
|
if self.is_symmetric:
|
||||||
self.is_symmetric = False
|
self.is_symmetric = False
|
||||||
self.symmetry_var.set("<<")
|
self.symmetry_var.set("<<")
|
||||||
|
@ -304,7 +307,7 @@ class LinkConfigurationDialog(Dialog):
|
||||||
self.asymmetric_frame.grid_forget()
|
self.asymmetric_frame.grid_forget()
|
||||||
self.symmetric_frame.grid(row=2, column=0)
|
self.symmetric_frame.grid(row=2, column=0)
|
||||||
|
|
||||||
def load_link_config(self):
|
def load_link_config(self) -> None:
|
||||||
"""
|
"""
|
||||||
populate link config to the table
|
populate link config to the table
|
||||||
"""
|
"""
|
||||||
|
@ -317,12 +320,12 @@ class LinkConfigurationDialog(Dialog):
|
||||||
self.bandwidth.set(str(link.options.bandwidth))
|
self.bandwidth.set(str(link.options.bandwidth))
|
||||||
self.jitter.set(str(link.options.jitter))
|
self.jitter.set(str(link.options.jitter))
|
||||||
self.duplicate.set(str(link.options.dup))
|
self.duplicate.set(str(link.options.dup))
|
||||||
self.loss.set(str(link.options.per))
|
self.loss.set(str(link.options.loss))
|
||||||
self.delay.set(str(link.options.delay))
|
self.delay.set(str(link.options.delay))
|
||||||
if not self.is_symmetric:
|
if not self.is_symmetric:
|
||||||
asym_link = self.edge.asymmetric_link
|
asym_link = self.edge.asymmetric_link
|
||||||
self.down_bandwidth.set(str(asym_link.options.bandwidth))
|
self.down_bandwidth.set(str(asym_link.options.bandwidth))
|
||||||
self.down_jitter.set(str(asym_link.options.jitter))
|
self.down_jitter.set(str(asym_link.options.jitter))
|
||||||
self.down_duplicate.set(str(asym_link.options.dup))
|
self.down_duplicate.set(str(asym_link.options.dup))
|
||||||
self.down_loss.set(str(asym_link.options.per))
|
self.down_loss.set(str(asym_link.options.loss))
|
||||||
self.down_delay.set(str(asym_link.options.delay))
|
self.down_delay.set(str(asym_link.options.delay))
|
||||||
|
|
|
@ -15,7 +15,7 @@ class MacConfigDialog(Dialog):
|
||||||
def __init__(self, app: "Application") -> None:
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "MAC Configuration")
|
super().__init__(app, "MAC Configuration")
|
||||||
mac = self.app.guiconfig.mac
|
mac = self.app.guiconfig.mac
|
||||||
self.mac_var = tk.StringVar(value=mac)
|
self.mac_var: tk.StringVar = tk.StringVar(value=mac)
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self) -> None:
|
def draw(self) -> None:
|
||||||
|
@ -55,7 +55,7 @@ class MacConfigDialog(Dialog):
|
||||||
if not netaddr.valid_mac(mac):
|
if not netaddr.valid_mac(mac):
|
||||||
messagebox.showerror("MAC Error", f"{mac} is an invalid mac")
|
messagebox.showerror("MAC Error", f"{mac} is an invalid mac")
|
||||||
else:
|
else:
|
||||||
self.app.core.interfaces_manager.mac = netaddr.EUI(mac)
|
self.app.core.ifaces_manager.mac = netaddr.EUI(mac)
|
||||||
self.app.guiconfig.mac = mac
|
self.app.guiconfig.mac = mac
|
||||||
self.app.save_config()
|
self.app.save_config()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
|
@ -2,10 +2,12 @@
|
||||||
mobility configuration
|
mobility configuration
|
||||||
"""
|
"""
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict, Optional
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
|
from core.api.grpc.common_pb2 import ConfigOption
|
||||||
|
from core.api.grpc.core_pb2 import Node
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.themes import PADX, PADY
|
from core.gui.themes import PADX, PADY
|
||||||
from core.gui.widgets import ConfigFrame
|
from core.gui.widgets import ConfigFrame
|
||||||
|
@ -16,23 +18,24 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class MobilityConfigDialog(Dialog):
|
class MobilityConfigDialog(Dialog):
|
||||||
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
|
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
|
||||||
super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration")
|
super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration")
|
||||||
self.canvas_node = canvas_node
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
self.node = canvas_node.core_node
|
self.node: Node = canvas_node.core_node
|
||||||
self.config_frame = None
|
self.config_frame: Optional[ConfigFrame] = None
|
||||||
self.has_error = False
|
self.has_error: bool = False
|
||||||
try:
|
try:
|
||||||
self.config = self.canvas_node.mobility_config
|
config = self.canvas_node.mobility_config
|
||||||
if not self.config:
|
if not config:
|
||||||
self.config = self.app.core.get_mobility_config(self.node.id)
|
config = self.app.core.get_mobility_config(self.node.id)
|
||||||
|
self.config: Dict[str, ConfigOption] = config
|
||||||
self.draw()
|
self.draw()
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Get Mobility Config Error", e)
|
self.app.show_grpc_exception("Get Mobility Config Error", e)
|
||||||
self.has_error = True
|
self.has_error: bool = True
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
self.config_frame = ConfigFrame(self.top, self.app, self.config)
|
self.config_frame = ConfigFrame(self.top, self.app, self.config)
|
||||||
|
@ -40,7 +43,7 @@ class MobilityConfigDialog(Dialog):
|
||||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||||
self.draw_apply_buttons()
|
self.draw_apply_buttons()
|
||||||
|
|
||||||
def draw_apply_buttons(self):
|
def draw_apply_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
|
@ -52,7 +55,7 @@ class MobilityConfigDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self) -> None:
|
||||||
self.config_frame.parse_config()
|
self.config_frame.parse_config()
|
||||||
self.canvas_node.mobility_config = self.config
|
self.canvas_node.mobility_config = self.config
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict, Optional
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
|
from core.api.grpc.common_pb2 import ConfigOption
|
||||||
|
from core.api.grpc.core_pb2 import Node
|
||||||
from core.api.grpc.mobility_pb2 import MobilityAction
|
from core.api.grpc.mobility_pb2 import MobilityAction
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.images import ImageEnum
|
from core.gui.images import ImageEnum
|
||||||
|
@ -13,18 +15,23 @@ if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
from core.gui.graph.node import CanvasNode
|
from core.gui.graph.node import CanvasNode
|
||||||
|
|
||||||
ICON_SIZE = 16
|
ICON_SIZE: int = 16
|
||||||
|
|
||||||
|
|
||||||
class MobilityPlayer:
|
class MobilityPlayer:
|
||||||
def __init__(self, app: "Application", canvas_node: "CanvasNode", config):
|
def __init__(
|
||||||
self.app = app
|
self,
|
||||||
self.canvas_node = canvas_node
|
app: "Application",
|
||||||
self.config = config
|
canvas_node: "CanvasNode",
|
||||||
self.dialog = None
|
config: Dict[str, ConfigOption],
|
||||||
self.state = None
|
) -> None:
|
||||||
|
self.app: "Application" = app
|
||||||
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
|
self.config: Dict[str, ConfigOption] = config
|
||||||
|
self.dialog: Optional[MobilityPlayerDialog] = None
|
||||||
|
self.state: Optional[MobilityAction] = None
|
||||||
|
|
||||||
def show(self):
|
def show(self) -> None:
|
||||||
if self.dialog:
|
if self.dialog:
|
||||||
self.dialog.destroy()
|
self.dialog.destroy()
|
||||||
self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config)
|
self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config)
|
||||||
|
@ -37,44 +44,49 @@ class MobilityPlayer:
|
||||||
self.set_stop()
|
self.set_stop()
|
||||||
self.dialog.show()
|
self.dialog.show()
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
if self.dialog:
|
if self.dialog:
|
||||||
self.dialog.destroy()
|
self.dialog.destroy()
|
||||||
self.dialog = None
|
self.dialog = None
|
||||||
|
|
||||||
def set_play(self):
|
def set_play(self) -> None:
|
||||||
self.state = MobilityAction.START
|
self.state = MobilityAction.START
|
||||||
if self.dialog:
|
if self.dialog:
|
||||||
self.dialog.set_play()
|
self.dialog.set_play()
|
||||||
|
|
||||||
def set_pause(self):
|
def set_pause(self) -> None:
|
||||||
self.state = MobilityAction.PAUSE
|
self.state = MobilityAction.PAUSE
|
||||||
if self.dialog:
|
if self.dialog:
|
||||||
self.dialog.set_pause()
|
self.dialog.set_pause()
|
||||||
|
|
||||||
def set_stop(self):
|
def set_stop(self) -> None:
|
||||||
self.state = MobilityAction.STOP
|
self.state = MobilityAction.STOP
|
||||||
if self.dialog:
|
if self.dialog:
|
||||||
self.dialog.set_stop()
|
self.dialog.set_stop()
|
||||||
|
|
||||||
|
|
||||||
class MobilityPlayerDialog(Dialog):
|
class MobilityPlayerDialog(Dialog):
|
||||||
def __init__(self, app: "Application", canvas_node: "CanvasNode", config):
|
def __init__(
|
||||||
|
self,
|
||||||
|
app: "Application",
|
||||||
|
canvas_node: "CanvasNode",
|
||||||
|
config: Dict[str, ConfigOption],
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
app, f"{canvas_node.core_node.name} Mobility Player", modal=False
|
app, f"{canvas_node.core_node.name} Mobility Player", modal=False
|
||||||
)
|
)
|
||||||
self.resizable(False, False)
|
self.resizable(False, False)
|
||||||
self.geometry("")
|
self.geometry("")
|
||||||
self.canvas_node = canvas_node
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
self.node = canvas_node.core_node
|
self.node: Node = canvas_node.core_node
|
||||||
self.config = config
|
self.config: Dict[str, ConfigOption] = config
|
||||||
self.play_button = None
|
self.play_button: Optional[ttk.Button] = None
|
||||||
self.pause_button = None
|
self.pause_button: Optional[ttk.Button] = None
|
||||||
self.stop_button = None
|
self.stop_button: Optional[ttk.Button] = None
|
||||||
self.progressbar = None
|
self.progressbar: Optional[ttk.Progressbar] = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
file_name = self.config["file"].value
|
file_name = self.config["file"].value
|
||||||
|
@ -114,27 +126,27 @@ class MobilityPlayerDialog(Dialog):
|
||||||
label = ttk.Label(frame, text=f"rate {rate} ms")
|
label = ttk.Label(frame, text=f"rate {rate} ms")
|
||||||
label.grid(row=0, column=4)
|
label.grid(row=0, column=4)
|
||||||
|
|
||||||
def clear_buttons(self):
|
def clear_buttons(self) -> None:
|
||||||
self.play_button.state(["!pressed"])
|
self.play_button.state(["!pressed"])
|
||||||
self.pause_button.state(["!pressed"])
|
self.pause_button.state(["!pressed"])
|
||||||
self.stop_button.state(["!pressed"])
|
self.stop_button.state(["!pressed"])
|
||||||
|
|
||||||
def set_play(self):
|
def set_play(self) -> None:
|
||||||
self.clear_buttons()
|
self.clear_buttons()
|
||||||
self.play_button.state(["pressed"])
|
self.play_button.state(["pressed"])
|
||||||
self.progressbar.start()
|
self.progressbar.start()
|
||||||
|
|
||||||
def set_pause(self):
|
def set_pause(self) -> None:
|
||||||
self.clear_buttons()
|
self.clear_buttons()
|
||||||
self.pause_button.state(["pressed"])
|
self.pause_button.state(["pressed"])
|
||||||
self.progressbar.stop()
|
self.progressbar.stop()
|
||||||
|
|
||||||
def set_stop(self):
|
def set_stop(self) -> None:
|
||||||
self.clear_buttons()
|
self.clear_buttons()
|
||||||
self.stop_button.state(["pressed"])
|
self.stop_button.state(["pressed"])
|
||||||
self.progressbar.stop()
|
self.progressbar.stop()
|
||||||
|
|
||||||
def click_play(self):
|
def click_play(self) -> None:
|
||||||
self.set_play()
|
self.set_play()
|
||||||
session_id = self.app.core.session_id
|
session_id = self.app.core.session_id
|
||||||
try:
|
try:
|
||||||
|
@ -144,7 +156,7 @@ class MobilityPlayerDialog(Dialog):
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Mobility Error", e)
|
self.app.show_grpc_exception("Mobility Error", e)
|
||||||
|
|
||||||
def click_pause(self):
|
def click_pause(self) -> None:
|
||||||
self.set_pause()
|
self.set_pause()
|
||||||
session_id = self.app.core.session_id
|
session_id = self.app.core.session_id
|
||||||
try:
|
try:
|
||||||
|
@ -154,7 +166,7 @@ class MobilityPlayerDialog(Dialog):
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Mobility Error", e)
|
self.app.show_grpc_exception("Mobility Error", e)
|
||||||
|
|
||||||
def click_stop(self):
|
def click_stop(self) -> None:
|
||||||
self.set_stop()
|
self.set_stop()
|
||||||
session_id = self.app.core.session_id
|
session_id = self.app.core.session_id
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -2,10 +2,12 @@ import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from tkinter import messagebox, ttk
|
from tkinter import messagebox, ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict, Optional
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
|
from PIL.ImageTk import PhotoImage
|
||||||
|
|
||||||
|
from core.api.grpc.core_pb2 import Node
|
||||||
from core.gui import nodeutils, validation
|
from core.gui import nodeutils, validation
|
||||||
from core.gui.appconfig import ICONS_PATH
|
from core.gui.appconfig import ICONS_PATH
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
@ -86,35 +88,35 @@ class InterfaceData:
|
||||||
mac: tk.StringVar,
|
mac: tk.StringVar,
|
||||||
ip4: tk.StringVar,
|
ip4: tk.StringVar,
|
||||||
ip6: tk.StringVar,
|
ip6: tk.StringVar,
|
||||||
):
|
) -> None:
|
||||||
self.is_auto = is_auto
|
self.is_auto: tk.BooleanVar = is_auto
|
||||||
self.mac = mac
|
self.mac: tk.StringVar = mac
|
||||||
self.ip4 = ip4
|
self.ip4: tk.StringVar = ip4
|
||||||
self.ip6 = ip6
|
self.ip6: tk.StringVar = ip6
|
||||||
|
|
||||||
|
|
||||||
class NodeConfigDialog(Dialog):
|
class NodeConfigDialog(Dialog):
|
||||||
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
|
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
|
||||||
"""
|
"""
|
||||||
create an instance of node configuration
|
create an instance of node configuration
|
||||||
"""
|
"""
|
||||||
super().__init__(app, f"{canvas_node.core_node.name} Configuration")
|
super().__init__(app, f"{canvas_node.core_node.name} Configuration")
|
||||||
self.canvas_node = canvas_node
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
self.node = canvas_node.core_node
|
self.node: Node = canvas_node.core_node
|
||||||
self.image = canvas_node.image
|
self.image: PhotoImage = canvas_node.image
|
||||||
self.image_file = None
|
self.image_file: Optional[str] = None
|
||||||
self.image_button = None
|
self.image_button: Optional[ttk.Button] = None
|
||||||
self.name = tk.StringVar(value=self.node.name)
|
self.name: tk.StringVar = tk.StringVar(value=self.node.name)
|
||||||
self.type = tk.StringVar(value=self.node.model)
|
self.type: tk.StringVar = tk.StringVar(value=self.node.model)
|
||||||
self.container_image = tk.StringVar(value=self.node.image)
|
self.container_image: tk.StringVar = tk.StringVar(value=self.node.image)
|
||||||
server = "localhost"
|
server = "localhost"
|
||||||
if self.node.server:
|
if self.node.server:
|
||||||
server = self.node.server
|
server = self.node.server
|
||||||
self.server = tk.StringVar(value=server)
|
self.server: tk.StringVar = tk.StringVar(value=server)
|
||||||
self.interfaces = {}
|
self.ifaces: Dict[int, InterfaceData] = {}
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
row = 0
|
row = 0
|
||||||
|
|
||||||
|
@ -183,53 +185,53 @@ class NodeConfigDialog(Dialog):
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
if NodeUtils.is_rj45_node(self.node.type):
|
if NodeUtils.is_rj45_node(self.node.type):
|
||||||
response = self.app.core.client.get_interfaces()
|
response = self.app.core.client.get_ifaces()
|
||||||
logging.debug("host machine available interfaces: %s", response)
|
logging.debug("host machine available interfaces: %s", response)
|
||||||
interfaces = ListboxScroll(frame)
|
ifaces = ListboxScroll(frame)
|
||||||
interfaces.listbox.config(state=state)
|
ifaces.listbox.config(state=state)
|
||||||
interfaces.grid(
|
ifaces.grid(
|
||||||
row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY
|
row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY
|
||||||
)
|
)
|
||||||
for inf in sorted(response.interfaces[:]):
|
for inf in sorted(response.ifaces[:]):
|
||||||
interfaces.listbox.insert(tk.END, inf)
|
ifaces.listbox.insert(tk.END, inf)
|
||||||
row += 1
|
row += 1
|
||||||
interfaces.listbox.bind("<<ListboxSelect>>", self.interface_select)
|
ifaces.listbox.bind("<<ListboxSelect>>", self.iface_select)
|
||||||
|
|
||||||
# interfaces
|
# interfaces
|
||||||
if self.canvas_node.interfaces:
|
if self.canvas_node.ifaces:
|
||||||
self.draw_interfaces()
|
self.draw_ifaces()
|
||||||
|
|
||||||
self.draw_spacer()
|
self.draw_spacer()
|
||||||
self.draw_buttons()
|
self.draw_buttons()
|
||||||
|
|
||||||
def draw_interfaces(self):
|
def draw_ifaces(self) -> None:
|
||||||
notebook = ttk.Notebook(self.top)
|
notebook = ttk.Notebook(self.top)
|
||||||
notebook.grid(sticky="nsew", pady=PADY)
|
notebook.grid(sticky="nsew", pady=PADY)
|
||||||
self.top.rowconfigure(notebook.grid_info()["row"], weight=1)
|
self.top.rowconfigure(notebook.grid_info()["row"], weight=1)
|
||||||
state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
|
state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
|
||||||
for interface_id in sorted(self.canvas_node.interfaces):
|
for iface_id in sorted(self.canvas_node.ifaces):
|
||||||
interface = self.canvas_node.interfaces[interface_id]
|
iface = self.canvas_node.ifaces[iface_id]
|
||||||
tab = ttk.Frame(notebook, padding=FRAME_PAD)
|
tab = ttk.Frame(notebook, padding=FRAME_PAD)
|
||||||
tab.grid(sticky="nsew", pady=PADY)
|
tab.grid(sticky="nsew", pady=PADY)
|
||||||
tab.columnconfigure(1, weight=1)
|
tab.columnconfigure(1, weight=1)
|
||||||
tab.columnconfigure(2, weight=1)
|
tab.columnconfigure(2, weight=1)
|
||||||
notebook.add(tab, text=interface.name)
|
notebook.add(tab, text=iface.name)
|
||||||
|
|
||||||
row = 0
|
row = 0
|
||||||
emane_node = self.canvas_node.has_emane_link(interface.id)
|
emane_node = self.canvas_node.has_emane_link(iface.id)
|
||||||
if emane_node:
|
if emane_node:
|
||||||
emane_model = emane_node.emane.split("_")[1]
|
emane_model = emane_node.emane.split("_")[1]
|
||||||
button = ttk.Button(
|
button = ttk.Button(
|
||||||
tab,
|
tab,
|
||||||
text=f"Configure EMANE {emane_model}",
|
text=f"Configure EMANE {emane_model}",
|
||||||
command=lambda: self.click_emane_config(emane_model, interface.id),
|
command=lambda: self.click_emane_config(emane_model, iface.id),
|
||||||
)
|
)
|
||||||
button.grid(row=row, sticky="ew", columnspan=3, pady=PADY)
|
button.grid(row=row, sticky="ew", columnspan=3, pady=PADY)
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
label = ttk.Label(tab, text="MAC")
|
label = ttk.Label(tab, text="MAC")
|
||||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||||
auto_set = not interface.mac
|
auto_set = not iface.mac
|
||||||
mac_state = tk.DISABLED if auto_set else tk.NORMAL
|
mac_state = tk.DISABLED if auto_set else tk.NORMAL
|
||||||
is_auto = tk.BooleanVar(value=auto_set)
|
is_auto = tk.BooleanVar(value=auto_set)
|
||||||
checkbutton = ttk.Checkbutton(
|
checkbutton = ttk.Checkbutton(
|
||||||
|
@ -237,7 +239,7 @@ class NodeConfigDialog(Dialog):
|
||||||
)
|
)
|
||||||
checkbutton.var = is_auto
|
checkbutton.var = is_auto
|
||||||
checkbutton.grid(row=row, column=1, padx=PADX)
|
checkbutton.grid(row=row, column=1, padx=PADX)
|
||||||
mac = tk.StringVar(value=interface.mac)
|
mac = tk.StringVar(value=iface.mac)
|
||||||
entry = ttk.Entry(tab, textvariable=mac, state=mac_state)
|
entry = ttk.Entry(tab, textvariable=mac, state=mac_state)
|
||||||
entry.grid(row=row, column=2, sticky="ew")
|
entry.grid(row=row, column=2, sticky="ew")
|
||||||
func = partial(mac_auto, is_auto, entry, mac)
|
func = partial(mac_auto, is_auto, entry, mac)
|
||||||
|
@ -247,8 +249,8 @@ class NodeConfigDialog(Dialog):
|
||||||
label = ttk.Label(tab, text="IPv4")
|
label = ttk.Label(tab, text="IPv4")
|
||||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||||
ip4_net = ""
|
ip4_net = ""
|
||||||
if interface.ip4:
|
if iface.ip4:
|
||||||
ip4_net = f"{interface.ip4}/{interface.ip4mask}"
|
ip4_net = f"{iface.ip4}/{iface.ip4_mask}"
|
||||||
ip4 = tk.StringVar(value=ip4_net)
|
ip4 = tk.StringVar(value=ip4_net)
|
||||||
entry = ttk.Entry(tab, textvariable=ip4, state=state)
|
entry = ttk.Entry(tab, textvariable=ip4, state=state)
|
||||||
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
||||||
|
@ -257,15 +259,15 @@ class NodeConfigDialog(Dialog):
|
||||||
label = ttk.Label(tab, text="IPv6")
|
label = ttk.Label(tab, text="IPv6")
|
||||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||||
ip6_net = ""
|
ip6_net = ""
|
||||||
if interface.ip6:
|
if iface.ip6:
|
||||||
ip6_net = f"{interface.ip6}/{interface.ip6mask}"
|
ip6_net = f"{iface.ip6}/{iface.ip6_mask}"
|
||||||
ip6 = tk.StringVar(value=ip6_net)
|
ip6 = tk.StringVar(value=ip6_net)
|
||||||
entry = ttk.Entry(tab, textvariable=ip6, state=state)
|
entry = ttk.Entry(tab, textvariable=ip6, state=state)
|
||||||
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
||||||
|
|
||||||
self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6)
|
self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6)
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
frame.columnconfigure(0, weight=1)
|
frame.columnconfigure(0, weight=1)
|
||||||
|
@ -277,20 +279,20 @@ class NodeConfigDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def click_emane_config(self, emane_model: str, interface_id: int):
|
def click_emane_config(self, emane_model: str, iface_id: int) -> None:
|
||||||
dialog = EmaneModelDialog(
|
dialog = EmaneModelDialog(
|
||||||
self, self.app, self.canvas_node, emane_model, interface_id
|
self, self.app, self.canvas_node, emane_model, iface_id
|
||||||
)
|
)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
def click_icon(self):
|
def click_icon(self) -> None:
|
||||||
file_path = image_chooser(self, ICONS_PATH)
|
file_path = image_chooser(self, ICONS_PATH)
|
||||||
if file_path:
|
if file_path:
|
||||||
self.image = Images.create(file_path, nodeutils.ICON_SIZE)
|
self.image = Images.create(file_path, nodeutils.ICON_SIZE)
|
||||||
self.image_button.config(image=self.image)
|
self.image_button.config(image=self.image)
|
||||||
self.image_file = file_path
|
self.image_file = file_path
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self) -> None:
|
||||||
error = False
|
error = False
|
||||||
|
|
||||||
# update core node
|
# update core node
|
||||||
|
@ -309,54 +311,54 @@ class NodeConfigDialog(Dialog):
|
||||||
self.canvas_node.image = self.image
|
self.canvas_node.image = self.image
|
||||||
|
|
||||||
# update node interface data
|
# update node interface data
|
||||||
for interface in self.canvas_node.interfaces.values():
|
for iface in self.canvas_node.ifaces.values():
|
||||||
data = self.interfaces[interface.id]
|
data = self.ifaces[iface.id]
|
||||||
|
|
||||||
# validate ip4
|
# validate ip4
|
||||||
ip4_net = data.ip4.get()
|
ip4_net = data.ip4.get()
|
||||||
if not check_ip4(self, interface.name, ip4_net):
|
if not check_ip4(self, iface.name, ip4_net):
|
||||||
error = True
|
error = True
|
||||||
break
|
break
|
||||||
if ip4_net:
|
if ip4_net:
|
||||||
ip4, ip4mask = ip4_net.split("/")
|
ip4, ip4_mask = ip4_net.split("/")
|
||||||
ip4mask = int(ip4mask)
|
ip4_mask = int(ip4_mask)
|
||||||
else:
|
else:
|
||||||
ip4, ip4mask = "", 0
|
ip4, ip4_mask = "", 0
|
||||||
interface.ip4 = ip4
|
iface.ip4 = ip4
|
||||||
interface.ip4mask = ip4mask
|
iface.ip4_mask = ip4_mask
|
||||||
|
|
||||||
# validate ip6
|
# validate ip6
|
||||||
ip6_net = data.ip6.get()
|
ip6_net = data.ip6.get()
|
||||||
if not check_ip6(self, interface.name, ip6_net):
|
if not check_ip6(self, iface.name, ip6_net):
|
||||||
error = True
|
error = True
|
||||||
break
|
break
|
||||||
if ip6_net:
|
if ip6_net:
|
||||||
ip6, ip6mask = ip6_net.split("/")
|
ip6, ip6_mask = ip6_net.split("/")
|
||||||
ip6mask = int(ip6mask)
|
ip6_mask = int(ip6_mask)
|
||||||
else:
|
else:
|
||||||
ip6, ip6mask = "", 0
|
ip6, ip6_mask = "", 0
|
||||||
interface.ip6 = ip6
|
iface.ip6 = ip6
|
||||||
interface.ip6mask = ip6mask
|
iface.ip6_mask = ip6_mask
|
||||||
|
|
||||||
mac = data.mac.get()
|
mac = data.mac.get()
|
||||||
auto_mac = data.is_auto.get()
|
auto_mac = data.is_auto.get()
|
||||||
if not auto_mac and not netaddr.valid_mac(mac):
|
if not auto_mac and not netaddr.valid_mac(mac):
|
||||||
title = f"MAC Error for {interface.name}"
|
title = f"MAC Error for {iface.name}"
|
||||||
messagebox.showerror(title, "Invalid MAC Address")
|
messagebox.showerror(title, "Invalid MAC Address")
|
||||||
error = True
|
error = True
|
||||||
break
|
break
|
||||||
elif not auto_mac:
|
elif not auto_mac:
|
||||||
mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded)
|
mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded)
|
||||||
interface.mac = str(mac)
|
iface.mac = str(mac)
|
||||||
|
|
||||||
# redraw
|
# redraw
|
||||||
if not error:
|
if not error:
|
||||||
self.canvas_node.redraw()
|
self.canvas_node.redraw()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def interface_select(self, event: tk.Event):
|
def iface_select(self, event: tk.Event) -> None:
|
||||||
listbox = event.widget
|
listbox = event.widget
|
||||||
cur = listbox.curselection()
|
cur = listbox.curselection()
|
||||||
if cur:
|
if cur:
|
||||||
interface = listbox.get(cur[0])
|
iface = listbox.get(cur[0])
|
||||||
self.name.set(interface)
|
self.name.set(iface)
|
||||||
|
|
|
@ -4,7 +4,7 @@ core node services
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox, ttk
|
from tkinter import messagebox, ttk
|
||||||
from typing import TYPE_CHECKING, Set
|
from typing import TYPE_CHECKING, Optional, Set
|
||||||
|
|
||||||
from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog
|
from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
@ -19,20 +19,20 @@ if TYPE_CHECKING:
|
||||||
class NodeConfigServiceDialog(Dialog):
|
class NodeConfigServiceDialog(Dialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None
|
self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None
|
||||||
):
|
) -> None:
|
||||||
title = f"{canvas_node.core_node.name} Config Services"
|
title = f"{canvas_node.core_node.name} Config Services"
|
||||||
super().__init__(app, title)
|
super().__init__(app, title)
|
||||||
self.canvas_node = canvas_node
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
self.node_id = canvas_node.core_node.id
|
self.node_id: int = canvas_node.core_node.id
|
||||||
self.groups = None
|
self.groups: Optional[ListboxScroll] = None
|
||||||
self.services = None
|
self.services: Optional[CheckboxList] = None
|
||||||
self.current = None
|
self.current: Optional[ListboxScroll] = None
|
||||||
if services is None:
|
if services is None:
|
||||||
services = set(canvas_node.core_node.config_services)
|
services = set(canvas_node.core_node.config_services)
|
||||||
self.current_services = services
|
self.current_services: Set[str] = services
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
@ -84,9 +84,9 @@ class NodeConfigServiceDialog(Dialog):
|
||||||
button.grid(row=0, column=3, sticky="ew")
|
button.grid(row=0, column=3, sticky="ew")
|
||||||
|
|
||||||
# trigger group change
|
# trigger group change
|
||||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
self.handle_group_change()
|
||||||
|
|
||||||
def handle_group_change(self, event: tk.Event = None):
|
def handle_group_change(self, event: tk.Event = None) -> None:
|
||||||
selection = self.groups.listbox.curselection()
|
selection = self.groups.listbox.curselection()
|
||||||
if selection:
|
if selection:
|
||||||
index = selection[0]
|
index = selection[0]
|
||||||
|
@ -96,7 +96,7 @@ class NodeConfigServiceDialog(Dialog):
|
||||||
checked = name in self.current_services
|
checked = name in self.current_services
|
||||||
self.services.add(name, checked)
|
self.services.add(name, checked)
|
||||||
|
|
||||||
def service_clicked(self, name: str, var: tk.IntVar):
|
def service_clicked(self, name: str, var: tk.IntVar) -> None:
|
||||||
if var.get() and name not in self.current_services:
|
if var.get() and name not in self.current_services:
|
||||||
self.current_services.add(name)
|
self.current_services.add(name)
|
||||||
elif not var.get() and name in self.current_services:
|
elif not var.get() and name in self.current_services:
|
||||||
|
@ -104,7 +104,7 @@ class NodeConfigServiceDialog(Dialog):
|
||||||
self.draw_current_services()
|
self.draw_current_services()
|
||||||
self.canvas_node.core_node.config_services[:] = self.current_services
|
self.canvas_node.core_node.config_services[:] = self.current_services
|
||||||
|
|
||||||
def click_configure(self):
|
def click_configure(self) -> None:
|
||||||
current_selection = self.current.listbox.curselection()
|
current_selection = self.current.listbox.curselection()
|
||||||
if len(current_selection):
|
if len(current_selection):
|
||||||
dialog = ConfigServiceConfigDialog(
|
dialog = ConfigServiceConfigDialog(
|
||||||
|
@ -124,25 +124,25 @@ class NodeConfigServiceDialog(Dialog):
|
||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
def draw_current_services(self):
|
def draw_current_services(self) -> None:
|
||||||
self.current.listbox.delete(0, tk.END)
|
self.current.listbox.delete(0, tk.END)
|
||||||
for name in sorted(self.current_services):
|
for name in sorted(self.current_services):
|
||||||
self.current.listbox.insert(tk.END, name)
|
self.current.listbox.insert(tk.END, name)
|
||||||
if self.is_custom_service(name):
|
if self.is_custom_service(name):
|
||||||
self.current.listbox.itemconfig(tk.END, bg="green")
|
self.current.listbox.itemconfig(tk.END, bg="green")
|
||||||
|
|
||||||
def click_save(self):
|
def click_save(self) -> None:
|
||||||
self.canvas_node.core_node.config_services[:] = self.current_services
|
self.canvas_node.core_node.config_services[:] = self.current_services
|
||||||
logging.info(
|
logging.info(
|
||||||
"saved node config services: %s", self.canvas_node.core_node.config_services
|
"saved node config services: %s", self.canvas_node.core_node.config_services
|
||||||
)
|
)
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def click_cancel(self):
|
def click_cancel(self) -> None:
|
||||||
self.current_services = None
|
self.current_services = None
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def click_remove(self):
|
def click_remove(self) -> None:
|
||||||
cur = self.current.listbox.curselection()
|
cur = self.current.listbox.curselection()
|
||||||
if cur:
|
if cur:
|
||||||
service = self.current.listbox.get(cur[0])
|
service = self.current.listbox.get(cur[0])
|
||||||
|
|
|
@ -3,7 +3,7 @@ core node services
|
||||||
"""
|
"""
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox, ttk
|
from tkinter import messagebox, ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional, Set
|
||||||
|
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.dialogs.serviceconfig import ServiceConfigDialog
|
from core.gui.dialogs.serviceconfig import ServiceConfigDialog
|
||||||
|
@ -16,19 +16,19 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class NodeServiceDialog(Dialog):
|
class NodeServiceDialog(Dialog):
|
||||||
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
|
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
|
||||||
title = f"{canvas_node.core_node.name} Services"
|
title = f"{canvas_node.core_node.name} Services"
|
||||||
super().__init__(app, title)
|
super().__init__(app, title)
|
||||||
self.canvas_node = canvas_node
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
self.node_id = canvas_node.core_node.id
|
self.node_id: int = canvas_node.core_node.id
|
||||||
self.groups = None
|
self.groups: Optional[ListboxScroll] = None
|
||||||
self.services = None
|
self.services: Optional[CheckboxList] = None
|
||||||
self.current = None
|
self.current: Optional[ListboxScroll] = None
|
||||||
services = set(canvas_node.core_node.services)
|
services = set(canvas_node.core_node.services)
|
||||||
self.current_services = services
|
self.current_services: Set[str] = services
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
@ -82,9 +82,9 @@ class NodeServiceDialog(Dialog):
|
||||||
button.grid(row=0, column=3, sticky="ew")
|
button.grid(row=0, column=3, sticky="ew")
|
||||||
|
|
||||||
# trigger group change
|
# trigger group change
|
||||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
self.handle_group_change()
|
||||||
|
|
||||||
def handle_group_change(self, event: tk.Event = None):
|
def handle_group_change(self, event: tk.Event = None) -> None:
|
||||||
selection = self.groups.listbox.curselection()
|
selection = self.groups.listbox.curselection()
|
||||||
if selection:
|
if selection:
|
||||||
index = selection[0]
|
index = selection[0]
|
||||||
|
@ -94,7 +94,7 @@ class NodeServiceDialog(Dialog):
|
||||||
checked = name in self.current_services
|
checked = name in self.current_services
|
||||||
self.services.add(name, checked)
|
self.services.add(name, checked)
|
||||||
|
|
||||||
def service_clicked(self, name: str, var: tk.IntVar):
|
def service_clicked(self, name: str, var: tk.IntVar) -> None:
|
||||||
if var.get() and name not in self.current_services:
|
if var.get() and name not in self.current_services:
|
||||||
self.current_services.add(name)
|
self.current_services.add(name)
|
||||||
elif not var.get() and name in self.current_services:
|
elif not var.get() and name in self.current_services:
|
||||||
|
@ -106,7 +106,7 @@ class NodeServiceDialog(Dialog):
|
||||||
self.current.listbox.itemconfig(tk.END, bg="green")
|
self.current.listbox.itemconfig(tk.END, bg="green")
|
||||||
self.canvas_node.core_node.services[:] = self.current_services
|
self.canvas_node.core_node.services[:] = self.current_services
|
||||||
|
|
||||||
def click_configure(self):
|
def click_configure(self) -> None:
|
||||||
current_selection = self.current.listbox.curselection()
|
current_selection = self.current.listbox.curselection()
|
||||||
if len(current_selection):
|
if len(current_selection):
|
||||||
dialog = ServiceConfigDialog(
|
dialog = ServiceConfigDialog(
|
||||||
|
@ -127,12 +127,12 @@ class NodeServiceDialog(Dialog):
|
||||||
"Service Configuration", "Select a service to configure", parent=self
|
"Service Configuration", "Select a service to configure", parent=self
|
||||||
)
|
)
|
||||||
|
|
||||||
def click_save(self):
|
def click_save(self) -> None:
|
||||||
core_node = self.canvas_node.core_node
|
core_node = self.canvas_node.core_node
|
||||||
core_node.services[:] = self.current_services
|
core_node.services[:] = self.current_services
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def click_remove(self):
|
def click_remove(self) -> None:
|
||||||
cur = self.current.listbox.curselection()
|
cur = self.current.listbox.curselection()
|
||||||
if cur:
|
if cur:
|
||||||
service = self.current.listbox.get(cur[0])
|
service = self.current.listbox.get(cur[0])
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox, ttk
|
from tkinter import messagebox, ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from core.gui.appconfig import Observer
|
from core.gui.appconfig import Observer
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
@ -12,18 +12,18 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class ObserverDialog(Dialog):
|
class ObserverDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "Observer Widgets")
|
super().__init__(app, "Observer Widgets")
|
||||||
self.observers = None
|
self.observers: Optional[tk.Listbox] = None
|
||||||
self.save_button = None
|
self.save_button: Optional[ttk.Button] = None
|
||||||
self.delete_button = None
|
self.delete_button: Optional[ttk.Button] = None
|
||||||
self.selected = None
|
self.selected: Optional[str] = None
|
||||||
self.selected_index = None
|
self.selected_index: Optional[int] = None
|
||||||
self.name = tk.StringVar()
|
self.name: tk.StringVar = tk.StringVar()
|
||||||
self.cmd = tk.StringVar()
|
self.cmd: tk.StringVar = tk.StringVar()
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
self.draw_listbox()
|
self.draw_listbox()
|
||||||
|
@ -31,7 +31,7 @@ class ObserverDialog(Dialog):
|
||||||
self.draw_config_buttons()
|
self.draw_config_buttons()
|
||||||
self.draw_apply_buttons()
|
self.draw_apply_buttons()
|
||||||
|
|
||||||
def draw_listbox(self):
|
def draw_listbox(self) -> None:
|
||||||
listbox_scroll = ListboxScroll(self.top)
|
listbox_scroll = ListboxScroll(self.top)
|
||||||
listbox_scroll.grid(sticky="nsew", pady=PADY)
|
listbox_scroll.grid(sticky="nsew", pady=PADY)
|
||||||
listbox_scroll.columnconfigure(0, weight=1)
|
listbox_scroll.columnconfigure(0, weight=1)
|
||||||
|
@ -42,7 +42,7 @@ class ObserverDialog(Dialog):
|
||||||
for name in sorted(self.app.core.custom_observers):
|
for name in sorted(self.app.core.custom_observers):
|
||||||
self.observers.insert(tk.END, name)
|
self.observers.insert(tk.END, name)
|
||||||
|
|
||||||
def draw_form_fields(self):
|
def draw_form_fields(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew", pady=PADY)
|
frame.grid(sticky="ew", pady=PADY)
|
||||||
frame.columnconfigure(1, weight=1)
|
frame.columnconfigure(1, weight=1)
|
||||||
|
@ -57,7 +57,7 @@ class ObserverDialog(Dialog):
|
||||||
entry = ttk.Entry(frame, textvariable=self.cmd)
|
entry = ttk.Entry(frame, textvariable=self.cmd)
|
||||||
entry.grid(row=1, column=1, sticky="ew")
|
entry.grid(row=1, column=1, sticky="ew")
|
||||||
|
|
||||||
def draw_config_buttons(self):
|
def draw_config_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew", pady=PADY)
|
frame.grid(sticky="ew", pady=PADY)
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
|
@ -76,7 +76,7 @@ class ObserverDialog(Dialog):
|
||||||
)
|
)
|
||||||
self.delete_button.grid(row=0, column=2, sticky="ew")
|
self.delete_button.grid(row=0, column=2, sticky="ew")
|
||||||
|
|
||||||
def draw_apply_buttons(self):
|
def draw_apply_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
|
@ -88,14 +88,14 @@ class ObserverDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def click_save_config(self):
|
def click_save_config(self) -> None:
|
||||||
self.app.guiconfig.observers.clear()
|
self.app.guiconfig.observers.clear()
|
||||||
for observer in self.app.core.custom_observers.values():
|
for observer in self.app.core.custom_observers.values():
|
||||||
self.app.guiconfig.observers.append(observer)
|
self.app.guiconfig.observers.append(observer)
|
||||||
self.app.save_config()
|
self.app.save_config()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def click_create(self):
|
def click_create(self) -> None:
|
||||||
name = self.name.get()
|
name = self.name.get()
|
||||||
if name not in self.app.core.custom_observers:
|
if name not in self.app.core.custom_observers:
|
||||||
cmd = self.cmd.get()
|
cmd = self.cmd.get()
|
||||||
|
@ -109,7 +109,7 @@ class ObserverDialog(Dialog):
|
||||||
else:
|
else:
|
||||||
messagebox.showerror("Observer Error", f"{name} already exists")
|
messagebox.showerror("Observer Error", f"{name} already exists")
|
||||||
|
|
||||||
def click_save(self):
|
def click_save(self) -> None:
|
||||||
name = self.name.get()
|
name = self.name.get()
|
||||||
if self.selected:
|
if self.selected:
|
||||||
previous_name = self.selected
|
previous_name = self.selected
|
||||||
|
@ -122,7 +122,7 @@ class ObserverDialog(Dialog):
|
||||||
self.observers.insert(self.selected_index, name)
|
self.observers.insert(self.selected_index, name)
|
||||||
self.observers.selection_set(self.selected_index)
|
self.observers.selection_set(self.selected_index)
|
||||||
|
|
||||||
def click_delete(self):
|
def click_delete(self) -> None:
|
||||||
if self.selected:
|
if self.selected:
|
||||||
self.observers.delete(self.selected_index)
|
self.observers.delete(self.selected_index)
|
||||||
del self.app.core.custom_observers[self.selected]
|
del self.app.core.custom_observers[self.selected]
|
||||||
|
@ -136,7 +136,7 @@ class ObserverDialog(Dialog):
|
||||||
self.app.menubar.observers_menu.draw_custom()
|
self.app.menubar.observers_menu.draw_custom()
|
||||||
self.app.toolbar.observers_menu.draw_custom()
|
self.app.toolbar.observers_menu.draw_custom()
|
||||||
|
|
||||||
def handle_observer_change(self, event: tk.Event):
|
def handle_observer_change(self, event: tk.Event) -> None:
|
||||||
selection = self.observers.curselection()
|
selection = self.observers.curselection()
|
||||||
if selection:
|
if selection:
|
||||||
self.selected_index = selection[0]
|
self.selected_index = selection[0]
|
||||||
|
|
|
@ -12,27 +12,27 @@ from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
|
||||||
SCALE_INTERVAL = 0.01
|
SCALE_INTERVAL: float = 0.01
|
||||||
|
|
||||||
|
|
||||||
class PreferencesDialog(Dialog):
|
class PreferencesDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "Preferences")
|
super().__init__(app, "Preferences")
|
||||||
self.gui_scale = tk.DoubleVar(value=self.app.app_scale)
|
self.gui_scale: tk.DoubleVar = tk.DoubleVar(value=self.app.app_scale)
|
||||||
preferences = self.app.guiconfig.preferences
|
preferences = self.app.guiconfig.preferences
|
||||||
self.editor = tk.StringVar(value=preferences.editor)
|
self.editor: tk.StringVar = tk.StringVar(value=preferences.editor)
|
||||||
self.theme = tk.StringVar(value=preferences.theme)
|
self.theme: tk.StringVar = tk.StringVar(value=preferences.theme)
|
||||||
self.terminal = tk.StringVar(value=preferences.terminal)
|
self.terminal: tk.StringVar = tk.StringVar(value=preferences.terminal)
|
||||||
self.gui3d = tk.StringVar(value=preferences.gui3d)
|
self.gui3d: tk.StringVar = tk.StringVar(value=preferences.gui3d)
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
self.draw_preferences()
|
self.draw_preferences()
|
||||||
self.draw_buttons()
|
self.draw_buttons()
|
||||||
|
|
||||||
def draw_preferences(self):
|
def draw_preferences(self) -> None:
|
||||||
frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD)
|
frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD)
|
||||||
frame.grid(sticky="nsew", pady=PADY)
|
frame.grid(sticky="nsew", pady=PADY)
|
||||||
frame.columnconfigure(1, weight=1)
|
frame.columnconfigure(1, weight=1)
|
||||||
|
@ -88,7 +88,7 @@ class PreferencesDialog(Dialog):
|
||||||
scrollbar = ttk.Scrollbar(scale_frame, command=self.adjust_scale)
|
scrollbar = ttk.Scrollbar(scale_frame, command=self.adjust_scale)
|
||||||
scrollbar.grid(row=0, column=2)
|
scrollbar.grid(row=0, column=2)
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
|
@ -100,12 +100,12 @@ class PreferencesDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def theme_change(self, event: tk.Event):
|
def theme_change(self, event: tk.Event) -> None:
|
||||||
theme = self.theme.get()
|
theme = self.theme.get()
|
||||||
logging.info("changing theme: %s", theme)
|
logging.info("changing theme: %s", theme)
|
||||||
self.app.style.theme_use(theme)
|
self.app.style.theme_use(theme)
|
||||||
|
|
||||||
def click_save(self):
|
def click_save(self) -> None:
|
||||||
preferences = self.app.guiconfig.preferences
|
preferences = self.app.guiconfig.preferences
|
||||||
preferences.terminal = self.terminal.get()
|
preferences.terminal = self.terminal.get()
|
||||||
preferences.editor = self.editor.get()
|
preferences.editor = self.editor.get()
|
||||||
|
@ -118,7 +118,7 @@ class PreferencesDialog(Dialog):
|
||||||
self.scale_adjust()
|
self.scale_adjust()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def scale_adjust(self):
|
def scale_adjust(self) -> None:
|
||||||
app_scale = self.gui_scale.get()
|
app_scale = self.gui_scale.get()
|
||||||
self.app.app_scale = app_scale
|
self.app.app_scale = app_scale
|
||||||
self.app.master.tk.call("tk", "scaling", app_scale)
|
self.app.master.tk.call("tk", "scaling", app_scale)
|
||||||
|
@ -136,7 +136,7 @@ class PreferencesDialog(Dialog):
|
||||||
self.app.toolbar.scale()
|
self.app.toolbar.scale()
|
||||||
self.app.canvas.scale_graph()
|
self.app.canvas.scale_graph()
|
||||||
|
|
||||||
def adjust_scale(self, arg1: str, arg2: str, arg3: str):
|
def adjust_scale(self, arg1: str, arg2: str, arg3: str) -> None:
|
||||||
scale_value = self.gui_scale.get()
|
scale_value = self.gui_scale.get()
|
||||||
if arg2 == "-1":
|
if arg2 == "-1":
|
||||||
if scale_value <= LARGEST_SCALE - SCALE_INTERVAL:
|
if scale_value <= LARGEST_SCALE - SCALE_INTERVAL:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict, Optional
|
||||||
|
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.nodeutils import NodeUtils
|
from core.gui.nodeutils import NodeUtils
|
||||||
|
@ -14,10 +14,10 @@ if TYPE_CHECKING:
|
||||||
class RunToolDialog(Dialog):
|
class RunToolDialog(Dialog):
|
||||||
def __init__(self, app: "Application") -> None:
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "Run Tool")
|
super().__init__(app, "Run Tool")
|
||||||
self.cmd = tk.StringVar(value="ps ax")
|
self.cmd: tk.StringVar = tk.StringVar(value="ps ax")
|
||||||
self.result = None
|
self.result: Optional[CodeText] = None
|
||||||
self.node_list = None
|
self.node_list: Optional[ListboxScroll] = None
|
||||||
self.executable_nodes = {}
|
self.executable_nodes: Dict[str, int] = {}
|
||||||
self.store_nodes()
|
self.store_nodes()
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from core.gui.appconfig import CoreServer
|
from core.gui.appconfig import CoreServer
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
@ -10,24 +10,24 @@ from core.gui.widgets import ListboxScroll
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
|
||||||
DEFAULT_NAME = "example"
|
DEFAULT_NAME: str = "example"
|
||||||
DEFAULT_ADDRESS = "127.0.0.1"
|
DEFAULT_ADDRESS: str = "127.0.0.1"
|
||||||
DEFAULT_PORT = 50051
|
DEFAULT_PORT: int = 50051
|
||||||
|
|
||||||
|
|
||||||
class ServersDialog(Dialog):
|
class ServersDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "CORE Servers")
|
super().__init__(app, "CORE Servers")
|
||||||
self.name = tk.StringVar(value=DEFAULT_NAME)
|
self.name: tk.StringVar = tk.StringVar(value=DEFAULT_NAME)
|
||||||
self.address = tk.StringVar(value=DEFAULT_ADDRESS)
|
self.address: tk.StringVar = tk.StringVar(value=DEFAULT_ADDRESS)
|
||||||
self.servers = None
|
self.servers: Optional[tk.Listbox] = None
|
||||||
self.selected_index = None
|
self.selected_index: Optional[int] = None
|
||||||
self.selected = None
|
self.selected: Optional[str] = None
|
||||||
self.save_button = None
|
self.save_button: Optional[ttk.Button] = None
|
||||||
self.delete_button = None
|
self.delete_button: Optional[ttk.Button] = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
self.draw_servers()
|
self.draw_servers()
|
||||||
|
@ -35,7 +35,7 @@ class ServersDialog(Dialog):
|
||||||
self.draw_server_configuration()
|
self.draw_server_configuration()
|
||||||
self.draw_apply_buttons()
|
self.draw_apply_buttons()
|
||||||
|
|
||||||
def draw_servers(self):
|
def draw_servers(self) -> None:
|
||||||
listbox_scroll = ListboxScroll(self.top)
|
listbox_scroll = ListboxScroll(self.top)
|
||||||
listbox_scroll.grid(pady=PADY, sticky="nsew")
|
listbox_scroll.grid(pady=PADY, sticky="nsew")
|
||||||
listbox_scroll.columnconfigure(0, weight=1)
|
listbox_scroll.columnconfigure(0, weight=1)
|
||||||
|
@ -48,7 +48,7 @@ class ServersDialog(Dialog):
|
||||||
for server in self.app.core.servers:
|
for server in self.app.core.servers:
|
||||||
self.servers.insert(tk.END, server)
|
self.servers.insert(tk.END, server)
|
||||||
|
|
||||||
def draw_server_configuration(self):
|
def draw_server_configuration(self) -> None:
|
||||||
frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD)
|
frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD)
|
||||||
frame.grid(pady=PADY, sticky="ew")
|
frame.grid(pady=PADY, sticky="ew")
|
||||||
frame.columnconfigure(1, weight=1)
|
frame.columnconfigure(1, weight=1)
|
||||||
|
@ -64,7 +64,7 @@ class ServersDialog(Dialog):
|
||||||
entry = ttk.Entry(frame, textvariable=self.address)
|
entry = ttk.Entry(frame, textvariable=self.address)
|
||||||
entry.grid(row=0, column=3, sticky="ew")
|
entry.grid(row=0, column=3, sticky="ew")
|
||||||
|
|
||||||
def draw_servers_buttons(self):
|
def draw_servers_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(pady=PADY, sticky="ew")
|
frame.grid(pady=PADY, sticky="ew")
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
|
@ -83,7 +83,7 @@ class ServersDialog(Dialog):
|
||||||
)
|
)
|
||||||
self.delete_button.grid(row=0, column=2, sticky="ew")
|
self.delete_button.grid(row=0, column=2, sticky="ew")
|
||||||
|
|
||||||
def draw_apply_buttons(self):
|
def draw_apply_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
|
@ -104,7 +104,7 @@ class ServersDialog(Dialog):
|
||||||
self.app.save_config()
|
self.app.save_config()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def click_create(self):
|
def click_create(self) -> None:
|
||||||
name = self.name.get()
|
name = self.name.get()
|
||||||
if name not in self.app.core.servers:
|
if name not in self.app.core.servers:
|
||||||
address = self.address.get()
|
address = self.address.get()
|
||||||
|
@ -112,7 +112,7 @@ class ServersDialog(Dialog):
|
||||||
self.app.core.servers[name] = server
|
self.app.core.servers[name] = server
|
||||||
self.servers.insert(tk.END, name)
|
self.servers.insert(tk.END, name)
|
||||||
|
|
||||||
def click_save(self):
|
def click_save(self) -> None:
|
||||||
name = self.name.get()
|
name = self.name.get()
|
||||||
if self.selected:
|
if self.selected:
|
||||||
previous_name = self.selected
|
previous_name = self.selected
|
||||||
|
@ -125,7 +125,7 @@ class ServersDialog(Dialog):
|
||||||
self.servers.insert(self.selected_index, name)
|
self.servers.insert(self.selected_index, name)
|
||||||
self.servers.selection_set(self.selected_index)
|
self.servers.selection_set(self.selected_index)
|
||||||
|
|
||||||
def click_delete(self):
|
def click_delete(self) -> None:
|
||||||
if self.selected:
|
if self.selected:
|
||||||
self.servers.delete(self.selected_index)
|
self.servers.delete(self.selected_index)
|
||||||
del self.app.core.servers[self.selected]
|
del self.app.core.servers[self.selected]
|
||||||
|
@ -137,7 +137,7 @@ class ServersDialog(Dialog):
|
||||||
self.save_button.config(state=tk.DISABLED)
|
self.save_button.config(state=tk.DISABLED)
|
||||||
self.delete_button.config(state=tk.DISABLED)
|
self.delete_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
def handle_server_change(self, event: tk.Event):
|
def handle_server_change(self, event: tk.Event) -> None:
|
||||||
selection = self.servers.curselection()
|
selection = self.servers.curselection()
|
||||||
if selection:
|
if selection:
|
||||||
self.selected_index = selection[0]
|
self.selected_index = selection[0]
|
||||||
|
|
|
@ -2,11 +2,12 @@ import logging
|
||||||
import os
|
import os
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import filedialog, ttk
|
from tkinter import filedialog, ttk
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
from PIL.ImageTk import PhotoImage
|
||||||
|
|
||||||
from core.api.grpc.services_pb2 import ServiceValidationMode
|
from core.api.grpc.services_pb2 import NodeServiceData, ServiceValidationMode
|
||||||
from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog
|
from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.images import ImageEnum, Images
|
from core.gui.images import ImageEnum, Images
|
||||||
|
@ -16,8 +17,9 @@ from core.gui.widgets import CodeText, ListboxScroll
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
from core.gui.graph.node import CanvasNode
|
from core.gui.graph.node import CanvasNode
|
||||||
|
from core.gui.coreclient import CoreClient
|
||||||
|
|
||||||
ICON_SIZE = 16
|
ICON_SIZE: int = 16
|
||||||
|
|
||||||
|
|
||||||
class ServiceConfigDialog(Dialog):
|
class ServiceConfigDialog(Dialog):
|
||||||
|
@ -28,54 +30,57 @@ class ServiceConfigDialog(Dialog):
|
||||||
service_name: str,
|
service_name: str,
|
||||||
canvas_node: "CanvasNode",
|
canvas_node: "CanvasNode",
|
||||||
node_id: int,
|
node_id: int,
|
||||||
):
|
) -> None:
|
||||||
title = f"{service_name} Service"
|
title = f"{service_name} Service"
|
||||||
super().__init__(app, title, master=master)
|
super().__init__(app, title, master=master)
|
||||||
self.core = app.core
|
self.core: "CoreClient" = app.core
|
||||||
self.canvas_node = canvas_node
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
self.node_id = node_id
|
self.node_id: int = node_id
|
||||||
self.service_name = service_name
|
self.service_name: str = service_name
|
||||||
self.radiovar = tk.IntVar()
|
self.radiovar: tk.IntVar = tk.IntVar(value=2)
|
||||||
self.radiovar.set(2)
|
self.metadata: str = ""
|
||||||
self.metadata = ""
|
self.filenames: List[str] = []
|
||||||
self.filenames = []
|
self.dependencies: List[str] = []
|
||||||
self.dependencies = []
|
self.executables: List[str] = []
|
||||||
self.executables = []
|
self.startup_commands: List[str] = []
|
||||||
self.startup_commands = []
|
self.validation_commands: List[str] = []
|
||||||
self.validation_commands = []
|
self.shutdown_commands: List[str] = []
|
||||||
self.shutdown_commands = []
|
self.default_startup: List[str] = []
|
||||||
self.default_startup = []
|
self.default_validate: List[str] = []
|
||||||
self.default_validate = []
|
self.default_shutdown: List[str] = []
|
||||||
self.default_shutdown = []
|
self.validation_mode: Optional[ServiceValidationMode] = None
|
||||||
self.validation_mode = None
|
self.validation_time: Optional[int] = None
|
||||||
self.validation_time = None
|
self.validation_period: Optional[float] = None
|
||||||
self.validation_period = None
|
self.directory_entry: Optional[ttk.Entry] = None
|
||||||
self.directory_entry = None
|
self.default_directories: List[str] = []
|
||||||
self.default_directories = []
|
self.temp_directories: List[str] = []
|
||||||
self.temp_directories = []
|
self.documentnew_img: PhotoImage = self.app.get_icon(
|
||||||
self.documentnew_img = self.app.get_icon(ImageEnum.DOCUMENTNEW, ICON_SIZE)
|
ImageEnum.DOCUMENTNEW, ICON_SIZE
|
||||||
self.editdelete_img = self.app.get_icon(ImageEnum.EDITDELETE, ICON_SIZE)
|
)
|
||||||
self.notebook = None
|
self.editdelete_img: PhotoImage = self.app.get_icon(
|
||||||
self.metadata_entry = None
|
ImageEnum.EDITDELETE, ICON_SIZE
|
||||||
self.filename_combobox = None
|
)
|
||||||
self.dir_list = None
|
self.notebook: Optional[ttk.Notebook] = None
|
||||||
self.startup_commands_listbox = None
|
self.metadata_entry: Optional[ttk.Entry] = None
|
||||||
self.shutdown_commands_listbox = None
|
self.filename_combobox: Optional[ttk.Combobox] = None
|
||||||
self.validate_commands_listbox = None
|
self.dir_list: Optional[ListboxScroll] = None
|
||||||
self.validation_time_entry = None
|
self.startup_commands_listbox: Optional[tk.Listbox] = None
|
||||||
self.validation_mode_entry = None
|
self.shutdown_commands_listbox: Optional[tk.Listbox] = None
|
||||||
self.service_file_data = None
|
self.validate_commands_listbox: Optional[tk.Listbox] = None
|
||||||
self.validation_period_entry = None
|
self.validation_time_entry: Optional[ttk.Entry] = None
|
||||||
self.original_service_files = {}
|
self.validation_mode_entry: Optional[ttk.Entry] = None
|
||||||
self.default_config = None
|
self.service_file_data: Optional[CodeText] = None
|
||||||
self.temp_service_files = {}
|
self.validation_period_entry: Optional[ttk.Entry] = None
|
||||||
self.modified_files = set()
|
self.original_service_files: Dict[str, str] = {}
|
||||||
self.has_error = False
|
self.default_config: NodeServiceData = None
|
||||||
|
self.temp_service_files: Dict[str, str] = {}
|
||||||
|
self.modified_files: Set[str] = set()
|
||||||
|
self.has_error: bool = False
|
||||||
self.load()
|
self.load()
|
||||||
if not self.has_error:
|
if not self.has_error:
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def load(self):
|
def load(self) -> None:
|
||||||
try:
|
try:
|
||||||
self.app.core.create_nodes_and_links()
|
self.app.core.create_nodes_and_links()
|
||||||
default_config = self.app.core.get_node_service(
|
default_config = self.app.core.get_node_service(
|
||||||
|
@ -119,7 +124,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
self.app.show_grpc_exception("Get Node Service Error", e)
|
self.app.show_grpc_exception("Get Node Service Error", e)
|
||||||
self.has_error = True
|
self.has_error = True
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(1, weight=1)
|
self.top.rowconfigure(1, weight=1)
|
||||||
|
|
||||||
|
@ -142,7 +147,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
|
|
||||||
self.draw_buttons()
|
self.draw_buttons()
|
||||||
|
|
||||||
def draw_tab_files(self):
|
def draw_tab_files(self) -> None:
|
||||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||||
tab.grid(sticky="nsew")
|
tab.grid(sticky="nsew")
|
||||||
tab.columnconfigure(0, weight=1)
|
tab.columnconfigure(0, weight=1)
|
||||||
|
@ -222,7 +227,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
"<FocusOut>", self.update_temp_service_file_data
|
"<FocusOut>", self.update_temp_service_file_data
|
||||||
)
|
)
|
||||||
|
|
||||||
def draw_tab_directories(self):
|
def draw_tab_directories(self) -> None:
|
||||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||||
tab.grid(sticky="nsew")
|
tab.grid(sticky="nsew")
|
||||||
tab.columnconfigure(0, weight=1)
|
tab.columnconfigure(0, weight=1)
|
||||||
|
@ -257,7 +262,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Remove", command=self.remove_directory)
|
button = ttk.Button(frame, text="Remove", command=self.remove_directory)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def draw_tab_startstop(self):
|
def draw_tab_startstop(self) -> None:
|
||||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||||
tab.grid(sticky="nsew")
|
tab.grid(sticky="nsew")
|
||||||
tab.columnconfigure(0, weight=1)
|
tab.columnconfigure(0, weight=1)
|
||||||
|
@ -311,7 +316,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
elif i == 2:
|
elif i == 2:
|
||||||
self.validate_commands_listbox = listbox_scroll.listbox
|
self.validate_commands_listbox = listbox_scroll.listbox
|
||||||
|
|
||||||
def draw_tab_configuration(self):
|
def draw_tab_configuration(self) -> None:
|
||||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||||
tab.grid(sticky="nsew")
|
tab.grid(sticky="nsew")
|
||||||
tab.columnconfigure(0, weight=1)
|
tab.columnconfigure(0, weight=1)
|
||||||
|
@ -370,7 +375,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
for dependency in self.dependencies:
|
for dependency in self.dependencies:
|
||||||
listbox_scroll.listbox.insert("end", dependency)
|
listbox_scroll.listbox.insert("end", dependency)
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
|
@ -384,7 +389,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=3, sticky="ew")
|
button.grid(row=0, column=3, sticky="ew")
|
||||||
|
|
||||||
def add_filename(self):
|
def add_filename(self) -> None:
|
||||||
filename = self.filename_combobox.get()
|
filename = self.filename_combobox.get()
|
||||||
if filename not in self.filename_combobox["values"]:
|
if filename not in self.filename_combobox["values"]:
|
||||||
self.filename_combobox["values"] += (filename,)
|
self.filename_combobox["values"] += (filename,)
|
||||||
|
@ -395,7 +400,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
else:
|
else:
|
||||||
logging.debug("file already existed")
|
logging.debug("file already existed")
|
||||||
|
|
||||||
def delete_filename(self):
|
def delete_filename(self) -> None:
|
||||||
cbb = self.filename_combobox
|
cbb = self.filename_combobox
|
||||||
filename = cbb.get()
|
filename = cbb.get()
|
||||||
if filename in cbb["values"]:
|
if filename in cbb["values"]:
|
||||||
|
@ -407,7 +412,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
self.modified_files.remove(filename)
|
self.modified_files.remove(filename)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_command(cls, event: tk.Event):
|
def add_command(cls, event: tk.Event) -> None:
|
||||||
frame_contains_button = event.widget.master
|
frame_contains_button = event.widget.master
|
||||||
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
|
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
|
||||||
command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get()
|
command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get()
|
||||||
|
@ -419,7 +424,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
listbox.insert(tk.END, command_to_add)
|
listbox.insert(tk.END, command_to_add)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_entry(cls, event: tk.Event):
|
def update_entry(cls, event: tk.Event) -> None:
|
||||||
listbox = event.widget
|
listbox = event.widget
|
||||||
current_selection = listbox.curselection()
|
current_selection = listbox.curselection()
|
||||||
if len(current_selection) > 0:
|
if len(current_selection) > 0:
|
||||||
|
@ -431,7 +436,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
entry.insert(0, cmd)
|
entry.insert(0, cmd)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete_command(cls, event: tk.Event):
|
def delete_command(cls, event: tk.Event) -> None:
|
||||||
button = event.widget
|
button = event.widget
|
||||||
frame_contains_button = button.master
|
frame_contains_button = button.master
|
||||||
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
|
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
|
||||||
|
@ -441,7 +446,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
entry = frame_contains_button.grid_slaves(row=0, column=0)[0]
|
entry = frame_contains_button.grid_slaves(row=0, column=0)[0]
|
||||||
entry.delete(0, tk.END)
|
entry.delete(0, tk.END)
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self) -> None:
|
||||||
if (
|
if (
|
||||||
not self.is_custom_command()
|
not self.is_custom_command()
|
||||||
and not self.is_custom_service_file()
|
and not self.is_custom_service_file()
|
||||||
|
@ -484,12 +489,12 @@ class ServiceConfigDialog(Dialog):
|
||||||
self.app.show_grpc_exception("Save Service Config Error", e)
|
self.app.show_grpc_exception("Save Service Config Error", e)
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def display_service_file_data(self, event: tk.Event):
|
def display_service_file_data(self, event: tk.Event) -> None:
|
||||||
filename = self.filename_combobox.get()
|
filename = self.filename_combobox.get()
|
||||||
self.service_file_data.text.delete(1.0, "end")
|
self.service_file_data.text.delete(1.0, "end")
|
||||||
self.service_file_data.text.insert("end", self.temp_service_files[filename])
|
self.service_file_data.text.insert("end", self.temp_service_files[filename])
|
||||||
|
|
||||||
def update_temp_service_file_data(self, event: tk.Event):
|
def update_temp_service_file_data(self, event: tk.Event) -> None:
|
||||||
filename = self.filename_combobox.get()
|
filename = self.filename_combobox.get()
|
||||||
self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end")
|
self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end")
|
||||||
if self.temp_service_files[filename] != self.original_service_files.get(
|
if self.temp_service_files[filename] != self.original_service_files.get(
|
||||||
|
@ -499,7 +504,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
else:
|
else:
|
||||||
self.modified_files.discard(filename)
|
self.modified_files.discard(filename)
|
||||||
|
|
||||||
def is_custom_command(self):
|
def is_custom_command(self) -> bool:
|
||||||
startup, validate, shutdown = self.get_commands()
|
startup, validate, shutdown = self.get_commands()
|
||||||
return (
|
return (
|
||||||
set(self.default_startup) != set(startup)
|
set(self.default_startup) != set(startup)
|
||||||
|
@ -507,16 +512,16 @@ class ServiceConfigDialog(Dialog):
|
||||||
or set(self.default_shutdown) != set(shutdown)
|
or set(self.default_shutdown) != set(shutdown)
|
||||||
)
|
)
|
||||||
|
|
||||||
def has_new_files(self):
|
def has_new_files(self) -> bool:
|
||||||
return set(self.filenames) != set(self.filename_combobox["values"])
|
return set(self.filenames) != set(self.filename_combobox["values"])
|
||||||
|
|
||||||
def is_custom_service_file(self):
|
def is_custom_service_file(self) -> bool:
|
||||||
return len(self.modified_files) > 0
|
return len(self.modified_files) > 0
|
||||||
|
|
||||||
def is_custom_directory(self):
|
def is_custom_directory(self) -> bool:
|
||||||
return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end"))
|
return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end"))
|
||||||
|
|
||||||
def click_defaults(self):
|
def click_defaults(self) -> None:
|
||||||
"""
|
"""
|
||||||
clears out any custom configuration permanently
|
clears out any custom configuration permanently
|
||||||
"""
|
"""
|
||||||
|
@ -557,37 +562,41 @@ class ServiceConfigDialog(Dialog):
|
||||||
|
|
||||||
self.current_service_color("")
|
self.current_service_color("")
|
||||||
|
|
||||||
def click_copy(self):
|
def click_copy(self) -> None:
|
||||||
dialog = CopyServiceConfigDialog(self, self.app, self.node_id)
|
file_name = self.filename_combobox.get()
|
||||||
|
name = self.canvas_node.core_node.name
|
||||||
|
dialog = CopyServiceConfigDialog(
|
||||||
|
self.app, self, name, self.service_name, file_name
|
||||||
|
)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def append_commands(
|
def append_commands(
|
||||||
cls, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
cls, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
||||||
):
|
) -> None:
|
||||||
for cmd in to_add:
|
for cmd in to_add:
|
||||||
commands.append(cmd)
|
commands.append(cmd)
|
||||||
listbox.insert(tk.END, cmd)
|
listbox.insert(tk.END, cmd)
|
||||||
|
|
||||||
def get_commands(self):
|
def get_commands(self) -> Tuple[List[str], List[str], List[str]]:
|
||||||
startup = self.startup_commands_listbox.get(0, "end")
|
startup = self.startup_commands_listbox.get(0, "end")
|
||||||
shutdown = self.shutdown_commands_listbox.get(0, "end")
|
shutdown = self.shutdown_commands_listbox.get(0, "end")
|
||||||
validate = self.validate_commands_listbox.get(0, "end")
|
validate = self.validate_commands_listbox.get(0, "end")
|
||||||
return startup, validate, shutdown
|
return startup, validate, shutdown
|
||||||
|
|
||||||
def find_directory_button(self):
|
def find_directory_button(self) -> None:
|
||||||
d = filedialog.askdirectory(initialdir="/")
|
d = filedialog.askdirectory(initialdir="/")
|
||||||
self.directory_entry.delete(0, "end")
|
self.directory_entry.delete(0, "end")
|
||||||
self.directory_entry.insert("end", d)
|
self.directory_entry.insert("end", d)
|
||||||
|
|
||||||
def add_directory(self):
|
def add_directory(self) -> None:
|
||||||
d = self.directory_entry.get()
|
d = self.directory_entry.get()
|
||||||
if os.path.isdir(d):
|
if os.path.isdir(d):
|
||||||
if d not in self.temp_directories:
|
if d not in self.temp_directories:
|
||||||
self.dir_list.listbox.insert("end", d)
|
self.dir_list.listbox.insert("end", d)
|
||||||
self.temp_directories.append(d)
|
self.temp_directories.append(d)
|
||||||
|
|
||||||
def remove_directory(self):
|
def remove_directory(self) -> None:
|
||||||
d = self.directory_entry.get()
|
d = self.directory_entry.get()
|
||||||
dirs = self.dir_list.listbox.get(0, "end")
|
dirs = self.dir_list.listbox.get(0, "end")
|
||||||
if d and d in self.temp_directories:
|
if d and d in self.temp_directories:
|
||||||
|
@ -599,14 +608,14 @@ class ServiceConfigDialog(Dialog):
|
||||||
logging.debug("directory is not in the list")
|
logging.debug("directory is not in the list")
|
||||||
self.directory_entry.delete(0, "end")
|
self.directory_entry.delete(0, "end")
|
||||||
|
|
||||||
def directory_select(self, event):
|
def directory_select(self, event) -> None:
|
||||||
i = self.dir_list.listbox.curselection()
|
i = self.dir_list.listbox.curselection()
|
||||||
if i:
|
if i:
|
||||||
d = self.dir_list.listbox.get(i)
|
d = self.dir_list.listbox.get(i)
|
||||||
self.directory_entry.delete(0, "end")
|
self.directory_entry.delete(0, "end")
|
||||||
self.directory_entry.insert("end", d)
|
self.directory_entry.insert("end", d)
|
||||||
|
|
||||||
def current_service_color(self, color=""):
|
def current_service_color(self, color="") -> None:
|
||||||
"""
|
"""
|
||||||
change the current service label color
|
change the current service label color
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import logging
|
import logging
|
||||||
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict, Optional
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
|
from core.api.grpc.common_pb2 import ConfigOption
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.themes import PADX, PADY
|
from core.gui.themes import PADX, PADY
|
||||||
from core.gui.widgets import ConfigFrame
|
from core.gui.widgets import ConfigFrame
|
||||||
|
@ -13,15 +15,16 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class SessionOptionsDialog(Dialog):
|
class SessionOptionsDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "Session Options")
|
super().__init__(app, "Session Options")
|
||||||
self.config_frame = None
|
self.config_frame: Optional[ConfigFrame] = None
|
||||||
self.has_error = False
|
self.has_error: bool = False
|
||||||
self.config = self.get_config()
|
self.config: Dict[str, ConfigOption] = self.get_config()
|
||||||
|
self.enabled: bool = not self.app.core.is_runtime()
|
||||||
if not self.has_error:
|
if not self.has_error:
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def get_config(self):
|
def get_config(self) -> Dict[str, ConfigOption]:
|
||||||
try:
|
try:
|
||||||
session_id = self.app.core.session_id
|
session_id = self.app.core.session_id
|
||||||
response = self.app.core.client.get_session_options(session_id)
|
response = self.app.core.client.get_session_options(session_id)
|
||||||
|
@ -31,11 +34,10 @@ class SessionOptionsDialog(Dialog):
|
||||||
self.has_error = True
|
self.has_error = True
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
|
self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled)
|
||||||
self.config_frame = ConfigFrame(self.top, self.app, config=self.config)
|
|
||||||
self.config_frame.draw_config()
|
self.config_frame.draw_config()
|
||||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||||
|
|
||||||
|
@ -43,12 +45,13 @@ class SessionOptionsDialog(Dialog):
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
frame.columnconfigure(i, weight=1)
|
frame.columnconfigure(i, weight=1)
|
||||||
button = ttk.Button(frame, text="Save", command=self.save)
|
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||||
|
button = ttk.Button(frame, text="Save", command=self.save, state=state)
|
||||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def save(self):
|
def save(self) -> None:
|
||||||
config = self.config_frame.parse_config()
|
config = self.config_frame.parse_config()
|
||||||
try:
|
try:
|
||||||
session_id = self.app.core.session_id
|
session_id = self.app.core.session_id
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox, ttk
|
from tkinter import messagebox, ttk
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
|
from core.api.grpc.core_pb2 import SessionSummary
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.images import ImageEnum, Images
|
from core.gui.images import ImageEnum, Images
|
||||||
from core.gui.task import ProgressTask
|
from core.gui.task import ProgressTask
|
||||||
|
@ -18,17 +19,17 @@ if TYPE_CHECKING:
|
||||||
class SessionsDialog(Dialog):
|
class SessionsDialog(Dialog):
|
||||||
def __init__(self, app: "Application", is_start_app: bool = False) -> None:
|
def __init__(self, app: "Application", is_start_app: bool = False) -> None:
|
||||||
super().__init__(app, "Sessions")
|
super().__init__(app, "Sessions")
|
||||||
self.is_start_app = is_start_app
|
self.is_start_app: bool = is_start_app
|
||||||
self.selected_session = None
|
self.selected_session: Optional[int] = None
|
||||||
self.selected_id = None
|
self.selected_id: Optional[int] = None
|
||||||
self.tree = None
|
self.tree: Optional[ttk.Treeview] = None
|
||||||
self.sessions = self.get_sessions()
|
self.sessions: List[SessionSummary] = self.get_sessions()
|
||||||
self.connect_button = None
|
self.connect_button: Optional[ttk.Button] = None
|
||||||
self.delete_button = None
|
self.delete_button: Optional[ttk.Button] = None
|
||||||
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def get_sessions(self) -> List[core_pb2.SessionSummary]:
|
def get_sessions(self) -> List[SessionSummary]:
|
||||||
try:
|
try:
|
||||||
response = self.app.core.client.get_sessions()
|
response = self.app.core.client.get_sessions()
|
||||||
logging.info("sessions: %s", response)
|
logging.info("sessions: %s", response)
|
||||||
|
|
|
@ -3,7 +3,7 @@ shape input dialog
|
||||||
"""
|
"""
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import font, ttk
|
from tkinter import font, ttk
|
||||||
from typing import TYPE_CHECKING, List, Union
|
from typing import TYPE_CHECKING, List, Optional, Union
|
||||||
|
|
||||||
from core.gui.dialogs.colorpicker import ColorPickerDialog
|
from core.gui.dialogs.colorpicker import ColorPickerDialog
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
@ -13,40 +13,41 @@ from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
from core.gui.graph.graph import CanvasGraph
|
||||||
from core.gui.graph.shape import Shape
|
from core.gui.graph.shape import Shape
|
||||||
|
|
||||||
FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
|
FONT_SIZES: List[int] = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
|
||||||
BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
BORDER_WIDTH: List[int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
|
|
||||||
|
|
||||||
class ShapeDialog(Dialog):
|
class ShapeDialog(Dialog):
|
||||||
def __init__(self, app: "Application", shape: "Shape"):
|
def __init__(self, app: "Application", shape: "Shape") -> None:
|
||||||
if is_draw_shape(shape.shape_type):
|
if is_draw_shape(shape.shape_type):
|
||||||
title = "Add Shape"
|
title = "Add Shape"
|
||||||
else:
|
else:
|
||||||
title = "Add Text"
|
title = "Add Text"
|
||||||
super().__init__(app, title)
|
super().__init__(app, title)
|
||||||
self.canvas = app.canvas
|
self.canvas: "CanvasGraph" = app.canvas
|
||||||
self.fill = None
|
self.fill: Optional[ttk.Label] = None
|
||||||
self.border = None
|
self.border: Optional[ttk.Label] = None
|
||||||
self.shape = shape
|
self.shape: "Shape" = shape
|
||||||
data = shape.shape_data
|
data = shape.shape_data
|
||||||
self.shape_text = tk.StringVar(value=data.text)
|
self.shape_text: tk.StringVar = tk.StringVar(value=data.text)
|
||||||
self.font = tk.StringVar(value=data.font)
|
self.font: tk.StringVar = tk.StringVar(value=data.font)
|
||||||
self.font_size = tk.IntVar(value=data.font_size)
|
self.font_size: tk.IntVar = tk.IntVar(value=data.font_size)
|
||||||
self.text_color = data.text_color
|
self.text_color: str = data.text_color
|
||||||
fill_color = data.fill_color
|
fill_color = data.fill_color
|
||||||
if not fill_color:
|
if not fill_color:
|
||||||
fill_color = "#CFCFFF"
|
fill_color = "#CFCFFF"
|
||||||
self.fill_color = fill_color
|
self.fill_color: str = fill_color
|
||||||
self.border_color = data.border_color
|
self.border_color: str = data.border_color
|
||||||
self.border_width = tk.IntVar(value=0)
|
self.border_width: tk.IntVar = tk.IntVar(value=0)
|
||||||
self.bold = tk.BooleanVar(value=data.bold)
|
self.bold: tk.BooleanVar = tk.BooleanVar(value=data.bold)
|
||||||
self.italic = tk.BooleanVar(value=data.italic)
|
self.italic: tk.BooleanVar = tk.BooleanVar(value=data.italic)
|
||||||
self.underline = tk.BooleanVar(value=data.underline)
|
self.underline: tk.BooleanVar = tk.BooleanVar(value=data.underline)
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.draw_label_options()
|
self.draw_label_options()
|
||||||
if is_draw_shape(self.shape.shape_type):
|
if is_draw_shape(self.shape.shape_type):
|
||||||
|
@ -54,7 +55,7 @@ class ShapeDialog(Dialog):
|
||||||
self.draw_spacer()
|
self.draw_spacer()
|
||||||
self.draw_buttons()
|
self.draw_buttons()
|
||||||
|
|
||||||
def draw_label_options(self):
|
def draw_label_options(self) -> None:
|
||||||
label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD)
|
label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD)
|
||||||
label_frame.grid(sticky="ew")
|
label_frame.grid(sticky="ew")
|
||||||
label_frame.columnconfigure(0, weight=1)
|
label_frame.columnconfigure(0, weight=1)
|
||||||
|
@ -94,7 +95,7 @@ class ShapeDialog(Dialog):
|
||||||
button = ttk.Checkbutton(frame, variable=self.underline, text="Underline")
|
button = ttk.Checkbutton(frame, variable=self.underline, text="Underline")
|
||||||
button.grid(row=0, column=2, sticky="ew")
|
button.grid(row=0, column=2, sticky="ew")
|
||||||
|
|
||||||
def draw_shape_options(self):
|
def draw_shape_options(self) -> None:
|
||||||
label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD)
|
label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD)
|
||||||
label_frame.grid(sticky="ew", pady=PADY)
|
label_frame.grid(sticky="ew", pady=PADY)
|
||||||
label_frame.columnconfigure(0, weight=1)
|
label_frame.columnconfigure(0, weight=1)
|
||||||
|
@ -129,7 +130,7 @@ class ShapeDialog(Dialog):
|
||||||
)
|
)
|
||||||
combobox.grid(row=0, column=1, sticky="nsew")
|
combobox.grid(row=0, column=1, sticky="nsew")
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self) -> None:
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="nsew")
|
frame.grid(sticky="nsew")
|
||||||
frame.columnconfigure(0, weight=1)
|
frame.columnconfigure(0, weight=1)
|
||||||
|
@ -139,28 +140,28 @@ class ShapeDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.cancel)
|
button = ttk.Button(frame, text="Cancel", command=self.cancel)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def choose_text_color(self):
|
def choose_text_color(self) -> None:
|
||||||
color_picker = ColorPickerDialog(self, self.app, self.text_color)
|
color_picker = ColorPickerDialog(self, self.app, self.text_color)
|
||||||
self.text_color = color_picker.askcolor()
|
self.text_color = color_picker.askcolor()
|
||||||
|
|
||||||
def choose_fill_color(self):
|
def choose_fill_color(self) -> None:
|
||||||
color_picker = ColorPickerDialog(self, self.app, self.fill_color)
|
color_picker = ColorPickerDialog(self, self.app, self.fill_color)
|
||||||
color = color_picker.askcolor()
|
color = color_picker.askcolor()
|
||||||
self.fill_color = color
|
self.fill_color = color
|
||||||
self.fill.config(background=color, text=color)
|
self.fill.config(background=color, text=color)
|
||||||
|
|
||||||
def choose_border_color(self):
|
def choose_border_color(self) -> None:
|
||||||
color_picker = ColorPickerDialog(self, self.app, self.border_color)
|
color_picker = ColorPickerDialog(self, self.app, self.border_color)
|
||||||
color = color_picker.askcolor()
|
color = color_picker.askcolor()
|
||||||
self.border_color = color
|
self.border_color = color
|
||||||
self.border.config(background=color, text=color)
|
self.border.config(background=color, text=color)
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self) -> None:
|
||||||
self.shape.delete()
|
self.shape.delete()
|
||||||
self.canvas.shapes.pop(self.shape.id)
|
self.canvas.shapes.pop(self.shape.id)
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def click_add(self):
|
def click_add(self) -> None:
|
||||||
if is_draw_shape(self.shape.shape_type):
|
if is_draw_shape(self.shape.shape_type):
|
||||||
self.add_shape()
|
self.add_shape()
|
||||||
elif is_shape_text(self.shape.shape_type):
|
elif is_shape_text(self.shape.shape_type):
|
||||||
|
@ -181,7 +182,7 @@ class ShapeDialog(Dialog):
|
||||||
text_font.append("underline")
|
text_font.append("underline")
|
||||||
return text_font
|
return text_font
|
||||||
|
|
||||||
def save_text(self):
|
def save_text(self) -> None:
|
||||||
"""
|
"""
|
||||||
save info related to text or shape label
|
save info related to text or shape label
|
||||||
"""
|
"""
|
||||||
|
@ -194,7 +195,7 @@ class ShapeDialog(Dialog):
|
||||||
data.italic = self.italic.get()
|
data.italic = self.italic.get()
|
||||||
data.underline = self.underline.get()
|
data.underline = self.underline.get()
|
||||||
|
|
||||||
def save_shape(self):
|
def save_shape(self) -> None:
|
||||||
"""
|
"""
|
||||||
save info related to shape
|
save info related to shape
|
||||||
"""
|
"""
|
||||||
|
@ -203,7 +204,7 @@ class ShapeDialog(Dialog):
|
||||||
data.border_color = self.border_color
|
data.border_color = self.border_color
|
||||||
data.border_width = int(self.border_width.get())
|
data.border_width = int(self.border_width.get())
|
||||||
|
|
||||||
def add_text(self):
|
def add_text(self) -> None:
|
||||||
"""
|
"""
|
||||||
add text to canvas
|
add text to canvas
|
||||||
"""
|
"""
|
||||||
|
@ -214,7 +215,7 @@ class ShapeDialog(Dialog):
|
||||||
)
|
)
|
||||||
self.save_text()
|
self.save_text()
|
||||||
|
|
||||||
def add_shape(self):
|
def add_shape(self) -> None:
|
||||||
self.canvas.itemconfig(
|
self.canvas.itemconfig(
|
||||||
self.shape.id,
|
self.shape.id,
|
||||||
fill=self.fill_color,
|
fill=self.fill_color,
|
||||||
|
|
|
@ -3,10 +3,11 @@ throughput dialog
|
||||||
"""
|
"""
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from core.gui.dialogs.colorpicker import ColorPickerDialog
|
from core.gui.dialogs.colorpicker import ColorPickerDialog
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
from core.gui.graph.graph import CanvasGraph
|
||||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -14,21 +15,23 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class ThroughputDialog(Dialog):
|
class ThroughputDialog(Dialog):
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application") -> None:
|
||||||
super().__init__(app, "Throughput Config")
|
super().__init__(app, "Throughput Config")
|
||||||
self.canvas = app.canvas
|
self.canvas: CanvasGraph = app.canvas
|
||||||
self.show_throughput = tk.IntVar(value=1)
|
self.show_throughput: tk.IntVar = tk.IntVar(value=1)
|
||||||
self.exponential_weight = tk.IntVar(value=1)
|
self.exponential_weight: tk.IntVar = tk.IntVar(value=1)
|
||||||
self.transmission = tk.IntVar(value=1)
|
self.transmission: tk.IntVar = tk.IntVar(value=1)
|
||||||
self.reception = tk.IntVar(value=1)
|
self.reception: tk.IntVar = tk.IntVar(value=1)
|
||||||
self.threshold = tk.DoubleVar(value=self.canvas.throughput_threshold)
|
self.threshold: tk.DoubleVar = tk.DoubleVar(
|
||||||
self.width = tk.IntVar(value=self.canvas.throughput_width)
|
value=self.canvas.throughput_threshold
|
||||||
self.color = self.canvas.throughput_color
|
)
|
||||||
self.color_button = None
|
self.width: tk.IntVar = tk.IntVar(value=self.canvas.throughput_width)
|
||||||
|
self.color: str = self.canvas.throughput_color
|
||||||
|
self.color_button: Optional[tk.Button] = None
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
button = ttk.Checkbutton(
|
button = ttk.Checkbutton(
|
||||||
self.top,
|
self.top,
|
||||||
variable=self.show_throughput,
|
variable=self.show_throughput,
|
||||||
|
@ -97,12 +100,12 @@ class ThroughputDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def click_color(self):
|
def click_color(self) -> None:
|
||||||
color_picker = ColorPickerDialog(self, self.app, self.color)
|
color_picker = ColorPickerDialog(self, self.app, self.color)
|
||||||
self.color = color_picker.askcolor()
|
self.color = color_picker.askcolor()
|
||||||
self.color_button.config(bg=self.color, text=self.color, bd=0)
|
self.color_button.config(bg=self.color, text=self.color, bd=0)
|
||||||
|
|
||||||
def click_save(self):
|
def click_save(self) -> None:
|
||||||
self.canvas.throughput_threshold = self.threshold.get()
|
self.canvas.throughput_threshold = self.threshold.get()
|
||||||
self.canvas.throughput_width = self.width.get()
|
self.canvas.throughput_width = self.width.get()
|
||||||
self.canvas.throughput_color = self.color
|
self.canvas.throughput_color = self.color
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict, Optional
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
|
from core.api.grpc.common_pb2 import ConfigOption
|
||||||
|
from core.api.grpc.core_pb2 import Node
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.themes import PADX, PADY
|
from core.gui.themes import PADX, PADY
|
||||||
from core.gui.widgets import ConfigFrame
|
from core.gui.widgets import ConfigFrame
|
||||||
|
@ -10,34 +12,36 @@ from core.gui.widgets import ConfigFrame
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
from core.gui.graph.node import CanvasNode
|
from core.gui.graph.node import CanvasNode
|
||||||
|
from core.gui.graph.graph import CanvasGraph
|
||||||
|
|
||||||
RANGE_COLOR = "#009933"
|
RANGE_COLOR: str = "#009933"
|
||||||
RANGE_WIDTH = 3
|
RANGE_WIDTH: int = 3
|
||||||
|
|
||||||
|
|
||||||
class WlanConfigDialog(Dialog):
|
class WlanConfigDialog(Dialog):
|
||||||
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
|
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
|
||||||
super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration")
|
super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration")
|
||||||
self.canvas_node = canvas_node
|
self.canvas: "CanvasGraph" = app.canvas
|
||||||
self.node = canvas_node.core_node
|
self.canvas_node: "CanvasNode" = canvas_node
|
||||||
self.config_frame = None
|
self.node: Node = canvas_node.core_node
|
||||||
self.range_entry = None
|
self.config_frame: Optional[ConfigFrame] = None
|
||||||
self.has_error = False
|
self.range_entry: Optional[ttk.Entry] = None
|
||||||
self.canvas = app.canvas
|
self.has_error: bool = False
|
||||||
self.ranges = {}
|
self.ranges: Dict[int, int] = {}
|
||||||
self.positive_int = self.app.master.register(self.validate_and_update)
|
self.positive_int: int = self.app.master.register(self.validate_and_update)
|
||||||
try:
|
try:
|
||||||
self.config = self.canvas_node.wlan_config
|
config = self.canvas_node.wlan_config
|
||||||
if not self.config:
|
if not config:
|
||||||
self.config = self.app.core.get_wlan_config(self.node.id)
|
config = self.app.core.get_wlan_config(self.node.id)
|
||||||
|
self.config: Dict[str, ConfigOption] = config
|
||||||
self.init_draw_range()
|
self.init_draw_range()
|
||||||
self.draw()
|
self.draw()
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("WLAN Config Error", e)
|
self.app.show_grpc_exception("WLAN Config Error", e)
|
||||||
self.has_error = True
|
self.has_error: bool = True
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def init_draw_range(self):
|
def init_draw_range(self) -> None:
|
||||||
if self.canvas_node.id in self.canvas.wireless_network:
|
if self.canvas_node.id in self.canvas.wireless_network:
|
||||||
for cid in self.canvas.wireless_network[self.canvas_node.id]:
|
for cid in self.canvas.wireless_network[self.canvas_node.id]:
|
||||||
x, y = self.canvas.coords(cid)
|
x, y = self.canvas.coords(cid)
|
||||||
|
@ -46,7 +50,7 @@ class WlanConfigDialog(Dialog):
|
||||||
)
|
)
|
||||||
self.ranges[cid] = range_id
|
self.ranges[cid] = range_id
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
self.config_frame = ConfigFrame(self.top, self.app, self.config)
|
self.config_frame = ConfigFrame(self.top, self.app, self.config)
|
||||||
|
@ -55,7 +59,7 @@ class WlanConfigDialog(Dialog):
|
||||||
self.draw_apply_buttons()
|
self.draw_apply_buttons()
|
||||||
self.top.bind("<Destroy>", self.remove_ranges)
|
self.top.bind("<Destroy>", self.remove_ranges)
|
||||||
|
|
||||||
def draw_apply_buttons(self):
|
def draw_apply_buttons(self) -> None:
|
||||||
"""
|
"""
|
||||||
create node configuration options
|
create node configuration options
|
||||||
"""
|
"""
|
||||||
|
@ -75,7 +79,7 @@ class WlanConfigDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=1, sticky="ew")
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self) -> None:
|
||||||
"""
|
"""
|
||||||
retrieve user's wlan configuration and store the new configuration values
|
retrieve user's wlan configuration and store the new configuration values
|
||||||
"""
|
"""
|
||||||
|
@ -87,7 +91,7 @@ class WlanConfigDialog(Dialog):
|
||||||
self.remove_ranges()
|
self.remove_ranges()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def remove_ranges(self, event=None):
|
def remove_ranges(self, event=None) -> None:
|
||||||
for cid in self.canvas.find_withtag("range"):
|
for cid in self.canvas.find_withtag("range"):
|
||||||
self.canvas.delete(cid)
|
self.canvas.delete(cid)
|
||||||
self.ranges.clear()
|
self.ranges.clear()
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue