commit
b6aef6d560
213 changed files with 11112 additions and 6131 deletions
2
.github/workflows/daemon-checks.yml
vendored
2
.github/workflows/daemon-checks.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
|||
- name: grpc
|
||||
run: |
|
||||
cd daemon/proto
|
||||
pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/core.proto
|
||||
pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto
|
||||
- name: test
|
||||
run: |
|
||||
cd daemon
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -18,8 +18,8 @@ debian
|
|||
stamp-h1
|
||||
|
||||
# generated protobuf files
|
||||
daemon/core/api/grpc/core_pb2.py
|
||||
daemon/core/api/grpc/core_pb2_grpc.py
|
||||
*_pb2.py
|
||||
*_pb2_grpc.py
|
||||
|
||||
# python build directory
|
||||
dist
|
||||
|
@ -54,7 +54,6 @@ coverage.xml
|
|||
# ignore built input files
|
||||
netns/setup.py
|
||||
daemon/setup.py
|
||||
ns3/setup.py
|
||||
|
||||
# ignore corefx build
|
||||
corefx/target
|
||||
|
|
158
CHANGELOG.md
158
CHANGELOG.md
|
@ -1,3 +1,137 @@
|
|||
## 2020-02-20 CORE 6.1.0
|
||||
* New
|
||||
* config services - these services leverage a proper template engine and have configurable parameters, given enough time may replace existing services
|
||||
* core-imn-to-xml - IMN to XML utility script
|
||||
* replaced internal code for determining ip/mac address with netaddr library
|
||||
* Enhancements
|
||||
* added distributed package for built packages
|
||||
* made use of python type hinting for functions and their return values
|
||||
* updated Quagga zebra service to remove deprecated warning
|
||||
* Removed
|
||||
* removed stale ns3 code
|
||||
* CORETK GUI
|
||||
* added logging
|
||||
* improved error dialog
|
||||
* properly use global ipv6 addresses for nodes
|
||||
* disable proxy usage by default, flag available to enable
|
||||
* gRPC API
|
||||
* add_link - now returns created interface information
|
||||
* set_node_service - can now set files and directories to properly replicate previous usage
|
||||
* get_emane_event_channel - return information related to the currently used emane event channel
|
||||
* Bugfixes
|
||||
* fixed session SDT functionality back to working order, due to python3 changes
|
||||
* avoid shutting down services for nodes that are not up
|
||||
* EMANE bypass model options will now display properly in GUIs
|
||||
* XML scenarios will now properly read in custom node icons
|
||||
* \#372 - fixed mobility waypoint comparisons
|
||||
* \#370 - fixed radvd service
|
||||
* \#368 - updated frr services to properly start staticd when needed
|
||||
* \#358 - fixed systemd service install path
|
||||
* \#350 - fixed frr babel wireless configuration
|
||||
* \#354 - updated frr to reset interfaces to properly take configurations
|
||||
|
||||
## 2020-01-01 CORE 6.0.0
|
||||
* New
|
||||
* beta release of the python based tk GUI, use **coretk-gui** to try it out, plan will be to eventually sunset the old GUI once this is good enough
|
||||
* this GUI will allow us to provide enhancements and a consistent python dev environment for developers
|
||||
* Major Changes
|
||||
* python3.6+ support only, due to python2 EOL https://pyfound.blogspot.com/2019/12/python-2-sunset.html
|
||||
* distributed sessions now leverages the fabric library for sending remote SSH commands
|
||||
* Enhancements
|
||||
* changed usage of bridge-utils to using ip based bridge commands due to deprecation
|
||||
* installation.sh script to help automate a standard make install or dev install
|
||||
* when sessions are created without an id they will now always start from 1 and return the next unused id
|
||||
* gRPC is now running by default
|
||||
* Session API
|
||||
* removed **create_emane_network** and **create_wlan_network** to help force using **add_node** for all cases
|
||||
* removed **session.master** as it was only used for previous distributed sessions
|
||||
* updated **add_node** to allow providing a custom class for node creation
|
||||
* gRPC API
|
||||
* added get all services configurations
|
||||
* added get all wlan configurations
|
||||
* added start/stop session calls, provides more freedom for startup and shutdown logic
|
||||
* session events now have a session id to help differentiate which session they are coming from
|
||||
* throughput events now require a session id and responses include session id for differentiating data
|
||||
* session events can now be subscribed to with a subset of events or all
|
||||
* emane model config data now include interface ids properly
|
||||
* sessions returned from get sessions call may include file names when created from xml
|
||||
* when opening an xml the session can now be started or not
|
||||
* edit node will now broadcast the edit for others to listen to
|
||||
* all config responses will now be in the form of a mapped value of key to ConfigOption, or a list of these when retrieving all, sometimes the config response may be wrapped in a different message to include other metadata
|
||||
* Bugfixes
|
||||
* \#311 - initialize ebtables chains for wlan networks only
|
||||
* \#312 - removed sudo from init script
|
||||
* \#313 - check if interface exists before flushing, previously would log an exception that didn't matter
|
||||
* \#314 - node locations stored as floats instead of ints to avoid mobility calculations due to loss of precision
|
||||
* \#321 - python installation path will be based on distr ibution/python building it
|
||||
* emane options xml parsing didn't properly take into account the **emane_prefix** configuration
|
||||
* updates services that checked for ipv4/ipv6 addresses to not fail for valid ipv6 addresses with a decimal
|
||||
* Documentation
|
||||
* updated NRL links to new GitHub locations
|
||||
* updates for distributed session
|
||||
* updates to dev guide
|
||||
* updates to examples LXD/Docker setup
|
||||
* updates to FRR service documentation
|
||||
* gRPC get node service file will not throw an exception when node doesn't exist
|
||||
|
||||
## 2019-10-12 CORE 5.5.2
|
||||
* gRPC
|
||||
* Added emane_link API for linking/unlinking EMANE nodes within the GUI
|
||||
* Bugfixes
|
||||
* Fixed python3 issues when configuring WLAN nodes
|
||||
* Fixed issue due to refactoring when running distributed
|
||||
* Fixed issue when running python script from GUI
|
||||
|
||||
## 2019-10-09 CORE 5.5.1
|
||||
* Bugfix
|
||||
* Fixed issue with 5.5.0 refactoring causing issues in python2.
|
||||
* Fixed python3 issues with NRL services
|
||||
|
||||
## 2019-10-03 CORE 5.5.0
|
||||
* Documentation
|
||||
* updated dependencies for building OSPF MDR on installation page
|
||||
* added python/pip instruction on installation page
|
||||
* added ethtool dependency for CORE
|
||||
* GUI
|
||||
* removed experimental OVS node to avoid confusion and issues related to using it
|
||||
* Daemon
|
||||
* fixed core-daemon --ovs flag back to working order for running CORE using OVS bridges instead of Linux bridges
|
||||
* updated requirements.txt to refer to configparser 4.0.2, due to 4.0.1 removal by developers
|
||||
* update to fail fast for dependent executables that are not found within PATH
|
||||
* update to not load services that fail during service.on_load and move on
|
||||
* Build
|
||||
* fixed issue with configure script when using option flags
|
||||
* python install path will use the native install path for AM_PATH_PYTHON, instead of coercing to python3
|
||||
* Issues
|
||||
* \#271 - OVS node error in GUI
|
||||
* \#291 - configparser 4.0.1 issue
|
||||
* \#290 - python3 path issue when building
|
||||
|
||||
## 2019-09-23 CORE 5.4.0
|
||||
* Documentation
|
||||
* Updates to documentation dev guide
|
||||
* Improvements
|
||||
* Added support for Pipenv for development
|
||||
* Added configuration to leverage pre-commit during development
|
||||
* Added configuration to leverage isort, black, and flake8 during development
|
||||
* Added Github Actions to help verify pull requests in the same way as pre-commit
|
||||
* Issues
|
||||
* \#279 - WLAN configuration does not get set by default
|
||||
* \#272 - error installing python package futures==3.2.0
|
||||
* Pull Requests
|
||||
* \#275 - Disable MAC learning on WLAN
|
||||
* \#281 - Bumped jackson version on corefx
|
||||
|
||||
## 2019-07-05 CORE 5.3.1
|
||||
* Documentation
|
||||
* Updates to provide more information regarding several of the included services
|
||||
* Issues
|
||||
* \#252 - fixed changing wlan configurations during runtime
|
||||
* \#256 - fixed mobility waypoint comparison for python3
|
||||
* \#174 - turn tx/rx checksums off by default as they will never be valid for virtual interfaces
|
||||
* \#259 - fixes for distributed EMANE
|
||||
* \#260 - fixed issue with how execfile was being used due to it not existing within python3
|
||||
|
||||
## 2019-06-10 CORE 5.3.0
|
||||
* Enhancements
|
||||
* python 2 / 3 support
|
||||
|
@ -12,11 +146,11 @@
|
|||
* Added EMANE prefix configuration when looking for emane model manifest files
|
||||
* requires configuring **emane_prefix** in /etc/core/core.conf
|
||||
* Cleanup
|
||||
* Refactoring of the core python package structure, trying to help provide better organization and
|
||||
* Refactoring of the core python package structure, trying to help provide better organization and
|
||||
logical groupings
|
||||
* Issues
|
||||
* \#246 - Fixed network to network link handling when reading xml files
|
||||
* \#236 - Fixed storing/reading of link configuration values within xml files
|
||||
* \#236 - Fixed storing/reading of link configuration values within xml files
|
||||
* \#170 - FRR Service
|
||||
* \#155 - EMANE path configuration
|
||||
* \#233 - Python 3 support
|
||||
|
@ -50,16 +184,16 @@
|
|||
|
||||
## 2018-05-22 CORE 5.1
|
||||
* DAEMON:
|
||||
* removed and cleared out code that is either legacy or no longer supported (Xen, BSD, Kernel patching, RPM/DEB
|
||||
* removed and cleared out code that is either legacy or no longer supported (Xen, BSD, Kernel patching, RPM/DEB
|
||||
specific files)
|
||||
* default nodes are now set in the node map
|
||||
* moved ns3 and netns directories to the top of the repo
|
||||
* changes to make use of fpm as the tool for building packages
|
||||
* removed usage of logzero to avoid dependency issues for built packages
|
||||
* removed daemon addons directory
|
||||
* added CoreEmu to core.emulator.coreemu to help begin serving as the basis for a more formal API for scripting
|
||||
* added CoreEmu to core.emulator.coreemu to help begin serving as the basis for a more formal API for scripting
|
||||
and creating new external APIs out of
|
||||
* cleaned up logging, moved more logging to DEBUG from INFO, tried to mold INFO message to be more simple and
|
||||
* cleaned up logging, moved more logging to DEBUG from INFO, tried to mold INFO message to be more simple and
|
||||
informative
|
||||
* EMANE 1.0.1-1.21 supported
|
||||
* updates to leverage EMANE python bindings for dynamically parsing phy/mac manifest files
|
||||
|
@ -73,7 +207,7 @@
|
|||
* updated broken help links in GUI Help->About
|
||||
* Packaging:
|
||||
* fixed PYTHON_PATH to PYTHONPATH in sysv script
|
||||
* added make command to leverage FPM as the tool for creating deb/rpm packages going forward, there is documentation
|
||||
* added make command to leverage FPM as the tool for creating deb/rpm packages going forward, there is documentation
|
||||
within README.md to try it out
|
||||
* TEST:
|
||||
* fixed some broken tests
|
||||
|
@ -82,7 +216,7 @@
|
|||
* \#142 - duplication of custom services
|
||||
* \#136 - sphinx-apidoc command not found
|
||||
* \#137 - make command fails when using distclean
|
||||
|
||||
|
||||
## 2017-09-01 CORE 5.0
|
||||
* DEVELOPMENT:
|
||||
* support for editorconfig to help standardize development across IDEs, from the defined configuration file
|
||||
|
@ -237,7 +371,7 @@
|
|||
* added "--addons" startup mode to pass control to code included from addons dir
|
||||
* added "Locked" entry to View menu to prevent moving items
|
||||
* use currently selected node type when invoking a topology generator
|
||||
* updated throughput plots with resizing, color picker, plot labels, locked scales, and save/load plot
|
||||
* updated throughput plots with resizing, color picker, plot labels, locked scales, and save/load plot
|
||||
configuration with imn file
|
||||
* improved session dialog
|
||||
* EMANE:
|
||||
|
@ -254,11 +388,11 @@
|
|||
* XML import and export
|
||||
* renamed "cored.py" to "cored", "coresendmsg.py" to "coresendmsg"
|
||||
* code reorganization and clean-up
|
||||
* updated XML export to write NetworkPlan, MotionPlan, and ServicePlan within a Scenario tag, added new
|
||||
* updated XML export to write NetworkPlan, MotionPlan, and ServicePlan within a Scenario tag, added new
|
||||
"Save As XML..." File menu entry
|
||||
* added script_start/pause/stop options to Ns2ScriptedMobility
|
||||
* "python" source sub-directory renamed to "daemon"
|
||||
* added "cored -e" option to execute a Python script, adding its session to the active sessions list, allowing for
|
||||
* added "cored -e" option to execute a Python script, adding its session to the active sessions list, allowing for
|
||||
GUI connection
|
||||
* support comma-separated list for custom_services_dir in core.conf file
|
||||
* updated kernel patches for Linux kernel 3.5
|
||||
|
@ -267,7 +401,7 @@
|
|||
* integrate ns-3 node location between CORE and ns-3 simulation
|
||||
* added ns-3 random walk mobility example
|
||||
* updated ns-3 Wifi example to allow GUI connection and moving of nodes
|
||||
* fixed the following bugs: 54, 103, 111, 136, 145, 153, 157, 160, 161, 162, 164, 165, 168, 170, 171, 173, 174, 176,
|
||||
* fixed the following bugs: 54, 103, 111, 136, 145, 153, 157, 160, 161, 162, 164, 165, 168, 170, 171, 173, 174, 176,
|
||||
184, 190, 193
|
||||
|
||||
## 2012-09-25 CORE 4.4
|
||||
|
@ -308,7 +442,7 @@
|
|||
* support /etc/core/environment and ~/.core/environment files
|
||||
* added Ns2ScriptedMobility model to Python, removed from the GUI
|
||||
* namespace nodes mount a private /sys
|
||||
* fixed the following bugs: 80, 81, 84, 99, 104, 109, 110, 122, 124, 131, 133, 134, 135, 137, 140, 143, 144, 146,
|
||||
* fixed the following bugs: 80, 81, 84, 99, 104, 109, 110, 122, 124, 131, 133, 134, 135, 137, 140, 143, 144, 146,
|
||||
147, 151, 154, 155
|
||||
|
||||
## 2012-03-07 CORE 4.3
|
||||
|
|
51
Makefile.am
51
Makefile.am
|
@ -15,7 +15,7 @@ if WANT_DAEMON
|
|||
endif
|
||||
|
||||
if WANT_NETNS
|
||||
NETNS = netns ns3
|
||||
NETNS = netns
|
||||
endif
|
||||
|
||||
# keep docs last due to dependencies on binaries
|
||||
|
@ -96,12 +96,60 @@ fpm -s dir -t deb -n core \
|
|||
-C $(DESTDIR)
|
||||
endef
|
||||
|
||||
define fpm-distributed-deb =
|
||||
fpm -s dir -t deb -n core-distributed \
|
||||
-m "$(PACKAGE_MAINTAINERS)" \
|
||||
--license "BSD" \
|
||||
--description "Common Open Research Emulator Distributed Package" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_distributed_VERSION_ARCH.deb \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
-d "ethtool" \
|
||||
-d "procps" \
|
||||
-d "libc6 >= 2.14" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "iproute2" \
|
||||
-d "libev4" \
|
||||
-d "openssh-server" \
|
||||
-d "xterm" \
|
||||
-C $(DESTDIR)
|
||||
endef
|
||||
|
||||
define fpm-distributed-rpm =
|
||||
fpm -s dir -t rpm -n core-distributed \
|
||||
-m "$(PACKAGE_MAINTAINERS)" \
|
||||
--license "BSD" \
|
||||
--description "Common Open Research Emulator Distributed Package" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_distributed_VERSION_ARCH.rpm \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
-d "ethtool" \
|
||||
-d "procps-ng" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "iproute" \
|
||||
-d "libev" \
|
||||
-d "net-tools" \
|
||||
-d "openssh-server" \
|
||||
-d "xterm" \
|
||||
-C $(DESTDIR)
|
||||
endef
|
||||
|
||||
.PHONY: fpm
|
||||
fpm: clean-local-fpm
|
||||
$(MAKE) install DESTDIR=$(DESTDIR)
|
||||
$(call fpm-deb)
|
||||
$(call fpm-rpm)
|
||||
|
||||
.PHONY: fpm-distributed
|
||||
fpm-distributed: clean-local-fpm
|
||||
$(MAKE) -C netns install DESTDIR=$(DESTDIR)
|
||||
$(call fpm-distributed-deb)
|
||||
$(call fpm-distributed-rpm)
|
||||
|
||||
.PHONY: clean-local-fpm
|
||||
clean-local-fpm:
|
||||
-rm -rf *.deb
|
||||
|
@ -137,7 +185,6 @@ change-files:
|
|||
$(call change-files,scripts/core-daemon.service)
|
||||
$(call change-files,scripts/core-daemon)
|
||||
$(call change-files,daemon/core/constants.py)
|
||||
$(call change-files,ns3/setup.py)
|
||||
$(call change-files,netns/setup.py)
|
||||
$(call change-files,daemon/setup.py)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
# this defines the CORE version number, must be static for AC_INIT
|
||||
AC_INIT(core, 6.0.0)
|
||||
AC_INIT(core, 6.1.0)
|
||||
|
||||
# autoconf and automake initialization
|
||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||
|
@ -262,8 +262,7 @@ AC_CONFIG_FILES([Makefile
|
|||
daemon/doc/conf.py
|
||||
daemon/proto/Makefile
|
||||
netns/Makefile
|
||||
netns/version.h
|
||||
ns3/Makefile],)
|
||||
netns/version.h],)
|
||||
AC_OUTPUT
|
||||
|
||||
# Summary text
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
graft core/gui/data
|
||||
graft core/configservices/*/templates
|
||||
|
|
297
daemon/Pipfile.lock
generated
297
daemon/Pipfile.lock
generated
|
@ -162,11 +162,11 @@
|
|||
},
|
||||
"invoke": {
|
||||
"hashes": [
|
||||
"sha256:c52274d2e8a6d64ef0d61093e1983268ea1fc0cd13facb9448c4ef0c9a7ac7da",
|
||||
"sha256:f4ec8a134c0122ea042c8912529f87652445d9f4de590b353d23f95bfa1f0efd",
|
||||
"sha256:fc803a5c9052f15e63310aa81a43498d7c55542beb18564db88a9d75a176fa44"
|
||||
"sha256:4668a4a594a47f2da2f0672ec2f7b1566f809cebf10bcd95ce2de9ecf39b95d1",
|
||||
"sha256:ae7b4513638bde9afcda0825e9535599637a3f65bd819a27098356027bb17c8a",
|
||||
"sha256:e04faba8ea7cdf6f5c912be42dcafd5c1074b7f2f306998992c4bfb40a9a690b"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
|
@ -199,6 +199,45 @@
|
|||
],
|
||||
"version": "==4.4.2"
|
||||
},
|
||||
"mako": {
|
||||
"hashes": [
|
||||
"sha256:2984a6733e1d472796ceef37ad48c26f4a984bb18119bb2dbc37a44d8f6e75a4"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||
"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:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"netaddr": {
|
||||
"hashes": [
|
||||
"sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd",
|
||||
|
@ -215,59 +254,53 @@
|
|||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031",
|
||||
"sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71",
|
||||
"sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c",
|
||||
"sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340",
|
||||
"sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa",
|
||||
"sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b",
|
||||
"sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573",
|
||||
"sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e",
|
||||
"sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab",
|
||||
"sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9",
|
||||
"sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e",
|
||||
"sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291",
|
||||
"sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12",
|
||||
"sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871",
|
||||
"sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281",
|
||||
"sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08",
|
||||
"sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41",
|
||||
"sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2",
|
||||
"sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5",
|
||||
"sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb",
|
||||
"sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547",
|
||||
"sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75",
|
||||
"sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9",
|
||||
"sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1",
|
||||
"sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a",
|
||||
"sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96",
|
||||
"sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132",
|
||||
"sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a",
|
||||
"sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5",
|
||||
"sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0"
|
||||
"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": "==6.2.1"
|
||||
"version": "==7.0.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd",
|
||||
"sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed",
|
||||
"sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057",
|
||||
"sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce",
|
||||
"sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03",
|
||||
"sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46",
|
||||
"sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33",
|
||||
"sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c",
|
||||
"sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9",
|
||||
"sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef",
|
||||
"sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b",
|
||||
"sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d",
|
||||
"sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8",
|
||||
"sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6",
|
||||
"sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941",
|
||||
"sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13"
|
||||
"sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37",
|
||||
"sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36",
|
||||
"sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4",
|
||||
"sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d",
|
||||
"sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574",
|
||||
"sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0",
|
||||
"sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5",
|
||||
"sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f",
|
||||
"sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946",
|
||||
"sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104",
|
||||
"sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e",
|
||||
"sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7",
|
||||
"sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a",
|
||||
"sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507",
|
||||
"sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1",
|
||||
"sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593",
|
||||
"sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c",
|
||||
"sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1"
|
||||
],
|
||||
"version": "==3.11.1"
|
||||
"version": "==3.11.2"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
|
@ -303,26 +336,26 @@
|
|||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
|
||||
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
|
||||
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
|
||||
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
|
||||
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
|
||||
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
|
||||
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
|
||||
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
|
||||
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
|
||||
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
|
||||
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
|
||||
"sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
|
||||
"sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
|
||||
"sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
|
||||
"sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
|
||||
"sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
|
||||
"sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
|
||||
"sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
|
||||
"sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
|
||||
"sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
|
||||
"sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
|
||||
],
|
||||
"version": "==5.2"
|
||||
"version": "==5.3"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"version": "==1.13.0"
|
||||
"version": "==1.14.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
|
@ -483,18 +516,18 @@
|
|||
},
|
||||
"identify": {
|
||||
"hashes": [
|
||||
"sha256:7782115794ec28b011702815d9f5e532244560cd2bf0789c4f09381d43befd90",
|
||||
"sha256:9e7521e9abeaede4d2d1092a106e418c65ddf6b3182b43930bcb3c8cfb974488"
|
||||
"sha256:418f3b2313ac0b531139311a6b426854e9cbdfcfb6175447a5039aa6291d8b30",
|
||||
"sha256:8ad99ed1f3a965612dcb881435bf58abcfbeb05e230bb8c352b51e8eac103360"
|
||||
],
|
||||
"version": "==1.4.8"
|
||||
"version": "==1.4.10"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
||||
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.3.0"
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"importlib-resources": {
|
||||
"hashes": [
|
||||
|
@ -529,23 +562,23 @@
|
|||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||
"sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
|
||||
"sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
|
||||
],
|
||||
"version": "==8.0.2"
|
||||
"version": "==8.1.0"
|
||||
},
|
||||
"nodeenv": {
|
||||
"hashes": [
|
||||
"sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a"
|
||||
"sha256:561057acd4ae3809e665a9aaaf214afff110bbb6a6d5c8a96121aea6878408b3"
|
||||
],
|
||||
"version": "==1.3.3"
|
||||
"version": "==1.3.4"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47",
|
||||
"sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"
|
||||
"sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73",
|
||||
"sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"
|
||||
],
|
||||
"version": "==19.2"
|
||||
"version": "==20.1"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
|
@ -556,39 +589,41 @@
|
|||
},
|
||||
"pre-commit": {
|
||||
"hashes": [
|
||||
"sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e",
|
||||
"sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50"
|
||||
"sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850",
|
||||
"sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.20.0"
|
||||
"version": "==1.21.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd",
|
||||
"sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed",
|
||||
"sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057",
|
||||
"sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce",
|
||||
"sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03",
|
||||
"sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46",
|
||||
"sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33",
|
||||
"sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c",
|
||||
"sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9",
|
||||
"sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef",
|
||||
"sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b",
|
||||
"sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d",
|
||||
"sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8",
|
||||
"sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6",
|
||||
"sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941",
|
||||
"sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13"
|
||||
"sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37",
|
||||
"sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36",
|
||||
"sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4",
|
||||
"sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d",
|
||||
"sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574",
|
||||
"sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0",
|
||||
"sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5",
|
||||
"sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f",
|
||||
"sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946",
|
||||
"sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104",
|
||||
"sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e",
|
||||
"sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7",
|
||||
"sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a",
|
||||
"sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507",
|
||||
"sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1",
|
||||
"sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593",
|
||||
"sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c",
|
||||
"sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1"
|
||||
],
|
||||
"version": "==3.11.1"
|
||||
"version": "==3.11.2"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
|
||||
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
|
||||
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
|
||||
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
|
||||
],
|
||||
"version": "==1.8.0"
|
||||
"version": "==1.8.1"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
|
@ -606,41 +641,41 @@
|
|||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f",
|
||||
"sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"
|
||||
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
|
||||
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
|
||||
],
|
||||
"version": "==2.4.5"
|
||||
"version": "==2.4.6"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa",
|
||||
"sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4"
|
||||
"sha256:1d122e8be54d1a709e56f82e2d85dcba3018313d64647f38a91aec88c239b600",
|
||||
"sha256:c13d1943c63e599b98cf118fcb9703e4d7bde7caa9a432567bcdcae4bf512d20"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.3.2"
|
||||
"version": "==5.3.4"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
|
||||
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
|
||||
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
|
||||
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
|
||||
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
|
||||
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
|
||||
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
|
||||
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
|
||||
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
|
||||
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
|
||||
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
|
||||
"sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
|
||||
"sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
|
||||
"sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
|
||||
"sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
|
||||
"sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
|
||||
"sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
|
||||
"sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
|
||||
"sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
|
||||
"sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
|
||||
"sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
|
||||
],
|
||||
"version": "==5.2"
|
||||
"version": "==5.3"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"version": "==1.13.0"
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
|
@ -658,17 +693,17 @@
|
|||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
|
||||
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
|
||||
"sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
|
||||
],
|
||||
"version": "==0.1.7"
|
||||
"version": "==0.1.8"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
|
||||
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
||||
"sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
|
||||
"sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
"version": "==2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
from queue import Empty, Queue
|
||||
from typing import Iterable
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc.grpcutils import convert_value
|
||||
|
@ -11,15 +12,15 @@ from core.emulator.data import (
|
|||
LinkData,
|
||||
NodeData,
|
||||
)
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
||||
def handle_node_event(event):
|
||||
def handle_node_event(event: NodeData) -> core_pb2.NodeEvent:
|
||||
"""
|
||||
Handle node event when there is a node event
|
||||
|
||||
:param core.emulator.data.NodeData event: node data
|
||||
:param event: node data
|
||||
:return: node event that contains node id, name, model, position, and services
|
||||
:rtype: core.api.grpc.core_pb2.NodeEvent
|
||||
"""
|
||||
position = core_pb2.Position(x=event.x_position, y=event.y_position)
|
||||
services = event.services or ""
|
||||
|
@ -34,13 +35,12 @@ def handle_node_event(event):
|
|||
return core_pb2.NodeEvent(node=node_proto, source=event.source)
|
||||
|
||||
|
||||
def handle_link_event(event):
|
||||
def handle_link_event(event: LinkData) -> core_pb2.LinkEvent:
|
||||
"""
|
||||
Handle link event when there is a link event
|
||||
|
||||
:param core.emulator.data.LinkData event: link data
|
||||
:param event: link data
|
||||
:return: link event that has message type and link information
|
||||
:rtype: core.api.grpc.core_pb2.LinkEvent
|
||||
"""
|
||||
interface_one = None
|
||||
if event.interface1_id is not None:
|
||||
|
@ -90,13 +90,12 @@ def handle_link_event(event):
|
|||
return core_pb2.LinkEvent(message_type=event.message_type, link=link)
|
||||
|
||||
|
||||
def handle_session_event(event):
|
||||
def handle_session_event(event: EventData) -> core_pb2.SessionEvent:
|
||||
"""
|
||||
Handle session event when there is a session event
|
||||
|
||||
:param core.emulator.data.EventData event: event data
|
||||
:param event: event data
|
||||
:return: session event
|
||||
:rtype: core.api.grpc.core_pb2.SessionEvent
|
||||
"""
|
||||
event_time = event.time
|
||||
if event_time is not None:
|
||||
|
@ -110,13 +109,12 @@ def handle_session_event(event):
|
|||
)
|
||||
|
||||
|
||||
def handle_config_event(event):
|
||||
def handle_config_event(event: ConfigData) -> core_pb2.ConfigEvent:
|
||||
"""
|
||||
Handle configuration event when there is configuration event
|
||||
|
||||
:param core.emulator.data.ConfigData event: configuration data
|
||||
:param event: configuration data
|
||||
:return: configuration event
|
||||
:rtype: core.api.grpc.core_pb2.ConfigEvent
|
||||
"""
|
||||
return core_pb2.ConfigEvent(
|
||||
message_type=event.message_type,
|
||||
|
@ -135,13 +133,12 @@ def handle_config_event(event):
|
|||
)
|
||||
|
||||
|
||||
def handle_exception_event(event):
|
||||
def handle_exception_event(event: ExceptionData) -> core_pb2.ExceptionEvent:
|
||||
"""
|
||||
Handle exception event when there is exception event
|
||||
|
||||
:param core.emulator.data.ExceptionData event: exception data
|
||||
:param event: exception data
|
||||
:return: exception event
|
||||
:rtype: core.api.grpc.core_pb2.ExceptionEvent
|
||||
"""
|
||||
return core_pb2.ExceptionEvent(
|
||||
node_id=event.node,
|
||||
|
@ -153,13 +150,12 @@ def handle_exception_event(event):
|
|||
)
|
||||
|
||||
|
||||
def handle_file_event(event):
|
||||
def handle_file_event(event: FileData) -> core_pb2.FileEvent:
|
||||
"""
|
||||
Handle file event
|
||||
|
||||
:param core.emulator.data.FileData event: file data
|
||||
:param event: file data
|
||||
:return: file event
|
||||
:rtype: core.api.grpc.core_pb2.FileEvent
|
||||
"""
|
||||
return core_pb2.FileEvent(
|
||||
message_type=event.message_type,
|
||||
|
@ -179,19 +175,21 @@ class EventStreamer:
|
|||
Processes session events to generate grpc events.
|
||||
"""
|
||||
|
||||
def __init__(self, session, event_types):
|
||||
def __init__(
|
||||
self, session: Session, event_types: Iterable[core_pb2.EventType]
|
||||
) -> None:
|
||||
"""
|
||||
Create a EventStreamer instance.
|
||||
|
||||
:param core.emulator.session.Session session: session to process events for
|
||||
:param set event_types: types of events to process
|
||||
:param session: session to process events for
|
||||
:param event_types: types of events to process
|
||||
"""
|
||||
self.session = session
|
||||
self.event_types = event_types
|
||||
self.queue = Queue()
|
||||
self.add_handlers()
|
||||
|
||||
def add_handlers(self):
|
||||
def add_handlers(self) -> None:
|
||||
"""
|
||||
Add a session event handler for desired event types.
|
||||
|
||||
|
@ -210,12 +208,11 @@ class EventStreamer:
|
|||
if core_pb2.EventType.SESSION in self.event_types:
|
||||
self.session.event_handlers.append(self.queue.put)
|
||||
|
||||
def process(self):
|
||||
def process(self) -> core_pb2.Event:
|
||||
"""
|
||||
Process the next event in the queue.
|
||||
|
||||
:return: grpc event, or None when invalid event or queue timeout
|
||||
:rtype: core.api.grpc.core_pb2.Event
|
||||
"""
|
||||
event = core_pb2.Event(session_id=self.session.id)
|
||||
try:
|
||||
|
@ -239,7 +236,7 @@ class EventStreamer:
|
|||
event = None
|
||||
return event
|
||||
|
||||
def remove_handlers(self):
|
||||
def remove_handlers(self) -> None:
|
||||
"""
|
||||
Remove session event handlers for events being watched.
|
||||
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
import logging
|
||||
import time
|
||||
from typing import Any, Dict, List, Tuple, Type
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc import common_pb2, core_pb2
|
||||
from core.config import ConfigurableOptions
|
||||
from core.emulator.data import LinkData
|
||||
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
|
||||
from core.emulator.enumerations import LinkTypes, NodeTypes
|
||||
from core.nodes.base import CoreNetworkBase
|
||||
from core.nodes.ipaddress import MacAddress
|
||||
from core.emulator.session import Session
|
||||
from core.nodes.base import CoreNetworkBase, NodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
WORKERS = 10
|
||||
|
||||
|
||||
def add_node_data(node_proto):
|
||||
def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOptions]:
|
||||
"""
|
||||
Convert node protobuf message to data for creating a node.
|
||||
|
||||
:param core_pb2.Node node_proto: node proto message
|
||||
:param node_proto: node proto message
|
||||
:return: node type, id, and options
|
||||
:rtype: tuple
|
||||
"""
|
||||
_id = node_proto.id
|
||||
_type = node_proto.type
|
||||
|
@ -30,6 +36,7 @@ def add_node_data(node_proto):
|
|||
options.opaque = node_proto.opaque
|
||||
options.image = node_proto.image
|
||||
options.services = node_proto.services
|
||||
options.config_services = node_proto.config_services
|
||||
if node_proto.emane:
|
||||
options.emane = node_proto.emane
|
||||
if node_proto.server:
|
||||
|
@ -41,13 +48,12 @@ def add_node_data(node_proto):
|
|||
return _type, _id, options
|
||||
|
||||
|
||||
def link_interface(interface_proto):
|
||||
def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData:
|
||||
"""
|
||||
Create interface data from interface proto.
|
||||
|
||||
:param core_pb2.Interface interface_proto: interface proto
|
||||
:param interface_proto: interface proto
|
||||
:return: interface data
|
||||
:rtype: InterfaceData
|
||||
"""
|
||||
interface = None
|
||||
if interface_proto:
|
||||
|
@ -57,8 +63,6 @@ def link_interface(interface_proto):
|
|||
mac = interface_proto.mac
|
||||
if mac == "":
|
||||
mac = None
|
||||
else:
|
||||
mac = MacAddress.from_string(mac)
|
||||
interface = InterfaceData(
|
||||
_id=interface_proto.id,
|
||||
name=name,
|
||||
|
@ -71,13 +75,14 @@ def link_interface(interface_proto):
|
|||
return interface
|
||||
|
||||
|
||||
def add_link_data(link_proto):
|
||||
def add_link_data(
|
||||
link_proto: core_pb2.Link
|
||||
) -> Tuple[InterfaceData, InterfaceData, LinkOptions]:
|
||||
"""
|
||||
Convert link proto to link interfaces and options data.
|
||||
|
||||
:param core_pb2.Link link_proto: link proto
|
||||
:param link_proto: link proto
|
||||
:return: link interfaces and options
|
||||
:rtype: tuple
|
||||
"""
|
||||
interface_one = link_interface(link_proto.interface_one)
|
||||
interface_two = link_interface(link_proto.interface_two)
|
||||
|
@ -105,14 +110,15 @@ def add_link_data(link_proto):
|
|||
return interface_one, interface_two, options
|
||||
|
||||
|
||||
def create_nodes(session, node_protos):
|
||||
def create_nodes(
|
||||
session: Session, node_protos: List[core_pb2.Node]
|
||||
) -> Tuple[List[NodeBase], List[Exception]]:
|
||||
"""
|
||||
Create nodes using a thread pool and wait for completion.
|
||||
|
||||
:param core.emulator.session.Session session: session to create nodes in
|
||||
:param list[core_pb2.Node] node_protos: node proto messages
|
||||
:param session: session to create nodes in
|
||||
:param node_protos: node proto messages
|
||||
:return: results and exceptions for created nodes
|
||||
:rtype: tuple
|
||||
"""
|
||||
funcs = []
|
||||
for node_proto in node_protos:
|
||||
|
@ -126,14 +132,15 @@ def create_nodes(session, node_protos):
|
|||
return results, exceptions
|
||||
|
||||
|
||||
def create_links(session, link_protos):
|
||||
def create_links(
|
||||
session: Session, link_protos: List[core_pb2.Link]
|
||||
) -> Tuple[List[NodeBase], List[Exception]]:
|
||||
"""
|
||||
Create links using a thread pool and wait for completion.
|
||||
|
||||
:param core.emulator.session.Session session: session to create nodes in
|
||||
:param list[core_pb2.Link] link_protos: link proto messages
|
||||
:param session: session to create nodes in
|
||||
:param link_protos: link proto messages
|
||||
:return: results and exceptions for created links
|
||||
:rtype: tuple
|
||||
"""
|
||||
funcs = []
|
||||
for link_proto in link_protos:
|
||||
|
@ -149,14 +156,15 @@ def create_links(session, link_protos):
|
|||
return results, exceptions
|
||||
|
||||
|
||||
def edit_links(session, link_protos):
|
||||
def edit_links(
|
||||
session: Session, link_protos: List[core_pb2.Link]
|
||||
) -> Tuple[List[None], List[Exception]]:
|
||||
"""
|
||||
Edit links using a thread pool and wait for completion.
|
||||
|
||||
:param core.emulator.session.Session session: session to create nodes in
|
||||
:param list[core_pb2.Link] link_protos: link proto messages
|
||||
:param session: session to create nodes in
|
||||
:param link_protos: link proto messages
|
||||
:return: results and exceptions for created links
|
||||
:rtype: tuple
|
||||
"""
|
||||
funcs = []
|
||||
for link_proto in link_protos:
|
||||
|
@ -172,32 +180,32 @@ def edit_links(session, link_protos):
|
|||
return results, exceptions
|
||||
|
||||
|
||||
def convert_value(value):
|
||||
def convert_value(value: Any) -> str:
|
||||
"""
|
||||
Convert value into string.
|
||||
|
||||
:param value: value
|
||||
:return: string conversion of the value
|
||||
:rtype: str
|
||||
"""
|
||||
if value is not None:
|
||||
value = str(value)
|
||||
return value
|
||||
|
||||
|
||||
def get_config_options(config, configurable_options):
|
||||
def get_config_options(
|
||||
config: Dict[str, str], configurable_options: Type[ConfigurableOptions]
|
||||
) -> Dict[str, common_pb2.ConfigOption]:
|
||||
"""
|
||||
Retrieve configuration options in a form that is used by the grpc server.
|
||||
|
||||
:param dict config: configuration
|
||||
:param core.config.ConfigurableOptions configurable_options: configurable options
|
||||
:param config: configuration
|
||||
:param configurable_options: configurable options
|
||||
:return: mapping of configuration ids to configuration options
|
||||
:rtype: dict[str,core.api.grpc.core_pb2.ConfigOption]
|
||||
"""
|
||||
results = {}
|
||||
for configuration in configurable_options.configurations():
|
||||
value = config[configuration.id]
|
||||
config_option = core_pb2.ConfigOption(
|
||||
config_option = common_pb2.ConfigOption(
|
||||
label=configuration.label,
|
||||
name=configuration.id,
|
||||
value=value,
|
||||
|
@ -214,12 +222,12 @@ def get_config_options(config, configurable_options):
|
|||
return results
|
||||
|
||||
|
||||
def get_links(session, node):
|
||||
def get_links(session: Session, node: NodeBase):
|
||||
"""
|
||||
Retrieve a list of links for grpc to use
|
||||
|
||||
:param core.emulator.Session session: node's section
|
||||
:param core.nodes.base.CoreNode node: node to get links from
|
||||
:param session: node's section
|
||||
:param node: node to get links from
|
||||
:return: [core.api.grpc.core_pb2.Link]
|
||||
"""
|
||||
links = []
|
||||
|
@ -229,14 +237,13 @@ def get_links(session, node):
|
|||
return links
|
||||
|
||||
|
||||
def get_emane_model_id(node_id, interface_id):
|
||||
def get_emane_model_id(node_id: int, interface_id: int) -> int:
|
||||
"""
|
||||
Get EMANE model id
|
||||
|
||||
:param int node_id: node id
|
||||
:param int interface_id: interface id
|
||||
:param node_id: node id
|
||||
:param interface_id: interface id
|
||||
:return: EMANE model id
|
||||
:rtype: int
|
||||
"""
|
||||
if interface_id >= 0:
|
||||
return node_id * 1000 + interface_id
|
||||
|
@ -244,13 +251,12 @@ def get_emane_model_id(node_id, interface_id):
|
|||
return node_id
|
||||
|
||||
|
||||
def parse_emane_model_id(_id):
|
||||
def parse_emane_model_id(_id: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Parses EMANE model id to get true node id and interface id.
|
||||
|
||||
:param _id: id to parse
|
||||
:return: node id and interface id
|
||||
:rtype: tuple
|
||||
"""
|
||||
interface = -1
|
||||
node_id = _id
|
||||
|
@ -260,14 +266,13 @@ def parse_emane_model_id(_id):
|
|||
return node_id, interface
|
||||
|
||||
|
||||
def convert_link(session, link_data):
|
||||
def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link:
|
||||
"""
|
||||
Convert link_data into core protobuf Link
|
||||
|
||||
:param core.emulator.session.Session session:
|
||||
:param core.emulator.data.LinkData link_data:
|
||||
:param session:
|
||||
:param link_data:
|
||||
:return: core protobuf Link
|
||||
:rtype: core.api.grpc.core_pb2.Link
|
||||
"""
|
||||
interface_one = None
|
||||
if link_data.interface1_id is not None:
|
||||
|
@ -327,12 +332,11 @@ def convert_link(session, link_data):
|
|||
)
|
||||
|
||||
|
||||
def get_net_stats():
|
||||
def get_net_stats() -> Dict[str, Dict]:
|
||||
"""
|
||||
Retrieve status about the current interfaces in the system
|
||||
|
||||
:return: send and receive status of the interfaces in the system
|
||||
:rtype: dict
|
||||
"""
|
||||
with open("/proc/net/dev", "r") as f:
|
||||
data = f.readlines()[2:]
|
||||
|
@ -349,12 +353,12 @@ def get_net_stats():
|
|||
return stats
|
||||
|
||||
|
||||
def session_location(session, location):
|
||||
def session_location(session: Session, location: core_pb2.SessionLocation) -> None:
|
||||
"""
|
||||
Set session location based on location proto.
|
||||
|
||||
:param core.emulator.session.Session session: session for location
|
||||
:param core_pb2.SessionLocation location: location to set
|
||||
:param session: session for location
|
||||
:param location: location to set
|
||||
:return: nothing
|
||||
"""
|
||||
session.location.refxyz = (location.x, location.y, location.z)
|
||||
|
@ -362,28 +366,34 @@ def session_location(session, location):
|
|||
session.location.refscale = location.scale
|
||||
|
||||
|
||||
def service_configuration(session, config):
|
||||
def service_configuration(session: Session, config: core_pb2.ServiceConfig) -> None:
|
||||
"""
|
||||
Convenience method for setting a node service configuration.
|
||||
|
||||
:param core.emulator.session.Session session: session for service configuration
|
||||
:param core_pb2.ServiceConfig config: service configuration
|
||||
:param session: session for service configuration
|
||||
:param config: service configuration
|
||||
:return:
|
||||
"""
|
||||
session.services.set_service(config.node_id, config.service)
|
||||
service = session.services.get_service(config.node_id, config.service)
|
||||
service.startup = tuple(config.startup)
|
||||
service.validate = tuple(config.validate)
|
||||
service.shutdown = tuple(config.shutdown)
|
||||
if config.files:
|
||||
service.files = tuple(config.files)
|
||||
if config.directories:
|
||||
service.directories = tuple(config.directories)
|
||||
if config.startup:
|
||||
service.startup = tuple(config.startup)
|
||||
if config.validate:
|
||||
service.validate = tuple(config.validate)
|
||||
if config.shutdown:
|
||||
service.shutdown = tuple(config.shutdown)
|
||||
|
||||
|
||||
def get_service_configuration(service):
|
||||
def get_service_configuration(service: Type[CoreService]) -> core_pb2.NodeServiceData:
|
||||
"""
|
||||
Convenience for converting a service to service data proto.
|
||||
|
||||
:param service: service to get proto data for
|
||||
:return: service proto data
|
||||
:rtype: core.api.grpc.core_pb2.NodeServiceData
|
||||
"""
|
||||
return core_pb2.NodeServiceData(
|
||||
executables=service.executables,
|
||||
|
@ -397,3 +407,40 @@ def get_service_configuration(service):
|
|||
shutdown=service.shutdown,
|
||||
meta=service.meta,
|
||||
)
|
||||
|
||||
|
||||
def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface:
|
||||
"""
|
||||
Convenience for converting a core interface to the protobuf representation.
|
||||
:param interface: interface to convert
|
||||
:return: interface proto
|
||||
"""
|
||||
net_id = None
|
||||
if interface.net:
|
||||
net_id = interface.net.id
|
||||
ip4 = None
|
||||
ip4mask = None
|
||||
ip6 = None
|
||||
ip6mask = None
|
||||
for addr in interface.addrlist:
|
||||
network = netaddr.IPNetwork(addr)
|
||||
mask = network.prefixlen
|
||||
ip = str(network.ip)
|
||||
if netaddr.valid_ipv4(ip) and not ip4:
|
||||
ip4 = ip
|
||||
ip4mask = mask
|
||||
elif netaddr.valid_ipv6(ip) and not ip6:
|
||||
ip6 = ip
|
||||
ip6mask = mask
|
||||
return core_pb2.Interface(
|
||||
id=interface.netindex,
|
||||
netid=net_id,
|
||||
name=interface.name,
|
||||
mac=str(interface.hwaddr),
|
||||
mtu=interface.mtu,
|
||||
flowid=interface.flow_id,
|
||||
ip4=ip4,
|
||||
ip4mask=ip4mask,
|
||||
ip6=ip6,
|
||||
ip6mask=ip6mask,
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,10 +5,13 @@ types and objects used for parsing and building CORE API messages.
|
|||
CORE API messaging is leveraged for communication with the GUI.
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import socket
|
||||
import struct
|
||||
from enum import Enum
|
||||
|
||||
import netaddr
|
||||
|
||||
from core.api.tlv import structutils
|
||||
from core.emulator.enumerations import (
|
||||
ConfigTlvs,
|
||||
|
@ -24,7 +27,6 @@ from core.emulator.enumerations import (
|
|||
RegisterTlvs,
|
||||
SessionTlvs,
|
||||
)
|
||||
from core.nodes.ipaddress import IpAddress, MacAddress
|
||||
|
||||
|
||||
class CoreTlvData:
|
||||
|
@ -258,7 +260,7 @@ class CoreTlvDataIpv4Addr(CoreTlvDataObj):
|
|||
Utility class for packing/unpacking Ipv4 addresses.
|
||||
"""
|
||||
|
||||
data_type = IpAddress.from_string
|
||||
data_type = str
|
||||
data_format = "!2x4s"
|
||||
pad_len = 2
|
||||
|
||||
|
@ -267,21 +269,22 @@ class CoreTlvDataIpv4Addr(CoreTlvDataObj):
|
|||
"""
|
||||
Retrieve Ipv4 address value from object.
|
||||
|
||||
:param core.misc.ipaddress.IpAddress obj: ip address to get value from
|
||||
:return:
|
||||
:param str obj: ip address to get value from
|
||||
:return: packed address
|
||||
:rtype: bytes
|
||||
"""
|
||||
return obj.addr
|
||||
return socket.inet_pton(socket.AF_INET, obj)
|
||||
|
||||
@staticmethod
|
||||
def new_obj(value):
|
||||
"""
|
||||
Retrieve Ipv4 address from a string representation.
|
||||
|
||||
:param str value: value to get Ipv4 address from
|
||||
:param bytes value: value to get Ipv4 address from
|
||||
:return: Ipv4 address
|
||||
:rtype: core.nodes.ipaddress.IpAddress
|
||||
:rtype: str
|
||||
"""
|
||||
return IpAddress(af=socket.AF_INET, address=value)
|
||||
return socket.inet_ntop(socket.AF_INET, value)
|
||||
|
||||
|
||||
class CoreTlvDataIPv6Addr(CoreTlvDataObj):
|
||||
|
@ -290,7 +293,7 @@ class CoreTlvDataIPv6Addr(CoreTlvDataObj):
|
|||
"""
|
||||
|
||||
data_format = "!16s2x"
|
||||
data_type = IpAddress.from_string
|
||||
data_type = str
|
||||
pad_len = 2
|
||||
|
||||
@staticmethod
|
||||
|
@ -298,21 +301,22 @@ class CoreTlvDataIPv6Addr(CoreTlvDataObj):
|
|||
"""
|
||||
Retrieve Ipv6 address value from object.
|
||||
|
||||
:param core.nodes.ipaddress.IpAddress obj: ip address to get value from
|
||||
:return:
|
||||
:param str obj: ip address to get value from
|
||||
:return: packed address
|
||||
:rtype: bytes
|
||||
"""
|
||||
return obj.addr
|
||||
return socket.inet_pton(socket.AF_INET6, obj)
|
||||
|
||||
@staticmethod
|
||||
def new_obj(value):
|
||||
"""
|
||||
Retrieve Ipv6 address from a string representation.
|
||||
|
||||
:param str value: value to get Ipv4 address from
|
||||
:param bytes value: value to get Ipv4 address from
|
||||
:return: Ipv4 address
|
||||
:rtype: core.nodes.ipaddress.IpAddress
|
||||
:rtype: str
|
||||
"""
|
||||
return IpAddress(af=socket.AF_INET6, address=value)
|
||||
return socket.inet_ntop(socket.AF_INET6, value)
|
||||
|
||||
|
||||
class CoreTlvDataMacAddr(CoreTlvDataObj):
|
||||
|
@ -321,7 +325,7 @@ class CoreTlvDataMacAddr(CoreTlvDataObj):
|
|||
"""
|
||||
|
||||
data_format = "!2x8s"
|
||||
data_type = MacAddress.from_string
|
||||
data_type = str
|
||||
pad_len = 2
|
||||
|
||||
@staticmethod
|
||||
|
@ -329,23 +333,27 @@ class CoreTlvDataMacAddr(CoreTlvDataObj):
|
|||
"""
|
||||
Retrieve Ipv6 address value from object.
|
||||
|
||||
:param core.nodes.ipaddress.MacAddress obj: mac address to get value from
|
||||
:return:
|
||||
:param str obj: mac address to get value from
|
||||
:return: packed mac address
|
||||
:rtype: bytes
|
||||
"""
|
||||
# extend to 64 bits
|
||||
return b"\0\0" + obj.addr
|
||||
return b"\0\0" + netaddr.EUI(obj).packed
|
||||
|
||||
@staticmethod
|
||||
def new_obj(value):
|
||||
"""
|
||||
Retrieve mac address from a string representation.
|
||||
|
||||
:param str value: value to get Ipv4 address from
|
||||
:return: Ipv4 address
|
||||
:rtype: core.nodes.ipaddress.MacAddress
|
||||
:param bytes value: value to get Ipv4 address from
|
||||
:return: mac address
|
||||
:rtype: str
|
||||
"""
|
||||
# only use 48 bits
|
||||
return MacAddress(address=value[2:])
|
||||
value = binascii.hexlify(value[2:]).decode()
|
||||
mac = netaddr.EUI(value)
|
||||
mac.dialect = netaddr.mac_unix
|
||||
return str(mac)
|
||||
|
||||
|
||||
class CoreTlv:
|
||||
|
|
|
@ -527,6 +527,10 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
|||
"%s handling message:\n%s", threading.currentThread().getName(), message
|
||||
)
|
||||
|
||||
# provide to sdt, if enabled
|
||||
if self.session and self.session.sdt.is_enabled():
|
||||
self.session.sdt.handle_distributed(message)
|
||||
|
||||
if message.message_type not in self.message_handlers:
|
||||
logging.error("no handler for message type: %s", message.type_str())
|
||||
return
|
||||
|
|
|
@ -4,8 +4,110 @@ Common support for configurable CORE objects.
|
|||
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Union
|
||||
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import ConfigData
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.location.mobility import WirelessModel
|
||||
|
||||
WirelessModelType = Type[WirelessModel]
|
||||
|
||||
|
||||
class ConfigGroup:
|
||||
"""
|
||||
Defines configuration group tabs used for display by ConfigurationOptions.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, start: int, stop: int) -> None:
|
||||
"""
|
||||
Creates a ConfigGroup object.
|
||||
|
||||
:param name: configuration group display name
|
||||
:param start: configurations start index for this group
|
||||
:param stop: configurations stop index for this group
|
||||
"""
|
||||
self.name = name
|
||||
self.start = start
|
||||
self.stop = stop
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
Represents a configuration options.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
_id: str,
|
||||
_type: ConfigDataTypes,
|
||||
label: str = None,
|
||||
default: str = "",
|
||||
options: List[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Creates a Configuration object.
|
||||
|
||||
:param _id: unique name for configuration
|
||||
:param _type: configuration data type
|
||||
:param label: configuration label for display
|
||||
:param default: default value for configuration
|
||||
:param options: list options if this is a configuration with a combobox
|
||||
"""
|
||||
self.id = _id
|
||||
self.type = _type
|
||||
self.default = default
|
||||
if not options:
|
||||
options = []
|
||||
self.options = options
|
||||
if not label:
|
||||
label = _id
|
||||
self.label = label
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}(id={self.id}, type={self.type}, default={self.default}, options={self.options})"
|
||||
|
||||
|
||||
class ConfigurableOptions:
|
||||
"""
|
||||
Provides a base for defining configuration options within CORE.
|
||||
"""
|
||||
|
||||
name = None
|
||||
bitmap = None
|
||||
options = []
|
||||
|
||||
@classmethod
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
"""
|
||||
Provides the configurations for this class.
|
||||
|
||||
:return: configurations
|
||||
"""
|
||||
return cls.options
|
||||
|
||||
@classmethod
|
||||
def config_groups(cls) -> List[ConfigGroup]:
|
||||
"""
|
||||
Defines how configurations are grouped.
|
||||
|
||||
:return: configuration group definition
|
||||
"""
|
||||
return [ConfigGroup("Options", 1, len(cls.configurations()))]
|
||||
|
||||
@classmethod
|
||||
def default_values(cls) -> Dict[str, str]:
|
||||
"""
|
||||
Provides an ordered mapping of configuration keys to default values.
|
||||
|
||||
:return: ordered configuration mapping default values
|
||||
"""
|
||||
return OrderedDict(
|
||||
[(config.id, config.default) for config in cls.configurations()]
|
||||
)
|
||||
|
||||
|
||||
class ConfigShim:
|
||||
|
@ -14,13 +116,12 @@ class ConfigShim:
|
|||
"""
|
||||
|
||||
@classmethod
|
||||
def str_to_dict(cls, key_values):
|
||||
def str_to_dict(cls, key_values: str) -> Dict[str, str]:
|
||||
"""
|
||||
Converts a TLV key/value string into an ordered mapping.
|
||||
|
||||
:param str key_values:
|
||||
:param key_values:
|
||||
:return: ordered mapping of key/value pairs
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
key_values = key_values.split("|")
|
||||
values = OrderedDict()
|
||||
|
@ -30,13 +131,12 @@ class ConfigShim:
|
|||
return values
|
||||
|
||||
@classmethod
|
||||
def groups_to_str(cls, config_groups):
|
||||
def groups_to_str(cls, config_groups: List[ConfigGroup]) -> str:
|
||||
"""
|
||||
Converts configuration groups to a TLV formatted string.
|
||||
|
||||
:param list[ConfigGroup] config_groups: configuration groups to format
|
||||
:param config_groups: configuration groups to format
|
||||
:return: TLV configuration group string
|
||||
:rtype: str
|
||||
"""
|
||||
group_strings = []
|
||||
for config_group in config_groups:
|
||||
|
@ -47,19 +147,25 @@ class ConfigShim:
|
|||
return "|".join(group_strings)
|
||||
|
||||
@classmethod
|
||||
def config_data(cls, flags, node_id, type_flags, configurable_options, config):
|
||||
def config_data(
|
||||
cls,
|
||||
flags: int,
|
||||
node_id: int,
|
||||
type_flags: int,
|
||||
configurable_options: ConfigurableOptions,
|
||||
config: Dict[str, str],
|
||||
) -> ConfigData:
|
||||
"""
|
||||
Convert this class to a Config API message. Some TLVs are defined
|
||||
by the class, but node number, conf type flags, and values must
|
||||
be passed in.
|
||||
|
||||
:param int flags: message flags
|
||||
:param int node_id: node id
|
||||
:param int type_flags: type flags
|
||||
:param ConfigurableOptions configurable_options: options to create config data for
|
||||
:param dict config: configuration values for options
|
||||
:param flags: message flags
|
||||
:param node_id: node id
|
||||
:param type_flags: type flags
|
||||
:param configurable_options: options to create config data for
|
||||
:param config: configuration values for options
|
||||
:return: configuration data object
|
||||
:rtype: ConfigData
|
||||
"""
|
||||
key_values = None
|
||||
captions = None
|
||||
|
@ -102,63 +208,34 @@ class ConfigShim:
|
|||
)
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
Represents a configuration options.
|
||||
"""
|
||||
|
||||
def __init__(self, _id, _type, label=None, default="", options=None):
|
||||
"""
|
||||
Creates a Configuration object.
|
||||
|
||||
:param str _id: unique name for configuration
|
||||
:param core.enumerations.ConfigDataTypes _type: configuration data type
|
||||
:param str label: configuration label for display
|
||||
:param str default: default value for configuration
|
||||
:param list options: list options if this is a configuration with a combobox
|
||||
"""
|
||||
self.id = _id
|
||||
self.type = _type
|
||||
self.default = default
|
||||
if not options:
|
||||
options = []
|
||||
self.options = options
|
||||
if not label:
|
||||
label = _id
|
||||
self.label = label
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}(id={self.id}, type={self.type}, default={self.default}, options={self.options})"
|
||||
|
||||
|
||||
class ConfigurableManager:
|
||||
"""
|
||||
Provides convenience methods for storing and retrieving configuration options for nodes.
|
||||
Provides convenience methods for storing and retrieving configuration options for
|
||||
nodes.
|
||||
"""
|
||||
|
||||
_default_node = -1
|
||||
_default_type = _default_node
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Creates a ConfigurableManager object.
|
||||
"""
|
||||
self.node_configurations = {}
|
||||
|
||||
def nodes(self):
|
||||
def nodes(self) -> List[int]:
|
||||
"""
|
||||
Retrieves the ids of all node configurations known by this manager.
|
||||
|
||||
:return: list of node ids
|
||||
:rtype: list
|
||||
"""
|
||||
return [x for x in self.node_configurations if x != self._default_node]
|
||||
|
||||
def config_reset(self, node_id=None):
|
||||
def config_reset(self, node_id: int = None) -> None:
|
||||
"""
|
||||
Clears all configurations or configuration for a specific node.
|
||||
|
||||
:param int node_id: node id to clear configurations for, default is None and clears all configurations
|
||||
:param node_id: node id to clear configurations for, default is None and clears all configurations
|
||||
:return: nothing
|
||||
"""
|
||||
if not node_id:
|
||||
|
@ -166,27 +243,38 @@ class ConfigurableManager:
|
|||
elif node_id in self.node_configurations:
|
||||
self.node_configurations.pop(node_id)
|
||||
|
||||
def set_config(self, _id, value, node_id=_default_node, config_type=_default_type):
|
||||
def set_config(
|
||||
self,
|
||||
_id: str,
|
||||
value: str,
|
||||
node_id: int = _default_node,
|
||||
config_type: str = _default_type,
|
||||
) -> None:
|
||||
"""
|
||||
Set a specific configuration value for a node and configuration type.
|
||||
|
||||
:param str _id: configuration key
|
||||
:param str value: configuration value
|
||||
:param int node_id: node id to store configuration for
|
||||
:param str config_type: configuration type to store configuration for
|
||||
:param _id: configuration key
|
||||
:param value: configuration value
|
||||
:param node_id: node id to store configuration for
|
||||
:param config_type: configuration type to store configuration for
|
||||
:return: nothing
|
||||
"""
|
||||
node_configs = self.node_configurations.setdefault(node_id, OrderedDict())
|
||||
node_type_configs = node_configs.setdefault(config_type, OrderedDict())
|
||||
node_type_configs[_id] = value
|
||||
|
||||
def set_configs(self, config, node_id=_default_node, config_type=_default_type):
|
||||
def set_configs(
|
||||
self,
|
||||
config: Dict[str, str],
|
||||
node_id: int = _default_node,
|
||||
config_type: str = _default_type,
|
||||
) -> None:
|
||||
"""
|
||||
Set configurations for a node and configuration type.
|
||||
|
||||
:param dict config: configurations to set
|
||||
:param int node_id: node id to store configuration for
|
||||
:param str config_type: configuration type to store configuration for
|
||||
:param config: configurations to set
|
||||
:param node_id: node id to store configuration for
|
||||
:param config_type: configuration type to store configuration for
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug(
|
||||
|
@ -196,17 +284,20 @@ class ConfigurableManager:
|
|||
node_configs[config_type] = config
|
||||
|
||||
def get_config(
|
||||
self, _id, node_id=_default_node, config_type=_default_type, default=None
|
||||
):
|
||||
self,
|
||||
_id: str,
|
||||
node_id: int = _default_node,
|
||||
config_type: str = _default_type,
|
||||
default: str = None,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieves a specific configuration for a node and configuration type.
|
||||
|
||||
:param str _id: specific configuration to retrieve
|
||||
:param int node_id: node id to store configuration for
|
||||
:param str config_type: configuration type to store configuration for
|
||||
:param _id: specific configuration to retrieve
|
||||
:param node_id: node id to store configuration for
|
||||
:param config_type: configuration type to store configuration for
|
||||
:param default: default value to return when value is not found
|
||||
:return: configuration value
|
||||
:rtype str
|
||||
"""
|
||||
result = default
|
||||
node_type_configs = self.get_configs(node_id, config_type)
|
||||
|
@ -214,14 +305,15 @@ class ConfigurableManager:
|
|||
result = node_type_configs.get(_id, default)
|
||||
return result
|
||||
|
||||
def get_configs(self, node_id=_default_node, config_type=_default_type):
|
||||
def get_configs(
|
||||
self, node_id: int = _default_node, config_type: str = _default_type
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Retrieve configurations for a node and configuration type.
|
||||
|
||||
:param int node_id: node id to store configuration for
|
||||
:param str config_type: configuration type to store configuration for
|
||||
:param node_id: node id to store configuration for
|
||||
:param config_type: configuration type to store configuration for
|
||||
:return: configurations
|
||||
:rtype: dict
|
||||
"""
|
||||
result = None
|
||||
node_configs = self.node_configurations.get(node_id)
|
||||
|
@ -229,83 +321,22 @@ class ConfigurableManager:
|
|||
result = node_configs.get(config_type)
|
||||
return result
|
||||
|
||||
def get_all_configs(self, node_id=_default_node):
|
||||
def get_all_configs(self, node_id: int = _default_node) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Retrieve all current configuration types for a node.
|
||||
|
||||
:param int node_id: node id to retrieve configurations for
|
||||
:param node_id: node id to retrieve configurations for
|
||||
:return: all configuration types for a node
|
||||
:rtype: dict
|
||||
"""
|
||||
return self.node_configurations.get(node_id)
|
||||
|
||||
|
||||
class ConfigGroup:
|
||||
"""
|
||||
Defines configuration group tabs used for display by ConfigurationOptions.
|
||||
"""
|
||||
|
||||
def __init__(self, name, start, stop):
|
||||
"""
|
||||
Creates a ConfigGroup object.
|
||||
|
||||
:param str name: configuration group display name
|
||||
:param int start: configurations start index for this group
|
||||
:param int stop: configurations stop index for this group
|
||||
"""
|
||||
self.name = name
|
||||
self.start = start
|
||||
self.stop = stop
|
||||
|
||||
|
||||
class ConfigurableOptions:
|
||||
"""
|
||||
Provides a base for defining configuration options within CORE.
|
||||
"""
|
||||
|
||||
name = None
|
||||
bitmap = None
|
||||
options = []
|
||||
|
||||
@classmethod
|
||||
def configurations(cls):
|
||||
"""
|
||||
Provides the configurations for this class.
|
||||
|
||||
:return: configurations
|
||||
:rtype: list[Configuration]
|
||||
"""
|
||||
return cls.options
|
||||
|
||||
@classmethod
|
||||
def config_groups(cls):
|
||||
"""
|
||||
Defines how configurations are grouped.
|
||||
|
||||
:return: configuration group definition
|
||||
:rtype: list[ConfigGroup]
|
||||
"""
|
||||
return [ConfigGroup("Options", 1, len(cls.configurations()))]
|
||||
|
||||
@classmethod
|
||||
def default_values(cls):
|
||||
"""
|
||||
Provides an ordered mapping of configuration keys to default values.
|
||||
|
||||
:return: ordered configuration mapping default values
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
return OrderedDict(
|
||||
[(config.id, config.default) for config in cls.configurations()]
|
||||
)
|
||||
|
||||
|
||||
class ModelManager(ConfigurableManager):
|
||||
"""
|
||||
Helps handle setting models for nodes and managing their model configurations.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Creates a ModelManager object.
|
||||
"""
|
||||
|
@ -313,13 +344,15 @@ class ModelManager(ConfigurableManager):
|
|||
self.models = {}
|
||||
self.node_models = {}
|
||||
|
||||
def set_model_config(self, node_id, model_name, config=None):
|
||||
def set_model_config(
|
||||
self, node_id: int, model_name: str, config: Dict[str, str] = None
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration data for a model.
|
||||
|
||||
:param int node_id: node id to set model configuration for
|
||||
:param str model_name: model to set configuration for
|
||||
:param dict config: configuration data to set for model
|
||||
:param node_id: node id to set model configuration for
|
||||
:param model_name: model to set configuration for
|
||||
:param config: configuration data to set for model
|
||||
:return: nothing
|
||||
"""
|
||||
# get model class to configure
|
||||
|
@ -341,14 +374,13 @@ class ModelManager(ConfigurableManager):
|
|||
# set configuration
|
||||
self.set_configs(model_config, node_id=node_id, config_type=model_name)
|
||||
|
||||
def get_model_config(self, node_id, model_name):
|
||||
def get_model_config(self, node_id: int, model_name: str) -> Dict[str, str]:
|
||||
"""
|
||||
Retrieve configuration data for a model.
|
||||
|
||||
:param int node_id: node id to set model configuration for
|
||||
:param str model_name: model to set configuration for
|
||||
:param node_id: node id to set model configuration for
|
||||
:param model_name: model to set configuration for
|
||||
:return: current model configuration for node
|
||||
:rtype: dict
|
||||
"""
|
||||
# get model class to configure
|
||||
model_class = self.models.get(model_name)
|
||||
|
@ -363,13 +395,18 @@ class ModelManager(ConfigurableManager):
|
|||
|
||||
return config
|
||||
|
||||
def set_model(self, node, model_class, config=None):
|
||||
def set_model(
|
||||
self,
|
||||
node: Union[WlanNode, EmaneNet],
|
||||
model_class: "WirelessModelType",
|
||||
config: Dict[str, str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set model and model configuration for node.
|
||||
|
||||
:param node: node to set model for
|
||||
:param model_class: model class to set for node
|
||||
:param dict config: model configuration, None for default configuration
|
||||
:param config: model configuration, None for default configuration
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug(
|
||||
|
@ -379,14 +416,15 @@ class ModelManager(ConfigurableManager):
|
|||
config = self.get_model_config(node.id, model_class.name)
|
||||
node.setmodel(model_class, config)
|
||||
|
||||
def get_models(self, node):
|
||||
def get_models(
|
||||
self, node: Union[WlanNode, EmaneNet]
|
||||
) -> List[Tuple[Type, Dict[str, str]]]:
|
||||
"""
|
||||
Return a list of model classes and values for a net if one has been
|
||||
configured. This is invoked when exporting a session to XML.
|
||||
|
||||
:param node: network node to get models for
|
||||
:return: list of model and values tuples for the network node
|
||||
:rtype: list
|
||||
"""
|
||||
all_configs = self.get_all_configs(node.id)
|
||||
if not all_configs:
|
||||
|
|
0
daemon/core/configservice/__init__.py
Normal file
0
daemon/core/configservice/__init__.py
Normal file
395
daemon/core/configservice/base.py
Normal file
395
daemon/core/configservice/base.py
Normal file
|
@ -0,0 +1,395 @@
|
|||
import abc
|
||||
import enum
|
||||
import inspect
|
||||
import logging
|
||||
import pathlib
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from mako import exceptions
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako.template import Template
|
||||
|
||||
from core.config import Configuration
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
|
||||
TEMPLATES_DIR = "templates"
|
||||
|
||||
|
||||
class ConfigServiceMode(enum.Enum):
|
||||
BLOCKING = 0
|
||||
NON_BLOCKING = 1
|
||||
TIMER = 2
|
||||
|
||||
|
||||
class ConfigServiceBootError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigService(abc.ABC):
|
||||
"""
|
||||
Base class for creating configurable services.
|
||||
"""
|
||||
|
||||
# validation period in seconds, how frequent validation is attempted
|
||||
validation_period = 0.5
|
||||
|
||||
# time to wait in seconds for determining if service started successfully
|
||||
validation_timer = 5
|
||||
|
||||
def __init__(self, node: CoreNode) -> None:
|
||||
"""
|
||||
Create ConfigService instance.
|
||||
|
||||
:param node: node this service is assigned to
|
||||
"""
|
||||
self.node = node
|
||||
class_file = inspect.getfile(self.__class__)
|
||||
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
|
||||
self.templates = TemplateLookup(directories=templates_path)
|
||||
self.config = {}
|
||||
self.custom_templates = {}
|
||||
self.custom_config = {}
|
||||
configs = self.default_configs[:]
|
||||
self._define_config(configs)
|
||||
|
||||
@staticmethod
|
||||
def clean_text(text: str) -> str:
|
||||
"""
|
||||
Returns space stripped text for string literals, while keeping space
|
||||
indentations.
|
||||
|
||||
:param text: text to clean
|
||||
:return: cleaned text
|
||||
"""
|
||||
return inspect.cleandoc(text)
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def group(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def directories(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def files(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def default_configs(self) -> List[Configuration]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def modes(self) -> Dict[str, Dict[str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def executables(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def dependencies(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def startup(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def validate(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def shutdown(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def validation_mode(self) -> ConfigServiceMode:
|
||||
raise NotImplementedError
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
Creates services files/directories, runs startup, and validates based on
|
||||
validation mode.
|
||||
|
||||
:return: nothing
|
||||
:raises ConfigServiceBootError: when there is an error starting service
|
||||
"""
|
||||
logging.info("node(%s) service(%s) starting...", self.node.name, self.name)
|
||||
self.create_dirs()
|
||||
self.create_files()
|
||||
wait = self.validation_mode == ConfigServiceMode.BLOCKING
|
||||
self.run_startup(wait)
|
||||
if not wait:
|
||||
if self.validation_mode == ConfigServiceMode.TIMER:
|
||||
self.wait_validation()
|
||||
else:
|
||||
self.run_validation()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
Stop service using shutdown commands.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
for cmd in self.shutdown:
|
||||
try:
|
||||
self.node.cmd(cmd)
|
||||
except CoreCommandError:
|
||||
logging.exception(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"failed shutdown: {cmd}"
|
||||
)
|
||||
|
||||
def restart(self) -> None:
|
||||
"""
|
||||
Restarts service by running stop and then start.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def create_dirs(self) -> None:
|
||||
"""
|
||||
Creates directories for service.
|
||||
|
||||
:return: nothing
|
||||
:raises CoreError: when there is a failure creating a directory
|
||||
"""
|
||||
for directory in self.directories:
|
||||
try:
|
||||
self.node.privatedir(directory)
|
||||
except (CoreCommandError, ValueError):
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"failure to create service directory: {directory}"
|
||||
)
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Returns key/value data, used when rendering file templates.
|
||||
|
||||
:return: key/value template data
|
||||
"""
|
||||
return {}
|
||||
|
||||
def set_template(self, name: str, template: str) -> None:
|
||||
"""
|
||||
Store custom template to render for a given file.
|
||||
|
||||
:param name: file to store custom template for
|
||||
:param template: custom template to render
|
||||
:return: nothing
|
||||
"""
|
||||
self.custom_templates[name] = template
|
||||
|
||||
def get_text_template(self, name: str) -> str:
|
||||
"""
|
||||
Retrieves text based template for files that do not have a file based template.
|
||||
|
||||
:param name: name of file to get template for
|
||||
:return: template to render
|
||||
"""
|
||||
raise CoreError(f"service({self.name}) unknown template({name})")
|
||||
|
||||
def get_templates(self) -> Dict[str, str]:
|
||||
"""
|
||||
Retrieves mapping of file names to templates for all cases, which
|
||||
includes custom templates, file templates, and text templates.
|
||||
|
||||
:return: mapping of files to templates
|
||||
"""
|
||||
templates = {}
|
||||
for name in self.files:
|
||||
basename = pathlib.Path(name).name
|
||||
if name in self.custom_templates:
|
||||
template = self.custom_templates[name]
|
||||
template = self.clean_text(template)
|
||||
elif self.templates.has_template(basename):
|
||||
template = self.templates.get_template(basename).source
|
||||
else:
|
||||
template = self.get_text_template(name)
|
||||
template = self.clean_text(template)
|
||||
templates[name] = template
|
||||
return templates
|
||||
|
||||
def create_files(self) -> None:
|
||||
"""
|
||||
Creates service files inside associated node.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
data = self.data()
|
||||
for name in self.files:
|
||||
basename = pathlib.Path(name).name
|
||||
if name in self.custom_templates:
|
||||
text = self.custom_templates[name]
|
||||
rendered = self.render_text(text, data)
|
||||
elif self.templates.has_template(basename):
|
||||
rendered = self.render_template(basename, data)
|
||||
else:
|
||||
text = self.get_text_template(name)
|
||||
rendered = self.render_text(text, data)
|
||||
logging.debug(
|
||||
"node(%s) service(%s) template(%s): \n%s",
|
||||
self.node.name,
|
||||
self.name,
|
||||
name,
|
||||
rendered,
|
||||
)
|
||||
self.node.nodefile(name, rendered)
|
||||
|
||||
def run_startup(self, wait: bool) -> None:
|
||||
"""
|
||||
Run startup commands for service on node.
|
||||
|
||||
:param wait: wait successful command exit status when True, ignore status
|
||||
otherwise
|
||||
:return: nothing
|
||||
:raises ConfigServiceBootError: when a command that waits fails
|
||||
"""
|
||||
for cmd in self.startup:
|
||||
try:
|
||||
self.node.cmd(cmd, wait=wait)
|
||||
except CoreCommandError as e:
|
||||
raise ConfigServiceBootError(
|
||||
f"node({self.node.name}) service({self.name}) failed startup: {e}"
|
||||
)
|
||||
|
||||
def wait_validation(self) -> None:
|
||||
"""
|
||||
Waits for a period of time to consider service started successfully.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
time.sleep(self.validation_timer)
|
||||
|
||||
def run_validation(self) -> None:
|
||||
"""
|
||||
Runs validation commands for service on node.
|
||||
|
||||
:return: nothing
|
||||
:raises ConfigServiceBootError: if there is a validation failure
|
||||
"""
|
||||
start = time.monotonic()
|
||||
cmds = self.validate[:]
|
||||
index = 0
|
||||
while cmds:
|
||||
cmd = cmds[index]
|
||||
try:
|
||||
self.node.cmd(cmd)
|
||||
del cmds[index]
|
||||
index += 1
|
||||
except CoreCommandError:
|
||||
logging.debug(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"validate command failed: {cmd}"
|
||||
)
|
||||
time.sleep(self.validation_period)
|
||||
|
||||
if cmds and time.monotonic() - start > self.validation_timer:
|
||||
raise ConfigServiceBootError(
|
||||
f"node({self.node.name}) service({self.name}) failed to validate"
|
||||
)
|
||||
|
||||
def _render(self, template: Template, data: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders template providing all associated data to template.
|
||||
|
||||
:param template: template to render
|
||||
:param data: service specific defined data for template
|
||||
:return: rendered template
|
||||
"""
|
||||
if data is None:
|
||||
data = {}
|
||||
return template.render_unicode(
|
||||
node=self.node, config=self.render_config(), **data
|
||||
)
|
||||
|
||||
def render_text(self, text: str, data: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders text based template providing all associated data to template.
|
||||
|
||||
:param text: text to render
|
||||
:param data: service specific defined data for template
|
||||
:return: rendered template
|
||||
"""
|
||||
text = self.clean_text(text)
|
||||
try:
|
||||
template = Template(text)
|
||||
return self._render(template, data)
|
||||
except Exception:
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"{exceptions.text_error_template().render_unicode()}"
|
||||
)
|
||||
|
||||
def render_template(self, basename: str, data: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders file based template providing all associated data to template.
|
||||
|
||||
:param basename: base name for file to render
|
||||
:param data: service specific defined data for template
|
||||
:return: rendered template
|
||||
"""
|
||||
try:
|
||||
template = self.templates.get_template(basename)
|
||||
return self._render(template, data)
|
||||
except Exception:
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"{exceptions.text_error_template().render_template()}"
|
||||
)
|
||||
|
||||
def _define_config(self, configs: List[Configuration]) -> None:
|
||||
"""
|
||||
Initializes default configuration data.
|
||||
|
||||
:param configs: configs to initialize
|
||||
:return: nothing
|
||||
"""
|
||||
for config in configs:
|
||||
self.config[config.id] = config
|
||||
|
||||
def render_config(self) -> Dict[str, str]:
|
||||
"""
|
||||
Returns configuration data key/value pairs for rendering a template.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if self.custom_config:
|
||||
return self.custom_config
|
||||
else:
|
||||
return {k: v.default for k, v in self.config.items()}
|
||||
|
||||
def set_config(self, data: Dict[str, str]) -> None:
|
||||
"""
|
||||
Set configuration data from key/value pairs.
|
||||
|
||||
:param data: configuration key/values to set
|
||||
:return: nothing
|
||||
:raise CoreError: when an unknown configuration value is given
|
||||
"""
|
||||
for key, value in data.items():
|
||||
if key not in self.config:
|
||||
raise CoreError(f"unknown config: {key}")
|
||||
self.custom_config[key] = value
|
123
daemon/core/configservice/dependencies.py
Normal file
123
daemon/core/configservice/dependencies.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.configservice.base import ConfigService
|
||||
|
||||
|
||||
class ConfigServiceDependencies:
|
||||
"""
|
||||
Generates sets of services to start in order of their dependencies.
|
||||
"""
|
||||
|
||||
def __init__(self, services: Dict[str, "ConfigService"]) -> None:
|
||||
"""
|
||||
Create a ConfigServiceDependencies instance.
|
||||
|
||||
:param services: services for determining dependency sets
|
||||
"""
|
||||
# helpers to check validity
|
||||
self.dependents = {}
|
||||
self.started = set()
|
||||
self.node_services = {}
|
||||
for service in services.values():
|
||||
self.node_services[service.name] = service
|
||||
for dependency in service.dependencies:
|
||||
dependents = self.dependents.setdefault(dependency, set())
|
||||
dependents.add(service.name)
|
||||
|
||||
# used to find paths
|
||||
self.path = []
|
||||
self.visited = set()
|
||||
self.visiting = set()
|
||||
|
||||
def startup_paths(self) -> List[List["ConfigService"]]:
|
||||
"""
|
||||
Find startup path sets based on service dependencies.
|
||||
|
||||
:return: lists of lists of services that can be started in parallel
|
||||
"""
|
||||
paths = []
|
||||
for name in self.node_services:
|
||||
service = self.node_services[name]
|
||||
if service.name in self.started:
|
||||
logging.debug(
|
||||
"skipping service that will already be started: %s", service.name
|
||||
)
|
||||
continue
|
||||
|
||||
path = self._start(service)
|
||||
if path:
|
||||
paths.append(path)
|
||||
|
||||
if self.started != set(self.node_services):
|
||||
raise ValueError(
|
||||
"failure to start all services: %s != %s"
|
||||
% (self.started, self.node_services.keys())
|
||||
)
|
||||
|
||||
return paths
|
||||
|
||||
def _reset(self) -> None:
|
||||
"""
|
||||
Clear out metadata used for finding service dependency sets.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.path = []
|
||||
self.visited.clear()
|
||||
self.visiting.clear()
|
||||
|
||||
def _start(self, service: "ConfigService") -> List["ConfigService"]:
|
||||
"""
|
||||
Starts a oath for checking dependencies for a given service.
|
||||
|
||||
:param service: service to check dependencies for
|
||||
:return: list of config services to start in order
|
||||
"""
|
||||
logging.debug("starting service dependency check: %s", service.name)
|
||||
self._reset()
|
||||
return self._visit(service)
|
||||
|
||||
def _visit(self, current_service: "ConfigService") -> List["ConfigService"]:
|
||||
"""
|
||||
Visits a service when discovering dependency chains for service.
|
||||
|
||||
:param current_service: service being visited
|
||||
:return: list of dependent services for a visited service
|
||||
"""
|
||||
logging.debug("visiting service(%s): %s", current_service.name, self.path)
|
||||
self.visited.add(current_service.name)
|
||||
self.visiting.add(current_service.name)
|
||||
|
||||
# dive down
|
||||
for service_name in current_service.dependencies:
|
||||
if service_name not in self.node_services:
|
||||
raise ValueError(
|
||||
"required dependency was not included in node services: %s"
|
||||
% service_name
|
||||
)
|
||||
|
||||
if service_name in self.visiting:
|
||||
raise ValueError(
|
||||
"cyclic dependency at service(%s): %s"
|
||||
% (current_service.name, service_name)
|
||||
)
|
||||
|
||||
if service_name not in self.visited:
|
||||
service = self.node_services[service_name]
|
||||
self._visit(service)
|
||||
|
||||
# add service when bottom is found
|
||||
logging.debug("adding service to startup path: %s", current_service.name)
|
||||
self.started.add(current_service.name)
|
||||
self.path.append(current_service)
|
||||
self.visiting.remove(current_service.name)
|
||||
|
||||
# rise back up
|
||||
for service_name in self.dependents.get(current_service.name, []):
|
||||
if service_name not in self.visited:
|
||||
service = self.node_services[service_name]
|
||||
self._visit(service)
|
||||
|
||||
return self.path
|
82
daemon/core/configservice/manager.py
Normal file
82
daemon/core/configservice/manager.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
import logging
|
||||
import pathlib
|
||||
from typing import List, Type
|
||||
|
||||
from core import utils
|
||||
from core.configservice.base import ConfigService
|
||||
from core.errors import CoreError
|
||||
|
||||
|
||||
class ConfigServiceManager:
|
||||
"""
|
||||
Manager for configurable services.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Create a ConfigServiceManager instance.
|
||||
"""
|
||||
self.services = {}
|
||||
|
||||
def get_service(self, name: str) -> Type[ConfigService]:
|
||||
"""
|
||||
Retrieve a service by name.
|
||||
|
||||
:param name: name of service
|
||||
:return: service class
|
||||
:raises CoreError: when service is not found
|
||||
"""
|
||||
service_class = self.services.get(name)
|
||||
if service_class is None:
|
||||
raise CoreError(f"service does not exit {name}")
|
||||
return service_class
|
||||
|
||||
def add(self, service: ConfigService) -> None:
|
||||
"""
|
||||
Add service to manager, checking service requirements have been met.
|
||||
|
||||
:param service: service to add to manager
|
||||
:return: nothing
|
||||
:raises CoreError: when service is a duplicate or has unmet executables
|
||||
"""
|
||||
name = service.name
|
||||
logging.debug("loading service: class(%s) name(%s)", service.__class__, name)
|
||||
|
||||
# avoid duplicate services
|
||||
if name in self.services:
|
||||
raise CoreError(f"duplicate service being added: {name}")
|
||||
|
||||
# validate dependent executables are present
|
||||
for executable in service.executables:
|
||||
try:
|
||||
utils.which(executable, required=True)
|
||||
except ValueError:
|
||||
raise CoreError(
|
||||
f"service({service.name}) missing executable {executable}"
|
||||
)
|
||||
|
||||
# make service available
|
||||
self.services[name] = service
|
||||
|
||||
def load(self, path: str) -> List[str]:
|
||||
"""
|
||||
Search path provided for configurable services and add them for being managed.
|
||||
|
||||
:param path: path to search configurable services
|
||||
:return: list errors when loading and adding services
|
||||
"""
|
||||
path = pathlib.Path(path)
|
||||
subdirs = [x for x in path.iterdir() if x.is_dir()]
|
||||
subdirs.append(path)
|
||||
service_errors = []
|
||||
for subdir in subdirs:
|
||||
logging.debug("loading config services from: %s", subdir)
|
||||
services = utils.load_classes(str(subdir), ConfigService)
|
||||
for service in services:
|
||||
logging.debug("found service: %s", service)
|
||||
try:
|
||||
self.add(service)
|
||||
except CoreError as e:
|
||||
service_errors.append(service.name)
|
||||
logging.debug("not loading service(%s): %s", service.name, e)
|
||||
return service_errors
|
0
daemon/core/configservices/__init__.py
Normal file
0
daemon/core/configservices/__init__.py
Normal file
0
daemon/core/configservices/frrservices/__init__.py
Normal file
0
daemon/core/configservices/frrservices/__init__.py
Normal file
391
daemon/core/configservices/frrservices/services.py
Normal file
391
daemon/core/configservices/frrservices/services.py
Normal file
|
@ -0,0 +1,391 @@
|
|||
import abc
|
||||
from typing import Any, Dict
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import constants
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.nodes.base import CoreNodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
GROUP = "FRR"
|
||||
|
||||
|
||||
def has_mtu_mismatch(ifc: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect MTU mismatch and add the appropriate FRR
|
||||
mtu-ignore command. This is needed when e.g. a node is linked via a
|
||||
GreTap device.
|
||||
"""
|
||||
if ifc.mtu != 1500:
|
||||
return True
|
||||
if not ifc.net:
|
||||
return False
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu != ifc.mtu:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_min_mtu(ifc):
|
||||
"""
|
||||
Helper to discover the minimum MTU of interfaces linked with the
|
||||
given interface.
|
||||
"""
|
||||
mtu = ifc.mtu
|
||||
if not ifc.net:
|
||||
return mtu
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu < mtu:
|
||||
mtu = i.mtu
|
||||
return mtu
|
||||
|
||||
|
||||
def get_router_id(node: CoreNodeBase) -> str:
|
||||
"""
|
||||
Helper to return the first IPv4 address of a node as its router ID.
|
||||
"""
|
||||
for ifc in node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
return a
|
||||
return "0.0.0.0"
|
||||
|
||||
|
||||
class FRRZebra(ConfigService):
|
||||
name = "FRRzebra"
|
||||
group = GROUP
|
||||
directories = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
|
||||
files = [
|
||||
"/usr/local/etc/frr/frr.conf",
|
||||
"frrboot.sh",
|
||||
"/usr/local/etc/frr/vtysh.conf",
|
||||
"/usr/local/etc/frr/daemons",
|
||||
]
|
||||
executables = ["zebra"]
|
||||
dependencies = []
|
||||
startup = ["sh frrboot.sh zebra"]
|
||||
validate = ["pidof zebra"]
|
||||
shutdown = ["killall zebra"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
frr_conf = self.files[0]
|
||||
frr_bin_search = self.node.session.options.get_config(
|
||||
"frr_bin_search", default="/usr/local/bin /usr/bin /usr/lib/frr"
|
||||
).strip('"')
|
||||
frr_sbin_search = self.node.session.options.get_config(
|
||||
"frr_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/frr"
|
||||
).strip('"')
|
||||
|
||||
services = []
|
||||
want_ip4 = False
|
||||
want_ip6 = False
|
||||
for service in self.node.config_services.values():
|
||||
if self.name not in service.dependencies:
|
||||
continue
|
||||
if service.ipv4_routing:
|
||||
want_ip4 = True
|
||||
if service.ipv6_routing:
|
||||
want_ip6 = True
|
||||
services.append(service)
|
||||
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
ip4s = []
|
||||
ip6s = []
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
ip4s.append(x)
|
||||
else:
|
||||
ip6s.append(x)
|
||||
is_control = getattr(ifc, "control", False)
|
||||
interfaces.append((ifc, ip4s, ip6s, is_control))
|
||||
|
||||
return dict(
|
||||
frr_conf=frr_conf,
|
||||
frr_sbin_search=frr_sbin_search,
|
||||
frr_bin_search=frr_bin_search,
|
||||
frr_state_dir=constants.FRR_STATE_DIR,
|
||||
interfaces=interfaces,
|
||||
want_ip4=want_ip4,
|
||||
want_ip6=want_ip6,
|
||||
services=services,
|
||||
)
|
||||
|
||||
|
||||
class FrrService(abc.ABC):
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = []
|
||||
executables = []
|
||||
dependencies = ["FRRzebra"]
|
||||
startup = []
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
ipv4_routing = False
|
||||
ipv6_routing = False
|
||||
|
||||
@abc.abstractmethod
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def frr_config(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class FRROspfv2(FrrService, ConfigService):
|
||||
"""
|
||||
The OSPFv2 service provides IPv4 routing for wired networks. It does
|
||||
not build its own configuration file but has hooks for adding to the
|
||||
unified frr.conf file.
|
||||
"""
|
||||
|
||||
name = "FRROSPFv2"
|
||||
startup = ()
|
||||
shutdown = ["killall ospfd"]
|
||||
validate = ["pidof ospfd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
addresses = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
addr = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
addresses.append(a)
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
router-id ${router_id}
|
||||
% for addr in addresses:
|
||||
network ${addr} area 0
|
||||
% endfor
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if has_mtu_mismatch(ifc):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
class FRROspfv3(FrrService, ConfigService):
|
||||
"""
|
||||
The OSPFv3 service provides IPv6 routing for wired networks. It does
|
||||
not build its own configuration file but has hooks for adding to the
|
||||
unified frr.conf file.
|
||||
"""
|
||||
|
||||
name = "FRROSPFv3"
|
||||
shutdown = ["killall ospf6d"]
|
||||
validate = ["pidof ospf6d"]
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
data = dict(router_id=router_id, ifnames=ifnames)
|
||||
text = """
|
||||
router ospf6
|
||||
router-id ${router_id}
|
||||
% for ifname in ifnames:
|
||||
interface ${ifname} area 0.0.0.0
|
||||
% endfor
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
mtu = get_min_mtu(ifc)
|
||||
if mtu < ifc.mtu:
|
||||
return f"ipv6 ospf6 ifmtu {mtu}"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
class FRRBgp(FrrService, ConfigService):
|
||||
"""
|
||||
The BGP service provides interdomain routing.
|
||||
Peers must be manually configured, with a full mesh for those
|
||||
having the same AS number.
|
||||
"""
|
||||
|
||||
name = "FRRBGP"
|
||||
shutdown = ["killall bgpd"]
|
||||
validate = ["pidof bgpd"]
|
||||
custom_needed = True
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
text = f"""
|
||||
! BGP configuration
|
||||
! You should configure the AS number below
|
||||
! along with this router's peers.
|
||||
router bgp {self.node.id}
|
||||
bgp router-id {router_id}
|
||||
redistribute connected
|
||||
!neighbor 1.2.3.4 remote-as 555
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class FRRRip(FrrService, ConfigService):
|
||||
"""
|
||||
The RIP service provides IPv4 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "FRRRIP"
|
||||
shutdown = ["killall ripd"]
|
||||
validate = ["pidof ripd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
text = """
|
||||
router rip
|
||||
redistribute static
|
||||
redistribute connected
|
||||
redistribute ospf
|
||||
network 0.0.0.0/0
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class FRRRipng(FrrService, ConfigService):
|
||||
"""
|
||||
The RIP NG service provides IPv6 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "FRRRIPNG"
|
||||
shutdown = ["killall ripngd"]
|
||||
validate = ["pidof ripngd"]
|
||||
ipv6_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
text = """
|
||||
router ripng
|
||||
redistribute static
|
||||
redistribute connected
|
||||
redistribute ospf6
|
||||
network ::/0
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class FRRBabel(FrrService, ConfigService):
|
||||
"""
|
||||
The Babel service provides a loop-avoiding distance-vector routing
|
||||
protocol for IPv6 and IPv4 with fast convergence properties.
|
||||
"""
|
||||
|
||||
name = "FRRBabel"
|
||||
shutdown = ["killall babeld"]
|
||||
validate = ["pidof babeld"]
|
||||
ipv6_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
text = """
|
||||
router babel
|
||||
% for ifname in ifnames:
|
||||
network ${ifname}
|
||||
% endfor
|
||||
redistribute static
|
||||
redistribute ipv4 connected
|
||||
!
|
||||
"""
|
||||
data = dict(ifnames=ifnames)
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
"""
|
||||
else:
|
||||
text = """
|
||||
babel wired
|
||||
babel split-horizon
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
|
||||
class FRRpimd(FrrService, ConfigService):
|
||||
"""
|
||||
PIM multicast routing based on XORP.
|
||||
"""
|
||||
|
||||
name = "FRRpimd"
|
||||
shutdown = ["killall pimd"]
|
||||
validate = ["pidof pimd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
ifname = "eth0"
|
||||
for ifc in self.node.netifs():
|
||||
if ifc.name != "lo":
|
||||
ifname = ifc.name
|
||||
break
|
||||
|
||||
text = f"""
|
||||
router mfea
|
||||
!
|
||||
router igmp
|
||||
!
|
||||
router pim
|
||||
!ip pim rp-address 10.0.0.1
|
||||
ip pim bsr-candidate {ifname}
|
||||
ip pim rp-candidate {ifname}
|
||||
!ip pim spt-threshold interval 10 bytes 80000
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
text = """
|
||||
ip mfea
|
||||
ip igmp
|
||||
ip pim
|
||||
"""
|
||||
return self.clean_text(text)
|
59
daemon/core/configservices/frrservices/templates/daemons
Normal file
59
daemon/core/configservices/frrservices/templates/daemons
Normal file
|
@ -0,0 +1,59 @@
|
|||
#
|
||||
# When activation a daemon at the first time, a config file, even if it is
|
||||
# empty, has to be present *and* be owned by the user and group "frr", else
|
||||
# the daemon will not be started by /etc/init.d/frr. The permissions should
|
||||
# be u=rw,g=r,o=.
|
||||
# When using "vtysh" such a config file is also needed. It should be owned by
|
||||
# group "frrvty" and set to ug=rw,o= though. Check /etc/pam.d/frr, too.
|
||||
#
|
||||
# The watchfrr and zebra daemons are always started.
|
||||
#
|
||||
bgpd=yes
|
||||
ospfd=yes
|
||||
ospf6d=yes
|
||||
ripd=yes
|
||||
ripngd=yes
|
||||
isisd=yes
|
||||
pimd=yes
|
||||
ldpd=yes
|
||||
nhrpd=yes
|
||||
eigrpd=yes
|
||||
babeld=yes
|
||||
sharpd=yes
|
||||
pbrd=yes
|
||||
bfdd=yes
|
||||
fabricd=yes
|
||||
|
||||
#
|
||||
# If this option is set the /etc/init.d/frr script automatically loads
|
||||
# the config via "vtysh -b" when the servers are started.
|
||||
# Check /etc/pam.d/frr if you intend to use "vtysh"!
|
||||
#
|
||||
vtysh_enable=yes
|
||||
zebra_options=" -A 127.0.0.1 -s 90000000"
|
||||
bgpd_options=" -A 127.0.0.1"
|
||||
ospfd_options=" -A 127.0.0.1"
|
||||
ospf6d_options=" -A ::1"
|
||||
ripd_options=" -A 127.0.0.1"
|
||||
ripngd_options=" -A ::1"
|
||||
isisd_options=" -A 127.0.0.1"
|
||||
pimd_options=" -A 127.0.0.1"
|
||||
ldpd_options=" -A 127.0.0.1"
|
||||
nhrpd_options=" -A 127.0.0.1"
|
||||
eigrpd_options=" -A 127.0.0.1"
|
||||
babeld_options=" -A 127.0.0.1"
|
||||
sharpd_options=" -A 127.0.0.1"
|
||||
pbrd_options=" -A 127.0.0.1"
|
||||
staticd_options="-A 127.0.0.1"
|
||||
bfdd_options=" -A 127.0.0.1"
|
||||
fabricd_options="-A 127.0.0.1"
|
||||
|
||||
# The list of daemons to watch is automatically generated by the init script.
|
||||
#watchfrr_options=""
|
||||
|
||||
# for debugging purposes, you can specify a "wrap" command to start instead
|
||||
# of starting the daemon directly, e.g. to use valgrind on ospfd:
|
||||
# ospfd_wrap="/usr/bin/valgrind"
|
||||
# or you can use "all_wrap" for all daemons, e.g. to use perf record:
|
||||
# all_wrap="/usr/bin/perf record --call-graph -"
|
||||
# the normal daemon command is added to this at the end.
|
25
daemon/core/configservices/frrservices/templates/frr.conf
Normal file
25
daemon/core/configservices/frrservices/templates/frr.conf
Normal file
|
@ -0,0 +1,25 @@
|
|||
% for ifc, ip4s, ip6s, is_control in interfaces:
|
||||
interface ${ifc.name}
|
||||
% if want_ip4:
|
||||
% for addr in ip4s:
|
||||
ip address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% if want_ip6:
|
||||
% for addr in ip6s:
|
||||
ipv6 address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% if not is_control:
|
||||
% for service in services:
|
||||
% for line in service.frr_interface_config(ifc).split("\n"):
|
||||
${line}
|
||||
% endfor
|
||||
% endfor
|
||||
% endif
|
||||
!
|
||||
% endfor
|
||||
|
||||
% for service in services:
|
||||
${service.frr_config()}
|
||||
% endfor
|
105
daemon/core/configservices/frrservices/templates/frrboot.sh
Normal file
105
daemon/core/configservices/frrservices/templates/frrboot.sh
Normal file
|
@ -0,0 +1,105 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by zebra service (frr.py)
|
||||
FRR_CONF="${frr_conf}"
|
||||
FRR_SBIN_SEARCH="${frr_sbin_search}"
|
||||
FRR_BIN_SEARCH="${frr_bin_search}"
|
||||
FRR_STATE_DIR="${frr_state_dir}"
|
||||
|
||||
searchforprog()
|
||||
{
|
||||
prog=$1
|
||||
searchpath=$@
|
||||
ret=
|
||||
for p in $searchpath; do
|
||||
if [ -x $p/$prog ]; then
|
||||
ret=$p
|
||||
break
|
||||
fi
|
||||
done
|
||||
echo $ret
|
||||
}
|
||||
|
||||
confcheck()
|
||||
{
|
||||
CONF_DIR=`dirname $FRR_CONF`
|
||||
# if /etc/frr exists, point /etc/frr/frr.conf -> CONF_DIR
|
||||
if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/frr.conf ]; then
|
||||
ln -s $CONF_DIR/frr.conf /etc/frr/frr.conf
|
||||
fi
|
||||
# if /etc/frr exists, point /etc/frr/vtysh.conf -> CONF_DIR
|
||||
if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/vtysh.conf ]; then
|
||||
ln -s $CONF_DIR/vtysh.conf /etc/frr/vtysh.conf
|
||||
fi
|
||||
}
|
||||
|
||||
bootdaemon()
|
||||
{
|
||||
FRR_SBIN_DIR=$(searchforprog $1 $FRR_SBIN_SEARCH)
|
||||
if [ "z$FRR_SBIN_DIR" = "z" ]; then
|
||||
echo "ERROR: FRR's '$1' daemon not found in search path:"
|
||||
echo " $FRR_SBIN_SEARCH"
|
||||
return 1
|
||||
fi
|
||||
|
||||
flags=""
|
||||
|
||||
if [ "$1" = "pimd" ] && \\
|
||||
grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $FRR_CONF; then
|
||||
flags="$flags -6"
|
||||
fi
|
||||
|
||||
#force FRR to use CORE generated conf file
|
||||
flags="$flags -d -f $FRR_CONF"
|
||||
$FRR_SBIN_DIR/$1 $flags
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "ERROR: FRR's '$1' daemon failed to start!:"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
bootfrr()
|
||||
{
|
||||
FRR_BIN_DIR=$(searchforprog 'vtysh' $FRR_BIN_SEARCH)
|
||||
if [ "z$FRR_BIN_DIR" = "z" ]; then
|
||||
echo "ERROR: FRR's 'vtysh' program not found in search path:"
|
||||
echo " $FRR_BIN_SEARCH"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# fix /var/run/frr permissions
|
||||
id -u frr 2>/dev/null >/dev/null
|
||||
if [ "$?" = "0" ]; then
|
||||
chown frr $FRR_STATE_DIR
|
||||
fi
|
||||
|
||||
bootdaemon "zebra"
|
||||
if grep -q "^ip route " $FRR_CONF; then
|
||||
bootdaemon "staticd"
|
||||
fi
|
||||
for r in rip ripng ospf6 ospf bgp babel; do
|
||||
if grep -q "^router \\<$${}{r}\\>" $FRR_CONF; then
|
||||
bootdaemon "$${}{r}d"
|
||||
fi
|
||||
done
|
||||
|
||||
if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $FRR_CONF; then
|
||||
bootdaemon "pimd"
|
||||
fi
|
||||
|
||||
$FRR_BIN_DIR/vtysh -b
|
||||
}
|
||||
|
||||
if [ "$1" != "zebra" ]; then
|
||||
echo "WARNING: '$1': all FRR daemons are launched by the 'zebra' service!"
|
||||
exit 1
|
||||
fi
|
||||
confcheck
|
||||
bootfrr
|
||||
|
||||
# reset interfaces
|
||||
% for ifc, _, _ , _ in interfaces:
|
||||
ip link set dev ${ifc.name} down
|
||||
sleep 1
|
||||
ip link set dev ${ifc.name} up
|
||||
% endfor
|
|
@ -0,0 +1 @@
|
|||
service integrated-vtysh-config
|
0
daemon/core/configservices/nrlservices/__init__.py
Normal file
0
daemon/core/configservices/nrlservices/__init__.py
Normal file
212
daemon/core/configservices/nrlservices/services.py
Normal file
212
daemon/core/configservices/nrlservices/services.py
Normal file
|
@ -0,0 +1,212 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
|
||||
GROUP = "ProtoSvc"
|
||||
|
||||
|
||||
class MgenSinkService(ConfigService):
|
||||
name = "MGEN_Sink"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["mgensink.sh", "sink.mgen"]
|
||||
executables = ["mgen"]
|
||||
dependencies = []
|
||||
startup = ["sh mgensink.sh"]
|
||||
validate = ["pidof mgen"]
|
||||
shutdown = ["killall mgen"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
name = utils.sysctl_devname(ifc.name)
|
||||
ifnames.append(name)
|
||||
return dict(ifnames=ifnames)
|
||||
|
||||
|
||||
class NrlNhdp(ConfigService):
|
||||
name = "NHDP"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["nrlnhdp.sh"]
|
||||
executables = ["nrlnhdp"]
|
||||
dependencies = []
|
||||
startup = ["sh nrlnhdp.sh"]
|
||||
validate = ["pidof nrlnhdp"]
|
||||
shutdown = ["killall nrlnhdp"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||
|
||||
|
||||
class NrlSmf(ConfigService):
|
||||
name = "SMF"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["startsmf.sh"]
|
||||
executables = ["nrlsmf", "killall"]
|
||||
dependencies = []
|
||||
startup = ["sh startsmf.sh"]
|
||||
validate = ["pidof nrlsmf"]
|
||||
shutdown = ["killall nrlsmf"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_arouted = "arouted" in self.node.config_services
|
||||
has_nhdp = "NHDP" in self.node.config_services
|
||||
has_olsr = "OLSR" in self.node.config_services
|
||||
ifnames = []
|
||||
ip4_prefix = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
if ip4_prefix:
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
ip4_prefix = f"{a}/{24}"
|
||||
break
|
||||
return dict(
|
||||
has_arouted=has_arouted,
|
||||
has_nhdp=has_nhdp,
|
||||
has_olsr=has_olsr,
|
||||
ifnames=ifnames,
|
||||
ip4_prefix=ip4_prefix,
|
||||
)
|
||||
|
||||
|
||||
class NrlOlsr(ConfigService):
|
||||
name = "OLSR"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["nrlolsrd.sh"]
|
||||
executables = ["nrlolsrd"]
|
||||
dependencies = []
|
||||
startup = ["sh nrlolsrd.sh"]
|
||||
validate = ["pidof nrlolsrd"]
|
||||
shutdown = ["killall nrlolsrd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
has_zebra = "zebra" in self.node.config_services
|
||||
ifname = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifname = ifc.name
|
||||
break
|
||||
return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname)
|
||||
|
||||
|
||||
class NrlOlsrv2(ConfigService):
|
||||
name = "OLSRv2"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["nrlolsrv2.sh"]
|
||||
executables = ["nrlolsrv2"]
|
||||
dependencies = []
|
||||
startup = ["sh nrlolsrv2.sh"]
|
||||
validate = ["pidof nrlolsrv2"]
|
||||
shutdown = ["killall nrlolsrv2"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||
|
||||
|
||||
class OlsrOrg(ConfigService):
|
||||
name = "OLSRORG"
|
||||
group = GROUP
|
||||
directories = ["/etc/olsrd"]
|
||||
files = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
|
||||
executables = ["olsrd"]
|
||||
dependencies = []
|
||||
startup = ["sh olsrd.sh"]
|
||||
validate = ["pidof olsrd"]
|
||||
shutdown = ["killall olsrd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||
|
||||
|
||||
class MgenActor(ConfigService):
|
||||
name = "MgenActor"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["start_mgen_actor.sh"]
|
||||
executables = ["mgen"]
|
||||
dependencies = []
|
||||
startup = ["sh start_mgen_actor.sh"]
|
||||
validate = ["pidof mgen"]
|
||||
shutdown = ["killall mgen"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class Arouted(ConfigService):
|
||||
name = "arouted"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["startarouted.sh"]
|
||||
executables = ["arouted"]
|
||||
dependencies = []
|
||||
startup = ["sh startarouted.sh"]
|
||||
validate = ["pidof arouted"]
|
||||
shutdown = ["pkill arouted"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ip4_prefix = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
if ip4_prefix:
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
ip4_prefix = f"{a}/{24}"
|
||||
break
|
||||
return dict(ip4_prefix=ip4_prefix)
|
|
@ -0,0 +1 @@
|
|||
mgen input sink.mgen output mgen_${node.name}.log
|
|
@ -0,0 +1,7 @@
|
|||
<%
|
||||
interfaces = "-i " + " -i ".join(ifnames)
|
||||
smf = ""
|
||||
if has_smf:
|
||||
smf = "-flooding ecds -smfClient %s_smf" % node.name
|
||||
%>
|
||||
nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${interfaces}
|
|
@ -0,0 +1,9 @@
|
|||
<%
|
||||
smf = ""
|
||||
if has_smf:
|
||||
smf = "-flooding s-mpr -smfClient %s_smf" % node.name
|
||||
zebra = ""
|
||||
if has_zebra:
|
||||
zebra = "-z"
|
||||
%>
|
||||
nrlolsrd -i ${ifname} -l /var/log/nrlolsrd.log -rpipe ${node.name}_olsr ${smf} ${zebra}
|
|
@ -0,0 +1,7 @@
|
|||
<%
|
||||
interfaces = "-i " + " -i ".join(ifnames)
|
||||
smf = ""
|
||||
if has_smf:
|
||||
smf = "-flooding ecds -smfClient %s_smf" % node.name
|
||||
%>
|
||||
nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${interfaces}
|
312
daemon/core/configservices/nrlservices/templates/olsrd.conf
Normal file
312
daemon/core/configservices/nrlservices/templates/olsrd.conf
Normal file
|
@ -0,0 +1,312 @@
|
|||
#
|
||||
# OLSR.org routing daemon config file
|
||||
# This file contains the usual options for an ETX based
|
||||
# stationary network without fisheye
|
||||
# (for other options see olsrd.conf.default.full)
|
||||
#
|
||||
# Lines starting with a # are discarded
|
||||
#
|
||||
|
||||
#### ATTENTION for IPv6 users ####
|
||||
# Because of limitations in the parser IPv6 addresses must NOT
|
||||
# begin with a ":", so please add a "0" as a prefix.
|
||||
|
||||
###########################
|
||||
### Basic configuration ###
|
||||
###########################
|
||||
# keep this settings at the beginning of your first configuration file
|
||||
|
||||
# Debug level (0-9)
|
||||
# If set to 0 the daemon runs in the background, unless "NoFork" is set to true
|
||||
# (Default is 1)
|
||||
|
||||
# DebugLevel 1
|
||||
|
||||
# IP version to use (4 or 6)
|
||||
# (Default is 4)
|
||||
|
||||
# IpVersion 4
|
||||
|
||||
#################################
|
||||
### OLSRd agent configuration ###
|
||||
#################################
|
||||
# this parameters control the settings of the routing agent which are not
|
||||
# related to the OLSR protocol and it's extensions
|
||||
|
||||
# FIBMetric controls the metric value of the host-routes OLSRd sets.
|
||||
# - "flat" means that the metric value is always 2. This is the preferred value
|
||||
# because it helps the linux kernel routing to clean up older routes
|
||||
# - "correct" use the hopcount as the metric value.
|
||||
# - "approx" use the hopcount as the metric value too, but does only update the
|
||||
# hopcount if the nexthop changes too
|
||||
# (Default is "flat")
|
||||
|
||||
# FIBMetric "flat"
|
||||
|
||||
#######################################
|
||||
### Linux specific OLSRd extensions ###
|
||||
#######################################
|
||||
# these parameters are only working on linux at the moment
|
||||
|
||||
# SrcIpRoutes tells OLSRd to set the Src flag of host routes to the originator-ip
|
||||
# of the node. In addition to this an additional localhost device is created
|
||||
# to make sure the returning traffic can be received.
|
||||
# (Default is "no")
|
||||
|
||||
# SrcIpRoutes no
|
||||
|
||||
# Specify the proto tag to be used for routes olsr inserts into kernel
|
||||
# currently only implemented for linux
|
||||
# valid values under linux are 1 .. 254
|
||||
# 1 gets remapped by olsrd to 0 UNSPECIFIED (1 is reserved for ICMP redirects)
|
||||
# 2 KERNEL routes (not very wise to use)
|
||||
# 3 BOOT (should in fact not be used by routing daemons)
|
||||
# 4 STATIC
|
||||
# 8 .. 15 various routing daemons (gated, zebra, bird, & co)
|
||||
# (defaults to 0 which gets replaced by an OS-specific default value
|
||||
# under linux 3 (BOOT) (for backward compatibility)
|
||||
|
||||
# RtProto 0
|
||||
|
||||
# Activates (in IPv6 mode) the automatic use of NIIT
|
||||
# (see README-Olsr-Extensions)
|
||||
# (default is "yes")
|
||||
|
||||
# UseNiit yes
|
||||
|
||||
# Activates the smartgateway ipip tunnel feature.
|
||||
# See README-Olsr-Extensions for a description of smartgateways.
|
||||
# (default is "no")
|
||||
|
||||
# SmartGateway no
|
||||
|
||||
# Signals that the server tunnel must always be removed on shutdown,
|
||||
# irrespective of the interface up/down state during startup.
|
||||
# (default is "no")
|
||||
|
||||
# SmartGatewayAlwaysRemoveServerTunnel no
|
||||
|
||||
# Determines the maximum number of gateways that can be in use at any given
|
||||
# time. This setting is used to mitigate the effects of breaking connections
|
||||
# (due to the selection of a new gateway) on a dynamic network.
|
||||
# (default is 1)
|
||||
|
||||
# SmartGatewayUseCount 1
|
||||
|
||||
# Determines the take-down percentage for a non-current smart gateway tunnel.
|
||||
# If the cost of the current smart gateway tunnel is less than this percentage
|
||||
# of the cost of the non-current smart gateway tunnel, then the non-current smart
|
||||
# gateway tunnel is taken down because it is then presumed to be 'too expensive'.
|
||||
# This setting is only relevant when SmartGatewayUseCount is larger than 1;
|
||||
# a value of 0 will result in the tunnels not being taken down proactively.
|
||||
# (default is 0)
|
||||
|
||||
# SmartGatewayTakeDownPercentage 0
|
||||
|
||||
# Determines the policy routing script that is executed during startup and
|
||||
# shutdown of olsrd. The script is only executed when SmartGatewayUseCount
|
||||
# is set to a value larger than 1. The script must setup policy routing
|
||||
# rules such that multi-gateway mode works. A sample script is included.
|
||||
# (default is not set)
|
||||
|
||||
# SmartGatewayPolicyRoutingScript ""
|
||||
|
||||
# Determines the egress interfaces that are part of the multi-gateway setup and
|
||||
# therefore only relevant when SmartGatewayUseCount is larger than 1 (in which
|
||||
# case it must be explicitly set).
|
||||
# (default is not set)
|
||||
|
||||
# SmartGatewayEgressInterfaces ""
|
||||
|
||||
# Determines the routing tables offset for multi-gateway policy routing tables
|
||||
# See the policy routing script for an explanation.
|
||||
# (default is 90)
|
||||
|
||||
# SmartGatewayTablesOffset 90
|
||||
|
||||
# Determines the policy routing rules offset for multi-gateway policy routing
|
||||
# rules. See the policy routing script for an explanation.
|
||||
# (default is 0, which indicates that the rules and tables should be aligned and
|
||||
# puts this value at SmartGatewayTablesOffset - # egress interfaces -
|
||||
# # olsr interfaces)
|
||||
|
||||
# SmartGatewayRulesOffset 87
|
||||
|
||||
# Allows the selection of a smartgateway with NAT (only for IPv4)
|
||||
# (default is "yes")
|
||||
|
||||
# SmartGatewayAllowNAT yes
|
||||
|
||||
# Determines the period (in milliseconds) on which a new smart gateway
|
||||
# selection is performed.
|
||||
# (default is 10000 milliseconds)
|
||||
|
||||
# SmartGatewayPeriod 10000
|
||||
|
||||
# Determines the number of times the link state database must be stable
|
||||
# before a new smart gateway is selected.
|
||||
# (default is 6)
|
||||
|
||||
# SmartGatewayStableCount 6
|
||||
|
||||
# When another gateway than the current one has a cost of less than the cost
|
||||
# of the current gateway multiplied by SmartGatewayThreshold then the smart
|
||||
# gateway is switched to the other gateway. The unit is percentage.
|
||||
# (defaults to 0)
|
||||
|
||||
# SmartGatewayThreshold 0
|
||||
|
||||
# The weighing factor for the gateway uplink bandwidth (exit link, uplink).
|
||||
# See README-Olsr-Extensions for a description of smart gateways.
|
||||
# (default is 1)
|
||||
|
||||
# SmartGatewayWeightExitLinkUp 1
|
||||
|
||||
# The weighing factor for the gateway downlink bandwidth (exit link, downlink).
|
||||
# See README-Olsr-Extensions for a description of smart gateways.
|
||||
# (default is 1)
|
||||
|
||||
# SmartGatewayWeightExitLinkDown 1
|
||||
|
||||
# The weighing factor for the ETX costs.
|
||||
# See README-Olsr-Extensions for a description of smart gateways.
|
||||
# (default is 1)
|
||||
|
||||
# SmartGatewayWeightEtx 1
|
||||
|
||||
# The divider for the ETX costs.
|
||||
# See README-Olsr-Extensions for a description of smart gateways.
|
||||
# (default is 0)
|
||||
|
||||
# SmartGatewayDividerEtx 0
|
||||
|
||||
# Defines what kind of Uplink this node will publish as a
|
||||
# smartgateway. The existence of the uplink is detected by
|
||||
# a route to 0.0.0.0/0, ::ffff:0:0/96 and/or 2000::/3.
|
||||
# possible values are "none", "ipv4", "ipv6", "both"
|
||||
# (default is "both")
|
||||
|
||||
# SmartGatewayUplink "both"
|
||||
|
||||
# Specifies if the local ipv4 uplink use NAT
|
||||
# (default is "yes")
|
||||
|
||||
# SmartGatewayUplinkNAT yes
|
||||
|
||||
# Specifies the speed of the uplink in kilobit/s.
|
||||
# First parameter is upstream, second parameter is downstream
|
||||
# (default is 128/1024)
|
||||
|
||||
# SmartGatewaySpeed 128 1024
|
||||
|
||||
# Specifies the EXTERNAL ipv6 prefix of the uplink. A prefix
|
||||
# length of more than 64 is not allowed.
|
||||
# (default is 0::/0
|
||||
|
||||
# SmartGatewayPrefix 0::/0
|
||||
|
||||
##############################
|
||||
### OLSR protocol settings ###
|
||||
##############################
|
||||
|
||||
# HNA (Host network association) allows the OLSR to announce
|
||||
# additional IPs or IP subnets to the net that are reachable
|
||||
# through this node.
|
||||
# Syntax for HNA4 is "network-address network-mask"
|
||||
# Syntax for HNA6 is "network-address prefix-length"
|
||||
# (default is no HNA)
|
||||
Hna4
|
||||
{
|
||||
# Internet gateway
|
||||
# 0.0.0.0 0.0.0.0
|
||||
# specific small networks reachable through this node
|
||||
# 15.15.0.0 255.255.255.0
|
||||
}
|
||||
Hna6
|
||||
{
|
||||
# Internet gateway
|
||||
# 0:: 0
|
||||
# specific small networks reachable through this node
|
||||
# fec0:2200:106:0:0:0:0:0 48
|
||||
}
|
||||
|
||||
################################
|
||||
### OLSR protocol extensions ###
|
||||
################################
|
||||
|
||||
# Link quality algorithm (only for lq level 2)
|
||||
# (see README-Olsr-Extensions)
|
||||
# - "etx_float", a floating point ETX with exponential aging
|
||||
# - "etx_fpm", same as ext_float, but with integer arithmetic
|
||||
# - "etx_ff" (ETX freifunk), an etx variant which use all OLSR
|
||||
# traffic (instead of only hellos) for ETX calculation
|
||||
# - "etx_ffeth", an incompatible variant of etx_ff that allows
|
||||
# ethernet links with ETX 0.1.
|
||||
# (defaults to "etx_ff")
|
||||
|
||||
# LinkQualityAlgorithm "etx_ff"
|
||||
|
||||
# Fisheye mechanism for TCs (0 meansoff, 1 means on)
|
||||
# (default is 1)
|
||||
|
||||
LinkQualityFishEye 0
|
||||
|
||||
#####################################
|
||||
### Example plugin configurations ###
|
||||
#####################################
|
||||
# Olsrd plugins to load
|
||||
# This must be the absolute path to the file
|
||||
# or the loader will use the following scheme:
|
||||
# - Try the paths in the LD_LIBRARY_PATH
|
||||
# environment variable.
|
||||
# - The list of libraries cached in /etc/ld.so.cache
|
||||
# - /lib, followed by /usr/lib
|
||||
#
|
||||
# the examples in this list are for linux, so check if the plugin is
|
||||
# available if you use windows.
|
||||
# each plugin should have a README file in it's lib subfolder
|
||||
|
||||
# LoadPlugin "olsrd_txtinfo.dll"
|
||||
#LoadPlugin "olsrd_txtinfo.so.0.1"
|
||||
#{
|
||||
# the default port is 2006 but you can change it like this:
|
||||
#PlParam "port" "8080"
|
||||
|
||||
# You can set a "accept" single address to allow to connect to
|
||||
# txtinfo. If no address is specified, then localhost (127.0.0.1)
|
||||
# is allowed by default. txtinfo will only use the first "accept"
|
||||
# parameter specified and will ignore the rest.
|
||||
|
||||
# to allow a specific host:
|
||||
#PlParam "accept" "172.29.44.23"
|
||||
# if you set it to 0.0.0.0, it will accept all connections
|
||||
#PlParam "accept" "0.0.0.0"
|
||||
#}
|
||||
|
||||
#############################################
|
||||
### OLSRD default interface configuration ###
|
||||
#############################################
|
||||
# the default interface section can have the same values as the following
|
||||
# interface configuration. It will allow you so set common options for all
|
||||
# interfaces.
|
||||
|
||||
InterfaceDefaults {
|
||||
Ip4Broadcast 255.255.255.255
|
||||
}
|
||||
|
||||
######################################
|
||||
### OLSRd Interfaces configuration ###
|
||||
######################################
|
||||
# multiple interfaces can be specified for a single configuration block
|
||||
# multiple configuration blocks can be specified
|
||||
|
||||
# WARNING, don't forget to insert your interface names here !
|
||||
#Interface "<OLSRd-Interface1>" "<OLSRd-Interface2>"
|
||||
#{
|
||||
# Interface Mode is used to prevent unnecessary
|
||||
# packet forwarding on switched ethernet interfaces
|
||||
# valid Modes are "mesh" and "ether"
|
||||
# (default is "mesh")
|
||||
|
||||
# Mode "mesh"
|
||||
#}
|
|
@ -0,0 +1,4 @@
|
|||
<%
|
||||
interfaces = "-i " + " -i ".join(ifnames)
|
||||
%>
|
||||
olsrd ${interfaces}
|
|
@ -0,0 +1,4 @@
|
|||
0.0 LISTEN UDP 5000
|
||||
% for ifname in ifnames:
|
||||
0.0 Join 224.225.1.2 INTERFACE ${ifname}
|
||||
% endfor
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by MgenActor service
|
||||
mgenBasicActor.py -n ${node.name} -a 0.0.0.0 < /dev/null > /dev/null 2>&1 &
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
for f in "/tmp/${node.name}_smf"; do
|
||||
count=1
|
||||
until [ -e "$f" ]; do
|
||||
if [ $count -eq 10 ]; then
|
||||
echo "ERROR: nrlmsf pipe not found: $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.1
|
||||
count=$(($count + 1))
|
||||
done
|
||||
done
|
||||
|
||||
ip route add ${ip4_prefix} dev lo
|
||||
arouted instance ${node.name}_smf tap ${node.name}_tap stability 10 2>&1 > /var/log/arouted.log &
|
15
daemon/core/configservices/nrlservices/templates/startsmf.sh
Normal file
15
daemon/core/configservices/nrlservices/templates/startsmf.sh
Normal file
|
@ -0,0 +1,15 @@
|
|||
<%
|
||||
interfaces = ",".join(ifnames)
|
||||
arouted = ""
|
||||
if has_arouted:
|
||||
arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0])
|
||||
if has_nhdp:
|
||||
flood = "ecds"
|
||||
elif has_olsr:
|
||||
flood = "smpr"
|
||||
else:
|
||||
flood = "cf"
|
||||
%>
|
||||
#!/bin/sh
|
||||
# auto-generated by NrlSmf service
|
||||
nrlsmf instance ${node.name}_smf ${interfaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
|
0
daemon/core/configservices/quaggaservices/__init__.py
Normal file
0
daemon/core/configservices/quaggaservices/__init__.py
Normal file
424
daemon/core/configservices/quaggaservices/services.py
Normal file
424
daemon/core/configservices/quaggaservices/services.py
Normal file
|
@ -0,0 +1,424 @@
|
|||
import abc
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import constants
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.nodes.base import CoreNodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
GROUP = "Quagga"
|
||||
|
||||
|
||||
def has_mtu_mismatch(ifc: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect MTU mismatch and add the appropriate OSPF
|
||||
mtu-ignore command. This is needed when e.g. a node is linked via a
|
||||
GreTap device.
|
||||
"""
|
||||
if ifc.mtu != 1500:
|
||||
return True
|
||||
if not ifc.net:
|
||||
return False
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu != ifc.mtu:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_min_mtu(ifc):
|
||||
"""
|
||||
Helper to discover the minimum MTU of interfaces linked with the
|
||||
given interface.
|
||||
"""
|
||||
mtu = ifc.mtu
|
||||
if not ifc.net:
|
||||
return mtu
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu < mtu:
|
||||
mtu = i.mtu
|
||||
return mtu
|
||||
|
||||
|
||||
def get_router_id(node: CoreNodeBase) -> str:
|
||||
"""
|
||||
Helper to return the first IPv4 address of a node as its router ID.
|
||||
"""
|
||||
for ifc in node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
return a
|
||||
return "0.0.0.0"
|
||||
|
||||
|
||||
class Zebra(ConfigService):
|
||||
name = "zebra"
|
||||
group = GROUP
|
||||
directories = ["/usr/local/etc/quagga", "/var/run/quagga"]
|
||||
files = [
|
||||
"/usr/local/etc/quagga/Quagga.conf",
|
||||
"quaggaboot.sh",
|
||||
"/usr/local/etc/quagga/vtysh.conf",
|
||||
]
|
||||
executables = ["zebra"]
|
||||
dependencies = []
|
||||
startup = ["sh quaggaboot.sh zebra"]
|
||||
validate = ["pidof zebra"]
|
||||
shutdown = ["killall zebra"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
quagga_bin_search = self.node.session.options.get_config(
|
||||
"quagga_bin_search", default="/usr/local/bin /usr/bin /usr/lib/quagga"
|
||||
).strip('"')
|
||||
quagga_sbin_search = self.node.session.options.get_config(
|
||||
"quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga"
|
||||
).strip('"')
|
||||
quagga_state_dir = constants.QUAGGA_STATE_DIR
|
||||
quagga_conf = self.files[0]
|
||||
|
||||
services = []
|
||||
want_ip4 = False
|
||||
want_ip6 = False
|
||||
for service in self.node.config_services.values():
|
||||
if self.name not in service.dependencies:
|
||||
continue
|
||||
if service.ipv4_routing:
|
||||
want_ip4 = True
|
||||
if service.ipv6_routing:
|
||||
want_ip6 = True
|
||||
services.append(service)
|
||||
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
ip4s = []
|
||||
ip6s = []
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
ip4s.append(x)
|
||||
else:
|
||||
ip6s.append(x)
|
||||
is_control = getattr(ifc, "control", False)
|
||||
interfaces.append((ifc, ip4s, ip6s, is_control))
|
||||
|
||||
return dict(
|
||||
quagga_bin_search=quagga_bin_search,
|
||||
quagga_sbin_search=quagga_sbin_search,
|
||||
quagga_state_dir=quagga_state_dir,
|
||||
quagga_conf=quagga_conf,
|
||||
interfaces=interfaces,
|
||||
want_ip4=want_ip4,
|
||||
want_ip6=want_ip6,
|
||||
services=services,
|
||||
)
|
||||
|
||||
|
||||
class QuaggaService(abc.ABC):
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = []
|
||||
executables = []
|
||||
dependencies = ["zebra"]
|
||||
startup = []
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
ipv4_routing = False
|
||||
ipv6_routing = False
|
||||
|
||||
@abc.abstractmethod
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def quagga_config(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Ospfv2(QuaggaService, ConfigService):
|
||||
"""
|
||||
The OSPFv2 service provides IPv4 routing for wired networks. It does
|
||||
not build its own configuration file but has hooks for adding to the
|
||||
unified Quagga.conf file.
|
||||
"""
|
||||
|
||||
name = "OSPFv2"
|
||||
validate = ["pidof ospfd"]
|
||||
shutdown = ["killall ospfd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if has_mtu_mismatch(ifc):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
addresses = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
addr = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
addresses.append(a)
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
router-id ${router_id}
|
||||
% for addr in addresses:
|
||||
network ${addr} area 0
|
||||
% endfor
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
|
||||
class Ospfv3(QuaggaService, ConfigService):
|
||||
"""
|
||||
The OSPFv3 service provides IPv6 routing for wired networks. It does
|
||||
not build its own configuration file but has hooks for adding to the
|
||||
unified Quagga.conf file.
|
||||
"""
|
||||
|
||||
name = "OSPFv3"
|
||||
shutdown = ("killall ospf6d",)
|
||||
validate = ("pidof ospf6d",)
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
mtu = get_min_mtu(ifc)
|
||||
if mtu < ifc.mtu:
|
||||
return f"ipv6 ospf6 ifmtu {mtu}"
|
||||
else:
|
||||
return ""
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
data = dict(router_id=router_id, ifnames=ifnames)
|
||||
text = """
|
||||
router ospf6
|
||||
instance-id 65
|
||||
router-id ${router_id}
|
||||
% for ifname in ifnames:
|
||||
interface ${ifname} area 0.0.0.0
|
||||
% endfor
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
|
||||
class Ospfv3mdr(Ospfv3):
|
||||
"""
|
||||
The OSPFv3 MANET Designated Router (MDR) service provides IPv6
|
||||
routing for wireless networks. It does not build its own
|
||||
configuration file but has hooks for adding to the
|
||||
unified Quagga.conf file.
|
||||
"""
|
||||
|
||||
name = "OSPFv3MDR"
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
for ifc in self.node.netifs():
|
||||
is_wireless = isinstance(ifc.net, (WlanNode, EmaneNet))
|
||||
logging.info("MDR wireless: %s", is_wireless)
|
||||
return dict()
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
config = super().quagga_interface_config(ifc)
|
||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
||||
config = self.clean_text(
|
||||
f"""
|
||||
{config}
|
||||
ipv6 ospf6 hello-interval 2
|
||||
ipv6 ospf6 dead-interval 6
|
||||
ipv6 ospf6 retransmit-interval 5
|
||||
ipv6 ospf6 network manet-designated-router
|
||||
ipv6 ospf6 twohoprefresh 3
|
||||
ipv6 ospf6 adjacencyconnectivity uniconnected
|
||||
ipv6 ospf6 lsafullness mincostlsa
|
||||
"""
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
class Bgp(QuaggaService, ConfigService):
|
||||
"""
|
||||
The BGP service provides interdomain routing.
|
||||
Peers must be manually configured, with a full mesh for those
|
||||
having the same AS number.
|
||||
"""
|
||||
|
||||
name = "BGP"
|
||||
shutdown = ["killall bgpd"]
|
||||
validate = ["pidof bgpd"]
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
return ""
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
text = f"""
|
||||
! BGP configuration
|
||||
! You should configure the AS number below
|
||||
! along with this router's peers.
|
||||
router bgp {self.node.id}
|
||||
bgp router-id {router_id}
|
||||
redistribute connected
|
||||
!neighbor 1.2.3.4 remote-as 555
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
|
||||
class Rip(QuaggaService, ConfigService):
|
||||
"""
|
||||
The RIP service provides IPv4 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "RIP"
|
||||
shutdown = ["killall ripd"]
|
||||
validate = ["pidof ripd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
text = """
|
||||
router rip
|
||||
redistribute static
|
||||
redistribute connected
|
||||
redistribute ospf
|
||||
network 0.0.0.0/0
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class Ripng(QuaggaService, ConfigService):
|
||||
"""
|
||||
The RIP NG service provides IPv6 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "RIPNG"
|
||||
shutdown = ["killall ripngd"]
|
||||
validate = ["pidof ripngd"]
|
||||
ipv6_routing = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
text = """
|
||||
router ripng
|
||||
redistribute static
|
||||
redistribute connected
|
||||
redistribute ospf6
|
||||
network ::/0
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class Babel(QuaggaService, ConfigService):
|
||||
"""
|
||||
The Babel service provides a loop-avoiding distance-vector routing
|
||||
protocol for IPv6 and IPv4 with fast convergence properties.
|
||||
"""
|
||||
|
||||
name = "Babel"
|
||||
shutdown = ["killall babeld"]
|
||||
validate = ["pidof babeld"]
|
||||
ipv6_routing = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
text = """
|
||||
router babel
|
||||
% for ifname in ifnames:
|
||||
network ${ifname}
|
||||
% endfor
|
||||
redistribute static
|
||||
redistribute connected
|
||||
!
|
||||
"""
|
||||
data = dict(ifnames=ifnames)
|
||||
return self.render_text(text, data)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
"""
|
||||
else:
|
||||
text = """
|
||||
babel wired
|
||||
babel split-horizon
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
|
||||
class Xpimd(QuaggaService, ConfigService):
|
||||
"""
|
||||
PIM multicast routing based on XORP.
|
||||
"""
|
||||
|
||||
name = "Xpimd"
|
||||
shutdown = ["killall xpimd"]
|
||||
validate = ["pidof xpimd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
ifname = "eth0"
|
||||
for ifc in self.node.netifs():
|
||||
if ifc.name != "lo":
|
||||
ifname = ifc.name
|
||||
break
|
||||
|
||||
text = f"""
|
||||
router mfea
|
||||
!
|
||||
router igmp
|
||||
!
|
||||
router pim
|
||||
!ip pim rp-address 10.0.0.1
|
||||
ip pim bsr-candidate {ifname}
|
||||
ip pim rp-candidate {ifname}
|
||||
!ip pim spt-threshold interval 10 bytes 80000
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
text = """
|
||||
ip mfea
|
||||
ip pim
|
||||
"""
|
||||
return self.clean_text(text)
|
|
@ -0,0 +1,25 @@
|
|||
% for ifc, ip4s, ip6s, is_control in interfaces:
|
||||
interface ${ifc.name}
|
||||
% if want_ip4:
|
||||
% for addr in ip4s:
|
||||
ip address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% if want_ip6:
|
||||
% for addr in ip6s:
|
||||
ipv6 address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% if not is_control:
|
||||
% for service in services:
|
||||
% for line in service.quagga_interface_config(ifc).split("\n"):
|
||||
${line}
|
||||
% endfor
|
||||
% endfor
|
||||
% endif
|
||||
!
|
||||
% endfor
|
||||
|
||||
% for service in services:
|
||||
${service.quagga_config()}
|
||||
% endfor
|
|
@ -0,0 +1,92 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by zebra service (quagga.py)
|
||||
QUAGGA_CONF="${quagga_conf}"
|
||||
QUAGGA_SBIN_SEARCH="${quagga_sbin_search}"
|
||||
QUAGGA_BIN_SEARCH="${quagga_bin_search}"
|
||||
QUAGGA_STATE_DIR="${quagga_state_dir}"
|
||||
|
||||
searchforprog()
|
||||
{
|
||||
prog=$1
|
||||
searchpath=$@
|
||||
ret=
|
||||
for p in $searchpath; do
|
||||
if [ -x $p/$prog ]; then
|
||||
ret=$p
|
||||
break
|
||||
fi
|
||||
done
|
||||
echo $ret
|
||||
}
|
||||
|
||||
confcheck()
|
||||
{
|
||||
CONF_DIR=`dirname $QUAGGA_CONF`
|
||||
# if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR
|
||||
if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then
|
||||
ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf
|
||||
fi
|
||||
# if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR
|
||||
if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then
|
||||
ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf
|
||||
fi
|
||||
}
|
||||
|
||||
bootdaemon()
|
||||
{
|
||||
QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH)
|
||||
if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then
|
||||
echo "ERROR: Quagga's '$1' daemon not found in search path:"
|
||||
echo " $QUAGGA_SBIN_SEARCH"
|
||||
return 1
|
||||
fi
|
||||
|
||||
flags=""
|
||||
|
||||
if [ "$1" = "xpimd" ] && \\
|
||||
grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then
|
||||
flags="$flags -6"
|
||||
fi
|
||||
|
||||
$QUAGGA_SBIN_DIR/$1 $flags -d
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "ERROR: Quagga's '$1' daemon failed to start!:"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
bootquagga()
|
||||
{
|
||||
QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH)
|
||||
if [ "z$QUAGGA_BIN_DIR" = "z" ]; then
|
||||
echo "ERROR: Quagga's 'vtysh' program not found in search path:"
|
||||
echo " $QUAGGA_BIN_SEARCH"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# fix /var/run/quagga permissions
|
||||
id -u quagga 2>/dev/null >/dev/null
|
||||
if [ "$?" = "0" ]; then
|
||||
chown quagga $QUAGGA_STATE_DIR
|
||||
fi
|
||||
|
||||
bootdaemon "zebra"
|
||||
for r in rip ripng ospf6 ospf bgp babel; do
|
||||
if grep -q "^router \\<$${}{r}\\>" $QUAGGA_CONF; then
|
||||
bootdaemon "$${}{r}d"
|
||||
fi
|
||||
done
|
||||
|
||||
if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then
|
||||
bootdaemon "xpimd"
|
||||
fi
|
||||
|
||||
$QUAGGA_BIN_DIR/vtysh -b
|
||||
}
|
||||
|
||||
if [ "$1" != "zebra" ]; then
|
||||
echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!"
|
||||
exit 1
|
||||
fi
|
||||
confcheck
|
||||
bootquagga
|
|
@ -0,0 +1 @@
|
|||
service integrated-vtysh-config
|
0
daemon/core/configservices/sercurityservices/__init__.py
Normal file
0
daemon/core/configservices/sercurityservices/__init__.py
Normal file
141
daemon/core/configservices/sercurityservices/services.py
Normal file
141
daemon/core/configservices/sercurityservices/services.py
Normal file
|
@ -0,0 +1,141 @@
|
|||
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)
|
|
@ -0,0 +1,30 @@
|
|||
# -------- CUSTOMIZATION REQUIRED --------
|
||||
#
|
||||
# Below are sample iptables firewall rules that you can uncomment and edit.
|
||||
# You can also use ip6tables rules for IPv6.
|
||||
#
|
||||
|
||||
# start by flushing all firewall rules (so this script may be re-run)
|
||||
#iptables -F
|
||||
|
||||
# allow traffic related to established connections
|
||||
#iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
# allow TCP packets from any source destined for 192.168.1.1
|
||||
#iptables -A INPUT -s 0/0 -i eth0 -d 192.168.1.1 -p TCP -j ACCEPT
|
||||
|
||||
# allow OpenVPN server traffic from eth0
|
||||
#iptables -A INPUT -p udp --dport 1194 -j ACCEPT
|
||||
#iptables -A INPUT -i eth0 -j DROP
|
||||
#iptables -A OUTPUT -p udp --sport 1194 -j ACCEPT
|
||||
#iptables -A OUTPUT -o eth0 -j DROP
|
||||
|
||||
# allow ICMP ping traffic
|
||||
#iptables -A OUTPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||
#iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
|
||||
# allow SSH traffic
|
||||
#iptables -A -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
|
||||
|
||||
# drop all other traffic coming in eth0
|
||||
#iptables -A INPUT -i eth0 -j DROP
|
114
daemon/core/configservices/sercurityservices/templates/ipsec.sh
Normal file
114
daemon/core/configservices/sercurityservices/templates/ipsec.sh
Normal file
|
@ -0,0 +1,114 @@
|
|||
# -------- CUSTOMIZATION REQUIRED --------
|
||||
#
|
||||
# The IPsec service builds ESP tunnels between the specified peers using the
|
||||
# racoon IKEv2 keying daemon. You need to provide keys and the addresses of
|
||||
# peers, along with subnets to tunnel.
|
||||
|
||||
# directory containing the certificate and key described below
|
||||
keydir=/etc/core/keys
|
||||
|
||||
# the name used for the "$certname.pem" x509 certificate and
|
||||
# "$certname.key" RSA private key, which can be generated using openssl
|
||||
certname=ipsec1
|
||||
|
||||
# list the public-facing IP addresses, starting with the localhost and followed
|
||||
# by each tunnel peer, separated with a single space
|
||||
tunnelhosts="172.16.0.1AND172.16.0.2 172.16.0.1AND172.16.2.1"
|
||||
|
||||
# Define T<i> where i is the index for each tunnel peer host from
|
||||
# the tunnel_hosts list above (0 is localhost).
|
||||
# T<i> is a list of IPsec tunnels with peer i, with a local subnet address
|
||||
# followed by the remote subnet address:
|
||||
# T<i>="<local>AND<remote> <local>AND<remote>"
|
||||
# For example, 172.16.0.0/24 is a local network (behind this node) to be
|
||||
# tunneled and 172.16.2.0/24 is a remote network (behind peer 1)
|
||||
T1="172.16.3.0/24AND172.16.5.0/24"
|
||||
T2="172.16.4.0/24AND172.16.5.0/24 172.16.4.0/24AND172.16.6.0/24"
|
||||
|
||||
# -------- END CUSTOMIZATION --------
|
||||
|
||||
echo "building config $PWD/ipsec.conf..."
|
||||
echo "building config $PWD/ipsec.conf..." > $PWD/ipsec.log
|
||||
|
||||
checkip=0
|
||||
if [ "$(dpkg -l | grep " sipcalc ")" = "" ]; then
|
||||
echo "WARNING: ip validation disabled because package sipcalc not installed
|
||||
" >> $PWD/ipsec.log
|
||||
checkip=1
|
||||
fi
|
||||
|
||||
echo "#!/usr/sbin/setkey -f
|
||||
# Flush the SAD and SPD
|
||||
flush;
|
||||
spdflush;
|
||||
|
||||
# Security policies " > $PWD/ipsec.conf
|
||||
i=0
|
||||
for hostpair in $tunnelhosts; do
|
||||
i=`expr $i + 1`
|
||||
# parse tunnel host IP
|
||||
thishost=$${}{hostpair%%AND*}
|
||||
peerhost=$${}{hostpair##*AND}
|
||||
if [ $checkip = "0" ] &&
|
||||
[ "$(sipcalc "$thishost" "$peerhost" | grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalid host address $thishost or $peerhost " >> $PWD/ipsec.log
|
||||
fi
|
||||
# parse each tunnel addresses
|
||||
tunnel_list_var_name=T$i
|
||||
eval tunnels="$"$tunnel_list_var_name""
|
||||
for ttunnel in $tunnels; do
|
||||
lclnet=$${}{ttunnel%%AND*}
|
||||
rmtnet=$${}{ttunnel##*AND}
|
||||
if [ $checkip = "0" ] &&
|
||||
[ "$(sipcalc "$lclnet" "$rmtnet"| grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalid tunnel address $lclnet and $rmtnet " >> $PWD/ipsec.log
|
||||
fi
|
||||
# add tunnel policies
|
||||
echo "
|
||||
spdadd $lclnet $rmtnet any -P out ipsec
|
||||
esp/tunnel/$thishost-$peerhost/require;
|
||||
spdadd $rmtnet $lclnet any -P in ipsec
|
||||
esp/tunnel/$peerhost-$thishost/require; " >> $PWD/ipsec.conf
|
||||
done
|
||||
done
|
||||
|
||||
echo "building config $PWD/racoon.conf..."
|
||||
if [ ! -e $keydir\/$certname.key ] || [ ! -e $keydir\/$certname.pem ]; then
|
||||
echo "ERROR: missing certification files under $keydir $certname.key or $certname.pem " >> $PWD/ipsec.log
|
||||
fi
|
||||
echo "
|
||||
path certificate \"$keydir\";
|
||||
listen {
|
||||
adminsock disabled;
|
||||
}
|
||||
remote anonymous
|
||||
{
|
||||
exchange_mode main;
|
||||
certificate_type x509 \"$certname.pem\" \"$certname.key\";
|
||||
ca_type x509 \"ca-cert.pem\";
|
||||
my_identifier asn1dn;
|
||||
peers_identifier asn1dn;
|
||||
|
||||
proposal {
|
||||
encryption_algorithm 3des ;
|
||||
hash_algorithm sha1;
|
||||
authentication_method rsasig ;
|
||||
dh_group modp768;
|
||||
}
|
||||
}
|
||||
sainfo anonymous
|
||||
{
|
||||
pfs_group modp768;
|
||||
lifetime time 1 hour ;
|
||||
encryption_algorithm 3des, blowfish 448, rijndael ;
|
||||
authentication_algorithm hmac_sha1, hmac_md5 ;
|
||||
compression_algorithm deflate ;
|
||||
}
|
||||
" > $PWD/racoon.conf
|
||||
|
||||
# the setkey program is required from the ipsec-tools package
|
||||
echo "running setkey -f $PWD/ipsec.conf..."
|
||||
setkey -f $PWD/ipsec.conf
|
||||
|
||||
echo "running racoon -d -f $PWD/racoon.conf..."
|
||||
racoon -d -f $PWD/racoon.conf -l racoon.log
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
# generated by security.py
|
||||
# NAT out the first interface by default
|
||||
% for index, ifname in enumerate(ifnames):
|
||||
% if index == 0:
|
||||
iptables -t nat -A POSTROUTING -o ${ifname} -j MASQUERADE
|
||||
iptables -A FORWARD -i ${ifname} -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
iptables -A FORWARD -i ${ifname} -j DROP
|
||||
% else:
|
||||
# iptables -t nat -A POSTROUTING -o ${ifname} -j MASQUERADE
|
||||
# iptables -A FORWARD -i ${ifname} -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# iptables -A FORWARD -i ${ifname} -j DROP
|
||||
% endif
|
||||
% endfor
|
|
@ -0,0 +1,61 @@
|
|||
# -------- CUSTOMIZATION REQUIRED --------
|
||||
#
|
||||
# The VPNClient service builds a VPN tunnel to the specified VPN server using
|
||||
# OpenVPN software and a virtual TUN/TAP device.
|
||||
|
||||
# directory containing the certificate and key described below
|
||||
keydir=${config["keydir"]}
|
||||
|
||||
# the name used for a "$keyname.crt" certificate and "$keyname.key" private key.
|
||||
keyname=${config["keyname"]}
|
||||
|
||||
# the public IP address of the VPN server this client should connect with
|
||||
vpnserver=${config["server"]}
|
||||
|
||||
# optional next hop for adding a static route to reach the VPN server
|
||||
#nexthop="10.0.1.1"
|
||||
|
||||
# --------- END CUSTOMIZATION --------
|
||||
|
||||
# validate addresses
|
||||
if [ "$(dpkg -l | grep " sipcalc ")" = "" ]; then
|
||||
echo "WARNING: ip validation disabled because package sipcalc not installed
|
||||
" > $PWD/vpnclient.log
|
||||
else
|
||||
if [ "$(sipcalc "$vpnserver" "$nexthop" | grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalide address $vpnserver or $nexthop " > $PWD/vpnclient.log
|
||||
fi
|
||||
fi
|
||||
|
||||
# validate key and certification files
|
||||
if [ ! -e $keydir\/$keyname.key ] || [ ! -e $keydir\/$keyname.crt ] \
|
||||
|| [ ! -e $keydir\/ca.crt ] || [ ! -e $keydir\/dh1024.pem ]; then
|
||||
echo "ERROR: missing certification or key files under $keydir $keyname.key or $keyname.crt or ca.crt or dh1024.pem" >> $PWD/vpnclient.log
|
||||
fi
|
||||
|
||||
# if necessary, add a static route for reaching the VPN server IP via the IF
|
||||
vpnservernet=$${}{vpnserver%.*}.0/24
|
||||
if [ "$nexthop" != "" ]; then
|
||||
ip route add $vpnservernet via $nexthop
|
||||
fi
|
||||
|
||||
# create openvpn client.conf
|
||||
(
|
||||
cat << EOF
|
||||
client
|
||||
dev tun
|
||||
proto udp
|
||||
remote $vpnserver 1194
|
||||
nobind
|
||||
ca $keydir/ca.crt
|
||||
cert $keydir/$keyname.crt
|
||||
key $keydir/$keyname.key
|
||||
dh $keydir/dh1024.pem
|
||||
cipher AES-256-CBC
|
||||
log $PWD/openvpn-client.log
|
||||
verb 4
|
||||
daemon
|
||||
EOF
|
||||
) > client.conf
|
||||
|
||||
openvpn --config client.conf
|
|
@ -0,0 +1,147 @@
|
|||
# -------- CUSTOMIZATION REQUIRED --------
|
||||
#
|
||||
# The VPNServer service sets up the OpenVPN server for building VPN tunnels
|
||||
# that allow access via TUN/TAP device to private networks.
|
||||
#
|
||||
# note that the IPForward and DefaultRoute services should be enabled
|
||||
|
||||
# directory containing the certificate and key described below, in addition to
|
||||
# a CA certificate and DH key
|
||||
keydir=${config["keydir"]}
|
||||
|
||||
# the name used for a "$keyname.crt" certificate and "$keyname.key" private key.
|
||||
keyname=${config["keyname"]}
|
||||
|
||||
# the VPN subnet address from which the client VPN IP (for the TUN/TAP)
|
||||
# will be allocated
|
||||
vpnsubnet=${config["subnet"]}
|
||||
|
||||
# public IP address of this vpn server (same as VPNClient vpnserver= setting)
|
||||
vpnserver=${address}
|
||||
|
||||
# optional list of private subnets reachable behind this VPN server
|
||||
# each subnet and next hop is separated by a space
|
||||
# "<subnet1>,<nexthop1> <subnet2>,<nexthop2> ..."
|
||||
#privatenets="10.0.11.0,10.0.10.1 10.0.12.0,10.0.10.1"
|
||||
|
||||
# optional list of VPN clients, for statically assigning IP addresses to
|
||||
# clients; also, an optional client subnet can be specified for adding static
|
||||
# routes via the client
|
||||
# Note: VPN addresses x.x.x.0-3 are reserved
|
||||
# "<keyname>,<vpnIP>,<subnetIP> <keyname>,<vpnIP>,<subnetIP> ..."
|
||||
#vpnclients="client1KeyFilename,10.0.200.5,10.0.0.0 client2KeyFilename,,"
|
||||
|
||||
# NOTE: you may need to enable the StaticRoutes service on nodes within the
|
||||
# private subnet, in order to have routes back to the client.
|
||||
# /sbin/ip ro add <vpnsubnet>/24 via <vpnServerRemoteInterface>
|
||||
# /sbin/ip ro add <vpnClientSubnet>/24 via <vpnServerRemoteInterface>
|
||||
|
||||
# -------- END CUSTOMIZATION --------
|
||||
|
||||
echo > $PWD/vpnserver.log
|
||||
rm -f -r $PWD/ccd
|
||||
|
||||
# validate key and certification files
|
||||
if [ ! -e $keydir\/$keyname.key ] || [ ! -e $keydir\/$keyname.crt ] \
|
||||
|| [ ! -e $keydir\/ca.crt ] || [ ! -e $keydir\/dh1024.pem ]; then
|
||||
echo "ERROR: missing certification or key files under $keydir \
|
||||
$keyname.key or $keyname.crt or ca.crt or dh1024.pem" >> $PWD/vpnserver.log
|
||||
fi
|
||||
|
||||
# validate configuration IP addresses
|
||||
checkip=0
|
||||
if [ "$(dpkg -l | grep " sipcalc ")" = "" ]; then
|
||||
echo "WARNING: ip validation disabled because package sipcalc not installed\
|
||||
" >> $PWD/vpnserver.log
|
||||
checkip=1
|
||||
else
|
||||
if [ "$(sipcalc "$vpnsubnet" "$vpnserver" | grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalid vpn subnet or server address \
|
||||
$vpnsubnet or $vpnserver " >> $PWD/vpnserver.log
|
||||
fi
|
||||
fi
|
||||
|
||||
# create client vpn ip pool file
|
||||
(
|
||||
cat << EOF
|
||||
EOF
|
||||
)> $PWD/ippool.txt
|
||||
|
||||
# create server.conf file
|
||||
(
|
||||
cat << EOF
|
||||
# openvpn server config
|
||||
local $vpnserver
|
||||
server $vpnsubnet 255.255.255.0
|
||||
push "redirect-gateway def1"
|
||||
EOF
|
||||
)> $PWD/server.conf
|
||||
|
||||
# add routes to VPN server private subnets, and push these routes to clients
|
||||
for privatenet in $privatenets; do
|
||||
if [ $privatenet != "" ]; then
|
||||
net=$${}{privatenet%%,*}
|
||||
nexthop=$${}{privatenet##*,}
|
||||
if [ $checkip = "0" ] &&
|
||||
[ "$(sipcalc "$net" "$nexthop" | grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalid vpn server private net address \
|
||||
$net or $nexthop " >> $PWD/vpnserver.log
|
||||
fi
|
||||
echo push route $net 255.255.255.0 >> $PWD/server.conf
|
||||
ip ro add $net/24 via $nexthop
|
||||
ip ro add $vpnsubnet/24 via $nexthop
|
||||
fi
|
||||
done
|
||||
|
||||
# allow subnet through this VPN, one route for each client subnet
|
||||
for client in $vpnclients; do
|
||||
if [ $client != "" ]; then
|
||||
cSubnetIP=$${}{client##*,}
|
||||
cVpnIP=$${}{client#*,}
|
||||
cVpnIP=$${}{cVpnIP%%,*}
|
||||
cKeyFilename=$${}{client%%,*}
|
||||
if [ "$cSubnetIP" != "" ]; then
|
||||
if [ $checkip = "0" ] &&
|
||||
[ "$(sipcalc "$cSubnetIP" "$cVpnIP" | grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalid vpn client and subnet address \
|
||||
$cSubnetIP or $cVpnIP " >> $PWD/vpnserver.log
|
||||
fi
|
||||
echo route $cSubnetIP 255.255.255.0 >> $PWD/server.conf
|
||||
if ! test -d $PWD/ccd; then
|
||||
mkdir -p $PWD/ccd
|
||||
echo client-config-dir $PWD/ccd >> $PWD/server.conf
|
||||
fi
|
||||
if test -e $PWD/ccd/$cKeyFilename; then
|
||||
echo iroute $cSubnetIP 255.255.255.0 >> $PWD/ccd/$cKeyFilename
|
||||
else
|
||||
echo iroute $cSubnetIP 255.255.255.0 > $PWD/ccd/$cKeyFilename
|
||||
fi
|
||||
fi
|
||||
if [ "$cVpnIP" != "" ]; then
|
||||
echo $cKeyFilename,$cVpnIP >> $PWD/ippool.txt
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
(
|
||||
cat << EOF
|
||||
keepalive 10 120
|
||||
ca $keydir/ca.crt
|
||||
cert $keydir/$keyname.crt
|
||||
key $keydir/$keyname.key
|
||||
dh $keydir/dh1024.pem
|
||||
cipher AES-256-CBC
|
||||
status /var/log/openvpn-status.log
|
||||
log /var/log/openvpn-server.log
|
||||
ifconfig-pool-linear
|
||||
ifconfig-pool-persist $PWD/ippool.txt
|
||||
port 1194
|
||||
proto udp
|
||||
dev tun
|
||||
verb 4
|
||||
daemon
|
||||
EOF
|
||||
)>> $PWD/server.conf
|
||||
|
||||
# start vpn server
|
||||
openvpn --config server.conf
|
47
daemon/core/configservices/simpleservice.py
Normal file
47
daemon/core/configservices/simpleservice.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
||||
class SimpleService(ConfigService):
|
||||
name = "Simple"
|
||||
group = "SimpleGroup"
|
||||
directories = ["/etc/quagga", "/usr/local/lib"]
|
||||
files = ["test1.sh", "test2.sh"]
|
||||
executables = []
|
||||
dependencies = []
|
||||
startup = []
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = [
|
||||
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
|
||||
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
|
||||
Configuration(
|
||||
_id="value3",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Multiple Choice",
|
||||
options=["value1", "value2", "value3"],
|
||||
),
|
||||
]
|
||||
modes = {
|
||||
"mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
|
||||
"mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
|
||||
"mode3": {"value1": "value3", "value2": "0", "value3": "value1"},
|
||||
}
|
||||
|
||||
def get_text_template(self, name: str) -> str:
|
||||
if name == "test1.sh":
|
||||
return """
|
||||
# sample script 1
|
||||
# node id(${node.id}) name(${node.name})
|
||||
# config: ${config}
|
||||
echo hello
|
||||
"""
|
||||
elif name == "test2.sh":
|
||||
return """
|
||||
# sample script 2
|
||||
# node id(${node.id}) name(${node.name})
|
||||
# config: ${config}
|
||||
echo hello2
|
||||
"""
|
0
daemon/core/configservices/utilservices/__init__.py
Normal file
0
daemon/core/configservices/utilservices/__init__.py
Normal file
302
daemon/core/configservices/utilservices/services.py
Normal file
302
daemon/core/configservices/utilservices/services.py
Normal file
|
@ -0,0 +1,302 @@
|
|||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
|
||||
GROUP_NAME = "Utility"
|
||||
|
||||
|
||||
class DefaultRouteService(ConfigService):
|
||||
name = "DefaultRoute"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["defaultroute.sh"]
|
||||
executables = ["ip"]
|
||||
dependencies = []
|
||||
startup = ["sh defaultroute.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
addresses = []
|
||||
for netif in self.node.netifs():
|
||||
if getattr(netif, "control", False):
|
||||
continue
|
||||
for addr in netif.addrlist:
|
||||
logging.info("default route address: %s", addr)
|
||||
net = netaddr.IPNetwork(addr)
|
||||
if net[1] != net[-2]:
|
||||
addresses.append(net[1])
|
||||
return dict(addresses=addresses)
|
||||
|
||||
|
||||
class DefaultMulticastRouteService(ConfigService):
|
||||
name = "DefaultMulticastRoute"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["defaultmroute.sh"]
|
||||
executables = []
|
||||
dependencies = []
|
||||
startup = ["sh defaultmroute.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifname = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifname = ifc.name
|
||||
break
|
||||
return dict(ifname=ifname)
|
||||
|
||||
|
||||
class StaticRouteService(ConfigService):
|
||||
name = "StaticRoute"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["staticroute.sh"]
|
||||
executables = []
|
||||
dependencies = []
|
||||
startup = ["sh staticroute.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
routes = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv6(addr):
|
||||
dst = "3ffe:4::/64"
|
||||
else:
|
||||
dst = "10.9.8.0/24"
|
||||
net = netaddr.IPNetwork(x)
|
||||
if net[-2] != net[1]:
|
||||
routes.append((dst, net[1]))
|
||||
return dict(routes=routes)
|
||||
|
||||
|
||||
class IpForwardService(ConfigService):
|
||||
name = "IPForward"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["ipforward.sh"]
|
||||
executables = ["sysctl"]
|
||||
dependencies = []
|
||||
startup = ["sh ipforward.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
devnames = []
|
||||
for ifc in self.node.netifs():
|
||||
devname = utils.sysctl_devname(ifc.name)
|
||||
devnames.append(devname)
|
||||
return dict(devnames=devnames)
|
||||
|
||||
|
||||
class SshService(ConfigService):
|
||||
name = "SSH"
|
||||
group = GROUP_NAME
|
||||
directories = ["/etc/ssh", "/var/run/sshd"]
|
||||
files = ["startsshd.sh", "/etc/ssh/sshd_config"]
|
||||
executables = ["sshd"]
|
||||
dependencies = []
|
||||
startup = ["sh startsshd.sh"]
|
||||
validate = []
|
||||
shutdown = ["killall sshd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
return dict(
|
||||
sshcfgdir=self.directories[0],
|
||||
sshstatedir=self.directories[1],
|
||||
sshlibdir="/usr/lib/openssh",
|
||||
)
|
||||
|
||||
|
||||
class DhcpService(ConfigService):
|
||||
name = "DHCP"
|
||||
group = GROUP_NAME
|
||||
directories = ["/etc/dhcp", "/var/lib/dhcp"]
|
||||
files = ["/etc/dhcp/dhcpd.conf"]
|
||||
executables = ["dhcpd"]
|
||||
dependencies = []
|
||||
startup = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
|
||||
validate = ["pidof dhcpd"]
|
||||
shutdown = ["killall dhcpd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
subnets = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
net = netaddr.IPNetwork(x)
|
||||
# divide the address space in half
|
||||
index = (net.size - 2) / 2
|
||||
rangelow = net[index]
|
||||
rangehigh = net[-2]
|
||||
subnets.append((net.ip, net.netmask, rangelow, rangehigh, addr))
|
||||
return dict(subnets=subnets)
|
||||
|
||||
|
||||
class DhcpClientService(ConfigService):
|
||||
name = "DHCPClient"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["startdhcpclient.sh"]
|
||||
executables = ["dhclient"]
|
||||
dependencies = []
|
||||
startup = ["sh startdhcpclient.sh"]
|
||||
validate = ["pidof dhclient"]
|
||||
shutdown = ["killall dhclient"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class FtpService(ConfigService):
|
||||
name = "FTP"
|
||||
group = GROUP_NAME
|
||||
directories = ["/var/run/vsftpd/empty", "/var/ftp"]
|
||||
files = ["vsftpd.conf"]
|
||||
executables = ["vsftpd"]
|
||||
dependencies = []
|
||||
startup = ["vsftpd ./vsftpd.conf"]
|
||||
validate = ["pidof vsftpd"]
|
||||
shutdown = ["killall vsftpd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class PcapService(ConfigService):
|
||||
name = "pcap"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["pcap.sh"]
|
||||
executables = ["tcpdump"]
|
||||
dependencies = []
|
||||
startup = ["sh pcap.sh start"]
|
||||
validate = ["pidof tcpdump"]
|
||||
shutdown = ["sh pcap.sh stop"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class RadvdService(ConfigService):
|
||||
name = "radvd"
|
||||
group = GROUP_NAME
|
||||
directories = ["/etc/radvd"]
|
||||
files = ["/etc/radvd/radvd.conf"]
|
||||
executables = ["radvd"]
|
||||
dependencies = []
|
||||
startup = ["radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"]
|
||||
validate = ["pidof radvd"]
|
||||
shutdown = ["pkill radvd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
prefixes = []
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv6(addr):
|
||||
prefixes.append(x)
|
||||
if not prefixes:
|
||||
continue
|
||||
interfaces.append((ifc.name, prefixes))
|
||||
return dict(interfaces=interfaces)
|
||||
|
||||
|
||||
class AtdService(ConfigService):
|
||||
name = "atd"
|
||||
group = GROUP_NAME
|
||||
directories = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
|
||||
files = ["startatd.sh"]
|
||||
executables = ["atd"]
|
||||
dependencies = []
|
||||
startup = ["sh startatd.sh"]
|
||||
validate = ["pidof atd"]
|
||||
shutdown = ["pkill atd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class HttpService(ConfigService):
|
||||
name = "HTTP"
|
||||
group = GROUP_NAME
|
||||
directories = [
|
||||
"/etc/apache2",
|
||||
"/var/run/apache2",
|
||||
"/var/log/apache2",
|
||||
"/run/lock",
|
||||
"/var/lock/apache2",
|
||||
"/var/www",
|
||||
]
|
||||
files = ["/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html"]
|
||||
executables = ["apache2ctl"]
|
||||
dependencies = []
|
||||
startup = ["chown www-data /var/lock/apache2", "apache2ctl start"]
|
||||
validate = ["pidof apache2"]
|
||||
shutdown = ["apache2ctl stop"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
interfaces.append(ifc)
|
||||
return dict(interfaces=interfaces)
|
102
daemon/core/configservices/utilservices/templates/apache2.conf
Normal file
102
daemon/core/configservices/utilservices/templates/apache2.conf
Normal file
|
@ -0,0 +1,102 @@
|
|||
# apache2.conf generated by utility.py:HttpService
|
||||
Mutex file:$APACHE_LOCK_DIR default
|
||||
|
||||
PidFile $APACHE_PID_FILE
|
||||
Timeout 300
|
||||
KeepAlive On
|
||||
MaxKeepAliveRequests 100
|
||||
KeepAliveTimeout 5
|
||||
|
||||
LoadModule mpm_worker_module /usr/lib/apache2/modules/mod_mpm_worker.so
|
||||
|
||||
<IfModule mpm_prefork_module>
|
||||
StartServers 5
|
||||
MinSpareServers 5
|
||||
MaxSpareServers 10
|
||||
MaxClients 150
|
||||
MaxRequestsPerChild 0
|
||||
</IfModule>
|
||||
|
||||
<IfModule mpm_worker_module>
|
||||
StartServers 2
|
||||
MinSpareThreads 25
|
||||
MaxSpareThreads 75
|
||||
ThreadLimit 64
|
||||
ThreadsPerChild 25
|
||||
MaxClients 150
|
||||
MaxRequestsPerChild 0
|
||||
</IfModule>
|
||||
|
||||
<IfModule mpm_event_module>
|
||||
StartServers 2
|
||||
MinSpareThreads 25
|
||||
MaxSpareThreads 75
|
||||
ThreadLimit 64
|
||||
ThreadsPerChild 25
|
||||
MaxClients 150
|
||||
MaxRequestsPerChild 0
|
||||
</IfModule>
|
||||
|
||||
User $APACHE_RUN_USER
|
||||
Group $APACHE_RUN_GROUP
|
||||
|
||||
AccessFileName .htaccess
|
||||
|
||||
<Files ~ "^\\.ht">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
DefaultType None
|
||||
|
||||
HostnameLookups Off
|
||||
|
||||
ErrorLog $APACHE_LOG_DIR/error.log
|
||||
LogLevel warn
|
||||
|
||||
#Include mods-enabled/*.load
|
||||
#Include mods-enabled/*.conf
|
||||
LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so
|
||||
LoadModule auth_basic_module /usr/lib/apache2/modules/mod_auth_basic.so
|
||||
LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so
|
||||
LoadModule authz_host_module /usr/lib/apache2/modules/mod_authz_host.so
|
||||
LoadModule authz_user_module /usr/lib/apache2/modules/mod_authz_user.so
|
||||
LoadModule autoindex_module /usr/lib/apache2/modules/mod_autoindex.so
|
||||
LoadModule dir_module /usr/lib/apache2/modules/mod_dir.so
|
||||
LoadModule env_module /usr/lib/apache2/modules/mod_env.so
|
||||
|
||||
NameVirtualHost *:80
|
||||
Listen 80
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
Listen 443
|
||||
</IfModule>
|
||||
<IfModule mod_gnutls.c>
|
||||
Listen 443
|
||||
</IfModule>
|
||||
|
||||
LogFormat "%v:%p %h %l %u %t \\"%r\\" %>s %O \\"%{Referer}i\\" \\"%{User-Agent}i\\"" vhost_combined
|
||||
LogFormat "%h %l %u %t \\"%r\\" %>s %O \\"%{Referer}i\\" \\"%{User-Agent}i\\"" combined
|
||||
LogFormat "%h %l %u %t \\"%r\\" %>s %O" common
|
||||
LogFormat "%{Referer}i -> %U" referer
|
||||
LogFormat "%{User-agent}i" agent
|
||||
|
||||
ServerTokens OS
|
||||
ServerSignature On
|
||||
TraceEnable Off
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www
|
||||
<Directory />
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
</Directory>
|
||||
<Directory /var/www/>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
ErrorLog $APACHE_LOG_DIR/error.log
|
||||
LogLevel warn
|
||||
CustomLog $APACHE_LOG_DIR/access.log combined
|
||||
</VirtualHost>
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by DefaultMulticastRoute service (utility.py)
|
||||
# the first interface is chosen below; please change it as needed
|
||||
ip route add 224.0.0.0/4 dev ${ifname}
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by DefaultRoute service
|
||||
% for address in addresses:
|
||||
ip route add default via ${address}
|
||||
% endfor
|
22
daemon/core/configservices/utilservices/templates/dhcpd.conf
Normal file
22
daemon/core/configservices/utilservices/templates/dhcpd.conf
Normal file
|
@ -0,0 +1,22 @@
|
|||
# auto-generated by DHCP service (utility.py)
|
||||
# NOTE: move these option lines into the desired pool { } block(s) below
|
||||
#option domain-name "test.com";
|
||||
#option domain-name-servers 10.0.0.1;
|
||||
#option routers 10.0.0.1;
|
||||
|
||||
log-facility local6;
|
||||
|
||||
default-lease-time 600;
|
||||
max-lease-time 7200;
|
||||
|
||||
ddns-update-style none;
|
||||
|
||||
% for subnet, netmask, rangelow, rangehigh, addr in subnets:
|
||||
subnet ${subnet} netmask ${netmask} {
|
||||
pool {
|
||||
range ${rangelow} ${rangehigh};
|
||||
default-lease-time 600;
|
||||
option routers ${addr};
|
||||
}
|
||||
}
|
||||
% endfor
|
10
daemon/core/configservices/utilservices/templates/envvars
Normal file
10
daemon/core/configservices/utilservices/templates/envvars
Normal file
|
@ -0,0 +1,10 @@
|
|||
# this file is used by apache2ctl - generated by utility.py:HttpService
|
||||
# these settings come from a default Ubuntu apache2 installation
|
||||
export APACHE_RUN_USER=www-data
|
||||
export APACHE_RUN_GROUP=www-data
|
||||
export APACHE_PID_FILE=/var/run/apache2.pid
|
||||
export APACHE_RUN_DIR=/var/run/apache2
|
||||
export APACHE_LOCK_DIR=/var/lock/apache2
|
||||
export APACHE_LOG_DIR=/var/log/apache2
|
||||
export LANG=C
|
||||
export LANG
|
13
daemon/core/configservices/utilservices/templates/index.html
Normal file
13
daemon/core/configservices/utilservices/templates/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!-- generated by utility.py:HttpService -->
|
||||
<html>
|
||||
<body>
|
||||
<h1>${node.name} web server</h1>
|
||||
<p>This is the default web page for this server.</p>
|
||||
<p>The web server software is running but no content has been added, yet.</p>
|
||||
<ul>
|
||||
% for ifc in interfaces:
|
||||
<li>${ifc.name} - ${ifc.addrlist}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by IPForward service (utility.py)
|
||||
sysctl -w net.ipv4.conf.all.forwarding=1
|
||||
sysctl -w net.ipv4.conf.default.forwarding=1
|
||||
sysctl -w net.ipv6.conf.all.forwarding=1
|
||||
sysctl -w net.ipv6.conf.default.forwarding=1
|
||||
sysctl -w net.ipv4.conf.all.send_redirects=0
|
||||
sysctl -w net.ipv4.conf.default.send_redirects=0
|
||||
sysctl -w net.ipv4.conf.all.rp_filter=0
|
||||
sysctl -w net.ipv4.conf.default.rp_filter=0
|
||||
# setup forwarding for node interfaces
|
||||
% for devname in devnames:
|
||||
sysctl -w net.ipv4.conf.${devname}.forwarding=1
|
||||
sysctl -w net.ipv4.conf.${devname}.send_redirects=0
|
||||
sysctl -w net.ipv4.conf.${devname}.rp_filter=0
|
||||
% endfor
|
11
daemon/core/configservices/utilservices/templates/pcap.sh
Normal file
11
daemon/core/configservices/utilservices/templates/pcap.sh
Normal file
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
# set tcpdump options here (see 'man tcpdump' for help)
|
||||
# (-s snap length, -C limit pcap file length, -n disable name resolution)
|
||||
if [ "x$1" = "xstart" ]; then
|
||||
% for ifname in ifnames:
|
||||
tcpdump -s 12288 -C 10 -n -w ${node.name}.${ifname}.pcap -i ${ifname} < /dev/null &
|
||||
% endfor
|
||||
elif [ "x$1" = "xstop" ]; then
|
||||
mkdir -p $SESSION_DIR/pcap
|
||||
mv *.pcap $SESSION_DIR/pcap
|
||||
fi;
|
19
daemon/core/configservices/utilservices/templates/radvd.conf
Normal file
19
daemon/core/configservices/utilservices/templates/radvd.conf
Normal file
|
@ -0,0 +1,19 @@
|
|||
# auto-generated by RADVD service (utility.py)
|
||||
% for ifname, prefixes in values:
|
||||
interface ${ifname}
|
||||
{
|
||||
AdvSendAdvert on;
|
||||
MinRtrAdvInterval 3;
|
||||
MaxRtrAdvInterval 10;
|
||||
AdvDefaultPreference low;
|
||||
AdvHomeAgentFlag off;
|
||||
% for prefix in prefixes:
|
||||
prefix ${prefix}
|
||||
{
|
||||
AdvOnLink on;
|
||||
AdvAutonomous on;
|
||||
AdvRouterAddr on;
|
||||
};
|
||||
% endfor
|
||||
};
|
||||
% endfor
|
|
@ -0,0 +1,37 @@
|
|||
# auto-generated by SSH service (utility.py)
|
||||
Port 22
|
||||
Protocol 2
|
||||
HostKey ${sshcfgdir}/ssh_host_rsa_key
|
||||
UsePrivilegeSeparation yes
|
||||
PidFile ${sshstatedir}/sshd.pid
|
||||
|
||||
KeyRegenerationInterval 3600
|
||||
ServerKeyBits 768
|
||||
|
||||
SyslogFacility AUTH
|
||||
LogLevel INFO
|
||||
|
||||
LoginGraceTime 120
|
||||
PermitRootLogin yes
|
||||
StrictModes yes
|
||||
|
||||
RSAAuthentication yes
|
||||
PubkeyAuthentication yes
|
||||
|
||||
IgnoreRhosts yes
|
||||
RhostsRSAAuthentication no
|
||||
HostbasedAuthentication no
|
||||
|
||||
PermitEmptyPasswords no
|
||||
ChallengeResponseAuthentication no
|
||||
|
||||
X11Forwarding yes
|
||||
X11DisplayOffset 10
|
||||
PrintMotd no
|
||||
PrintLastLog yes
|
||||
TCPKeepAlive yes
|
||||
|
||||
AcceptEnv LANG LC_*
|
||||
Subsystem sftp ${sshlibdir}/sftp-server
|
||||
UsePAM yes
|
||||
UseDNS no
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
echo 00001 > /var/spool/cron/atjobs/.SEQ
|
||||
chown -R daemon /var/spool/cron/*
|
||||
chmod -R 700 /var/spool/cron/*
|
||||
atd
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by DHCPClient service (utility.py)
|
||||
# uncomment this mkdir line and symlink line to enable client-side DNS\n# resolution based on the DHCP server response.
|
||||
#mkdir -p /var/run/resolvconf/interface
|
||||
% for ifname in ifnames:
|
||||
#ln -s /var/run/resolvconf/interface/${ifname}.dhclient /var/run/resolvconf/resolv.conf
|
||||
dhclient -nw -pf /var/run/dhclient-${ifname}.pid -lf /var/run/dhclient-${ifname}.lease ${ifname}
|
||||
% endfor
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by SSH service (utility.py)
|
||||
ssh-keygen -q -t rsa -N "" -f ${sshcfgdir}/ssh_host_rsa_key
|
||||
chmod 655 ${sshstatedir}
|
||||
# wait until RSA host key has been generated to launch sshd
|
||||
$(which sshd) -f ${sshcfgdir}/sshd_config
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by StaticRoute service (utility.py)
|
||||
# NOTE: this service must be customized to be of any use
|
||||
# Below are samples that you can uncomment and edit.
|
||||
% for dest, addr in routes:
|
||||
#ip route add ${dest} via ${addr}
|
||||
% endfor
|
|
@ -0,0 +1,12 @@
|
|||
# vsftpd.conf auto-generated by FTP service (utility.py)
|
||||
listen=YES
|
||||
anonymous_enable=YES
|
||||
local_enable=YES
|
||||
dirmessage_enable=YES
|
||||
use_localtime=YES
|
||||
xferlog_enable=YES
|
||||
connect_from_port_20=YES
|
||||
xferlog_file=/var/log/vsftpd.log
|
||||
ftpd_banner=Welcome to the CORE FTP service
|
||||
secure_chroot_dir=/var/run/vsftpd/empty
|
||||
anon_root=/var/ftp
|
|
@ -1,7 +1,8 @@
|
|||
"""
|
||||
EMANE Bypass model for CORE
|
||||
"""
|
||||
from core.config import ConfigGroup, Configuration
|
||||
|
||||
from core.config import Configuration
|
||||
from core.emane import emanemodel
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
@ -19,7 +20,6 @@ class EmaneBypassModel(emanemodel.EmaneModel):
|
|||
_id="none",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
options=["True", "False"],
|
||||
label="There are no parameters for the bypass model.",
|
||||
)
|
||||
]
|
||||
|
@ -29,11 +29,6 @@ class EmaneBypassModel(emanemodel.EmaneModel):
|
|||
phy_config = []
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix):
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
# ignore default logic
|
||||
pass
|
||||
|
||||
# override config groups
|
||||
@classmethod
|
||||
def config_groups(cls):
|
||||
return [ConfigGroup("Bypass Parameters", 1, 1)]
|
||||
|
|
|
@ -4,11 +4,13 @@ commeffect.py: EMANE CommEffect model for CORE
|
|||
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, List
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from core.config import ConfigGroup
|
||||
from core.config import ConfigGroup, Configuration
|
||||
from core.emane import emanemanifest, emanemodel
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.xml import emanexml
|
||||
|
||||
try:
|
||||
|
@ -20,7 +22,7 @@ except ImportError:
|
|||
logging.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
def convert_none(x):
|
||||
def convert_none(x: float) -> int:
|
||||
"""
|
||||
Helper to use 0 for None values.
|
||||
"""
|
||||
|
@ -45,26 +47,28 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
external_config = []
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix):
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
shim_xml_path = os.path.join(emane_prefix, "share/emane/manifest", cls.shim_xml)
|
||||
cls.config_shim = emanemanifest.parse(shim_xml_path, cls.shim_defaults)
|
||||
|
||||
@classmethod
|
||||
def configurations(cls):
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
return cls.config_shim
|
||||
|
||||
@classmethod
|
||||
def config_groups(cls):
|
||||
def config_groups(cls) -> List[ConfigGroup]:
|
||||
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
|
||||
|
||||
def build_xml_files(self, config, interface=None):
|
||||
def build_xml_files(
|
||||
self, config: Dict[str, str], interface: CoreInterface = None
|
||||
) -> None:
|
||||
"""
|
||||
Build the necessary nem and commeffect XMLs in the given path.
|
||||
If an individual NEM has a nonstandard config, we need to build
|
||||
that file also. Otherwise the WLAN-wide
|
||||
nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used.
|
||||
|
||||
:param dict 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
|
||||
:return: nothing
|
||||
"""
|
||||
|
@ -109,14 +113,14 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
|
||||
def linkconfig(
|
||||
self,
|
||||
netif,
|
||||
bw=None,
|
||||
delay=None,
|
||||
loss=None,
|
||||
duplicate=None,
|
||||
jitter=None,
|
||||
netif2=None,
|
||||
):
|
||||
netif: CoreInterface,
|
||||
bw: float = None,
|
||||
delay: float = None,
|
||||
loss: float = None,
|
||||
duplicate: float = None,
|
||||
jitter: float = None,
|
||||
netif2: CoreInterface = None,
|
||||
) -> None:
|
||||
"""
|
||||
Generate CommEffect events when a Link Message is received having
|
||||
link parameters.
|
||||
|
|
|
@ -6,6 +6,7 @@ import logging
|
|||
import os
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, Tuple, Type
|
||||
|
||||
from core import utils
|
||||
from core.config import ConfigGroup, Configuration, ModelManager
|
||||
|
@ -19,8 +20,15 @@ from core.emane.rfpipe import EmaneRfPipeModel
|
|||
from core.emane.tdma import EmaneTdmaModel
|
||||
from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import CtrlNet
|
||||
from core.xml import emanexml
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
||||
try:
|
||||
from emane.events import EventService
|
||||
from emane.events import LocationEvent
|
||||
|
@ -57,11 +65,11 @@ class EmaneManager(ModelManager):
|
|||
EVENTCFGVAR = "LIBEMANEEVENTSERVICECONFIG"
|
||||
DEFAULT_LOG_LEVEL = 3
|
||||
|
||||
def __init__(self, session):
|
||||
def __init__(self, session: "Session") -> None:
|
||||
"""
|
||||
Creates a Emane instance.
|
||||
|
||||
:param core.session.Session session: session this manager is tied to
|
||||
:param session: session this manager is tied to
|
||||
:return: nothing
|
||||
"""
|
||||
super().__init__()
|
||||
|
@ -83,18 +91,20 @@ class EmaneManager(ModelManager):
|
|||
self.set_configs(self.emane_config.default_values())
|
||||
|
||||
self.service = None
|
||||
self.eventchannel = None
|
||||
self.event_device = None
|
||||
self.emane_check()
|
||||
|
||||
def getifcconfig(self, node_id, interface, model_name):
|
||||
def getifcconfig(
|
||||
self, node_id: int, interface: CoreInterface, model_name: str
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Retrieve interface configuration or node configuration if not provided.
|
||||
|
||||
:param int node_id: node id
|
||||
:param node_id: node id
|
||||
:param interface: node interface
|
||||
:param str model_name: model to get configuration for
|
||||
:param model_name: model to get configuration for
|
||||
:return: node/interface model configuration
|
||||
:rtype: dict
|
||||
"""
|
||||
# use the network-wide config values or interface(NEM)-specific values?
|
||||
if interface is None:
|
||||
|
@ -129,11 +139,11 @@ class EmaneManager(ModelManager):
|
|||
|
||||
return config
|
||||
|
||||
def config_reset(self, node_id=None):
|
||||
def config_reset(self, node_id: int = None) -> None:
|
||||
super().config_reset(node_id)
|
||||
self.set_configs(self.emane_config.default_values())
|
||||
|
||||
def emane_check(self):
|
||||
def emane_check(self) -> None:
|
||||
"""
|
||||
Check if emane is installed and load models.
|
||||
|
||||
|
@ -157,7 +167,7 @@ class EmaneManager(ModelManager):
|
|||
except CoreCommandError:
|
||||
logging.info("emane is not installed")
|
||||
|
||||
def deleteeventservice(self):
|
||||
def deleteeventservice(self) -> None:
|
||||
if self.service:
|
||||
for fd in self.service._readFd, self.service._writeFd:
|
||||
if fd >= 0:
|
||||
|
@ -168,7 +178,7 @@ class EmaneManager(ModelManager):
|
|||
self.service = None
|
||||
self.event_device = None
|
||||
|
||||
def initeventservice(self, filename=None, shutdown=False):
|
||||
def initeventservice(self, filename: str = None, shutdown: bool = False) -> None:
|
||||
"""
|
||||
Re-initialize the EMANE Event service.
|
||||
The multicast group and/or port may be configured.
|
||||
|
@ -186,7 +196,7 @@ class EmaneManager(ModelManager):
|
|||
logging.error(
|
||||
"invalid emane event service device provided: %s", self.event_device
|
||||
)
|
||||
return False
|
||||
return
|
||||
|
||||
# make sure the event control network is in place
|
||||
eventnet = self.session.add_remove_control_net(
|
||||
|
@ -195,19 +205,17 @@ class EmaneManager(ModelManager):
|
|||
if eventnet is not None:
|
||||
# direct EMANE events towards control net bridge
|
||||
self.event_device = eventnet.brname
|
||||
eventchannel = (group, int(port), self.event_device)
|
||||
self.eventchannel = (group, int(port), self.event_device)
|
||||
|
||||
# disabled otachannel for event service
|
||||
# only needed for e.g. antennaprofile events xmit by models
|
||||
logging.info("using %s for event service traffic", self.event_device)
|
||||
try:
|
||||
self.service = EventService(eventchannel=eventchannel, otachannel=None)
|
||||
self.service = EventService(eventchannel=self.eventchannel, otachannel=None)
|
||||
except EventServiceException:
|
||||
logging.exception("error instantiating emane EventService")
|
||||
|
||||
return True
|
||||
|
||||
def load_models(self, emane_models):
|
||||
def load_models(self, emane_models: List[Type[EmaneModel]]) -> None:
|
||||
"""
|
||||
Load EMANE models and make them available.
|
||||
"""
|
||||
|
@ -219,11 +227,11 @@ class EmaneManager(ModelManager):
|
|||
emane_model.load(emane_prefix)
|
||||
self.models[emane_model.name] = emane_model
|
||||
|
||||
def add_node(self, emane_net):
|
||||
def add_node(self, emane_net: EmaneNet) -> None:
|
||||
"""
|
||||
Add EMANE network object to this manager.
|
||||
|
||||
:param core.emane.nodes.EmaneNet emane_net: emane node to add
|
||||
:param emane_net: emane node to add
|
||||
:return: nothing
|
||||
"""
|
||||
with self._emane_node_lock:
|
||||
|
@ -233,7 +241,7 @@ class EmaneManager(ModelManager):
|
|||
)
|
||||
self._emane_nets[emane_net.id] = emane_net
|
||||
|
||||
def getnodes(self):
|
||||
def getnodes(self) -> Set[CoreNode]:
|
||||
"""
|
||||
Return a set of CoreNodes that are linked to an EMANE network,
|
||||
e.g. containers having one or more radio interfaces.
|
||||
|
@ -245,13 +253,12 @@ class EmaneManager(ModelManager):
|
|||
nodes.add(netif.node)
|
||||
return nodes
|
||||
|
||||
def setup(self):
|
||||
def setup(self) -> int:
|
||||
"""
|
||||
Setup duties for EMANE manager.
|
||||
|
||||
:return: SUCCESS, NOT_NEEDED, NOT_READY in order to delay session
|
||||
instantiation
|
||||
:rtype: int
|
||||
"""
|
||||
logging.debug("emane setup")
|
||||
|
||||
|
@ -303,14 +310,13 @@ class EmaneManager(ModelManager):
|
|||
self.check_node_models()
|
||||
return EmaneManager.SUCCESS
|
||||
|
||||
def startup(self):
|
||||
def startup(self) -> int:
|
||||
"""
|
||||
After all the EMANE networks have been added, build XML files
|
||||
and start the daemons.
|
||||
|
||||
:return: SUCCESS, NOT_NEEDED, NOT_READY in order to delay session
|
||||
instantiation
|
||||
:rtype: int
|
||||
"""
|
||||
self.reset()
|
||||
r = self.setup()
|
||||
|
@ -347,7 +353,7 @@ class EmaneManager(ModelManager):
|
|||
|
||||
return EmaneManager.SUCCESS
|
||||
|
||||
def poststartup(self):
|
||||
def poststartup(self) -> None:
|
||||
"""
|
||||
Retransmit location events now that all NEMs are active.
|
||||
"""
|
||||
|
@ -367,7 +373,7 @@ class EmaneManager(ModelManager):
|
|||
x, y, z = netif.node.position.get()
|
||||
emane_node.setnemposition(netif, x, y, z)
|
||||
|
||||
def reset(self):
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Remove all EMANE networks from the dictionary, reset port numbers and
|
||||
nem id counters
|
||||
|
@ -382,7 +388,7 @@ class EmaneManager(ModelManager):
|
|||
"emane_transform_port", 8200
|
||||
)
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
stop all EMANE daemons
|
||||
"""
|
||||
|
@ -394,7 +400,7 @@ class EmaneManager(ModelManager):
|
|||
self.stopdaemons()
|
||||
self.stopeventmonitor()
|
||||
|
||||
def buildxml(self):
|
||||
def buildxml(self) -> None:
|
||||
"""
|
||||
Build XML files required to run EMANE on each node.
|
||||
NEMs run inside containers using the control network for passing
|
||||
|
@ -410,7 +416,7 @@ class EmaneManager(ModelManager):
|
|||
self.buildnemxml()
|
||||
self.buildeventservicexml()
|
||||
|
||||
def check_node_models(self):
|
||||
def check_node_models(self) -> None:
|
||||
"""
|
||||
Associate EMANE model classes with EMANE network nodes.
|
||||
"""
|
||||
|
@ -438,7 +444,7 @@ class EmaneManager(ModelManager):
|
|||
model_class = self.models[model_name]
|
||||
emane_node.setmodel(model_class, config)
|
||||
|
||||
def nemlookup(self, nemid):
|
||||
def nemlookup(self, nemid) -> Tuple[EmaneNet, CoreInterface]:
|
||||
"""
|
||||
Look for the given numerical NEM ID and return the first matching
|
||||
EMANE network and NEM interface.
|
||||
|
@ -456,7 +462,7 @@ class EmaneManager(ModelManager):
|
|||
|
||||
return emane_node, netif
|
||||
|
||||
def numnems(self):
|
||||
def numnems(self) -> int:
|
||||
"""
|
||||
Return the number of NEMs emulated locally.
|
||||
"""
|
||||
|
@ -466,7 +472,7 @@ class EmaneManager(ModelManager):
|
|||
count += len(emane_node.netifs())
|
||||
return count
|
||||
|
||||
def buildplatformxml(self, ctrlnet):
|
||||
def buildplatformxml(self, ctrlnet: CtrlNet) -> None:
|
||||
"""
|
||||
Build a platform.xml file now that all nodes are configured.
|
||||
"""
|
||||
|
@ -480,7 +486,7 @@ class EmaneManager(ModelManager):
|
|||
self, ctrlnet, emane_node, nemid, platform_xmls
|
||||
)
|
||||
|
||||
def buildnemxml(self):
|
||||
def buildnemxml(self) -> None:
|
||||
"""
|
||||
Builds the nem, mac, and phy xml files for each EMANE network.
|
||||
"""
|
||||
|
@ -488,7 +494,7 @@ class EmaneManager(ModelManager):
|
|||
emane_net = self._emane_nets[key]
|
||||
emanexml.build_xml_files(self, emane_net)
|
||||
|
||||
def buildeventservicexml(self):
|
||||
def buildeventservicexml(self) -> None:
|
||||
"""
|
||||
Build the libemaneeventservice.xml file if event service options
|
||||
were changed in the global config.
|
||||
|
@ -520,7 +526,7 @@ class EmaneManager(ModelManager):
|
|||
)
|
||||
)
|
||||
|
||||
def startdaemons(self):
|
||||
def startdaemons(self) -> None:
|
||||
"""
|
||||
Start one EMANE daemon per node having a radio.
|
||||
Add a control network even if the user has not configured one.
|
||||
|
@ -596,7 +602,7 @@ class EmaneManager(ModelManager):
|
|||
self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path))
|
||||
logging.info("host emane daemon running: %s", emanecmd)
|
||||
|
||||
def stopdaemons(self):
|
||||
def stopdaemons(self) -> None:
|
||||
"""
|
||||
Kill the appropriate EMANE daemons.
|
||||
"""
|
||||
|
@ -623,7 +629,7 @@ class EmaneManager(ModelManager):
|
|||
except CoreCommandError:
|
||||
logging.exception("error shutting down emane daemons")
|
||||
|
||||
def installnetifs(self):
|
||||
def installnetifs(self) -> None:
|
||||
"""
|
||||
Install TUN/TAP virtual interfaces into their proper namespaces
|
||||
now that the EMANE daemons are running.
|
||||
|
@ -633,7 +639,7 @@ class EmaneManager(ModelManager):
|
|||
logging.info("emane install netifs for node: %d", key)
|
||||
emane_node.installnetifs()
|
||||
|
||||
def deinstallnetifs(self):
|
||||
def deinstallnetifs(self) -> None:
|
||||
"""
|
||||
Uninstall TUN/TAP virtual interfaces.
|
||||
"""
|
||||
|
@ -641,7 +647,7 @@ class EmaneManager(ModelManager):
|
|||
emane_node = self._emane_nets[key]
|
||||
emane_node.deinstallnetifs()
|
||||
|
||||
def doeventmonitor(self):
|
||||
def doeventmonitor(self) -> bool:
|
||||
"""
|
||||
Returns boolean whether or not EMANE events will be monitored.
|
||||
"""
|
||||
|
@ -649,7 +655,7 @@ class EmaneManager(ModelManager):
|
|||
# generate the EMANE events when nodes are moved
|
||||
return self.session.options.get_config_bool("emane_event_monitor")
|
||||
|
||||
def genlocationevents(self):
|
||||
def genlocationevents(self) -> bool:
|
||||
"""
|
||||
Returns boolean whether or not EMANE events will be generated.
|
||||
"""
|
||||
|
@ -660,7 +666,7 @@ class EmaneManager(ModelManager):
|
|||
tmp = not self.doeventmonitor()
|
||||
return tmp
|
||||
|
||||
def starteventmonitor(self):
|
||||
def starteventmonitor(self) -> None:
|
||||
"""
|
||||
Start monitoring EMANE location events if configured to do so.
|
||||
"""
|
||||
|
@ -681,7 +687,7 @@ class EmaneManager(ModelManager):
|
|||
self.eventmonthread.daemon = True
|
||||
self.eventmonthread.start()
|
||||
|
||||
def stopeventmonitor(self):
|
||||
def stopeventmonitor(self) -> None:
|
||||
"""
|
||||
Stop monitoring EMANE location events.
|
||||
"""
|
||||
|
@ -697,7 +703,7 @@ class EmaneManager(ModelManager):
|
|||
self.eventmonthread.join()
|
||||
self.eventmonthread = None
|
||||
|
||||
def eventmonitorloop(self):
|
||||
def eventmonitorloop(self) -> None:
|
||||
"""
|
||||
Thread target that monitors EMANE location events.
|
||||
"""
|
||||
|
@ -724,7 +730,7 @@ class EmaneManager(ModelManager):
|
|||
threading.currentThread().getName(),
|
||||
)
|
||||
|
||||
def handlelocationevent(self, rxnemid, eid, data):
|
||||
def handlelocationevent(self, rxnemid: int, eid: int, data: str) -> None:
|
||||
"""
|
||||
Handle an EMANE location event.
|
||||
"""
|
||||
|
@ -747,7 +753,9 @@ class EmaneManager(ModelManager):
|
|||
logging.debug("emane location event: %s,%s,%s", lat, lon, alt)
|
||||
self.handlelocationeventtoxyz(txnemid, lat, lon, alt)
|
||||
|
||||
def handlelocationeventtoxyz(self, nemid, lat, lon, alt):
|
||||
def handlelocationeventtoxyz(
|
||||
self, nemid: int, lat: float, lon: float, alt: float
|
||||
) -> bool:
|
||||
"""
|
||||
Convert the (NEM ID, lat, long, alt) from a received location event
|
||||
into a node and x,y,z coordinate values, sending a Node Message.
|
||||
|
@ -800,11 +808,11 @@ class EmaneManager(ModelManager):
|
|||
|
||||
# don"t use node.setposition(x,y,z) which generates an event
|
||||
node.position.set(x, y, z)
|
||||
node_data = node.data(message_type=0, lat=str(lat), lon=str(lon), alt=str(alt))
|
||||
node_data = node.data(message_type=0, lat=lat, lon=lon, alt=alt)
|
||||
self.session.broadcast_node(node_data)
|
||||
return True
|
||||
|
||||
def emanerunning(self, node):
|
||||
def emanerunning(self, node: CoreNode) -> bool:
|
||||
"""
|
||||
Return True if an EMANE process associated with the given node is running,
|
||||
False otherwise.
|
||||
|
@ -827,7 +835,7 @@ class EmaneGlobalModel:
|
|||
name = "emane"
|
||||
bitmap = None
|
||||
|
||||
def __init__(self, session):
|
||||
def __init__(self, session: "Session") -> None:
|
||||
self.session = session
|
||||
self.nem_config = [
|
||||
Configuration(
|
||||
|
@ -840,7 +848,7 @@ class EmaneGlobalModel:
|
|||
self.emulator_config = None
|
||||
self.parse_config()
|
||||
|
||||
def parse_config(self):
|
||||
def parse_config(self) -> None:
|
||||
emane_prefix = self.session.options.get_config(
|
||||
"emane_prefix", default=DEFAULT_EMANE_PREFIX
|
||||
)
|
||||
|
@ -862,10 +870,10 @@ class EmaneGlobalModel:
|
|||
),
|
||||
)
|
||||
|
||||
def configurations(self):
|
||||
def configurations(self) -> List[Configuration]:
|
||||
return self.emulator_config + self.nem_config
|
||||
|
||||
def config_groups(self):
|
||||
def config_groups(self) -> List[ConfigGroup]:
|
||||
emulator_len = len(self.emulator_config)
|
||||
config_len = len(self.configurations())
|
||||
return [
|
||||
|
@ -873,7 +881,7 @@ class EmaneGlobalModel:
|
|||
ConfigGroup("NEM Parameters", emulator_len + 1, config_len),
|
||||
]
|
||||
|
||||
def default_values(self):
|
||||
def default_values(self) -> Dict[str, str]:
|
||||
return OrderedDict(
|
||||
[(config.id, config.default) for config in self.configurations()]
|
||||
)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from typing import Dict, List
|
||||
|
||||
from core.config import Configuration
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
@ -13,12 +14,12 @@ except ImportError:
|
|||
logging.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
def _type_value(config_type):
|
||||
def _type_value(config_type: str) -> ConfigDataTypes:
|
||||
"""
|
||||
Convert emane configuration type to core configuration value.
|
||||
|
||||
:param str config_type: emane configuration type
|
||||
:return:
|
||||
:param config_type: emane configuration type
|
||||
:return: core config type
|
||||
"""
|
||||
config_type = config_type.upper()
|
||||
if config_type == "DOUBLE":
|
||||
|
@ -28,14 +29,13 @@ def _type_value(config_type):
|
|||
return ConfigDataTypes[config_type]
|
||||
|
||||
|
||||
def _get_possible(config_type, config_regex):
|
||||
def _get_possible(config_type: str, config_regex: str) -> List[str]:
|
||||
"""
|
||||
Retrieve possible config value options based on emane regexes.
|
||||
|
||||
:param str config_type: emane configuration type
|
||||
:param str config_regex: emane configuration regex
|
||||
:param config_type: emane configuration type
|
||||
:param config_regex: emane configuration regex
|
||||
:return: a string listing comma delimited values, if needed, empty string otherwise
|
||||
:rtype: list
|
||||
"""
|
||||
if config_type == "bool":
|
||||
return ["On", "Off"]
|
||||
|
@ -47,16 +47,14 @@ def _get_possible(config_type, config_regex):
|
|||
return []
|
||||
|
||||
|
||||
def _get_default(config_type_name, config_value):
|
||||
def _get_default(config_type_name: str, config_value: List[str]) -> str:
|
||||
"""
|
||||
Convert default configuration values to one used by core.
|
||||
|
||||
:param str config_type_name: emane configuration type name
|
||||
:param list config_value: emane configuration value list
|
||||
:param config_type_name: emane configuration type name
|
||||
:param config_value: emane configuration value list
|
||||
:return: default core config value
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
config_default = ""
|
||||
|
||||
if config_type_name == "bool":
|
||||
|
@ -72,14 +70,13 @@ def _get_default(config_type_name, config_value):
|
|||
return config_default
|
||||
|
||||
|
||||
def parse(manifest_path, defaults):
|
||||
def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]:
|
||||
"""
|
||||
Parses a valid emane manifest file and converts the provided configuration values into ones used by core.
|
||||
|
||||
:param str manifest_path: absolute manifest file path
|
||||
:param dict defaults: used to override default values for configurations
|
||||
:param manifest_path: absolute manifest file path
|
||||
:param defaults: used to override default values for configurations
|
||||
:return: list of core configuration values
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
# no results when emane bindings are not present
|
||||
|
|
|
@ -3,12 +3,14 @@ Defines Emane Models used within CORE.
|
|||
"""
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, List
|
||||
|
||||
from core.config import ConfigGroup, Configuration
|
||||
from core.emane import emanemanifest
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
from core.errors import CoreError
|
||||
from core.location.mobility import WirelessModel
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.xml import emanexml
|
||||
|
||||
|
||||
|
@ -45,12 +47,12 @@ class EmaneModel(WirelessModel):
|
|||
config_ignore = set()
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix):
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
"""
|
||||
Called after being loaded within the EmaneManager. Provides configured emane_prefix for
|
||||
parsing xml files.
|
||||
|
||||
:param str emane_prefix: configured emane prefix path
|
||||
:param emane_prefix: configured emane prefix path
|
||||
:return: nothing
|
||||
"""
|
||||
manifest_path = "share/emane/manifest"
|
||||
|
@ -63,22 +65,20 @@ class EmaneModel(WirelessModel):
|
|||
cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults)
|
||||
|
||||
@classmethod
|
||||
def configurations(cls):
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
"""
|
||||
Returns the combination all all configurations (mac, phy, and external).
|
||||
|
||||
:return: all configurations
|
||||
:rtype: list[Configuration]
|
||||
"""
|
||||
return cls.mac_config + cls.phy_config + cls.external_config
|
||||
|
||||
@classmethod
|
||||
def config_groups(cls):
|
||||
def config_groups(cls) -> List[ConfigGroup]:
|
||||
"""
|
||||
Returns the defined configuration groups.
|
||||
|
||||
:return: list of configuration groups.
|
||||
:rtype: list[ConfigGroup]
|
||||
"""
|
||||
mac_len = len(cls.mac_config)
|
||||
phy_len = len(cls.phy_config) + mac_len
|
||||
|
@ -89,12 +89,14 @@ class EmaneModel(WirelessModel):
|
|||
ConfigGroup("External Parameters", phy_len + 1, config_len),
|
||||
]
|
||||
|
||||
def build_xml_files(self, config, interface=None):
|
||||
def build_xml_files(
|
||||
self, config: Dict[str, str], interface: CoreInterface = None
|
||||
) -> None:
|
||||
"""
|
||||
Builds xml files for this emane model. Creates a nem.xml file that points to both mac.xml and phy.xml
|
||||
definitions.
|
||||
Builds xml files for this emane model. Creates a nem.xml file that points to
|
||||
both mac.xml and phy.xml definitions.
|
||||
|
||||
:param dict 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
|
||||
:return: nothing
|
||||
"""
|
||||
|
@ -127,7 +129,7 @@ class EmaneModel(WirelessModel):
|
|||
phy_file = os.path.join(self.session.session_dir, phy_name)
|
||||
emanexml.create_phy_xml(self, config, phy_file, server)
|
||||
|
||||
def post_startup(self):
|
||||
def post_startup(self) -> None:
|
||||
"""
|
||||
Logic to execute after the emane manager is finished with startup.
|
||||
|
||||
|
@ -135,15 +137,15 @@ class EmaneModel(WirelessModel):
|
|||
"""
|
||||
logging.debug("emane model(%s) has no post setup tasks", self.name)
|
||||
|
||||
def update(self, moved, moved_netifs):
|
||||
def update(self, moved: bool, moved_netifs: List[CoreInterface]) -> None:
|
||||
"""
|
||||
Invoked from MobilityModel when nodes are moved; this causes
|
||||
emane location events to be generated for the nodes in the moved
|
||||
list, making EmaneModels compatible with Ns2ScriptedMobility.
|
||||
|
||||
:param bool moved: were nodes moved
|
||||
:param list moved_netifs: interfaces that were moved
|
||||
:return:
|
||||
:param moved: were nodes moved
|
||||
:param moved_netifs: interfaces that were moved
|
||||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
wlan = self.session.get_node(self.id)
|
||||
|
@ -153,24 +155,24 @@ class EmaneModel(WirelessModel):
|
|||
|
||||
def linkconfig(
|
||||
self,
|
||||
netif,
|
||||
bw=None,
|
||||
delay=None,
|
||||
loss=None,
|
||||
duplicate=None,
|
||||
jitter=None,
|
||||
netif2=None,
|
||||
):
|
||||
netif: CoreInterface,
|
||||
bw: float = None,
|
||||
delay: float = None,
|
||||
loss: float = None,
|
||||
duplicate: float = None,
|
||||
jitter: float = None,
|
||||
netif2: CoreInterface = None,
|
||||
) -> None:
|
||||
"""
|
||||
Invoked when a Link Message is received. Default is unimplemented.
|
||||
|
||||
:param core.nodes.interface.Veth netif: interface one
|
||||
:param netif: interface one
|
||||
:param bw: bandwidth to set to
|
||||
:param delay: packet delay to set to
|
||||
:param loss: packet loss to set to
|
||||
:param duplicate: duplicate percentage to set to
|
||||
:param jitter: jitter to set to
|
||||
:param core.netns.vif.Veth netif2: interface two
|
||||
:param netif2: interface two
|
||||
:return: nothing
|
||||
"""
|
||||
logging.warning(
|
||||
|
|
|
@ -15,7 +15,7 @@ class EmaneIeee80211abgModel(emanemodel.EmaneModel):
|
|||
mac_xml = "ieee80211abgmaclayer.xml"
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix):
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix, "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml"
|
||||
)
|
||||
|
|
|
@ -4,9 +4,18 @@ share the same MAC+PHY model.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Type
|
||||
|
||||
from core.emulator.distributed import DistributedServer
|
||||
from core.emulator.enumerations import LinkTypes, NodeTypes, RegisterTlvs
|
||||
from core.nodes.base import CoreNetworkBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
from core.location.mobility import WirelessModel
|
||||
|
||||
WirelessModelType = Type[WirelessModel]
|
||||
|
||||
try:
|
||||
from emane.events import LocationEvent
|
||||
|
@ -29,7 +38,14 @@ class EmaneNet(CoreNetworkBase):
|
|||
type = "wlan"
|
||||
is_emane = True
|
||||
|
||||
def __init__(self, session, _id=None, name=None, start=True, server=None):
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
start: bool = True,
|
||||
server: DistributedServer = None,
|
||||
) -> None:
|
||||
super().__init__(session, _id, name, start, server)
|
||||
self.conf = ""
|
||||
self.up = False
|
||||
|
@ -39,20 +55,20 @@ class EmaneNet(CoreNetworkBase):
|
|||
|
||||
def linkconfig(
|
||||
self,
|
||||
netif,
|
||||
bw=None,
|
||||
delay=None,
|
||||
loss=None,
|
||||
duplicate=None,
|
||||
jitter=None,
|
||||
netif2=None,
|
||||
):
|
||||
netif: CoreInterface,
|
||||
bw: float = None,
|
||||
delay: float = None,
|
||||
loss: float = None,
|
||||
duplicate: float = None,
|
||||
jitter: float = None,
|
||||
netif2: CoreInterface = None,
|
||||
) -> None:
|
||||
"""
|
||||
The CommEffect model supports link configuration.
|
||||
"""
|
||||
if not self.model:
|
||||
return
|
||||
return self.model.linkconfig(
|
||||
self.model.linkconfig(
|
||||
netif=netif,
|
||||
bw=bw,
|
||||
delay=delay,
|
||||
|
@ -62,19 +78,19 @@ class EmaneNet(CoreNetworkBase):
|
|||
netif2=netif2,
|
||||
)
|
||||
|
||||
def config(self, conf):
|
||||
def config(self, conf: str) -> None:
|
||||
self.conf = conf
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self) -> None:
|
||||
pass
|
||||
|
||||
def link(self, netif1, netif2):
|
||||
def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
|
||||
pass
|
||||
|
||||
def unlink(self, netif1, netif2):
|
||||
def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
|
||||
pass
|
||||
|
||||
def updatemodel(self, config):
|
||||
def updatemodel(self, config: Dict[str, str]) -> None:
|
||||
if not self.model:
|
||||
raise ValueError("no model set to update for node(%s)", self.id)
|
||||
logging.info(
|
||||
|
@ -82,7 +98,7 @@ class EmaneNet(CoreNetworkBase):
|
|||
)
|
||||
self.model.set_configs(config, node_id=self.id)
|
||||
|
||||
def setmodel(self, model, config):
|
||||
def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None:
|
||||
"""
|
||||
set the EmaneModel associated with this node
|
||||
"""
|
||||
|
@ -96,14 +112,14 @@ class EmaneNet(CoreNetworkBase):
|
|||
self.mobility = model(session=self.session, _id=self.id)
|
||||
self.mobility.update_config(config)
|
||||
|
||||
def setnemid(self, netif, nemid):
|
||||
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):
|
||||
def getnemid(self, netif: CoreInterface) -> Optional[int]:
|
||||
"""
|
||||
Given an interface, return its numerical ID.
|
||||
"""
|
||||
|
@ -112,7 +128,7 @@ class EmaneNet(CoreNetworkBase):
|
|||
else:
|
||||
return self.nemidmap[netif]
|
||||
|
||||
def getnemnetif(self, nemid):
|
||||
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.
|
||||
|
@ -122,13 +138,13 @@ class EmaneNet(CoreNetworkBase):
|
|||
return netif
|
||||
return None
|
||||
|
||||
def netifs(self, sort=True):
|
||||
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):
|
||||
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
|
||||
|
@ -159,7 +175,7 @@ class EmaneNet(CoreNetworkBase):
|
|||
x, y, z = netif.node.position.get()
|
||||
self.setnemposition(netif, x, y, z)
|
||||
|
||||
def deinstallnetifs(self):
|
||||
def deinstallnetifs(self) -> None:
|
||||
"""
|
||||
Uninstall TAP devices. This invokes their shutdown method for
|
||||
any required cleanup; the device may be actually removed when
|
||||
|
@ -170,7 +186,9 @@ class EmaneNet(CoreNetworkBase):
|
|||
netif.shutdown()
|
||||
netif.poshook = None
|
||||
|
||||
def setnemposition(self, netif, x, y, z):
|
||||
def setnemposition(
|
||||
self, netif: CoreInterface, x: float, y: float, z: float
|
||||
) -> None:
|
||||
"""
|
||||
Publish a NEM location change event using the EMANE event service.
|
||||
"""
|
||||
|
@ -191,7 +209,7 @@ class EmaneNet(CoreNetworkBase):
|
|||
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
||||
self.session.emane.service.publish(0, event)
|
||||
|
||||
def setnempositions(self, moved_netifs):
|
||||
def setnempositions(self, moved_netifs: List[CoreInterface]) -> None:
|
||||
"""
|
||||
Several NEMs have moved, from e.g. a WaypointMobilityModel
|
||||
calculation. Generate an EMANE Location Event having several
|
||||
|
|
|
@ -15,7 +15,7 @@ class EmaneRfPipeModel(emanemodel.EmaneModel):
|
|||
mac_xml = "rfpipemaclayer.xml"
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix):
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix, "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
|
||||
)
|
||||
|
|
|
@ -27,7 +27,7 @@ class EmaneTdmaModel(emanemodel.EmaneModel):
|
|||
config_ignore = {schedule_name}
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix):
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix,
|
||||
"share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml",
|
||||
|
@ -43,7 +43,7 @@ class EmaneTdmaModel(emanemodel.EmaneModel):
|
|||
),
|
||||
)
|
||||
|
||||
def post_startup(self):
|
||||
def post_startup(self) -> None:
|
||||
"""
|
||||
Logic to execute after the emane manager is finished with startup.
|
||||
|
||||
|
|
|
@ -3,17 +3,20 @@ import logging
|
|||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import Mapping, Type
|
||||
|
||||
import core.services
|
||||
from core import configservices
|
||||
from core.configservice.manager import ConfigServiceManager
|
||||
from core.emulator.session import Session
|
||||
from core.services.coreservices import ServiceManager
|
||||
|
||||
|
||||
def signal_handler(signal_number, _):
|
||||
def signal_handler(signal_number: int, _) -> None:
|
||||
"""
|
||||
Handle signals and force an exit with cleanup.
|
||||
|
||||
:param int signal_number: signal number
|
||||
:param signal_number: signal number
|
||||
:param _: ignored
|
||||
:return: nothing
|
||||
"""
|
||||
|
@ -33,11 +36,11 @@ class CoreEmu:
|
|||
Provides logic for creating and configuring CORE sessions and the nodes within them.
|
||||
"""
|
||||
|
||||
def __init__(self, config=None):
|
||||
def __init__(self, config: Mapping[str, str] = None) -> None:
|
||||
"""
|
||||
Create a CoreEmu object.
|
||||
|
||||
:param dict config: configuration options
|
||||
:param config: configuration options
|
||||
"""
|
||||
# set umask 0
|
||||
os.umask(0)
|
||||
|
@ -54,10 +57,18 @@ class CoreEmu:
|
|||
self.service_errors = []
|
||||
self.load_services()
|
||||
|
||||
# config services
|
||||
self.service_manager = ConfigServiceManager()
|
||||
config_services_path = os.path.abspath(os.path.dirname(configservices.__file__))
|
||||
self.service_manager.load(config_services_path)
|
||||
custom_dir = self.config.get("custom_config_services_dir")
|
||||
if custom_dir:
|
||||
self.service_manager.load(custom_dir)
|
||||
|
||||
# catch exit event
|
||||
atexit.register(self.shutdown)
|
||||
|
||||
def load_services(self):
|
||||
def load_services(self) -> None:
|
||||
# load default services
|
||||
self.service_errors = core.services.load()
|
||||
|
||||
|
@ -70,7 +81,7 @@ class CoreEmu:
|
|||
custom_service_errors = ServiceManager.add_services(service_path)
|
||||
self.service_errors.extend(custom_service_errors)
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Shutdown all CORE session.
|
||||
|
||||
|
@ -83,31 +94,30 @@ class CoreEmu:
|
|||
session = sessions[_id]
|
||||
session.shutdown()
|
||||
|
||||
def create_session(self, _id=None, _cls=Session):
|
||||
def create_session(self, _id: int = None, _cls: Type[Session] = Session) -> Session:
|
||||
"""
|
||||
Create a new CORE session.
|
||||
|
||||
:param int _id: session id for new session
|
||||
:param class _cls: Session class to use
|
||||
:param _id: session id for new session
|
||||
:param _cls: Session class to use
|
||||
:return: created session
|
||||
:rtype: EmuSession
|
||||
"""
|
||||
if not _id:
|
||||
_id = 1
|
||||
while _id in self.sessions:
|
||||
_id += 1
|
||||
session = _cls(_id, config=self.config)
|
||||
session.service_manager = self.service_manager
|
||||
logging.info("created session: %s", _id)
|
||||
self.sessions[_id] = session
|
||||
return session
|
||||
|
||||
def delete_session(self, _id):
|
||||
def delete_session(self, _id: int) -> bool:
|
||||
"""
|
||||
Shutdown and delete a CORE session.
|
||||
|
||||
:param int _id: session id to delete
|
||||
:param _id: session id to delete
|
||||
:return: True if deleted, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.info("deleting session: %s", _id)
|
||||
session = self.sessions.pop(_id, None)
|
||||
|
|
|
@ -7,16 +7,20 @@ import os
|
|||
import threading
|
||||
from collections import OrderedDict
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Tuple
|
||||
|
||||
import netaddr
|
||||
from fabric import Connection
|
||||
from invoke import UnexpectedExit
|
||||
|
||||
from core import utils
|
||||
from core.errors import CoreCommandError
|
||||
from core.nodes.interface import GreTap
|
||||
from core.nodes.ipaddress import IpAddress
|
||||
from core.nodes.network import CoreNetwork, CtrlNet
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
LOCK = threading.Lock()
|
||||
CMD_HIDE = True
|
||||
|
||||
|
@ -26,29 +30,30 @@ class DistributedServer:
|
|||
Provides distributed server interactions.
|
||||
"""
|
||||
|
||||
def __init__(self, name, host):
|
||||
def __init__(self, name: str, host: str) -> None:
|
||||
"""
|
||||
Create a DistributedServer instance.
|
||||
|
||||
:param str name: convenience name to associate with host
|
||||
:param str host: host to connect to
|
||||
:param name: convenience name to associate with host
|
||||
:param host: host to connect to
|
||||
"""
|
||||
self.name = name
|
||||
self.host = host
|
||||
self.conn = Connection(host, user="root")
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def remote_cmd(self, cmd, env=None, cwd=None, wait=True):
|
||||
def remote_cmd(
|
||||
self, cmd: str, env: Dict[str, str] = None, cwd: str = None, wait: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Run command remotely using server connection.
|
||||
|
||||
:param str cmd: command to run
|
||||
:param dict env: environment for remote command, default is None
|
||||
:param str cwd: directory to run command in, defaults to None, which is the
|
||||
:param cmd: command to run
|
||||
:param env: environment for remote command, default is None
|
||||
:param cwd: directory to run command in, defaults to None, which is the
|
||||
user's home directory
|
||||
:param bool wait: True to wait for status, False to background process
|
||||
:param wait: True to wait for status, False to background process
|
||||
:return: stdout when success
|
||||
:rtype: str
|
||||
:raises CoreCommandError: when a non-zero exit status occurs
|
||||
"""
|
||||
|
||||
|
@ -73,24 +78,24 @@ class DistributedServer:
|
|||
stdout, stderr = e.streams_for_display()
|
||||
raise CoreCommandError(e.result.exited, cmd, stdout, stderr)
|
||||
|
||||
def remote_put(self, source, destination):
|
||||
def remote_put(self, source: str, destination: str) -> None:
|
||||
"""
|
||||
Push file to remote server.
|
||||
|
||||
:param str source: source file to push
|
||||
:param str destination: destination file location
|
||||
:param source: source file to push
|
||||
:param destination: destination file location
|
||||
:return: nothing
|
||||
"""
|
||||
with self.lock:
|
||||
self.conn.put(source, destination)
|
||||
|
||||
def remote_put_temp(self, destination, data):
|
||||
def remote_put_temp(self, destination: str, data: str) -> None:
|
||||
"""
|
||||
Remote push file contents to a remote server, using a temp file as an
|
||||
intermediate step.
|
||||
|
||||
:param str destination: file destination for data
|
||||
:param str data: data to store in remote file
|
||||
:param destination: file destination for data
|
||||
:param data: data to store in remote file
|
||||
:return: nothing
|
||||
"""
|
||||
with self.lock:
|
||||
|
@ -106,11 +111,11 @@ class DistributedController:
|
|||
Provides logic for dealing with remote tunnels and distributed servers.
|
||||
"""
|
||||
|
||||
def __init__(self, session):
|
||||
def __init__(self, session: "Session") -> None:
|
||||
"""
|
||||
Create
|
||||
|
||||
:param session:
|
||||
:param session: session
|
||||
"""
|
||||
self.session = session
|
||||
self.servers = OrderedDict()
|
||||
|
@ -119,12 +124,12 @@ class DistributedController:
|
|||
"distributed_address", default=None
|
||||
)
|
||||
|
||||
def add_server(self, name, host):
|
||||
def add_server(self, name: str, host: str) -> None:
|
||||
"""
|
||||
Add distributed server configuration.
|
||||
|
||||
:param str name: distributed server name
|
||||
:param str host: distributed server host address
|
||||
:param name: distributed server name
|
||||
:param host: distributed server host address
|
||||
:return: nothing
|
||||
"""
|
||||
server = DistributedServer(name, host)
|
||||
|
@ -132,7 +137,7 @@ class DistributedController:
|
|||
cmd = f"mkdir -p {self.session.session_dir}"
|
||||
server.remote_cmd(cmd)
|
||||
|
||||
def execute(self, func):
|
||||
def execute(self, func: Callable[[DistributedServer], None]) -> None:
|
||||
"""
|
||||
Convenience for executing logic against all distributed servers.
|
||||
|
||||
|
@ -143,7 +148,7 @@ class DistributedController:
|
|||
server = self.servers[name]
|
||||
func(server)
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Shutdown logic for dealing with distributed tunnels and server session
|
||||
directories.
|
||||
|
@ -165,7 +170,7 @@ class DistributedController:
|
|||
# clear tunnels
|
||||
self.tunnels.clear()
|
||||
|
||||
def start(self):
|
||||
def start(self) -> None:
|
||||
"""
|
||||
Start distributed network tunnels.
|
||||
|
||||
|
@ -184,19 +189,20 @@ class DistributedController:
|
|||
server = self.servers[name]
|
||||
self.create_gre_tunnel(node, server)
|
||||
|
||||
def create_gre_tunnel(self, node, server):
|
||||
def create_gre_tunnel(
|
||||
self, node: CoreNetwork, server: DistributedServer
|
||||
) -> Tuple[GreTap, GreTap]:
|
||||
"""
|
||||
Create gre tunnel using a pair of gre taps between the local and remote server.
|
||||
|
||||
|
||||
:param core.nodes.network.CoreNetwork node: node to create gre tunnel for
|
||||
:param core.emulator.distributed.DistributedServer server: server to create
|
||||
:param node: node to create gre tunnel for
|
||||
:param server: server to create
|
||||
tunnel for
|
||||
:return: local and remote gre taps created for tunnel
|
||||
:rtype: tuple
|
||||
"""
|
||||
host = server.host
|
||||
key = self.tunnel_key(node.id, IpAddress.to_int(host))
|
||||
key = self.tunnel_key(node.id, netaddr.IPAddress(host).value)
|
||||
tunnel = self.tunnels.get(key)
|
||||
if tunnel is not None:
|
||||
return tunnel
|
||||
|
@ -222,16 +228,15 @@ class DistributedController:
|
|||
self.tunnels[key] = tunnel
|
||||
return tunnel
|
||||
|
||||
def tunnel_key(self, n1_id, n2_id):
|
||||
def tunnel_key(self, n1_id: int, n2_id: int) -> int:
|
||||
"""
|
||||
Compute a 32-bit key used to uniquely identify a GRE tunnel.
|
||||
The hash(n1num), hash(n2num) values are used, so node numbers may be
|
||||
None or string values (used for e.g. "ctrlnet").
|
||||
|
||||
:param int n1_id: node one id
|
||||
:param int n2_id: node two id
|
||||
:param n1_id: node one id
|
||||
:param n2_id: node two id
|
||||
:return: tunnel key for the node pair
|
||||
:rtype: int
|
||||
"""
|
||||
logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id)
|
||||
key = (
|
||||
|
@ -239,12 +244,12 @@ class DistributedController:
|
|||
)
|
||||
return key & 0xFFFFFFFF
|
||||
|
||||
def get_tunnel(self, n1_id, n2_id):
|
||||
def get_tunnel(self, n1_id: int, n2_id: int) -> Tuple[GreTap, GreTap]:
|
||||
"""
|
||||
Return the GreTap between two nodes if it exists.
|
||||
|
||||
:param int n1_id: node one id
|
||||
:param int n2_id: node two id
|
||||
:param n1_id: node one id
|
||||
:param n2_id: node two id
|
||||
:return: gre tap between nodes or None
|
||||
"""
|
||||
key = self.tunnel_key(n1_id, n2_id)
|
||||
|
|
|
@ -1,45 +1,39 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.api.grpc.core_pb2 import LinkOptions
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.enumerations import LinkTypes
|
||||
from core.nodes.ipaddress import Ipv4Prefix, Ipv6Prefix, MacAddress
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.physical import PhysicalNode
|
||||
|
||||
|
||||
class IdGen:
|
||||
def __init__(self, _id=0):
|
||||
def __init__(self, _id: int = 0) -> None:
|
||||
self.id = _id
|
||||
|
||||
def next(self):
|
||||
def next(self) -> int:
|
||||
self.id += 1
|
||||
return self.id
|
||||
|
||||
|
||||
def create_interface(node, network, interface_data):
|
||||
"""
|
||||
Create an interface for a node on a network using provided interface data.
|
||||
|
||||
:param node: node to create interface for
|
||||
:param core.nodes.base.CoreNetworkBase network: network to associate interface with
|
||||
:param core.emulator.emudata.InterfaceData interface_data: interface data
|
||||
:return: created interface
|
||||
"""
|
||||
node.newnetif(
|
||||
network,
|
||||
addrlist=interface_data.get_addresses(),
|
||||
hwaddr=interface_data.mac,
|
||||
ifindex=interface_data.id,
|
||||
ifname=interface_data.name,
|
||||
)
|
||||
return node.netif(interface_data.id)
|
||||
|
||||
|
||||
def link_config(network, interface, link_options, devname=None, interface_two=None):
|
||||
def link_config(
|
||||
network: CoreNetworkBase,
|
||||
interface: CoreInterface,
|
||||
link_options: LinkOptions,
|
||||
devname: str = None,
|
||||
interface_two: CoreInterface = None,
|
||||
) -> None:
|
||||
"""
|
||||
Convenience method for configuring a link,
|
||||
|
||||
:param network: network to configure link for
|
||||
:param interface: interface to configure
|
||||
:param core.emulator.emudata.LinkOptions link_options: data to configure link with
|
||||
:param str devname: device name, default is None
|
||||
:param link_options: data to configure link with
|
||||
:param devname: device name, default is None
|
||||
:param interface_two: other interface associated, default is None
|
||||
:return: nothing
|
||||
"""
|
||||
|
@ -66,14 +60,14 @@ class NodeOptions:
|
|||
Options for creating and updating nodes within core.
|
||||
"""
|
||||
|
||||
def __init__(self, name=None, model="PC", image=None):
|
||||
def __init__(self, name: str = None, model: str = "PC", image: str = None) -> None:
|
||||
"""
|
||||
Create a NodeOptions object.
|
||||
|
||||
:param str name: name of node, defaults to node class name postfix with its id
|
||||
:param str model: defines services for default and physical nodes, defaults to
|
||||
:param name: name of node, defaults to node class name postfix with its id
|
||||
:param model: defines services for default and physical nodes, defaults to
|
||||
"router"
|
||||
:param str image: image to use for docker nodes
|
||||
:param image: image to use for docker nodes
|
||||
"""
|
||||
self.name = name
|
||||
self.model = model
|
||||
|
@ -81,6 +75,7 @@ class NodeOptions:
|
|||
self.icon = None
|
||||
self.opaque = None
|
||||
self.services = []
|
||||
self.config_services = []
|
||||
self.x = None
|
||||
self.y = None
|
||||
self.lat = None
|
||||
|
@ -91,24 +86,24 @@ class NodeOptions:
|
|||
self.image = image
|
||||
self.emane = None
|
||||
|
||||
def set_position(self, x, y):
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Convenience method for setting position.
|
||||
|
||||
:param float x: x position
|
||||
:param float y: y position
|
||||
:param x: x position
|
||||
:param y: y position
|
||||
:return: nothing
|
||||
"""
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def set_location(self, lat, lon, alt):
|
||||
def set_location(self, lat: float, lon: float, alt: float) -> None:
|
||||
"""
|
||||
Convenience method for setting location.
|
||||
|
||||
:param float lat: latitude
|
||||
:param float lon: longitude
|
||||
:param float alt: altitude
|
||||
:param lat: latitude
|
||||
:param lon: longitude
|
||||
:param alt: altitude
|
||||
:return: nothing
|
||||
"""
|
||||
self.lat = lat
|
||||
|
@ -121,11 +116,11 @@ class LinkOptions:
|
|||
Options for creating and updating links within core.
|
||||
"""
|
||||
|
||||
def __init__(self, _type=LinkTypes.WIRED):
|
||||
def __init__(self, _type: LinkTypes = LinkTypes.WIRED) -> None:
|
||||
"""
|
||||
Create a LinkOptions object.
|
||||
|
||||
:param core.emulator.enumerations.LinkTypes _type: type of link, defaults to
|
||||
:param _type: type of link, defaults to
|
||||
wired
|
||||
"""
|
||||
self.type = _type
|
||||
|
@ -146,17 +141,100 @@ class LinkOptions:
|
|||
self.opaque = None
|
||||
|
||||
|
||||
class InterfaceData:
|
||||
"""
|
||||
Convenience class for storing interface data.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
_id: int,
|
||||
name: str,
|
||||
mac: str,
|
||||
ip4: str,
|
||||
ip4_mask: int,
|
||||
ip6: str,
|
||||
ip6_mask: int,
|
||||
) -> None:
|
||||
"""
|
||||
Creates an InterfaceData object.
|
||||
|
||||
:param _id: interface id
|
||||
:param name: name for interface
|
||||
:param mac: mac address
|
||||
:param ip4: ipv4 address
|
||||
:param ip4_mask: ipv4 bit mask
|
||||
:param ip6: ipv6 address
|
||||
:param ip6_mask: ipv6 bit mask
|
||||
"""
|
||||
self.id = _id
|
||||
self.name = name
|
||||
self.mac = mac
|
||||
self.ip4 = ip4
|
||||
self.ip4_mask = ip4_mask
|
||||
self.ip6 = ip6
|
||||
self.ip6_mask = ip6_mask
|
||||
|
||||
def has_ip4(self) -> bool:
|
||||
"""
|
||||
Determines if interface has an ip4 address.
|
||||
|
||||
:return: True if has ip4, False otherwise
|
||||
"""
|
||||
return all([self.ip4, self.ip4_mask])
|
||||
|
||||
def has_ip6(self) -> bool:
|
||||
"""
|
||||
Determines if interface has an ip6 address.
|
||||
|
||||
:return: True if has ip6, False otherwise
|
||||
"""
|
||||
return all([self.ip6, self.ip6_mask])
|
||||
|
||||
def ip4_address(self) -> Optional[str]:
|
||||
"""
|
||||
Retrieve a string representation of the ip4 address and netmask.
|
||||
|
||||
:return: ip4 string or None
|
||||
"""
|
||||
if self.has_ip4():
|
||||
return f"{self.ip4}/{self.ip4_mask}"
|
||||
else:
|
||||
return None
|
||||
|
||||
def ip6_address(self) -> Optional[str]:
|
||||
"""
|
||||
Retrieve a string representation of the ip6 address and netmask.
|
||||
|
||||
:return: ip4 string or None
|
||||
"""
|
||||
if self.has_ip6():
|
||||
return f"{self.ip6}/{self.ip6_mask}"
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_addresses(self) -> List[str]:
|
||||
"""
|
||||
Returns a list of ip4 and ip6 address when present.
|
||||
|
||||
:return: list of addresses
|
||||
"""
|
||||
ip4 = self.ip4_address()
|
||||
ip6 = self.ip6_address()
|
||||
return [i for i in [ip4, ip6] if i]
|
||||
|
||||
|
||||
class IpPrefixes:
|
||||
"""
|
||||
Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
|
||||
"""
|
||||
|
||||
def __init__(self, ip4_prefix=None, ip6_prefix=None):
|
||||
def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
|
||||
"""
|
||||
Creates an IpPrefixes object.
|
||||
|
||||
:param str ip4_prefix: ip4 prefix to use for generation
|
||||
:param str ip6_prefix: ip6 prefix to use for generation
|
||||
: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:
|
||||
|
@ -164,46 +242,45 @@ class IpPrefixes:
|
|||
|
||||
self.ip4 = None
|
||||
if ip4_prefix:
|
||||
self.ip4 = Ipv4Prefix(ip4_prefix)
|
||||
self.ip4 = netaddr.IPNetwork(ip4_prefix)
|
||||
self.ip6 = None
|
||||
if ip6_prefix:
|
||||
self.ip6 = Ipv6Prefix(ip6_prefix)
|
||||
self.ip6 = netaddr.IPNetwork(ip6_prefix)
|
||||
|
||||
def ip4_address(self, node):
|
||||
def ip4_address(self, node: CoreNode) -> str:
|
||||
"""
|
||||
Convenience method to return the IP4 address for a node.
|
||||
|
||||
:param node: node to get IP4 address for
|
||||
:return: IP4 address or None
|
||||
:rtype: str
|
||||
"""
|
||||
if not self.ip4:
|
||||
raise ValueError("ip4 prefixes have not been set")
|
||||
return str(self.ip4.addr(node.id))
|
||||
return str(self.ip4[node.id])
|
||||
|
||||
def ip6_address(self, node):
|
||||
def ip6_address(self, node: CoreNode) -> str:
|
||||
"""
|
||||
Convenience method to return the IP6 address for a node.
|
||||
|
||||
:param node: node to get IP6 address for
|
||||
:return: IP4 address or None
|
||||
:rtype: str
|
||||
"""
|
||||
if not self.ip6:
|
||||
raise ValueError("ip6 prefixes have not been set")
|
||||
return str(self.ip6.addr(node.id))
|
||||
return str(self.ip6[node.id])
|
||||
|
||||
def create_interface(self, node, name=None, mac=None):
|
||||
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 core.nodes.base.CoreNode node: node to create interface for
|
||||
:param str name: name to set for interface, default is eth{id}
|
||||
:param str mac: mac address to use for this interface, default is random
|
||||
: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
|
||||
:rtype: InterfaceData
|
||||
"""
|
||||
# interface id
|
||||
inteface_id = node.newifindex()
|
||||
|
@ -212,19 +289,19 @@ class IpPrefixes:
|
|||
ip4 = None
|
||||
ip4_mask = None
|
||||
if self.ip4:
|
||||
ip4 = str(self.ip4.addr(node.id))
|
||||
ip4 = self.ip4_address(node)
|
||||
ip4_mask = self.ip4.prefixlen
|
||||
|
||||
# generate ip6 data
|
||||
ip6 = None
|
||||
ip6_mask = None
|
||||
if self.ip6:
|
||||
ip6 = str(self.ip6.addr(node.id))
|
||||
ip6 = self.ip6_address(node)
|
||||
ip6_mask = self.ip6.prefixlen
|
||||
|
||||
# random mac
|
||||
if not mac:
|
||||
mac = MacAddress.random()
|
||||
mac = utils.random_mac()
|
||||
|
||||
return InterfaceData(
|
||||
_id=inteface_id,
|
||||
|
@ -237,76 +314,22 @@ class IpPrefixes:
|
|||
)
|
||||
|
||||
|
||||
class InterfaceData:
|
||||
def create_interface(
|
||||
node: CoreNode, network: CoreNetworkBase, interface_data: InterfaceData
|
||||
):
|
||||
"""
|
||||
Convenience class for storing interface data.
|
||||
Create an interface for a node on a network using provided interface data.
|
||||
|
||||
:param node: node to create interface for
|
||||
:param network: network to associate interface with
|
||||
:param interface_data: interface data
|
||||
:return: created interface
|
||||
"""
|
||||
|
||||
def __init__(self, _id, name, mac, ip4, ip4_mask, ip6, ip6_mask):
|
||||
"""
|
||||
Creates an InterfaceData object.
|
||||
|
||||
:param int _id: interface id
|
||||
:param str name: name for interface
|
||||
:param core.nodes.ipaddress.MacAddress mac: mac address
|
||||
:param str ip4: ipv4 address
|
||||
:param int ip4_mask: ipv4 bit mask
|
||||
:param str ip6: ipv6 address
|
||||
:param int ip6_mask: ipv6 bit mask
|
||||
"""
|
||||
self.id = _id
|
||||
self.name = name
|
||||
self.mac = mac
|
||||
self.ip4 = ip4
|
||||
self.ip4_mask = ip4_mask
|
||||
self.ip6 = ip6
|
||||
self.ip6_mask = ip6_mask
|
||||
|
||||
def has_ip4(self):
|
||||
"""
|
||||
Determines if interface has an ip4 address.
|
||||
|
||||
:return: True if has ip4, False otherwise
|
||||
"""
|
||||
return all([self.ip4, self.ip4_mask])
|
||||
|
||||
def has_ip6(self):
|
||||
"""
|
||||
Determines if interface has an ip6 address.
|
||||
|
||||
:return: True if has ip6, False otherwise
|
||||
"""
|
||||
return all([self.ip6, self.ip6_mask])
|
||||
|
||||
def ip4_address(self):
|
||||
"""
|
||||
Retrieve a string representation of the ip4 address and netmask.
|
||||
|
||||
:return: ip4 string or None
|
||||
"""
|
||||
if self.has_ip4():
|
||||
return f"{self.ip4}/{self.ip4_mask}"
|
||||
else:
|
||||
return None
|
||||
|
||||
def ip6_address(self):
|
||||
"""
|
||||
Retrieve a string representation of the ip6 address and netmask.
|
||||
|
||||
:return: ip4 string or None
|
||||
"""
|
||||
if self.has_ip6():
|
||||
return f"{self.ip6}/{self.ip6_mask}"
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_addresses(self):
|
||||
"""
|
||||
Returns a list of ip4 and ip6 address when present.
|
||||
|
||||
:return: list of addresses
|
||||
:rtype: list
|
||||
"""
|
||||
ip4 = self.ip4_address()
|
||||
ip6 = self.ip6_address()
|
||||
return [i for i in [ip4, ip6] if i]
|
||||
node.newnetif(
|
||||
network,
|
||||
addrlist=interface_data.get_addresses(),
|
||||
hwaddr=interface_data.mac,
|
||||
ifindex=interface_data.id,
|
||||
ifname=interface_data.name,
|
||||
)
|
||||
return node.netif(interface_data.id)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,5 @@
|
|||
from typing import Any
|
||||
|
||||
from core.config import ConfigurableManager, ConfigurableOptions, Configuration
|
||||
from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs
|
||||
from core.plugins.sdt import Sdt
|
||||
|
@ -34,21 +36,18 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
|||
_id="enablerj45",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="1",
|
||||
options=["On", "Off"],
|
||||
label="Enable RJ45s",
|
||||
),
|
||||
Configuration(
|
||||
_id="preservedir",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
options=["On", "Off"],
|
||||
label="Preserve session dir",
|
||||
),
|
||||
Configuration(
|
||||
_id="enablesdt",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
options=["On", "Off"],
|
||||
label="Enable SDT3D output",
|
||||
),
|
||||
Configuration(
|
||||
|
@ -60,29 +59,52 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
|||
]
|
||||
config_type = RegisterTlvs.UTILITY.value
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.set_configs(self.default_values())
|
||||
|
||||
def get_config(
|
||||
self,
|
||||
_id,
|
||||
node_id=ConfigurableManager._default_node,
|
||||
config_type=ConfigurableManager._default_type,
|
||||
default=None,
|
||||
):
|
||||
_id: str,
|
||||
node_id: int = ConfigurableManager._default_node,
|
||||
config_type: str = ConfigurableManager._default_type,
|
||||
default: Any = None,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieves a specific configuration for a node and configuration type.
|
||||
|
||||
:param _id: specific configuration to retrieve
|
||||
:param node_id: node id to store configuration for
|
||||
:param config_type: configuration type to store configuration for
|
||||
:param default: default value to return when value is not found
|
||||
:return: configuration value
|
||||
"""
|
||||
value = super().get_config(_id, node_id, config_type, default)
|
||||
if value == "":
|
||||
value = default
|
||||
return value
|
||||
|
||||
def get_config_bool(self, name, default=None):
|
||||
def get_config_bool(self, name: str, default: Any = None) -> bool:
|
||||
"""
|
||||
Get configuration value as a boolean.
|
||||
|
||||
:param name: configuration name
|
||||
:param default: default value if not found
|
||||
:return: boolean for configuration value
|
||||
"""
|
||||
value = self.get_config(name)
|
||||
if value is None:
|
||||
return default
|
||||
return value.lower() == "true"
|
||||
|
||||
def get_config_int(self, name, default=None):
|
||||
def get_config_int(self, name: str, default: Any = None) -> int:
|
||||
"""
|
||||
Get configuration value as int.
|
||||
|
||||
:param name: configuration name
|
||||
:param default: default value if not found
|
||||
:return: int for configuration value
|
||||
"""
|
||||
value = self.get_config(name, default=default)
|
||||
if value is not None:
|
||||
value = int(value)
|
||||
|
|
|
@ -9,7 +9,7 @@ class CoreCommandError(subprocess.CalledProcessError):
|
|||
Used when encountering internal CORE command errors.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"Command({self.cmd}), Status({self.returncode}):\n"
|
||||
f"stdout: {self.output}\nstderr: {self.stderr}"
|
||||
|
|
|
@ -17,8 +17,8 @@ HEIGHT = 800
|
|||
|
||||
|
||||
class Application(tk.Frame):
|
||||
def __init__(self, master=None):
|
||||
super().__init__(master)
|
||||
def __init__(self, proxy: bool):
|
||||
super().__init__(master=None)
|
||||
# load node icons
|
||||
NodeUtils.setup()
|
||||
|
||||
|
@ -33,7 +33,7 @@ class Application(tk.Frame):
|
|||
self.guiconfig = appconfig.read()
|
||||
self.style = ttk.Style()
|
||||
self.setup_theme()
|
||||
self.core = CoreClient(self)
|
||||
self.core = CoreClient(self, proxy)
|
||||
self.setup_app()
|
||||
self.draw()
|
||||
self.core.set_up()
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
@ -16,6 +15,7 @@ ICONS_PATH = HOME_PATH.joinpath("icons")
|
|||
MOBILITY_PATH = HOME_PATH.joinpath("mobility")
|
||||
XMLS_PATH = HOME_PATH.joinpath("xmls")
|
||||
CONFIG_PATH = HOME_PATH.joinpath("gui.yaml")
|
||||
LOG_PATH = HOME_PATH.joinpath("gui.log")
|
||||
|
||||
# local paths
|
||||
DATA_PATH = Path(__file__).parent.joinpath("data")
|
||||
|
@ -52,9 +52,7 @@ def copy_files(current_path, new_path):
|
|||
|
||||
def check_directory():
|
||||
if HOME_PATH.exists():
|
||||
logging.info("~/.coretk exists")
|
||||
return
|
||||
logging.info("creating ~/.coretk")
|
||||
HOME_PATH.mkdir()
|
||||
BACKGROUNDS_PATH.mkdir()
|
||||
CUSTOM_EMANE_PATH.mkdir()
|
||||
|
@ -96,6 +94,7 @@ def check_directory():
|
|||
},
|
||||
"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}],
|
||||
"nodes": [],
|
||||
"recentfiles": [],
|
||||
"observers": [{"name": "hello", "cmd": "echo hello"}],
|
||||
}
|
||||
save(config)
|
||||
|
|
|
@ -5,20 +5,26 @@ import json
|
|||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import client, core_pb2
|
||||
from core.api.grpc import client, common_pb2, configservices_pb2, core_pb2
|
||||
from core.gui import appconfig
|
||||
from core.gui.dialogs.mobilityplayer import MobilityPlayer
|
||||
from core.gui.dialogs.sessions import SessionsDialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.graph import tags
|
||||
from core.gui.graph.edges import CanvasEdge
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.graph.shape import AnnotationData, Shape
|
||||
from core.gui.graph.shapeutils import ShapeType
|
||||
from core.gui.interface import InterfaceManager
|
||||
from core.gui.nodeutils import NodeDraw, NodeUtils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
GUI_SOURCE = "gui"
|
||||
OBSERVERS = {
|
||||
"processes": "ps",
|
||||
|
@ -32,31 +38,44 @@ OBSERVERS = {
|
|||
"IPSec policies": "setkey -DP",
|
||||
}
|
||||
|
||||
DEFAULT_TERMS = {
|
||||
"xterm": "xterm -e",
|
||||
"aterm": "aterm -e",
|
||||
"eterm": "eterm -e",
|
||||
"rxvt": "rxvt -e",
|
||||
"konsole": "konsole -e",
|
||||
"lxterminal": "lxterminal -e",
|
||||
"xfce4-terminal": "xfce4-terminal -x",
|
||||
"gnome-terminal": "gnome-terminal --window--",
|
||||
}
|
||||
|
||||
|
||||
class CoreServer:
|
||||
def __init__(self, name, address, port):
|
||||
def __init__(self, name: str, address: str, port: int):
|
||||
self.name = name
|
||||
self.address = address
|
||||
self.port = port
|
||||
|
||||
|
||||
class Observer:
|
||||
def __init__(self, name, cmd):
|
||||
def __init__(self, name: str, cmd: str):
|
||||
self.name = name
|
||||
self.cmd = cmd
|
||||
|
||||
|
||||
class CoreClient:
|
||||
def __init__(self, app):
|
||||
def __init__(self, app: "Application", proxy: bool):
|
||||
"""
|
||||
Create a CoreGrpc instance
|
||||
"""
|
||||
self.client = client.CoreGrpcClient()
|
||||
self.client = client.CoreGrpcClient(proxy=proxy)
|
||||
self.session_id = None
|
||||
self.node_ids = []
|
||||
self.app = app
|
||||
self.master = app.master
|
||||
self.services = {}
|
||||
self.config_services_groups = {}
|
||||
self.config_services = {}
|
||||
self.default_services = {}
|
||||
self.emane_models = []
|
||||
self.observer = None
|
||||
|
@ -82,11 +101,17 @@ class CoreClient:
|
|||
self.emane_model_configs = {}
|
||||
self.emane_config = None
|
||||
self.service_configs = {}
|
||||
self.config_service_configs = {}
|
||||
self.file_configs = {}
|
||||
self.mobility_players = {}
|
||||
self.handling_throughputs = None
|
||||
self.handling_events = None
|
||||
|
||||
self.xml_dir = None
|
||||
self.xml_file = None
|
||||
|
||||
self.modified_service_nodes = set()
|
||||
|
||||
def reset(self):
|
||||
# helpers
|
||||
self.interfaces_manager.reset()
|
||||
|
@ -101,6 +126,9 @@ class CoreClient:
|
|||
self.emane_config = None
|
||||
self.service_configs.clear()
|
||||
self.file_configs.clear()
|
||||
self.modified_service_nodes.clear()
|
||||
for mobility_player in self.mobility_players.values():
|
||||
mobility_player.handle_close()
|
||||
self.mobility_players.clear()
|
||||
# clear streams
|
||||
if self.handling_throughputs:
|
||||
|
@ -110,7 +138,7 @@ class CoreClient:
|
|||
self.handling_events.cancel()
|
||||
self.handling_events = None
|
||||
|
||||
def set_observer(self, value):
|
||||
def set_observer(self, value: str):
|
||||
self.observer = value
|
||||
|
||||
def read_config(self):
|
||||
|
@ -132,9 +160,9 @@ class CoreClient:
|
|||
observer = Observer(config["name"], config["cmd"])
|
||||
self.custom_observers[observer.name] = observer
|
||||
|
||||
def handle_events(self, event):
|
||||
def handle_events(self, event: core_pb2.Event):
|
||||
if event.session_id != self.session_id:
|
||||
logging.warn(
|
||||
logging.warning(
|
||||
"ignoring event session(%s) current(%s)",
|
||||
event.session_id,
|
||||
self.session_id,
|
||||
|
@ -142,7 +170,6 @@ class CoreClient:
|
|||
return
|
||||
|
||||
if event.HasField("link_event"):
|
||||
logging.info("link event: %s", event)
|
||||
self.handle_link_event(event.link_event)
|
||||
elif event.HasField("session_event"):
|
||||
logging.info("session event: %s", event)
|
||||
|
@ -170,7 +197,8 @@ class CoreClient:
|
|||
else:
|
||||
logging.info("unhandled event: %s", event)
|
||||
|
||||
def handle_link_event(self, event):
|
||||
def handle_link_event(self, event: core_pb2.LinkEvent):
|
||||
logging.debug("Link event: %s", event)
|
||||
node_one_id = event.link.node_one_id
|
||||
node_two_id = event.link.node_two_id
|
||||
canvas_node_one = self.canvas_nodes[node_one_id]
|
||||
|
@ -183,7 +211,8 @@ class CoreClient:
|
|||
else:
|
||||
logging.warning("unknown link event: %s", event.message_type)
|
||||
|
||||
def handle_node_event(self, event):
|
||||
def handle_node_event(self, event: core_pb2.NodeEvent):
|
||||
logging.debug("node event: %s", event)
|
||||
if event.source == GUI_SOURCE:
|
||||
return
|
||||
node_id = event.node.id
|
||||
|
@ -201,7 +230,7 @@ class CoreClient:
|
|||
self.handling_throughputs.cancel()
|
||||
self.handling_throughputs = None
|
||||
|
||||
def handle_throughputs(self, event):
|
||||
def handle_throughputs(self, event: core_pb2.ThroughputsEvent):
|
||||
if event.session_id != self.session_id:
|
||||
logging.warning(
|
||||
"ignoring throughput event session(%s) current(%s)",
|
||||
|
@ -209,14 +238,15 @@ class CoreClient:
|
|||
self.session_id,
|
||||
)
|
||||
return
|
||||
logging.info("handling throughputs event: %s", event)
|
||||
logging.debug("handling throughputs event: %s", event)
|
||||
self.app.canvas.set_throughputs(event)
|
||||
|
||||
def handle_exception_event(self, event):
|
||||
def handle_exception_event(self, event: core_pb2.ExceptionEvent):
|
||||
logging.info("exception event: %s", event)
|
||||
self.app.statusbar.core_alarms.append(event)
|
||||
|
||||
def join_session(self, session_id, query_location=True):
|
||||
def join_session(self, session_id: int, query_location: bool = True):
|
||||
logging.info("join session(%s)", session_id)
|
||||
# update session and title
|
||||
self.session_id = session_id
|
||||
self.master.title(f"CORE Session({self.session_id})")
|
||||
|
@ -278,32 +308,45 @@ class CoreClient:
|
|||
for config in response.configs:
|
||||
service_configs = self.service_configs.setdefault(config.node_id, {})
|
||||
service_configs[config.service] = config.data
|
||||
logging.info("service file configs: %s", config.files)
|
||||
logging.debug("service file configs: %s", config.files)
|
||||
for file_name in config.files:
|
||||
file_configs = self.file_configs.setdefault(config.node_id, {})
|
||||
files = file_configs.setdefault(config.service, {})
|
||||
data = config.files[file_name]
|
||||
files[file_name] = data
|
||||
|
||||
# get config service configurations
|
||||
response = self.client.get_node_config_service_configs(self.session_id)
|
||||
for config in response.configs:
|
||||
node_configs = self.config_service_configs.setdefault(
|
||||
config.node_id, {}
|
||||
)
|
||||
service_config = node_configs.setdefault(config.name, {})
|
||||
if config.templates:
|
||||
service_config["templates"] = config.templates
|
||||
if config.config:
|
||||
service_config["config"] = config.config
|
||||
|
||||
# draw session
|
||||
self.app.canvas.reset_and_redraw(session)
|
||||
|
||||
# get metadata
|
||||
response = self.client.get_session_metadata(self.session_id)
|
||||
self.parse_metadata(response.config)
|
||||
|
||||
except grpc.RpcError as e:
|
||||
self.app.after(0, show_grpc_error, e)
|
||||
self.app.after(0, show_grpc_error, e, self.app, self.app)
|
||||
|
||||
# update ui to represent current state
|
||||
self.app.after(0, self.app.joined_session_update)
|
||||
|
||||
def is_runtime(self):
|
||||
def is_runtime(self) -> bool:
|
||||
return self.state == core_pb2.SessionState.RUNTIME
|
||||
|
||||
def parse_metadata(self, config):
|
||||
def parse_metadata(self, config: Dict[str, str]):
|
||||
# canvas setting
|
||||
canvas_config = config.get("canvas")
|
||||
logging.info("canvas metadata: %s", canvas_config)
|
||||
logging.debug("canvas metadata: %s", canvas_config)
|
||||
if canvas_config:
|
||||
canvas_config = json.loads(canvas_config)
|
||||
|
||||
|
@ -364,8 +407,6 @@ class CoreClient:
|
|||
def create_new_session(self):
|
||||
"""
|
||||
Create a new session
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
response = self.client.create_session()
|
||||
|
@ -382,59 +423,67 @@ class CoreClient:
|
|||
)
|
||||
self.join_session(response.session_id, query_location=False)
|
||||
except grpc.RpcError as e:
|
||||
self.app.after(0, show_grpc_error, e)
|
||||
self.app.after(0, show_grpc_error, e, self.app, self.app)
|
||||
|
||||
def delete_session(self, session_id=None):
|
||||
def delete_session(self, session_id: int = None, parent_frame=None):
|
||||
if session_id is None:
|
||||
session_id = self.session_id
|
||||
try:
|
||||
response = self.client.delete_session(session_id)
|
||||
logging.info("deleted session result: %s", response)
|
||||
logging.info("deleted session(%s), Result: %s", session_id, response)
|
||||
except grpc.RpcError as e:
|
||||
self.app.after(0, show_grpc_error, e)
|
||||
# use the right master widget so the error dialog displays right on top of it
|
||||
master = self.app
|
||||
if parent_frame:
|
||||
master = parent_frame
|
||||
self.app.after(0, show_grpc_error, e, master, self.app)
|
||||
|
||||
def set_up(self):
|
||||
"""
|
||||
Query sessions, if there exist any, prompt whether to join one
|
||||
|
||||
:return: existing sessions
|
||||
"""
|
||||
try:
|
||||
self.client.connect()
|
||||
|
||||
# get service information
|
||||
response = self.client.get_services()
|
||||
for service in response.services:
|
||||
group_services = self.services.setdefault(service.group, set())
|
||||
group_services.add(service.name)
|
||||
|
||||
# get config service informations
|
||||
response = self.client.get_config_services()
|
||||
for service in response.services:
|
||||
self.config_services[service.name] = service
|
||||
group_services = self.config_services_groups.setdefault(
|
||||
service.group, set()
|
||||
)
|
||||
group_services.add(service.name)
|
||||
|
||||
# if there are no sessions, create a new session, else join a session
|
||||
response = self.client.get_sessions()
|
||||
logging.info("current sessions: %s", response)
|
||||
sessions = response.sessions
|
||||
if len(sessions) == 0:
|
||||
self.create_new_session()
|
||||
else:
|
||||
dialog = SessionsDialog(self.app, self.app)
|
||||
dialog = SessionsDialog(self.app, self.app, True)
|
||||
dialog.show()
|
||||
|
||||
response = self.client.get_service_defaults(self.session_id)
|
||||
self.default_services = {
|
||||
x.node_type: set(x.services) for x in response.defaults
|
||||
}
|
||||
except grpc.RpcError as e:
|
||||
self.app.after(0, show_grpc_error, e)
|
||||
self.app.after(0, show_grpc_error, e, self.app, self.app)
|
||||
self.app.close()
|
||||
|
||||
def edit_node(self, core_node):
|
||||
def edit_node(self, core_node: core_pb2.Node):
|
||||
try:
|
||||
self.client.edit_node(
|
||||
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
self.app.after(0, show_grpc_error, e)
|
||||
self.app.after(0, show_grpc_error, e, self.app, self.app)
|
||||
|
||||
def start_session(self):
|
||||
def start_session(self) -> core_pb2.StartSessionResponse:
|
||||
nodes = [x.core_node for x in self.canvas_nodes.values()]
|
||||
links = [x.link for x in self.links.values()]
|
||||
wlan_configs = self.get_wlan_configs_proto()
|
||||
|
@ -446,6 +495,7 @@ class CoreClient:
|
|||
asymmetric_links = [
|
||||
x.asymmetric_link for x in self.links.values() if x.asymmetric_link
|
||||
]
|
||||
config_service_configs = self.get_config_service_configs_proto()
|
||||
if self.emane_config:
|
||||
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||
else:
|
||||
|
@ -466,26 +516,27 @@ class CoreClient:
|
|||
service_configs,
|
||||
file_configs,
|
||||
asymmetric_links,
|
||||
config_service_configs,
|
||||
)
|
||||
logging.debug(
|
||||
logging.info(
|
||||
"start session(%s), result: %s", self.session_id, response.result
|
||||
)
|
||||
|
||||
if response.result:
|
||||
self.set_metadata()
|
||||
except grpc.RpcError as e:
|
||||
self.app.after(0, show_grpc_error, e)
|
||||
self.app.after(0, show_grpc_error, e, self.app, self.app)
|
||||
return response
|
||||
|
||||
def stop_session(self, session_id=None):
|
||||
def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse:
|
||||
if not session_id:
|
||||
session_id = self.session_id
|
||||
response = core_pb2.StopSessionResponse(result=False)
|
||||
try:
|
||||
response = self.client.stop_session(session_id)
|
||||
logging.debug("stopped session(%s), result: %s", session_id, response)
|
||||
logging.info("stopped session(%s), result: %s", session_id, response)
|
||||
except grpc.RpcError as e:
|
||||
self.app.after(0, show_grpc_error, e)
|
||||
self.app.after(0, show_grpc_error, e, self.app, self.app)
|
||||
return response
|
||||
|
||||
def show_mobility_players(self):
|
||||
|
@ -517,81 +568,115 @@ class CoreClient:
|
|||
|
||||
metadata = {"canvas": canvas_config, "shapes": shapes}
|
||||
response = self.client.set_session_metadata(self.session_id, metadata)
|
||||
logging.info("set session metadata: %s", response)
|
||||
logging.info("set session metadata %s, result: %s", metadata, response)
|
||||
|
||||
def launch_terminal(self, node_id):
|
||||
def launch_terminal(self, node_id: int):
|
||||
try:
|
||||
terminal = self.app.guiconfig["preferences"]["terminal"]
|
||||
response = self.client.get_node_terminal(self.session_id, node_id)
|
||||
logging.info("get terminal %s", response.terminal)
|
||||
os.system(f"{terminal} {response.terminal} &")
|
||||
output = os.popen(f"echo {terminal}").read()[:-1]
|
||||
if output in DEFAULT_TERMS:
|
||||
terminal = DEFAULT_TERMS[output]
|
||||
cmd = f'{terminal} "{response.terminal}" &'
|
||||
logging.info("launching terminal %s", cmd)
|
||||
os.system(cmd)
|
||||
except grpc.RpcError as e:
|
||||
self.app.after(0, show_grpc_error, e)
|
||||
self.app.after(0, show_grpc_error, e, self.app, self.app)
|
||||
|
||||
def save_xml(self, file_path):
|
||||
def save_xml(self, file_path: str):
|
||||
"""
|
||||
Save core session as to an xml file
|
||||
|
||||
:param str file_path: file path that user pick
|
||||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
if self.state != core_pb2.SessionState.RUNTIME:
|
||||
logging.debug(
|
||||
"session state not runtime, send session data to the daemon..."
|
||||
)
|
||||
logging.debug("Send session data to the daemon")
|
||||
self.send_data()
|
||||
response = self.client.save_xml(self.session_id, file_path)
|
||||
logging.info("saved xml(%s): %s", file_path, response)
|
||||
logging.info("saved xml file %s, result: %s", file_path, response)
|
||||
except grpc.RpcError as e:
|
||||
self.app.after(0, show_grpc_error, e)
|
||||
self.app.after(0, show_grpc_error, e, self.app, self.app)
|
||||
|
||||
def open_xml(self, file_path):
|
||||
def open_xml(self, file_path: str):
|
||||
"""
|
||||
Open core xml
|
||||
|
||||
:param str file_path: file to open
|
||||
:return: session id
|
||||
"""
|
||||
try:
|
||||
response = self.client.open_xml(file_path)
|
||||
logging.debug("open xml: %s", response)
|
||||
logging.info("open xml file %s, response: %s", file_path, response)
|
||||
self.join_session(response.session_id)
|
||||
except grpc.RpcError as e:
|
||||
self.app.after(0, show_grpc_error, e)
|
||||
self.app.after(0, show_grpc_error, e, self.app, self.app)
|
||||
|
||||
def get_node_service(self, node_id, service_name):
|
||||
def get_node_service(
|
||||
self, node_id: int, service_name: str
|
||||
) -> core_pb2.NodeServiceData:
|
||||
response = self.client.get_node_service(self.session_id, node_id, service_name)
|
||||
logging.debug("get node service %s", response)
|
||||
return response.service
|
||||
|
||||
def set_node_service(self, node_id, service_name, startups, validations, shutdowns):
|
||||
response = self.client.set_node_service(
|
||||
self.session_id, node_id, service_name, startups, validations, shutdowns
|
||||
logging.debug(
|
||||
"get node(%s) %s service, response: %s", node_id, service_name, response
|
||||
)
|
||||
logging.debug("set node service %s", response)
|
||||
response = self.client.get_node_service(self.session_id, node_id, service_name)
|
||||
logging.debug("get node service : %s", response)
|
||||
return response.service
|
||||
|
||||
def get_node_service_file(self, node_id, service_name, file_name):
|
||||
def set_node_service(
|
||||
self,
|
||||
node_id: int,
|
||||
service_name: str,
|
||||
startups: List[str],
|
||||
validations: List[str],
|
||||
shutdowns: List[str],
|
||||
) -> core_pb2.NodeServiceData:
|
||||
response = self.client.set_node_service(
|
||||
self.session_id,
|
||||
node_id,
|
||||
service_name,
|
||||
startup=startups,
|
||||
validate=validations,
|
||||
shutdown=shutdowns,
|
||||
)
|
||||
logging.info(
|
||||
"Set %s service for node(%s), Startup: %s, Validation: %s, Shutdown: %s, Result: %s",
|
||||
service_name,
|
||||
node_id,
|
||||
startups,
|
||||
validations,
|
||||
shutdowns,
|
||||
response,
|
||||
)
|
||||
response = self.client.get_node_service(self.session_id, node_id, service_name)
|
||||
return response.service
|
||||
|
||||
def get_node_service_file(
|
||||
self, node_id: int, service_name: str, file_name: str
|
||||
) -> str:
|
||||
response = self.client.get_node_service_file(
|
||||
self.session_id, node_id, service_name, file_name
|
||||
)
|
||||
logging.debug("get service file %s", response)
|
||||
logging.debug(
|
||||
"get service file for node(%s), service: %s, file: %s, result: %s",
|
||||
node_id,
|
||||
service_name,
|
||||
file_name,
|
||||
response,
|
||||
)
|
||||
return response.data
|
||||
|
||||
def set_node_service_file(self, node_id, service_name, file_name, data):
|
||||
def set_node_service_file(
|
||||
self, node_id: int, service_name: str, file_name: str, data: bytes
|
||||
):
|
||||
response = self.client.set_node_service_file(
|
||||
self.session_id, node_id, service_name, file_name, data
|
||||
)
|
||||
logging.debug("set node service file %s", response)
|
||||
logging.info(
|
||||
"set node(%s) service file, service: %s, file: %s, data: %s, result: %s",
|
||||
node_id,
|
||||
service_name,
|
||||
file_name,
|
||||
data,
|
||||
response,
|
||||
)
|
||||
|
||||
def create_nodes_and_links(self):
|
||||
"""
|
||||
create nodes and links that have not been created yet
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
node_protos = [x.core_node for x in self.canvas_nodes.values()]
|
||||
link_protos = [x.link for x in self.links.values()]
|
||||
|
@ -618,8 +703,6 @@ class CoreClient:
|
|||
def send_data(self):
|
||||
"""
|
||||
send to daemon all session info, but don't start the session
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.create_nodes_and_links()
|
||||
for config_proto in self.get_wlan_configs_proto():
|
||||
|
@ -635,9 +718,9 @@ class CoreClient:
|
|||
self.session_id,
|
||||
config_proto.node_id,
|
||||
config_proto.service,
|
||||
config_proto.startup,
|
||||
config_proto.validate,
|
||||
config_proto.shutdown,
|
||||
startup=config_proto.startup,
|
||||
validate=config_proto.validate,
|
||||
shutdown=config_proto.shutdown,
|
||||
)
|
||||
for config_proto in self.get_service_file_configs_proto():
|
||||
self.client.set_node_service_file(
|
||||
|
@ -661,21 +744,18 @@ class CoreClient:
|
|||
config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||
self.client.set_emane_config(self.session_id, config)
|
||||
|
||||
self.set_metadata()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Clean ups when done using grpc
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug("close grpc")
|
||||
self.client.close()
|
||||
|
||||
def next_node_id(self):
|
||||
def next_node_id(self) -> int:
|
||||
"""
|
||||
Get the next usable node id.
|
||||
|
||||
:return: the next id to be used
|
||||
:rtype: int
|
||||
"""
|
||||
i = 1
|
||||
while True:
|
||||
|
@ -684,15 +764,11 @@ class CoreClient:
|
|||
i += 1
|
||||
return i
|
||||
|
||||
def create_node(self, x, y, node_type, model):
|
||||
def create_node(
|
||||
self, x: float, y: float, node_type: core_pb2.NodeType, model: str
|
||||
) -> core_pb2.Node:
|
||||
"""
|
||||
Add node, with information filled in, to grpc manager
|
||||
|
||||
:param int x: x coord
|
||||
:param int y: y coord
|
||||
:param core_pb2.NodeType node_type: node type
|
||||
:param str model: node model
|
||||
:return: nothing
|
||||
"""
|
||||
node_id = self.next_node_id()
|
||||
position = core_pb2.Position(x=x, y=y)
|
||||
|
@ -702,31 +778,38 @@ class CoreClient:
|
|||
emane = None
|
||||
if node_type == core_pb2.NodeType.EMANE:
|
||||
emane = self.emane_models[0]
|
||||
name = f"EMANE{node_id}"
|
||||
elif node_type == core_pb2.NodeType.WIRELESS_LAN:
|
||||
name = f"WLAN{node_id}"
|
||||
elif node_type in [core_pb2.NodeType.RJ45, core_pb2.NodeType.TUNNEL]:
|
||||
name = "UNASSIGNED"
|
||||
else:
|
||||
name = f"n{node_id}"
|
||||
node = core_pb2.Node(
|
||||
id=node_id,
|
||||
type=node_type,
|
||||
name=f"n{node_id}",
|
||||
name=name,
|
||||
model=model,
|
||||
position=position,
|
||||
image=image,
|
||||
emane=emane,
|
||||
)
|
||||
logging.debug(
|
||||
"adding node to core session: %s, coords: (%s, %s), name: %s",
|
||||
if NodeUtils.is_custom(model):
|
||||
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
|
||||
node.services[:] = services
|
||||
logging.info(
|
||||
"add node(%s) to session(%s), coordinates(%s, %s)",
|
||||
node.name,
|
||||
self.session_id,
|
||||
x,
|
||||
y,
|
||||
node.name,
|
||||
)
|
||||
return node
|
||||
|
||||
def delete_graph_nodes(self, canvas_nodes):
|
||||
def delete_graph_nodes(self, canvas_nodes: List[core_pb2.Node]):
|
||||
"""
|
||||
remove the nodes selected by the user and anything related to that node
|
||||
such as link, configurations, interfaces
|
||||
|
||||
:param list canvas_nodes: list of nodes to delete
|
||||
:return: nothing
|
||||
"""
|
||||
edges = set()
|
||||
for canvas_node in canvas_nodes:
|
||||
|
@ -735,6 +818,9 @@ class CoreClient:
|
|||
logging.error("unknown node: %s", node_id)
|
||||
continue
|
||||
del self.canvas_nodes[node_id]
|
||||
|
||||
self.modified_service_nodes.discard(node_id)
|
||||
|
||||
if node_id in self.mobility_configs:
|
||||
del self.mobility_configs[node_id]
|
||||
if node_id in self.wlan_configs:
|
||||
|
@ -748,44 +834,45 @@ class CoreClient:
|
|||
if edge in edges:
|
||||
continue
|
||||
edges.add(edge)
|
||||
#
|
||||
# if edge.token not in self.links:
|
||||
# logging.error("unknown edge: %s", edge.token)
|
||||
self.links.pop(edge.token, None)
|
||||
|
||||
def create_interface(self, canvas_node):
|
||||
def create_interface(self, canvas_node: CanvasNode) -> core_pb2.Interface:
|
||||
node = canvas_node.core_node
|
||||
ip4, ip6, prefix = self.interfaces_manager.get_ips(node.id)
|
||||
ip4, ip6 = self.interfaces_manager.get_ips(node.id)
|
||||
ip4_mask = self.interfaces_manager.ip4_mask
|
||||
ip6_mask = self.interfaces_manager.ip6_mask
|
||||
interface_id = len(canvas_node.interfaces)
|
||||
name = f"eth{interface_id}"
|
||||
interface = core_pb2.Interface(
|
||||
id=interface_id, name=name, ip4=ip4, ip4mask=prefix, ip6=ip6, ip6mask=prefix
|
||||
id=interface_id,
|
||||
name=name,
|
||||
ip4=ip4,
|
||||
ip4mask=ip4_mask,
|
||||
ip6=ip6,
|
||||
ip6mask=ip6_mask,
|
||||
)
|
||||
canvas_node.interfaces.append(interface)
|
||||
logging.debug(
|
||||
"create node(%s) interface IPv4: %s, name: %s",
|
||||
"create node(%s) interface(%s) IPv4(%s) IPv6(%s)",
|
||||
node.name,
|
||||
interface.ip4,
|
||||
interface.name,
|
||||
interface.ip4,
|
||||
interface.ip6,
|
||||
)
|
||||
return interface
|
||||
|
||||
def create_link(self, edge, canvas_src_node, canvas_dst_node):
|
||||
def create_link(
|
||||
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
|
||||
):
|
||||
"""
|
||||
Create core link for a pair of canvas nodes, with token referencing
|
||||
the canvas edge.
|
||||
|
||||
:param edge: edge for link
|
||||
:param canvas_src_node: canvas node one
|
||||
:param canvas_dst_node: canvas node two
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
src_node = canvas_src_node.core_node
|
||||
dst_node = canvas_dst_node.core_node
|
||||
|
||||
# determine subnet
|
||||
self.interfaces_manager.determine_subnet(canvas_src_node, canvas_dst_node)
|
||||
self.interfaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)
|
||||
|
||||
src_interface = None
|
||||
if NodeUtils.is_container_node(src_node.type):
|
||||
|
@ -808,8 +895,9 @@ class CoreClient:
|
|||
)
|
||||
edge.set_link(link)
|
||||
self.links[edge.token] = edge
|
||||
logging.info("Add link between %s and %s", src_node.name, dst_node.name)
|
||||
|
||||
def get_wlan_configs_proto(self):
|
||||
def get_wlan_configs_proto(self) -> List[core_pb2.WlanConfig]:
|
||||
configs = []
|
||||
for node_id, config in self.wlan_configs.items():
|
||||
config = {x: config[x].value for x in config}
|
||||
|
@ -817,7 +905,7 @@ class CoreClient:
|
|||
configs.append(wlan_config)
|
||||
return configs
|
||||
|
||||
def get_mobility_configs_proto(self):
|
||||
def get_mobility_configs_proto(self) -> List[core_pb2.MobilityConfig]:
|
||||
configs = []
|
||||
for node_id, config in self.mobility_configs.items():
|
||||
config = {x: config[x].value for x in config}
|
||||
|
@ -825,7 +913,7 @@ class CoreClient:
|
|||
configs.append(mobility_config)
|
||||
return configs
|
||||
|
||||
def get_emane_model_configs_proto(self):
|
||||
def get_emane_model_configs_proto(self) -> List[core_pb2.EmaneModelConfig]:
|
||||
configs = []
|
||||
for key, config in self.emane_model_configs.items():
|
||||
node_id, model, interface = key
|
||||
|
@ -838,7 +926,7 @@ class CoreClient:
|
|||
configs.append(config_proto)
|
||||
return configs
|
||||
|
||||
def get_service_configs_proto(self):
|
||||
def get_service_configs_proto(self) -> List[core_pb2.ServiceConfig]:
|
||||
configs = []
|
||||
for node_id, services in self.service_configs.items():
|
||||
for name, config in services.items():
|
||||
|
@ -852,7 +940,7 @@ class CoreClient:
|
|||
configs.append(config_proto)
|
||||
return configs
|
||||
|
||||
def get_service_file_configs_proto(self):
|
||||
def get_service_file_configs_proto(self) -> List[core_pb2.ServiceFileConfig]:
|
||||
configs = []
|
||||
for (node_id, file_configs) in self.file_configs.items():
|
||||
for service, file_config in file_configs.items():
|
||||
|
@ -863,26 +951,53 @@ class CoreClient:
|
|||
configs.append(config_proto)
|
||||
return configs
|
||||
|
||||
def run(self, node_id):
|
||||
def get_config_service_configs_proto(
|
||||
self
|
||||
) -> List[configservices_pb2.ConfigServiceConfig]:
|
||||
config_service_protos = []
|
||||
for node_id, node_config in self.config_service_configs.items():
|
||||
for name, service_config in node_config.items():
|
||||
config = service_config.get("config", {})
|
||||
config_proto = configservices_pb2.ConfigServiceConfig(
|
||||
node_id=node_id,
|
||||
name=name,
|
||||
templates=service_config["templates"],
|
||||
config=config,
|
||||
)
|
||||
config_service_protos.append(config_proto)
|
||||
return config_service_protos
|
||||
|
||||
def run(self, node_id: int) -> str:
|
||||
logging.info("running node(%s) cmd: %s", node_id, self.observer)
|
||||
return self.client.node_command(self.session_id, node_id, self.observer).output
|
||||
|
||||
def get_wlan_config(self, node_id):
|
||||
def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
|
||||
config = self.wlan_configs.get(node_id)
|
||||
if not config:
|
||||
response = self.client.get_wlan_config(self.session_id, node_id)
|
||||
config = response.config
|
||||
logging.debug(
|
||||
"get wlan configuration from node %s, result configuration: %s",
|
||||
node_id,
|
||||
config,
|
||||
)
|
||||
return config
|
||||
|
||||
def get_mobility_config(self, node_id):
|
||||
def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
|
||||
config = self.mobility_configs.get(node_id)
|
||||
if not config:
|
||||
response = self.client.get_mobility_config(self.session_id, node_id)
|
||||
config = response.config
|
||||
logging.debug(
|
||||
"get mobility config from node %s, result configuration: %s",
|
||||
node_id,
|
||||
config,
|
||||
)
|
||||
return config
|
||||
|
||||
def get_emane_model_config(self, node_id, model, interface=None):
|
||||
logging.info("getting emane model config: %s %s %s", node_id, model, interface)
|
||||
def get_emane_model_config(
|
||||
self, node_id: int, model: str, interface: int = None
|
||||
) -> Dict[str, common_pb2.ConfigOption]:
|
||||
config = self.emane_model_configs.get((node_id, model, interface))
|
||||
if not config:
|
||||
if interface is None:
|
||||
|
@ -891,17 +1006,37 @@ class CoreClient:
|
|||
self.session_id, node_id, model, interface
|
||||
)
|
||||
config = response.config
|
||||
logging.debug(
|
||||
"get emane model config: node id: %s, EMANE model: %s, interface: %s, config: %s",
|
||||
node_id,
|
||||
model,
|
||||
interface,
|
||||
config,
|
||||
)
|
||||
return config
|
||||
|
||||
def set_emane_model_config(self, node_id, model, config, interface=None):
|
||||
logging.info("setting emane model config: %s %s %s", node_id, model, interface)
|
||||
def set_emane_model_config(
|
||||
self,
|
||||
node_id: int,
|
||||
model: str,
|
||||
config: Dict[str, common_pb2.ConfigOption],
|
||||
interface: int = None,
|
||||
):
|
||||
logging.info(
|
||||
"set emane model config: node id: %s, EMANE Model: %s, interface: %s, config: %s",
|
||||
node_id,
|
||||
model,
|
||||
interface,
|
||||
config,
|
||||
)
|
||||
self.emane_model_configs[(node_id, model, interface)] = config
|
||||
|
||||
def copy_node_service(self, _from, _to):
|
||||
def copy_node_service(self, _from: int, _to: int):
|
||||
services = self.canvas_nodes[_from].core_node.services
|
||||
self.canvas_nodes[_to].core_node.services[:] = services
|
||||
logging.debug("copying node %s service to node %s", _from, _to)
|
||||
|
||||
def copy_node_config(self, _from, _to):
|
||||
def copy_node_config(self, _from: int, _to: int):
|
||||
node_type = self.canvas_nodes[_from].core_node.type
|
||||
if node_type == core_pb2.NodeType.DEFAULT:
|
||||
services = self.canvas_nodes[_from].core_node.services
|
||||
|
@ -926,3 +1061,6 @@ class CoreClient:
|
|||
config = self.emane_model_configs.get(_from)
|
||||
if config:
|
||||
self.emane_model_configs[_to] = config
|
||||
|
||||
def service_been_modified(self, node_id: int) -> bool:
|
||||
return node_id in self.modified_service_nodes
|
||||
|
|
BIN
daemon/core/gui/data/icons/cancel.png
Normal file
BIN
daemon/core/gui/data/icons/cancel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
daemon/core/gui/data/icons/delete.png
Normal file
BIN
daemon/core/gui/data/icons/delete.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 387 B |
BIN
daemon/core/gui/data/icons/error.png
Normal file
BIN
daemon/core/gui/data/icons/error.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
daemon/core/gui/data/icons/shutdown.png
Normal file
BIN
daemon/core/gui/data/icons/shutdown.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -1,9 +1,13 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.widgets import CodeText
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
LICENSE = """\
|
||||
Copyright (c) 2005-2020, the Boeing Company.
|
||||
|
||||
|
@ -31,7 +35,7 @@ THE POSSIBILITY OF SUCH DAMAGE.\
|
|||
|
||||
|
||||
class AboutDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
def __init__(self, master: "Application", app: "Application"):
|
||||
super().__init__(master, app, "About CORE", modal=True)
|
||||
self.draw()
|
||||
|
||||
|
|
|
@ -3,15 +3,19 @@ check engine light
|
|||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.api.grpc.core_pb2 import ExceptionLevel
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import CodeText
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class AlertsDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
def __init__(self, master: "Application", app: "Application"):
|
||||
super().__init__(master, app, "Alerts", modal=True)
|
||||
self.app = app
|
||||
self.tree = None
|
||||
|
@ -110,7 +114,7 @@ class AlertsDialog(Dialog):
|
|||
dialog = DaemonLog(self, self.app)
|
||||
dialog.show()
|
||||
|
||||
def click_select(self, event):
|
||||
def click_select(self, event: tk.Event):
|
||||
current = self.tree.selection()[0]
|
||||
alarm = self.alarm_map[current]
|
||||
self.codetext.text.config(state=tk.NORMAL)
|
||||
|
@ -120,7 +124,7 @@ class AlertsDialog(Dialog):
|
|||
|
||||
|
||||
class DaemonLog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
def __init__(self, master: tk.Widget, app: "Application"):
|
||||
super().__init__(master, app, "core-daemon log", modal=True)
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.path = tk.StringVar(value="/var/log/core-daemon.log")
|
||||
|
|
|
@ -3,19 +3,21 @@ size and scale
|
|||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import font, ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
PIXEL_SCALE = 100
|
||||
|
||||
|
||||
class SizeAndScaleDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
def __init__(self, master: "Application", app: "Application"):
|
||||
"""
|
||||
create an instance for size and scale object
|
||||
|
||||
:param app: main application
|
||||
"""
|
||||
super().__init__(master, app, "Canvas Size and Scale", modal=True)
|
||||
self.canvas = self.app.canvas
|
||||
|
|
|
@ -4,6 +4,7 @@ set wallpaper
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.gui.appconfig import BACKGROUNDS_PATH
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
|
@ -11,13 +12,14 @@ from core.gui.images import Images
|
|||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import image_chooser
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class CanvasWallpaperDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
def __init__(self, master: "Application", app: "Application"):
|
||||
"""
|
||||
create an instance of CanvasWallpaper object
|
||||
|
||||
:param coretk.app.Application app: root application
|
||||
"""
|
||||
super().__init__(master, app, "Canvas Background", modal=True)
|
||||
self.canvas = self.app.canvas
|
||||
|
@ -140,8 +142,6 @@ class CanvasWallpaperDialog(Dialog):
|
|||
def click_clear(self):
|
||||
"""
|
||||
delete like shown in image link entry if there is any
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
# delete entry
|
||||
self.filename.set("")
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
"""
|
||||
custom color picker
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class ColorPickerDialog(Dialog):
|
||||
def __init__(self, master, app, initcolor="#000000"):
|
||||
def __init__(self, master: Any, app: "Application", initcolor: str = "#000000"):
|
||||
super().__init__(master, app, "color picker", modal=True)
|
||||
self.red_entry = None
|
||||
self.blue_entry = None
|
||||
|
@ -31,7 +34,7 @@ class ColorPickerDialog(Dialog):
|
|||
self.draw()
|
||||
self.set_bindings()
|
||||
|
||||
def askcolor(self):
|
||||
def askcolor(self) -> str:
|
||||
self.show()
|
||||
return self.color
|
||||
|
||||
|
@ -171,23 +174,19 @@ class ColorPickerDialog(Dialog):
|
|||
self.hex.trace_add("write", self.update_color)
|
||||
|
||||
def button_ok(self):
|
||||
logging.debug("not implemented")
|
||||
self.color = self.hex.get()
|
||||
self.destroy()
|
||||
|
||||
def get_hex(self):
|
||||
def get_hex(self) -> str:
|
||||
"""
|
||||
convert current RGB values into hex color
|
||||
|
||||
:rtype: str
|
||||
:return: hex color
|
||||
"""
|
||||
red = self.red_entry.get()
|
||||
blue = self.blue_entry.get()
|
||||
green = self.green_entry.get()
|
||||
return "#%02x%02x%02x" % (int(red), int(green), int(blue))
|
||||
|
||||
def current_focus(self, focus):
|
||||
def current_focus(self, focus: str):
|
||||
self.focus = focus
|
||||
|
||||
def update_color(self, arg1=None, arg2=None, arg3=None):
|
||||
|
@ -210,35 +209,31 @@ class ColorPickerDialog(Dialog):
|
|||
self.set_entry(red, green, blue)
|
||||
self.set_scale(red, green, blue)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(red, green, blue)
|
||||
self.set_label(str(red), str(green), str(blue))
|
||||
|
||||
def scale_callback(self, var, color_var):
|
||||
def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar):
|
||||
color_var.set(var.get())
|
||||
self.focus = "rgb"
|
||||
self.update_color()
|
||||
|
||||
def set_scale(self, red, green, blue):
|
||||
def set_scale(self, red: int, green: int, blue: int):
|
||||
self.red_scale.set(red)
|
||||
self.green_scale.set(green)
|
||||
self.blue_scale.set(blue)
|
||||
|
||||
def set_entry(self, red, green, blue):
|
||||
def set_entry(self, red: int, green: int, blue: int):
|
||||
self.red.set(red)
|
||||
self.green.set(green)
|
||||
self.blue.set(blue)
|
||||
|
||||
def set_label(self, red, green, blue):
|
||||
def set_label(self, red: str, green: str, blue: str):
|
||||
self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0))
|
||||
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
|
||||
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue)))
|
||||
|
||||
def get_rgb(self, hex_code):
|
||||
def get_rgb(self, hex_code: str) -> [int, int, int]:
|
||||
"""
|
||||
convert a valid hex code to RGB values
|
||||
|
||||
:param string hex_code: color in hex
|
||||
:rtype: tuple(int, int, int)
|
||||
:return: the RGB values
|
||||
"""
|
||||
if len(hex_code) == 4:
|
||||
red = hex_code[1]
|
||||
|
|
387
daemon/core/gui/dialogs/configserviceconfig.py
Normal file
387
daemon/core/gui/dialogs/configserviceconfig.py
Normal file
|
@ -0,0 +1,387 @@
|
|||
"""
|
||||
Service configuration dialog
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Any, List
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class ConfigServiceConfigDialog(Dialog):
|
||||
def __init__(
|
||||
self, master: Any, app: "Application", service_name: str, node_id: int
|
||||
):
|
||||
title = f"{service_name} Config Service"
|
||||
super().__init__(master, app, title, modal=True)
|
||||
self.master = master
|
||||
self.app = app
|
||||
self.core = app.core
|
||||
self.node_id = node_id
|
||||
self.service_name = service_name
|
||||
self.service_configs = app.core.config_service_configs
|
||||
|
||||
self.radiovar = tk.IntVar()
|
||||
self.radiovar.set(2)
|
||||
self.directories = []
|
||||
self.templates = []
|
||||
self.dependencies = []
|
||||
self.executables = []
|
||||
self.startup_commands = []
|
||||
self.validation_commands = []
|
||||
self.shutdown_commands = []
|
||||
self.default_startup = []
|
||||
self.default_validate = []
|
||||
self.default_shutdown = []
|
||||
self.validation_mode = None
|
||||
self.validation_time = None
|
||||
self.validation_period = tk.StringVar()
|
||||
self.modes = []
|
||||
self.mode_configs = {}
|
||||
|
||||
self.notebook = None
|
||||
self.templates_combobox = None
|
||||
self.modes_combobox = None
|
||||
self.startup_commands_listbox = None
|
||||
self.shutdown_commands_listbox = None
|
||||
self.validate_commands_listbox = None
|
||||
self.validation_time_entry = None
|
||||
self.validation_mode_entry = None
|
||||
self.template_text = None
|
||||
self.validation_period_entry = None
|
||||
self.original_service_files = {}
|
||||
self.temp_service_files = {}
|
||||
self.modified_files = set()
|
||||
self.config_frame = None
|
||||
self.default_config = None
|
||||
self.config = None
|
||||
|
||||
self.has_error = False
|
||||
|
||||
self.load()
|
||||
|
||||
if not self.has_error:
|
||||
self.draw()
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
self.core.create_nodes_and_links()
|
||||
service = self.core.config_services[self.service_name]
|
||||
self.dependencies = service.dependencies[:]
|
||||
self.executables = service.executables[:]
|
||||
self.directories = service.directories[:]
|
||||
self.templates = service.files[:]
|
||||
self.startup_commands = service.startup[:]
|
||||
self.validation_commands = service.validate[:]
|
||||
self.shutdown_commands = service.shutdown[:]
|
||||
self.validation_mode = service.validation_mode
|
||||
self.validation_time = service.validation_timer
|
||||
self.validation_period.set(service.validation_period)
|
||||
|
||||
response = self.core.client.get_config_service_defaults(self.service_name)
|
||||
self.original_service_files = response.templates
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
|
||||
self.modes = sorted(x.name for x in response.modes)
|
||||
self.mode_configs = {x.name: x.config for x in response.modes}
|
||||
|
||||
node_configs = self.service_configs.get(self.node_id, {})
|
||||
service_config = node_configs.get(self.service_name, {})
|
||||
|
||||
self.config = response.config
|
||||
self.default_config = {x.name: x.value for x in self.config.values()}
|
||||
custom_config = service_config.get("config")
|
||||
if custom_config:
|
||||
for key, value in custom_config.items():
|
||||
self.config[key].value = value
|
||||
logging.info("default config: %s", self.default_config)
|
||||
|
||||
custom_templates = service_config.get("templates", {})
|
||||
for file, data in custom_templates.items():
|
||||
self.modified_files.add(file)
|
||||
self.temp_service_files[file] = data
|
||||
except grpc.RpcError as e:
|
||||
self.has_error = True
|
||||
show_grpc_error(e, self.app, self.app)
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
# draw notebook
|
||||
self.notebook = ttk.Notebook(self.top)
|
||||
self.notebook.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_tab_files()
|
||||
if self.config:
|
||||
self.draw_tab_config()
|
||||
self.draw_tab_startstop()
|
||||
self.draw_tab_validation()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_tab_files(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Directories/Files")
|
||||
|
||||
label = ttk.Label(
|
||||
tab, text="Directories and templates that will be used for this service."
|
||||
)
|
||||
label.grid(pady=PADY)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Directories")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
directories_combobox = ttk.Combobox(
|
||||
frame, values=self.directories, state="readonly"
|
||||
)
|
||||
directories_combobox.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
if self.directories:
|
||||
directories_combobox.current(0)
|
||||
|
||||
label = ttk.Label(frame, text="Templates")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
self.templates_combobox = ttk.Combobox(
|
||||
frame, values=self.templates, state="readonly"
|
||||
)
|
||||
self.templates_combobox.bind(
|
||||
"<<ComboboxSelected>>", self.handle_template_changed
|
||||
)
|
||||
self.templates_combobox.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
self.template_text = CodeText(tab)
|
||||
self.template_text.grid(sticky="nsew")
|
||||
tab.rowconfigure(self.template_text.grid_info()["row"], weight=1)
|
||||
if self.templates:
|
||||
self.templates_combobox.current(0)
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert(
|
||||
"end", self.temp_service_files[self.templates[0]]
|
||||
)
|
||||
self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||
|
||||
def draw_tab_config(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Configuration")
|
||||
|
||||
if self.modes:
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Modes")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
self.modes_combobox = ttk.Combobox(
|
||||
frame, values=self.modes, state="readonly"
|
||||
)
|
||||
self.modes_combobox.bind("<<ComboboxSelected>>", self.handle_mode_changed)
|
||||
self.modes_combobox.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
logging.info("config service config: %s", self.config)
|
||||
self.config_frame = ConfigFrame(tab, self.app, self.config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1)
|
||||
|
||||
def draw_tab_startstop(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
tab.rowconfigure(i, weight=1)
|
||||
self.notebook.add(tab, text="Startup/Shutdown")
|
||||
commands = []
|
||||
# tab 3
|
||||
for i in range(3):
|
||||
label_frame = None
|
||||
if i == 0:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Startup Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.startup_commands
|
||||
elif i == 1:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Shutdown Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.shutdown_commands
|
||||
elif i == 2:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Validation Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.validation_commands
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
for command in commands:
|
||||
listbox_scroll.listbox.insert("end", command)
|
||||
listbox_scroll.listbox.config(height=4)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
if i == 0:
|
||||
self.startup_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 1:
|
||||
self.shutdown_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 2:
|
||||
self.validate_commands_listbox = listbox_scroll.listbox
|
||||
|
||||
def draw_tab_validation(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="ew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Validation", sticky="nsew")
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Time")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
self.validation_time_entry = ttk.Entry(frame)
|
||||
self.validation_time_entry.insert("end", self.validation_time)
|
||||
self.validation_time_entry.config(state=tk.DISABLED)
|
||||
self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Mode")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING:
|
||||
mode = "BLOCKING"
|
||||
elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING:
|
||||
mode = "NON_BLOCKING"
|
||||
else:
|
||||
mode = "TIMER"
|
||||
self.validation_mode_entry = ttk.Entry(
|
||||
frame, textvariable=tk.StringVar(value=mode)
|
||||
)
|
||||
self.validation_mode_entry.insert("end", mode)
|
||||
self.validation_mode_entry.config(state=tk.DISABLED)
|
||||
self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Period")
|
||||
label.grid(row=2, column=0, sticky="w", padx=PADX)
|
||||
self.validation_period_entry = ttk.Entry(
|
||||
frame, state=tk.DISABLED, textvariable=self.validation_period
|
||||
)
|
||||
self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for executable in self.executables:
|
||||
listbox_scroll.listbox.insert("end", executable)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for dependency in self.dependencies:
|
||||
listbox_scroll.listbox.insert("end", dependency)
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Defaults", command=self.click_defaults)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Copy...", command=self.click_copy)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
def click_apply(self):
|
||||
current_listbox = self.master.current.listbox
|
||||
if not self.is_custom():
|
||||
if self.node_id in self.service_configs:
|
||||
self.service_configs[self.node_id].pop(self.service_name, None)
|
||||
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
|
||||
self.destroy()
|
||||
return
|
||||
|
||||
try:
|
||||
node_config = self.service_configs.setdefault(self.node_id, {})
|
||||
service_config = node_config.setdefault(self.service_name, {})
|
||||
if self.config_frame:
|
||||
self.config_frame.parse_config()
|
||||
service_config["config"] = {
|
||||
x.name: x.value for x in self.config.values()
|
||||
}
|
||||
templates_config = service_config.setdefault("templates", {})
|
||||
for file in self.modified_files:
|
||||
templates_config[file] = self.temp_service_files[file]
|
||||
all_current = current_listbox.get(0, tk.END)
|
||||
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e, self.top, self.app)
|
||||
self.destroy()
|
||||
|
||||
def handle_template_changed(self, event: tk.Event):
|
||||
template = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[template])
|
||||
|
||||
def handle_mode_changed(self, event: tk.Event):
|
||||
mode = self.modes_combobox.get()
|
||||
config = self.mode_configs[mode]
|
||||
logging.info("mode config: %s", config)
|
||||
self.config_frame.set_values(config)
|
||||
|
||||
def update_template_file_data(self, event: tk.Event):
|
||||
scrolledtext = event.widget
|
||||
template = self.templates_combobox.get()
|
||||
self.temp_service_files[template] = scrolledtext.get(1.0, "end")
|
||||
if self.temp_service_files[template] != self.original_service_files[template]:
|
||||
self.modified_files.add(template)
|
||||
else:
|
||||
self.modified_files.discard(template)
|
||||
|
||||
def is_custom(self):
|
||||
has_custom_templates = len(self.modified_files) > 0
|
||||
has_custom_config = False
|
||||
if self.config_frame:
|
||||
current = self.config_frame.parse_config()
|
||||
has_custom_config = self.default_config != current
|
||||
return has_custom_templates or has_custom_config
|
||||
|
||||
def click_defaults(self):
|
||||
if self.node_id in self.service_configs:
|
||||
node_config = self.service_configs.get(self.node_id, {})
|
||||
node_config.pop(self.service_name, None)
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
filename = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[filename])
|
||||
if self.config_frame:
|
||||
logging.info("resetting defaults: %s", self.default_config)
|
||||
self.config_frame.set_values(self.default_config)
|
||||
|
||||
def click_copy(self):
|
||||
pass
|
||||
|
||||
def append_commands(
|
||||
self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
||||
):
|
||||
for cmd in to_add:
|
||||
commands.append(cmd)
|
||||
listbox.insert(tk.END, cmd)
|
204
daemon/core/gui/dialogs/copyserviceconfig.py
Normal file
204
daemon/core/gui/dialogs/copyserviceconfig.py
Normal file
|
@ -0,0 +1,204 @@
|
|||
"""
|
||||
copy service config dialog
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Any, Tuple
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX
|
||||
from core.gui.widgets import CodeText
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class CopyServiceConfigDialog(Dialog):
|
||||
def __init__(self, master: Any, app: "Application", node_id: int):
|
||||
super().__init__(master, app, f"Copy services to node {node_id}", modal=True)
|
||||
self.parent = master
|
||||
self.app = app
|
||||
self.node_id = node_id
|
||||
self.service_configs = app.core.service_configs
|
||||
self.file_configs = app.core.file_configs
|
||||
|
||||
self.tree = None
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.tree = ttk.Treeview(self.top)
|
||||
self.tree.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
self.tree["columns"] = ()
|
||||
self.tree.column("#0", width=270, minwidth=270, stretch=tk.YES)
|
||||
self.tree.heading("#0", text="Service configuration items", anchor=tk.CENTER)
|
||||
custom_nodes = set(self.service_configs).union(set(self.file_configs))
|
||||
for nid in custom_nodes:
|
||||
treeid = self.tree.insert("", "end", text=f"n{nid}", tags="node")
|
||||
services = self.service_configs.get(nid, None)
|
||||
files = self.file_configs.get(nid, None)
|
||||
tree_ids = {}
|
||||
if services:
|
||||
for service, config in services.items():
|
||||
serviceid = self.tree.insert(
|
||||
treeid, "end", text=service, tags="service"
|
||||
)
|
||||
tree_ids[service] = serviceid
|
||||
cmdup = config.startup[:]
|
||||
cmddown = config.shutdown[:]
|
||||
cmdval = config.validate[:]
|
||||
self.tree.insert(
|
||||
serviceid,
|
||||
"end",
|
||||
text=f"cmdup=({str(cmdup)[1:-1]})",
|
||||
tags=("cmd", "up"),
|
||||
)
|
||||
self.tree.insert(
|
||||
serviceid,
|
||||
"end",
|
||||
text=f"cmddown=({str(cmddown)[1:-1]})",
|
||||
tags=("cmd", "down"),
|
||||
)
|
||||
self.tree.insert(
|
||||
serviceid,
|
||||
"end",
|
||||
text=f"cmdval=({str(cmdval)[1:-1]})",
|
||||
tags=("cmd", "val"),
|
||||
)
|
||||
if files:
|
||||
for service, configs in files.items():
|
||||
if service in tree_ids:
|
||||
serviceid = tree_ids[service]
|
||||
else:
|
||||
serviceid = self.tree.insert(
|
||||
treeid, "end", text=service, tags="service"
|
||||
)
|
||||
tree_ids[service] = serviceid
|
||||
for filename, data in configs.items():
|
||||
self.tree.insert(serviceid, "end", text=filename, tags="file")
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=1, column=0)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Copy", command=self.click_copy)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="View", command=self.click_view)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
|
||||
def click_copy(self):
|
||||
selected = self.tree.selection()
|
||||
if selected:
|
||||
item = self.tree.item(selected[0])
|
||||
if "file" in item["tags"]:
|
||||
filename = item["text"]
|
||||
nid, service = self.get_node_service(selected)
|
||||
data = self.file_configs[nid][service][filename]
|
||||
if service == self.parent.service_name:
|
||||
self.parent.temp_service_files[filename] = data
|
||||
self.parent.modified_files.add(filename)
|
||||
if self.parent.filename_combobox.get() == filename:
|
||||
self.parent.service_file_data.text.delete(1.0, "end")
|
||||
self.parent.service_file_data.text.insert("end", data)
|
||||
if "cmd" in item["tags"]:
|
||||
nid, service = self.get_node_service(selected)
|
||||
if service == self.master.service_name:
|
||||
cmds = self.service_configs[nid][service]
|
||||
if "up" in item["tags"]:
|
||||
self.master.append_commands(
|
||||
self.master.startup_commands,
|
||||
self.master.startup_commands_listbox,
|
||||
cmds.startup,
|
||||
)
|
||||
elif "down" in item["tags"]:
|
||||
self.master.append_commands(
|
||||
self.master.shutdown_commands,
|
||||
self.master.shutdown_commands_listbox,
|
||||
cmds.shutdown,
|
||||
)
|
||||
|
||||
elif "val" in item["tags"]:
|
||||
self.master.append_commands(
|
||||
self.master.validate_commands,
|
||||
self.master.validate_commands_listbox,
|
||||
cmds.validate,
|
||||
)
|
||||
self.destroy()
|
||||
|
||||
def click_view(self):
|
||||
selected = self.tree.selection()
|
||||
data = ""
|
||||
if selected:
|
||||
item = self.tree.item(selected[0])
|
||||
if "file" in item["tags"]:
|
||||
nid, service = self.get_node_service(selected)
|
||||
data = self.file_configs[nid][service][item["text"]]
|
||||
dialog = ViewConfigDialog(
|
||||
self, self.app, nid, data, item["text"].split("/")[-1]
|
||||
)
|
||||
dialog.show()
|
||||
if "cmd" in item["tags"]:
|
||||
nid, service = self.get_node_service(selected)
|
||||
cmds = self.service_configs[nid][service]
|
||||
if "up" in item["tags"]:
|
||||
data = f"({str(cmds.startup[:])[1:-1]})"
|
||||
dialog = ViewConfigDialog(
|
||||
self, self.app, self.node_id, data, "cmdup"
|
||||
)
|
||||
elif "down" in item["tags"]:
|
||||
data = f"({str(cmds.shutdown[:])[1:-1]})"
|
||||
dialog = ViewConfigDialog(
|
||||
self, self.app, self.node_id, data, "cmdup"
|
||||
)
|
||||
elif "val" in item["tags"]:
|
||||
data = f"({str(cmds.validate[:])[1:-1]})"
|
||||
dialog = ViewConfigDialog(
|
||||
self, self.app, self.node_id, data, "cmdup"
|
||||
)
|
||||
dialog.show()
|
||||
|
||||
def get_node_service(self, selected: Tuple[str]) -> [int, str]:
|
||||
service_tree_id = self.tree.parent(selected[0])
|
||||
service_name = self.tree.item(service_tree_id)["text"]
|
||||
node_tree_id = self.tree.parent(service_tree_id)
|
||||
node_id = int(self.tree.item(node_tree_id)["text"][1:])
|
||||
return node_id, service_name
|
||||
|
||||
|
||||
class ViewConfigDialog(Dialog):
|
||||
def __init__(
|
||||
self,
|
||||
master: Any,
|
||||
app: "Application",
|
||||
node_id: int,
|
||||
data: str,
|
||||
filename: str = None,
|
||||
):
|
||||
super().__init__(master, app, f"n{node_id} config data", modal=True)
|
||||
self.data = data
|
||||
self.service_data = None
|
||||
self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}")
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=10)
|
||||
frame.grid(row=0, column=0, sticky="ew")
|
||||
label = ttk.Label(frame, text="File: ")
|
||||
label.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.filepath)
|
||||
entry.config(state="disabled")
|
||||
entry.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
self.service_data = CodeText(self.top)
|
||||
self.service_data.grid(row=1, column=0, sticky="nsew")
|
||||
self.service_data.text.insert("end", self.data)
|
||||
self.service_data.text.config(state="disabled")
|
||||
|
||||
button = ttk.Button(self.top, text="Close", command=self.destroy)
|
||||
button.grid(row=2, column=0, sticky="ew", padx=PADX)
|
|
@ -2,6 +2,7 @@ import logging
|
|||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Any, Set
|
||||
|
||||
from core.gui import nodeutils
|
||||
from core.gui.appconfig import ICONS_PATH
|
||||
|
@ -11,9 +12,12 @@ from core.gui.nodeutils import NodeDraw
|
|||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class ServicesSelectDialog(Dialog):
|
||||
def __init__(self, master, app, current_services):
|
||||
def __init__(self, master: Any, app: "Application", current_services: Set[str]):
|
||||
super().__init__(master, app, "Node Services", modal=True)
|
||||
self.groups = None
|
||||
self.services = None
|
||||
|
@ -71,7 +75,7 @@ class ServicesSelectDialog(Dialog):
|
|||
# trigger group change
|
||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
||||
|
||||
def handle_group_change(self, event):
|
||||
def handle_group_change(self, event: tk.Event):
|
||||
selection = self.groups.listbox.curselection()
|
||||
if selection:
|
||||
index = selection[0]
|
||||
|
@ -81,7 +85,7 @@ class ServicesSelectDialog(Dialog):
|
|||
checked = name in self.current_services
|
||||
self.services.add(name, checked)
|
||||
|
||||
def service_clicked(self, name, var):
|
||||
def service_clicked(self, name: str, var: tk.BooleanVar):
|
||||
if var.get() and name not in self.current_services:
|
||||
self.current_services.add(name)
|
||||
elif not var.get() and name in self.current_services:
|
||||
|
@ -96,7 +100,7 @@ class ServicesSelectDialog(Dialog):
|
|||
|
||||
|
||||
class CustomNodesDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
def __init__(self, master: "Application", app: "Application"):
|
||||
super().__init__(master, app, "Custom Nodes", modal=True)
|
||||
self.edit_button = None
|
||||
self.delete_button = None
|
||||
|
@ -214,6 +218,12 @@ class CustomNodesDialog(Dialog):
|
|||
if name not in self.app.core.custom_nodes:
|
||||
image_file = Path(self.image_file).stem
|
||||
node_draw = NodeDraw.from_custom(name, image_file, set(self.services))
|
||||
logging.info(
|
||||
"created new custom node (%s), image file (%s), services: (%s)",
|
||||
name,
|
||||
image_file,
|
||||
self.services,
|
||||
)
|
||||
self.app.core.custom_nodes[name] = node_draw
|
||||
self.nodes_list.listbox.insert(tk.END, name)
|
||||
self.reset_values()
|
||||
|
@ -228,6 +238,12 @@ class CustomNodesDialog(Dialog):
|
|||
node_draw.image_file = Path(self.image_file).stem
|
||||
node_draw.image = self.image
|
||||
node_draw.services = self.services
|
||||
logging.debug(
|
||||
"edit custom node (%s), image: (%s), services (%s)",
|
||||
name,
|
||||
self.image_file,
|
||||
self.services,
|
||||
)
|
||||
self.app.core.custom_nodes[name] = node_draw
|
||||
self.nodes_list.listbox.delete(self.selected_index)
|
||||
self.nodes_list.listbox.insert(self.selected_index, name)
|
||||
|
@ -241,7 +257,7 @@ class CustomNodesDialog(Dialog):
|
|||
self.nodes_list.listbox.selection_clear(0, tk.END)
|
||||
self.nodes_list.listbox.event_generate("<<ListboxSelect>>")
|
||||
|
||||
def handle_node_select(self, event):
|
||||
def handle_node_select(self, event: tk.Event):
|
||||
selection = self.nodes_list.listbox.curselection()
|
||||
if selection:
|
||||
self.selected_index = selection[0]
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import DIALOG_PAD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class Dialog(tk.Toplevel):
|
||||
def __init__(self, master, app, title, modal=False):
|
||||
def __init__(
|
||||
self, master: tk.Widget, app: "Application", title: str, modal: bool = False
|
||||
):
|
||||
super().__init__(master)
|
||||
self.withdraw()
|
||||
self.app = app
|
||||
|
@ -30,7 +36,7 @@ class Dialog(tk.Toplevel):
|
|||
self.grab_set()
|
||||
self.wait_window()
|
||||
|
||||
def draw_spacer(self, row=None):
|
||||
def draw_spacer(self, row: int = None):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=row, sticky="nsew")
|
||||
frame.rowconfigure(0, weight=1)
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
"""
|
||||
emane configuration
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
import webbrowser
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
|
||||
class GlobalEmaneDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
def __init__(self, master: Any, app: "Application"):
|
||||
super().__init__(master, app, "EMANE Configuration", modal=True)
|
||||
self.config_frame = None
|
||||
self.draw()
|
||||
|
@ -47,20 +52,29 @@ class GlobalEmaneDialog(Dialog):
|
|||
|
||||
|
||||
class EmaneModelDialog(Dialog):
|
||||
def __init__(self, master, app, node, model, interface=None):
|
||||
def __init__(
|
||||
self,
|
||||
master: Any,
|
||||
app: "Application",
|
||||
node: core_pb2.Node,
|
||||
model: str,
|
||||
interface: int = None,
|
||||
):
|
||||
super().__init__(master, app, f"{node.name} {model} Configuration", modal=True)
|
||||
self.node = node
|
||||
self.model = f"emane_{model}"
|
||||
self.interface = interface
|
||||
self.config_frame = None
|
||||
self.has_error = False
|
||||
try:
|
||||
self.config = self.app.core.get_emane_model_config(
|
||||
self.node.id, self.model, self.interface
|
||||
)
|
||||
self.draw()
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
show_grpc_error(e, self.app, self.app)
|
||||
self.has_error = True
|
||||
self.destroy()
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
|
@ -91,7 +105,9 @@ class EmaneModelDialog(Dialog):
|
|||
|
||||
|
||||
class EmaneConfigDialog(Dialog):
|
||||
def __init__(self, master, app, canvas_node):
|
||||
def __init__(
|
||||
self, master: "Application", app: "Application", canvas_node: "CanvasNode"
|
||||
):
|
||||
super().__init__(
|
||||
master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True
|
||||
)
|
||||
|
@ -116,8 +132,6 @@ class EmaneConfigDialog(Dialog):
|
|||
def draw_emane_configuration(self):
|
||||
"""
|
||||
draw the main frame for emane configuration
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
label = ttk.Label(
|
||||
self.top,
|
||||
|
@ -143,8 +157,6 @@ class EmaneConfigDialog(Dialog):
|
|||
def draw_emane_models(self):
|
||||
"""
|
||||
create a combobox that has all the known emane models
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
|
@ -210,22 +222,17 @@ class EmaneConfigDialog(Dialog):
|
|||
def click_model_config(self):
|
||||
"""
|
||||
draw emane model configuration
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
model_name = self.emane_model.get()
|
||||
logging.info("configuring emane model: %s", model_name)
|
||||
dialog = EmaneModelDialog(
|
||||
self, self.app, self.canvas_node.core_node, model_name
|
||||
)
|
||||
dialog.show()
|
||||
if not dialog.has_error:
|
||||
dialog.show()
|
||||
|
||||
def emane_model_change(self, event):
|
||||
def emane_model_change(self, event: tk.Event):
|
||||
"""
|
||||
update emane model options button
|
||||
|
||||
:param event:
|
||||
:return: nothing
|
||||
"""
|
||||
model_name = self.emane_model.get()
|
||||
self.emane_model_button.config(text=f"{model_name} options")
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import CodeText, ListboxScroll
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class HookDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
def __init__(self, master: Any, app: "Application"):
|
||||
super().__init__(master, app, "Hook", modal=True)
|
||||
self.name = tk.StringVar()
|
||||
self.codetext = None
|
||||
|
@ -62,11 +66,11 @@ class HookDialog(Dialog):
|
|||
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def state_change(self, event):
|
||||
def state_change(self, event: tk.Event):
|
||||
state_name = self.state.get()
|
||||
self.name.set(f"{state_name.lower()}_hook.sh")
|
||||
|
||||
def set(self, hook):
|
||||
def set(self, hook: core_pb2.Hook):
|
||||
self.hook = hook
|
||||
self.name.set(hook.file)
|
||||
self.codetext.text.delete(1.0, tk.END)
|
||||
|
@ -84,7 +88,7 @@ class HookDialog(Dialog):
|
|||
|
||||
|
||||
class HooksDialog(Dialog):
|
||||
def __init__(self, master, app):
|
||||
def __init__(self, master: "Application", app: "Application"):
|
||||
super().__init__(master, app, "Hooks", modal=True)
|
||||
self.listbox = None
|
||||
self.edit_button = None
|
||||
|
@ -140,7 +144,7 @@ class HooksDialog(Dialog):
|
|||
self.edit_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
||||
|
||||
def select(self, event):
|
||||
def select(self, event: tk.Event):
|
||||
if self.listbox.curselection():
|
||||
index = self.listbox.curselection()[0]
|
||||
self.selected = self.listbox.get(index)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue