Merge pull request #373 from coreemu/develop

release-6.1.0
This commit is contained in:
bharnden 2020-02-20 10:00:22 -08:00 committed by GitHub
commit b6aef6d560
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
213 changed files with 11112 additions and 6131 deletions

View file

@ -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
View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 6.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

View file

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

297
daemon/Pipfile.lock generated
View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

View 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

View 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

View 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

View file

View 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)

View 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.

View 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

View 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

View file

@ -0,0 +1 @@
service integrated-vtysh-config

View 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)

View file

@ -0,0 +1 @@
mgen input sink.mgen output mgen_${node.name}.log

View file

@ -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}

View file

@ -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}

View file

@ -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}

View 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"
#}

View file

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

View file

@ -0,0 +1,4 @@
0.0 LISTEN UDP 5000
% for ifname in ifnames:
0.0 Join 224.225.1.2 INTERFACE ${ifname}
% endfor

View file

@ -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 &

View file

@ -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 &

View 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 &

View 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)

View 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.quagga_interface_config(ifc).split("\n"):
${line}
% endfor
% endfor
% endif
!
% endfor
% for service in services:
${service.quagga_config()}
% endfor

View file

@ -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

View file

@ -0,0 +1 @@
service integrated-vtysh-config

View 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)

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View 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
"""

View 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)

View 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>

View file

@ -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}

View file

@ -0,0 +1,5 @@
#!/bin/sh
# auto-generated by DefaultRoute service
% for address in addresses:
ip route add default via ${address}
% endfor

View 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

View 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

View 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>

View file

@ -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

View 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;

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)]

View file

@ -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.

View file

@ -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()]
)

View file

@ -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

View file

@ -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(

View file

@ -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"
)

View file

@ -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

View file

@ -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"
)

View file

@ -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.

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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}"

View file

@ -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()

View file

@ -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)

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -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()

View file

@ -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")

View file

@ -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

View file

@ -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("")

View file

@ -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]

View 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)

View 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)

View file

@ -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]

View file

@ -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)

View file

@ -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")

View file

@ -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