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 - name: grpc
run: | run: |
cd daemon/proto cd daemon/proto
pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/core.proto pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto
- name: test - name: test
run: | run: |
cd daemon cd daemon

5
.gitignore vendored
View file

@ -18,8 +18,8 @@ debian
stamp-h1 stamp-h1
# generated protobuf files # generated protobuf files
daemon/core/api/grpc/core_pb2.py *_pb2.py
daemon/core/api/grpc/core_pb2_grpc.py *_pb2_grpc.py
# python build directory # python build directory
dist dist
@ -54,7 +54,6 @@ coverage.xml
# ignore built input files # ignore built input files
netns/setup.py netns/setup.py
daemon/setup.py daemon/setup.py
ns3/setup.py
# ignore corefx build # ignore corefx build
corefx/target 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 ## 2019-06-10 CORE 5.3.0
* Enhancements * Enhancements
* python 2 / 3 support * python 2 / 3 support

View file

@ -15,7 +15,7 @@ if WANT_DAEMON
endif endif
if WANT_NETNS if WANT_NETNS
NETNS = netns ns3 NETNS = netns
endif endif
# keep docs last due to dependencies on binaries # keep docs last due to dependencies on binaries
@ -96,12 +96,60 @@ fpm -s dir -t deb -n core \
-C $(DESTDIR) -C $(DESTDIR)
endef 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 .PHONY: fpm
fpm: clean-local-fpm fpm: clean-local-fpm
$(MAKE) install DESTDIR=$(DESTDIR) $(MAKE) install DESTDIR=$(DESTDIR)
$(call fpm-deb) $(call fpm-deb)
$(call fpm-rpm) $(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 .PHONY: clean-local-fpm
clean-local-fpm: clean-local-fpm:
-rm -rf *.deb -rm -rf *.deb
@ -137,7 +185,6 @@ change-files:
$(call change-files,scripts/core-daemon.service) $(call change-files,scripts/core-daemon.service)
$(call change-files,scripts/core-daemon) $(call change-files,scripts/core-daemon)
$(call change-files,daemon/core/constants.py) $(call change-files,daemon/core/constants.py)
$(call change-files,ns3/setup.py)
$(call change-files,netns/setup.py) $(call change-files,netns/setup.py)
$(call change-files,daemon/setup.py) $(call change-files,daemon/setup.py)

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 6.0.0) AC_INIT(core, 6.1.0)
# autoconf and automake initialization # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) AC_CONFIG_SRCDIR([netns/version.h.in])
@ -262,8 +262,7 @@ AC_CONFIG_FILES([Makefile
daemon/doc/conf.py daemon/doc/conf.py
daemon/proto/Makefile daemon/proto/Makefile
netns/Makefile netns/Makefile
netns/version.h netns/version.h],)
ns3/Makefile],)
AC_OUTPUT AC_OUTPUT
# Summary text # Summary text

View file

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

297
daemon/Pipfile.lock generated
View file

@ -162,11 +162,11 @@
}, },
"invoke": { "invoke": {
"hashes": [ "hashes": [
"sha256:c52274d2e8a6d64ef0d61093e1983268ea1fc0cd13facb9448c4ef0c9a7ac7da", "sha256:4668a4a594a47f2da2f0672ec2f7b1566f809cebf10bcd95ce2de9ecf39b95d1",
"sha256:f4ec8a134c0122ea042c8912529f87652445d9f4de590b353d23f95bfa1f0efd", "sha256:ae7b4513638bde9afcda0825e9535599637a3f65bd819a27098356027bb17c8a",
"sha256:fc803a5c9052f15e63310aa81a43498d7c55542beb18564db88a9d75a176fa44" "sha256:e04faba8ea7cdf6f5c912be42dcafd5c1074b7f2f306998992c4bfb40a9a690b"
], ],
"version": "==1.3.0" "version": "==1.4.0"
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
@ -199,6 +199,45 @@
], ],
"version": "==4.4.2" "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": { "netaddr": {
"hashes": [ "hashes": [
"sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd", "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd",
@ -215,59 +254,53 @@
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
"sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031", "sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be",
"sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71", "sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946",
"sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c", "sha256:54ebae163e8412aff0b9df1e88adab65788f5f5b58e625dc5c7f51eaf14a6837",
"sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340", "sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f",
"sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa", "sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00",
"sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b", "sha256:5f7ae9126d16194f114435ebb79cc536b5682002a4fa57fa7bb2cbcde65f2f4d",
"sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573", "sha256:62a889aeb0a79e50ecf5af272e9e3c164148f4bd9636cc6bcfa182a52c8b0533",
"sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e", "sha256:7406f5a9b2fd966e79e6abdaf700585a4522e98d6559ce37fc52e5c955fade0a",
"sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab", "sha256:8453f914f4e5a3d828281a6628cf517832abfa13ff50679a4848926dac7c0358",
"sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9", "sha256:87269cc6ce1e3dee11f23fa515e4249ae678dbbe2704598a51cee76c52e19cda",
"sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e", "sha256:875358310ed7abd5320f21dd97351d62de4929b0426cdb1eaa904b64ac36b435",
"sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291", "sha256:8ac6ce7ff3892e5deaab7abaec763538ffd011f74dc1801d93d3c5fc541feee2",
"sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12", "sha256:91b710e3353aea6fc758cdb7136d9bbdcb26b53cefe43e2cba953ac3ee1d3313",
"sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871", "sha256:9d2ba4ed13af381233e2d810ff3bab84ef9f18430a9b336ab69eaf3cd24299ff",
"sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281", "sha256:a62ec5e13e227399be73303ff301f2865bf68657d15ea50b038d25fc41097317",
"sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08", "sha256:ab76e5580b0ed647a8d8d2d2daee170e8e9f8aad225ede314f684e297e3643c2",
"sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41", "sha256:bf4003aa538af3f4205c5fac56eacaa67a6dd81e454ffd9e9f055fff9f1bc614",
"sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2", "sha256:bf598d2e37cf8edb1a2f26ed3fb255191f5232badea4003c16301cb94ac5bdd0",
"sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5", "sha256:c18f70dc27cc5d236f10e7834236aff60aadc71346a5bc1f4f83a4b3abee6386",
"sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb", "sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9",
"sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547", "sha256:dc058b7833184970d1248135b8b0ab702e6daa833be14035179f2acb78ff5636",
"sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75", "sha256:ff3797f2f16bf9d17d53257612da84dd0758db33935777149b3334c01ff68865"
"sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9",
"sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1",
"sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a",
"sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96",
"sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132",
"sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a",
"sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5",
"sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0"
], ],
"version": "==6.2.1" "version": "==7.0.0"
}, },
"protobuf": { "protobuf": {
"hashes": [ "hashes": [
"sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd", "sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37",
"sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed", "sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36",
"sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057", "sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4",
"sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce", "sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d",
"sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03", "sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574",
"sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46", "sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0",
"sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33", "sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5",
"sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c", "sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f",
"sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9", "sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946",
"sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef", "sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104",
"sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b", "sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e",
"sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d", "sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7",
"sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8", "sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a",
"sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6", "sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507",
"sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941", "sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1",
"sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13" "sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593",
"sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c",
"sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1"
], ],
"version": "==3.11.1" "version": "==3.11.2"
}, },
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
@ -303,26 +336,26 @@
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
], ],
"version": "==5.2" "version": "==5.3"
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
], ],
"version": "==1.13.0" "version": "==1.14.0"
} }
}, },
"develop": { "develop": {
@ -483,18 +516,18 @@
}, },
"identify": { "identify": {
"hashes": [ "hashes": [
"sha256:7782115794ec28b011702815d9f5e532244560cd2bf0789c4f09381d43befd90", "sha256:418f3b2313ac0b531139311a6b426854e9cbdfcfb6175447a5039aa6291d8b30",
"sha256:9e7521e9abeaede4d2d1092a106e418c65ddf6b3182b43930bcb3c8cfb974488" "sha256:8ad99ed1f3a965612dcb881435bf58abcfbeb05e230bb8c352b51e8eac103360"
], ],
"version": "==1.4.8" "version": "==1.4.10"
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f" "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
], ],
"markers": "python_version < '3.8'", "markers": "python_version < '3.8'",
"version": "==1.3.0" "version": "==1.4.0"
}, },
"importlib-resources": { "importlib-resources": {
"hashes": [ "hashes": [
@ -529,23 +562,23 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564" "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
], ],
"version": "==8.0.2" "version": "==8.1.0"
}, },
"nodeenv": { "nodeenv": {
"hashes": [ "hashes": [
"sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" "sha256:561057acd4ae3809e665a9aaaf214afff110bbb6a6d5c8a96121aea6878408b3"
], ],
"version": "==1.3.3" "version": "==1.3.4"
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73",
"sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"
], ],
"version": "==19.2" "version": "==20.1"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
@ -556,39 +589,41 @@
}, },
"pre-commit": { "pre-commit": {
"hashes": [ "hashes": [
"sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e", "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850",
"sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50" "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.20.0" "version": "==1.21.0"
}, },
"protobuf": { "protobuf": {
"hashes": [ "hashes": [
"sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd", "sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37",
"sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed", "sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36",
"sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057", "sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4",
"sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce", "sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d",
"sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03", "sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574",
"sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46", "sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0",
"sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33", "sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5",
"sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c", "sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f",
"sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9", "sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946",
"sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef", "sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104",
"sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b", "sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e",
"sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d", "sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7",
"sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8", "sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a",
"sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6", "sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507",
"sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941", "sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1",
"sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13" "sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593",
"sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c",
"sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1"
], ],
"version": "==3.11.1" "version": "==3.11.2"
}, },
"py": { "py": {
"hashes": [ "hashes": [
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
], ],
"version": "==1.8.0" "version": "==1.8.1"
}, },
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
@ -606,41 +641,41 @@
}, },
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f", "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
"sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a" "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
], ],
"version": "==2.4.5" "version": "==2.4.6"
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa", "sha256:1d122e8be54d1a709e56f82e2d85dcba3018313d64647f38a91aec88c239b600",
"sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4" "sha256:c13d1943c63e599b98cf118fcb9703e4d7bde7caa9a432567bcdcae4bf512d20"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.3.2" "version": "==5.3.4"
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
], ],
"version": "==5.2" "version": "==5.3"
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
], ],
"version": "==1.13.0" "version": "==1.14.0"
}, },
"toml": { "toml": {
"hashes": [ "hashes": [
@ -658,17 +693,17 @@
}, },
"wcwidth": { "wcwidth": {
"hashes": [ "hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
], ],
"version": "==0.1.7" "version": "==0.1.8"
}, },
"zipp": { "zipp": {
"hashes": [ "hashes": [
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" "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 import logging
from queue import Empty, Queue from queue import Empty, Queue
from typing import Iterable
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.api.grpc.grpcutils import convert_value from core.api.grpc.grpcutils import convert_value
@ -11,15 +12,15 @@ from core.emulator.data import (
LinkData, LinkData,
NodeData, 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 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 :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) position = core_pb2.Position(x=event.x_position, y=event.y_position)
services = event.services or "" services = event.services or ""
@ -34,13 +35,12 @@ def handle_node_event(event):
return core_pb2.NodeEvent(node=node_proto, source=event.source) 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 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 :return: link event that has message type and link information
:rtype: core.api.grpc.core_pb2.LinkEvent
""" """
interface_one = None interface_one = None
if event.interface1_id is not 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) 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 Handle session event when there is a session event
:param core.emulator.data.EventData event: event data :param event: event data
:return: session event :return: session event
:rtype: core.api.grpc.core_pb2.SessionEvent
""" """
event_time = event.time event_time = event.time
if event_time is not None: 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 Handle configuration event when there is configuration event
:param core.emulator.data.ConfigData event: configuration data :param event: configuration data
:return: configuration event :return: configuration event
:rtype: core.api.grpc.core_pb2.ConfigEvent
""" """
return core_pb2.ConfigEvent( return core_pb2.ConfigEvent(
message_type=event.message_type, 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 Handle exception event when there is exception event
:param core.emulator.data.ExceptionData event: exception data :param event: exception data
:return: exception event :return: exception event
:rtype: core.api.grpc.core_pb2.ExceptionEvent
""" """
return core_pb2.ExceptionEvent( return core_pb2.ExceptionEvent(
node_id=event.node, 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 Handle file event
:param core.emulator.data.FileData event: file data :param event: file data
:return: file event :return: file event
:rtype: core.api.grpc.core_pb2.FileEvent
""" """
return core_pb2.FileEvent( return core_pb2.FileEvent(
message_type=event.message_type, message_type=event.message_type,
@ -179,19 +175,21 @@ class EventStreamer:
Processes session events to generate grpc events. 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. Create a EventStreamer instance.
:param core.emulator.session.Session session: session to process events for :param session: session to process events for
:param set event_types: types of events to process :param event_types: types of events to process
""" """
self.session = session self.session = session
self.event_types = event_types self.event_types = event_types
self.queue = Queue() self.queue = Queue()
self.add_handlers() self.add_handlers()
def add_handlers(self): def add_handlers(self) -> None:
""" """
Add a session event handler for desired event types. Add a session event handler for desired event types.
@ -210,12 +208,11 @@ class EventStreamer:
if core_pb2.EventType.SESSION in self.event_types: if core_pb2.EventType.SESSION in self.event_types:
self.session.event_handlers.append(self.queue.put) self.session.event_handlers.append(self.queue.put)
def process(self): def process(self) -> core_pb2.Event:
""" """
Process the next event in the queue. Process the next event in the queue.
:return: grpc event, or None when invalid event or queue timeout :return: grpc event, or None when invalid event or queue timeout
:rtype: core.api.grpc.core_pb2.Event
""" """
event = core_pb2.Event(session_id=self.session.id) event = core_pb2.Event(session_id=self.session.id)
try: try:
@ -239,7 +236,7 @@ class EventStreamer:
event = None event = None
return event return event
def remove_handlers(self): def remove_handlers(self) -> None:
""" """
Remove session event handlers for events being watched. Remove session event handlers for events being watched.

View file

@ -1,23 +1,29 @@
import logging import logging
import time import time
from typing import Any, Dict, List, Tuple, Type
import netaddr
from core import utils 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.emudata import InterfaceData, LinkOptions, NodeOptions
from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.enumerations import LinkTypes, NodeTypes
from core.nodes.base import CoreNetworkBase from core.emulator.session import Session
from core.nodes.ipaddress import MacAddress from core.nodes.base import CoreNetworkBase, NodeBase
from core.nodes.interface import CoreInterface
from core.services.coreservices import CoreService
WORKERS = 10 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. 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 :return: node type, id, and options
:rtype: tuple
""" """
_id = node_proto.id _id = node_proto.id
_type = node_proto.type _type = node_proto.type
@ -30,6 +36,7 @@ def add_node_data(node_proto):
options.opaque = node_proto.opaque options.opaque = node_proto.opaque
options.image = node_proto.image options.image = node_proto.image
options.services = node_proto.services options.services = node_proto.services
options.config_services = node_proto.config_services
if node_proto.emane: if node_proto.emane:
options.emane = node_proto.emane options.emane = node_proto.emane
if node_proto.server: if node_proto.server:
@ -41,13 +48,12 @@ def add_node_data(node_proto):
return _type, _id, options return _type, _id, options
def link_interface(interface_proto): def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData:
""" """
Create interface data from interface proto. Create interface data from interface proto.
:param core_pb2.Interface interface_proto: interface proto :param interface_proto: interface proto
:return: interface data :return: interface data
:rtype: InterfaceData
""" """
interface = None interface = None
if interface_proto: if interface_proto:
@ -57,8 +63,6 @@ def link_interface(interface_proto):
mac = interface_proto.mac mac = interface_proto.mac
if mac == "": if mac == "":
mac = None mac = None
else:
mac = MacAddress.from_string(mac)
interface = InterfaceData( interface = InterfaceData(
_id=interface_proto.id, _id=interface_proto.id,
name=name, name=name,
@ -71,13 +75,14 @@ def link_interface(interface_proto):
return interface 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. 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 :return: link interfaces and options
:rtype: tuple
""" """
interface_one = link_interface(link_proto.interface_one) interface_one = link_interface(link_proto.interface_one)
interface_two = link_interface(link_proto.interface_two) interface_two = link_interface(link_proto.interface_two)
@ -105,14 +110,15 @@ def add_link_data(link_proto):
return interface_one, interface_two, options 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. Create nodes using a thread pool and wait for completion.
:param core.emulator.session.Session session: session to create nodes in :param session: session to create nodes in
:param list[core_pb2.Node] node_protos: node proto messages :param node_protos: node proto messages
:return: results and exceptions for created nodes :return: results and exceptions for created nodes
:rtype: tuple
""" """
funcs = [] funcs = []
for node_proto in node_protos: for node_proto in node_protos:
@ -126,14 +132,15 @@ def create_nodes(session, node_protos):
return results, exceptions 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. Create links using a thread pool and wait for completion.
:param core.emulator.session.Session session: session to create nodes in :param session: session to create nodes in
:param list[core_pb2.Link] link_protos: link proto messages :param link_protos: link proto messages
:return: results and exceptions for created links :return: results and exceptions for created links
:rtype: tuple
""" """
funcs = [] funcs = []
for link_proto in link_protos: for link_proto in link_protos:
@ -149,14 +156,15 @@ def create_links(session, link_protos):
return results, exceptions 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. Edit links using a thread pool and wait for completion.
:param core.emulator.session.Session session: session to create nodes in :param session: session to create nodes in
:param list[core_pb2.Link] link_protos: link proto messages :param link_protos: link proto messages
:return: results and exceptions for created links :return: results and exceptions for created links
:rtype: tuple
""" """
funcs = [] funcs = []
for link_proto in link_protos: for link_proto in link_protos:
@ -172,32 +180,32 @@ def edit_links(session, link_protos):
return results, exceptions return results, exceptions
def convert_value(value): def convert_value(value: Any) -> str:
""" """
Convert value into string. Convert value into string.
:param value: value :param value: value
:return: string conversion of the value :return: string conversion of the value
:rtype: str
""" """
if value is not None: if value is not None:
value = str(value) value = str(value)
return 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. Retrieve configuration options in a form that is used by the grpc server.
:param dict config: configuration :param config: configuration
:param core.config.ConfigurableOptions configurable_options: configurable options :param configurable_options: configurable options
:return: mapping of configuration ids to configuration options :return: mapping of configuration ids to configuration options
:rtype: dict[str,core.api.grpc.core_pb2.ConfigOption]
""" """
results = {} results = {}
for configuration in configurable_options.configurations(): for configuration in configurable_options.configurations():
value = config[configuration.id] value = config[configuration.id]
config_option = core_pb2.ConfigOption( config_option = common_pb2.ConfigOption(
label=configuration.label, label=configuration.label,
name=configuration.id, name=configuration.id,
value=value, value=value,
@ -214,12 +222,12 @@ def get_config_options(config, configurable_options):
return results return results
def get_links(session, node): def get_links(session: Session, node: NodeBase):
""" """
Retrieve a list of links for grpc to use Retrieve a list of links for grpc to use
:param core.emulator.Session session: node's section :param session: node's section
:param core.nodes.base.CoreNode node: node to get links from :param node: node to get links from
:return: [core.api.grpc.core_pb2.Link] :return: [core.api.grpc.core_pb2.Link]
""" """
links = [] links = []
@ -229,14 +237,13 @@ def get_links(session, node):
return links 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 Get EMANE model id
:param int node_id: node id :param node_id: node id
:param int interface_id: interface id :param interface_id: interface id
:return: EMANE model id :return: EMANE model id
:rtype: int
""" """
if interface_id >= 0: if interface_id >= 0:
return node_id * 1000 + interface_id return node_id * 1000 + interface_id
@ -244,13 +251,12 @@ def get_emane_model_id(node_id, interface_id):
return node_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. Parses EMANE model id to get true node id and interface id.
:param _id: id to parse :param _id: id to parse
:return: node id and interface id :return: node id and interface id
:rtype: tuple
""" """
interface = -1 interface = -1
node_id = _id node_id = _id
@ -260,14 +266,13 @@ def parse_emane_model_id(_id):
return node_id, interface 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 Convert link_data into core protobuf Link
:param core.emulator.session.Session session: :param session:
:param core.emulator.data.LinkData link_data: :param link_data:
:return: core protobuf Link :return: core protobuf Link
:rtype: core.api.grpc.core_pb2.Link
""" """
interface_one = None interface_one = None
if link_data.interface1_id is not 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 Retrieve status about the current interfaces in the system
:return: send and receive status of the 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: with open("/proc/net/dev", "r") as f:
data = f.readlines()[2:] data = f.readlines()[2:]
@ -349,12 +353,12 @@ def get_net_stats():
return 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. Set session location based on location proto.
:param core.emulator.session.Session session: session for location :param session: session for location
:param core_pb2.SessionLocation location: location to set :param location: location to set
:return: nothing :return: nothing
""" """
session.location.refxyz = (location.x, location.y, location.z) session.location.refxyz = (location.x, location.y, location.z)
@ -362,28 +366,34 @@ def session_location(session, location):
session.location.refscale = location.scale 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. Convenience method for setting a node service configuration.
:param core.emulator.session.Session session: session for service configuration :param session: session for service configuration
:param core_pb2.ServiceConfig config: service configuration :param config: service configuration
:return: :return:
""" """
session.services.set_service(config.node_id, config.service) session.services.set_service(config.node_id, config.service)
service = session.services.get_service(config.node_id, config.service) service = session.services.get_service(config.node_id, config.service)
service.startup = tuple(config.startup) if config.files:
service.validate = tuple(config.validate) service.files = tuple(config.files)
service.shutdown = tuple(config.shutdown) 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. Convenience for converting a service to service data proto.
:param service: service to get proto data for :param service: service to get proto data for
:return: service proto data :return: service proto data
:rtype: core.api.grpc.core_pb2.NodeServiceData
""" """
return core_pb2.NodeServiceData( return core_pb2.NodeServiceData(
executables=service.executables, executables=service.executables,
@ -397,3 +407,40 @@ def get_service_configuration(service):
shutdown=service.shutdown, shutdown=service.shutdown,
meta=service.meta, 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. CORE API messaging is leveraged for communication with the GUI.
""" """
import binascii
import socket import socket
import struct import struct
from enum import Enum from enum import Enum
import netaddr
from core.api.tlv import structutils from core.api.tlv import structutils
from core.emulator.enumerations import ( from core.emulator.enumerations import (
ConfigTlvs, ConfigTlvs,
@ -24,7 +27,6 @@ from core.emulator.enumerations import (
RegisterTlvs, RegisterTlvs,
SessionTlvs, SessionTlvs,
) )
from core.nodes.ipaddress import IpAddress, MacAddress
class CoreTlvData: class CoreTlvData:
@ -258,7 +260,7 @@ class CoreTlvDataIpv4Addr(CoreTlvDataObj):
Utility class for packing/unpacking Ipv4 addresses. Utility class for packing/unpacking Ipv4 addresses.
""" """
data_type = IpAddress.from_string data_type = str
data_format = "!2x4s" data_format = "!2x4s"
pad_len = 2 pad_len = 2
@ -267,21 +269,22 @@ class CoreTlvDataIpv4Addr(CoreTlvDataObj):
""" """
Retrieve Ipv4 address value from object. Retrieve Ipv4 address value from object.
:param core.misc.ipaddress.IpAddress obj: ip address to get value from :param str obj: ip address to get value from
:return: :return: packed address
:rtype: bytes
""" """
return obj.addr return socket.inet_pton(socket.AF_INET, obj)
@staticmethod @staticmethod
def new_obj(value): def new_obj(value):
""" """
Retrieve Ipv4 address from a string representation. 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 :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): class CoreTlvDataIPv6Addr(CoreTlvDataObj):
@ -290,7 +293,7 @@ class CoreTlvDataIPv6Addr(CoreTlvDataObj):
""" """
data_format = "!16s2x" data_format = "!16s2x"
data_type = IpAddress.from_string data_type = str
pad_len = 2 pad_len = 2
@staticmethod @staticmethod
@ -298,21 +301,22 @@ class CoreTlvDataIPv6Addr(CoreTlvDataObj):
""" """
Retrieve Ipv6 address value from object. Retrieve Ipv6 address value from object.
:param core.nodes.ipaddress.IpAddress obj: ip address to get value from :param str obj: ip address to get value from
:return: :return: packed address
:rtype: bytes
""" """
return obj.addr return socket.inet_pton(socket.AF_INET6, obj)
@staticmethod @staticmethod
def new_obj(value): def new_obj(value):
""" """
Retrieve Ipv6 address from a string representation. 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 :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): class CoreTlvDataMacAddr(CoreTlvDataObj):
@ -321,7 +325,7 @@ class CoreTlvDataMacAddr(CoreTlvDataObj):
""" """
data_format = "!2x8s" data_format = "!2x8s"
data_type = MacAddress.from_string data_type = str
pad_len = 2 pad_len = 2
@staticmethod @staticmethod
@ -329,23 +333,27 @@ class CoreTlvDataMacAddr(CoreTlvDataObj):
""" """
Retrieve Ipv6 address value from object. Retrieve Ipv6 address value from object.
:param core.nodes.ipaddress.MacAddress obj: mac address to get value from :param str obj: mac address to get value from
:return: :return: packed mac address
:rtype: bytes
""" """
# extend to 64 bits # extend to 64 bits
return b"\0\0" + obj.addr return b"\0\0" + netaddr.EUI(obj).packed
@staticmethod @staticmethod
def new_obj(value): def new_obj(value):
""" """
Retrieve mac address from a string representation. Retrieve mac 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 :return: mac address
:rtype: core.nodes.ipaddress.MacAddress :rtype: str
""" """
# only use 48 bits # 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: class CoreTlv:

View file

@ -527,6 +527,10 @@ class CoreHandler(socketserver.BaseRequestHandler):
"%s handling message:\n%s", threading.currentThread().getName(), message "%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: if message.message_type not in self.message_handlers:
logging.error("no handler for message type: %s", message.type_str()) logging.error("no handler for message type: %s", message.type_str())
return return

View file

@ -4,8 +4,110 @@ Common support for configurable CORE objects.
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Union
from core.emane.nodes import EmaneNet
from core.emulator.data import ConfigData 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: class ConfigShim:
@ -14,13 +116,12 @@ class ConfigShim:
""" """
@classmethod @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. Converts a TLV key/value string into an ordered mapping.
:param str key_values: :param key_values:
:return: ordered mapping of key/value pairs :return: ordered mapping of key/value pairs
:rtype: OrderedDict
""" """
key_values = key_values.split("|") key_values = key_values.split("|")
values = OrderedDict() values = OrderedDict()
@ -30,13 +131,12 @@ class ConfigShim:
return values return values
@classmethod @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. 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 :return: TLV configuration group string
:rtype: str
""" """
group_strings = [] group_strings = []
for config_group in config_groups: for config_group in config_groups:
@ -47,19 +147,25 @@ class ConfigShim:
return "|".join(group_strings) return "|".join(group_strings)
@classmethod @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 Convert this class to a Config API message. Some TLVs are defined
by the class, but node number, conf type flags, and values must by the class, but node number, conf type flags, and values must
be passed in. be passed in.
:param int flags: message flags :param flags: message flags
:param int node_id: node id :param node_id: node id
:param int type_flags: type flags :param type_flags: type flags
:param ConfigurableOptions configurable_options: options to create config data for :param configurable_options: options to create config data for
:param dict config: configuration values for options :param config: configuration values for options
:return: configuration data object :return: configuration data object
:rtype: ConfigData
""" """
key_values = None key_values = None
captions = 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: 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_node = -1
_default_type = _default_node _default_type = _default_node
def __init__(self): def __init__(self) -> None:
""" """
Creates a ConfigurableManager object. Creates a ConfigurableManager object.
""" """
self.node_configurations = {} self.node_configurations = {}
def nodes(self): def nodes(self) -> List[int]:
""" """
Retrieves the ids of all node configurations known by this manager. Retrieves the ids of all node configurations known by this manager.
:return: list of node ids :return: list of node ids
:rtype: list
""" """
return [x for x in self.node_configurations if x != self._default_node] 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. 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 :return: nothing
""" """
if not node_id: if not node_id:
@ -166,27 +243,38 @@ class ConfigurableManager:
elif node_id in self.node_configurations: elif node_id in self.node_configurations:
self.node_configurations.pop(node_id) 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. Set a specific configuration value for a node and configuration type.
:param str _id: configuration key :param _id: configuration key
:param str value: configuration value :param value: configuration value
:param int node_id: node id to store configuration for :param node_id: node id to store configuration for
:param str config_type: configuration type to store configuration for :param config_type: configuration type to store configuration for
:return: nothing :return: nothing
""" """
node_configs = self.node_configurations.setdefault(node_id, OrderedDict()) node_configs = self.node_configurations.setdefault(node_id, OrderedDict())
node_type_configs = node_configs.setdefault(config_type, OrderedDict()) node_type_configs = node_configs.setdefault(config_type, OrderedDict())
node_type_configs[_id] = value 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. Set configurations for a node and configuration type.
:param dict config: configurations to set :param config: configurations to set
:param int node_id: node id to store configuration for :param node_id: node id to store configuration for
:param str config_type: configuration type to store configuration for :param config_type: configuration type to store configuration for
:return: nothing :return: nothing
""" """
logging.debug( logging.debug(
@ -196,17 +284,20 @@ class ConfigurableManager:
node_configs[config_type] = config node_configs[config_type] = config
def get_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. Retrieves a specific configuration for a node and configuration type.
:param str _id: specific configuration to retrieve :param _id: specific configuration to retrieve
:param int node_id: node id to store configuration for :param node_id: node id to store configuration for
:param str config_type: configuration type to store configuration for :param config_type: configuration type to store configuration for
:param default: default value to return when value is not found :param default: default value to return when value is not found
:return: configuration value :return: configuration value
:rtype str
""" """
result = default result = default
node_type_configs = self.get_configs(node_id, config_type) node_type_configs = self.get_configs(node_id, config_type)
@ -214,14 +305,15 @@ class ConfigurableManager:
result = node_type_configs.get(_id, default) result = node_type_configs.get(_id, default)
return result 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. Retrieve configurations for a node and configuration type.
:param int node_id: node id to store configuration for :param node_id: node id to store configuration for
:param str config_type: configuration type to store configuration for :param config_type: configuration type to store configuration for
:return: configurations :return: configurations
:rtype: dict
""" """
result = None result = None
node_configs = self.node_configurations.get(node_id) node_configs = self.node_configurations.get(node_id)
@ -229,83 +321,22 @@ class ConfigurableManager:
result = node_configs.get(config_type) result = node_configs.get(config_type)
return result return result
def get_all_configs(self, node_id=_default_node): def get_all_configs(self, node_id: int = _default_node) -> List[Dict[str, str]]:
""" """
Retrieve all current configuration types for a node. 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 :return: all configuration types for a node
:rtype: dict
""" """
return self.node_configurations.get(node_id) 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): class ModelManager(ConfigurableManager):
""" """
Helps handle setting models for nodes and managing their model configurations. Helps handle setting models for nodes and managing their model configurations.
""" """
def __init__(self): def __init__(self) -> None:
""" """
Creates a ModelManager object. Creates a ModelManager object.
""" """
@ -313,13 +344,15 @@ class ModelManager(ConfigurableManager):
self.models = {} self.models = {}
self.node_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. Set configuration data for a model.
:param int node_id: node id to set model configuration for :param node_id: node id to set model configuration for
:param str model_name: model to set configuration for :param model_name: model to set configuration for
:param dict config: configuration data to set for model :param config: configuration data to set for model
:return: nothing :return: nothing
""" """
# get model class to configure # get model class to configure
@ -341,14 +374,13 @@ class ModelManager(ConfigurableManager):
# set configuration # set configuration
self.set_configs(model_config, node_id=node_id, config_type=model_name) 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. Retrieve configuration data for a model.
:param int node_id: node id to set model configuration for :param node_id: node id to set model configuration for
:param str model_name: model to set configuration for :param model_name: model to set configuration for
:return: current model configuration for node :return: current model configuration for node
:rtype: dict
""" """
# get model class to configure # get model class to configure
model_class = self.models.get(model_name) model_class = self.models.get(model_name)
@ -363,13 +395,18 @@ class ModelManager(ConfigurableManager):
return config 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. Set model and model configuration for node.
:param node: node to set model for :param node: node to set model for
:param model_class: model class to set for node :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 :return: nothing
""" """
logging.debug( logging.debug(
@ -379,14 +416,15 @@ class ModelManager(ConfigurableManager):
config = self.get_model_config(node.id, model_class.name) config = self.get_model_config(node.id, model_class.name)
node.setmodel(model_class, config) 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 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. configured. This is invoked when exporting a session to XML.
:param node: network node to get models for :param node: network node to get models for
:return: list of model and values tuples for the network node :return: list of model and values tuples for the network node
:rtype: list
""" """
all_configs = self.get_all_configs(node.id) all_configs = self.get_all_configs(node.id)
if not all_configs: 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 EMANE Bypass model for CORE
""" """
from core.config import ConfigGroup, Configuration
from core.config import Configuration
from core.emane import emanemodel from core.emane import emanemodel
from core.emulator.enumerations import ConfigDataTypes from core.emulator.enumerations import ConfigDataTypes
@ -19,7 +20,6 @@ class EmaneBypassModel(emanemodel.EmaneModel):
_id="none", _id="none",
_type=ConfigDataTypes.BOOL, _type=ConfigDataTypes.BOOL,
default="0", default="0",
options=["True", "False"],
label="There are no parameters for the bypass model.", label="There are no parameters for the bypass model.",
) )
] ]
@ -29,11 +29,6 @@ class EmaneBypassModel(emanemodel.EmaneModel):
phy_config = [] phy_config = []
@classmethod @classmethod
def load(cls, emane_prefix): def load(cls, emane_prefix: str) -> None:
# ignore default logic # ignore default logic
pass 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 logging
import os import os
from typing import Dict, List
from lxml import etree from lxml import etree
from core.config import ConfigGroup from core.config import ConfigGroup, Configuration
from core.emane import emanemanifest, emanemodel from core.emane import emanemanifest, emanemodel
from core.nodes.interface import CoreInterface
from core.xml import emanexml from core.xml import emanexml
try: try:
@ -20,7 +22,7 @@ except ImportError:
logging.debug("compatible emane python bindings not installed") 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. Helper to use 0 for None values.
""" """
@ -45,26 +47,28 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
external_config = [] external_config = []
@classmethod @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) 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) cls.config_shim = emanemanifest.parse(shim_xml_path, cls.shim_defaults)
@classmethod @classmethod
def configurations(cls): def configurations(cls) -> List[Configuration]:
return cls.config_shim return cls.config_shim
@classmethod @classmethod
def config_groups(cls): def config_groups(cls) -> List[ConfigGroup]:
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))] return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
def build_xml_files(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. Build the necessary nem and commeffect XMLs in the given path.
If an individual NEM has a nonstandard config, we need to build If an individual NEM has a nonstandard config, we need to build
that file also. Otherwise the WLAN-wide that file also. Otherwise the WLAN-wide
nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used. 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 :param interface: interface for the emane node
:return: nothing :return: nothing
""" """
@ -109,14 +113,14 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
def linkconfig( def linkconfig(
self, self,
netif, netif: CoreInterface,
bw=None, bw: float = None,
delay=None, delay: float = None,
loss=None, loss: float = None,
duplicate=None, duplicate: float = None,
jitter=None, jitter: float = None,
netif2=None, netif2: CoreInterface = None,
): ) -> None:
""" """
Generate CommEffect events when a Link Message is received having Generate CommEffect events when a Link Message is received having
link parameters. link parameters.

View file

@ -6,6 +6,7 @@ import logging
import os import os
import threading import threading
from collections import OrderedDict from collections import OrderedDict
from typing import TYPE_CHECKING, Dict, List, Set, Tuple, Type
from core import utils from core import utils
from core.config import ConfigGroup, Configuration, ModelManager 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.emane.tdma import EmaneTdmaModel
from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs
from core.errors import CoreCommandError, CoreError 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 from core.xml import emanexml
if TYPE_CHECKING:
from core.emulator.session import Session
try: try:
from emane.events import EventService from emane.events import EventService
from emane.events import LocationEvent from emane.events import LocationEvent
@ -57,11 +65,11 @@ class EmaneManager(ModelManager):
EVENTCFGVAR = "LIBEMANEEVENTSERVICECONFIG" EVENTCFGVAR = "LIBEMANEEVENTSERVICECONFIG"
DEFAULT_LOG_LEVEL = 3 DEFAULT_LOG_LEVEL = 3
def __init__(self, session): def __init__(self, session: "Session") -> None:
""" """
Creates a Emane instance. 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 :return: nothing
""" """
super().__init__() super().__init__()
@ -83,18 +91,20 @@ class EmaneManager(ModelManager):
self.set_configs(self.emane_config.default_values()) self.set_configs(self.emane_config.default_values())
self.service = None self.service = None
self.eventchannel = None
self.event_device = None self.event_device = None
self.emane_check() 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. 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 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 :return: node/interface model configuration
:rtype: dict
""" """
# use the network-wide config values or interface(NEM)-specific values? # use the network-wide config values or interface(NEM)-specific values?
if interface is None: if interface is None:
@ -129,11 +139,11 @@ class EmaneManager(ModelManager):
return config return config
def config_reset(self, node_id=None): def config_reset(self, node_id: int = None) -> None:
super().config_reset(node_id) super().config_reset(node_id)
self.set_configs(self.emane_config.default_values()) 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. Check if emane is installed and load models.
@ -157,7 +167,7 @@ class EmaneManager(ModelManager):
except CoreCommandError: except CoreCommandError:
logging.info("emane is not installed") logging.info("emane is not installed")
def deleteeventservice(self): def deleteeventservice(self) -> None:
if self.service: if self.service:
for fd in self.service._readFd, self.service._writeFd: for fd in self.service._readFd, self.service._writeFd:
if fd >= 0: if fd >= 0:
@ -168,7 +178,7 @@ class EmaneManager(ModelManager):
self.service = None self.service = None
self.event_device = 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. Re-initialize the EMANE Event service.
The multicast group and/or port may be configured. The multicast group and/or port may be configured.
@ -186,7 +196,7 @@ class EmaneManager(ModelManager):
logging.error( logging.error(
"invalid emane event service device provided: %s", self.event_device "invalid emane event service device provided: %s", self.event_device
) )
return False return
# make sure the event control network is in place # make sure the event control network is in place
eventnet = self.session.add_remove_control_net( eventnet = self.session.add_remove_control_net(
@ -195,19 +205,17 @@ class EmaneManager(ModelManager):
if eventnet is not None: if eventnet is not None:
# direct EMANE events towards control net bridge # direct EMANE events towards control net bridge
self.event_device = eventnet.brname 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 # disabled otachannel for event service
# only needed for e.g. antennaprofile events xmit by models # only needed for e.g. antennaprofile events xmit by models
logging.info("using %s for event service traffic", self.event_device) logging.info("using %s for event service traffic", self.event_device)
try: try:
self.service = EventService(eventchannel=eventchannel, otachannel=None) self.service = EventService(eventchannel=self.eventchannel, otachannel=None)
except EventServiceException: except EventServiceException:
logging.exception("error instantiating emane EventService") logging.exception("error instantiating emane EventService")
return True def load_models(self, emane_models: List[Type[EmaneModel]]) -> None:
def load_models(self, emane_models):
""" """
Load EMANE models and make them available. Load EMANE models and make them available.
""" """
@ -219,11 +227,11 @@ class EmaneManager(ModelManager):
emane_model.load(emane_prefix) emane_model.load(emane_prefix)
self.models[emane_model.name] = emane_model 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. 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 :return: nothing
""" """
with self._emane_node_lock: with self._emane_node_lock:
@ -233,7 +241,7 @@ class EmaneManager(ModelManager):
) )
self._emane_nets[emane_net.id] = emane_net 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, Return a set of CoreNodes that are linked to an EMANE network,
e.g. containers having one or more radio interfaces. e.g. containers having one or more radio interfaces.
@ -245,13 +253,12 @@ class EmaneManager(ModelManager):
nodes.add(netif.node) nodes.add(netif.node)
return nodes return nodes
def setup(self): def setup(self) -> int:
""" """
Setup duties for EMANE manager. Setup duties for EMANE manager.
:return: SUCCESS, NOT_NEEDED, NOT_READY in order to delay session :return: SUCCESS, NOT_NEEDED, NOT_READY in order to delay session
instantiation instantiation
:rtype: int
""" """
logging.debug("emane setup") logging.debug("emane setup")
@ -303,14 +310,13 @@ class EmaneManager(ModelManager):
self.check_node_models() self.check_node_models()
return EmaneManager.SUCCESS return EmaneManager.SUCCESS
def startup(self): def startup(self) -> int:
""" """
After all the EMANE networks have been added, build XML files After all the EMANE networks have been added, build XML files
and start the daemons. and start the daemons.
:return: SUCCESS, NOT_NEEDED, NOT_READY in order to delay session :return: SUCCESS, NOT_NEEDED, NOT_READY in order to delay session
instantiation instantiation
:rtype: int
""" """
self.reset() self.reset()
r = self.setup() r = self.setup()
@ -347,7 +353,7 @@ class EmaneManager(ModelManager):
return EmaneManager.SUCCESS return EmaneManager.SUCCESS
def poststartup(self): def poststartup(self) -> None:
""" """
Retransmit location events now that all NEMs are active. Retransmit location events now that all NEMs are active.
""" """
@ -367,7 +373,7 @@ class EmaneManager(ModelManager):
x, y, z = netif.node.position.get() x, y, z = netif.node.position.get()
emane_node.setnemposition(netif, x, y, z) 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 Remove all EMANE networks from the dictionary, reset port numbers and
nem id counters nem id counters
@ -382,7 +388,7 @@ class EmaneManager(ModelManager):
"emane_transform_port", 8200 "emane_transform_port", 8200
) )
def shutdown(self): def shutdown(self) -> None:
""" """
stop all EMANE daemons stop all EMANE daemons
""" """
@ -394,7 +400,7 @@ class EmaneManager(ModelManager):
self.stopdaemons() self.stopdaemons()
self.stopeventmonitor() self.stopeventmonitor()
def buildxml(self): def buildxml(self) -> None:
""" """
Build XML files required to run EMANE on each node. Build XML files required to run EMANE on each node.
NEMs run inside containers using the control network for passing NEMs run inside containers using the control network for passing
@ -410,7 +416,7 @@ class EmaneManager(ModelManager):
self.buildnemxml() self.buildnemxml()
self.buildeventservicexml() self.buildeventservicexml()
def check_node_models(self): def check_node_models(self) -> None:
""" """
Associate EMANE model classes with EMANE network nodes. Associate EMANE model classes with EMANE network nodes.
""" """
@ -438,7 +444,7 @@ class EmaneManager(ModelManager):
model_class = self.models[model_name] model_class = self.models[model_name]
emane_node.setmodel(model_class, config) 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 Look for the given numerical NEM ID and return the first matching
EMANE network and NEM interface. EMANE network and NEM interface.
@ -456,7 +462,7 @@ class EmaneManager(ModelManager):
return emane_node, netif return emane_node, netif
def numnems(self): def numnems(self) -> int:
""" """
Return the number of NEMs emulated locally. Return the number of NEMs emulated locally.
""" """
@ -466,7 +472,7 @@ class EmaneManager(ModelManager):
count += len(emane_node.netifs()) count += len(emane_node.netifs())
return count return count
def buildplatformxml(self, ctrlnet): def buildplatformxml(self, ctrlnet: CtrlNet) -> None:
""" """
Build a platform.xml file now that all nodes are configured. 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 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. 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] emane_net = self._emane_nets[key]
emanexml.build_xml_files(self, emane_net) emanexml.build_xml_files(self, emane_net)
def buildeventservicexml(self): def buildeventservicexml(self) -> None:
""" """
Build the libemaneeventservice.xml file if event service options Build the libemaneeventservice.xml file if event service options
were changed in the global config. 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. Start one EMANE daemon per node having a radio.
Add a control network even if the user has not configured one. Add a control network even if the user has not configured one.
@ -596,7 +602,7 @@ class EmaneManager(ModelManager):
self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path)) self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path))
logging.info("host emane daemon running: %s", emanecmd) logging.info("host emane daemon running: %s", emanecmd)
def stopdaemons(self): def stopdaemons(self) -> None:
""" """
Kill the appropriate EMANE daemons. Kill the appropriate EMANE daemons.
""" """
@ -623,7 +629,7 @@ class EmaneManager(ModelManager):
except CoreCommandError: except CoreCommandError:
logging.exception("error shutting down emane daemons") logging.exception("error shutting down emane daemons")
def installnetifs(self): def installnetifs(self) -> None:
""" """
Install TUN/TAP virtual interfaces into their proper namespaces Install TUN/TAP virtual interfaces into their proper namespaces
now that the EMANE daemons are running. now that the EMANE daemons are running.
@ -633,7 +639,7 @@ class EmaneManager(ModelManager):
logging.info("emane install netifs for node: %d", key) logging.info("emane install netifs for node: %d", key)
emane_node.installnetifs() emane_node.installnetifs()
def deinstallnetifs(self): def deinstallnetifs(self) -> None:
""" """
Uninstall TUN/TAP virtual interfaces. Uninstall TUN/TAP virtual interfaces.
""" """
@ -641,7 +647,7 @@ class EmaneManager(ModelManager):
emane_node = self._emane_nets[key] emane_node = self._emane_nets[key]
emane_node.deinstallnetifs() emane_node.deinstallnetifs()
def doeventmonitor(self): def doeventmonitor(self) -> bool:
""" """
Returns boolean whether or not EMANE events will be monitored. 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 # generate the EMANE events when nodes are moved
return self.session.options.get_config_bool("emane_event_monitor") 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. Returns boolean whether or not EMANE events will be generated.
""" """
@ -660,7 +666,7 @@ class EmaneManager(ModelManager):
tmp = not self.doeventmonitor() tmp = not self.doeventmonitor()
return tmp return tmp
def starteventmonitor(self): def starteventmonitor(self) -> None:
""" """
Start monitoring EMANE location events if configured to do so. Start monitoring EMANE location events if configured to do so.
""" """
@ -681,7 +687,7 @@ class EmaneManager(ModelManager):
self.eventmonthread.daemon = True self.eventmonthread.daemon = True
self.eventmonthread.start() self.eventmonthread.start()
def stopeventmonitor(self): def stopeventmonitor(self) -> None:
""" """
Stop monitoring EMANE location events. Stop monitoring EMANE location events.
""" """
@ -697,7 +703,7 @@ class EmaneManager(ModelManager):
self.eventmonthread.join() self.eventmonthread.join()
self.eventmonthread = None self.eventmonthread = None
def eventmonitorloop(self): def eventmonitorloop(self) -> None:
""" """
Thread target that monitors EMANE location events. Thread target that monitors EMANE location events.
""" """
@ -724,7 +730,7 @@ class EmaneManager(ModelManager):
threading.currentThread().getName(), threading.currentThread().getName(),
) )
def handlelocationevent(self, rxnemid, eid, data): def handlelocationevent(self, rxnemid: int, eid: int, data: str) -> None:
""" """
Handle an EMANE location event. Handle an EMANE location event.
""" """
@ -747,7 +753,9 @@ class EmaneManager(ModelManager):
logging.debug("emane location event: %s,%s,%s", lat, lon, alt) logging.debug("emane location event: %s,%s,%s", lat, lon, alt)
self.handlelocationeventtoxyz(txnemid, 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 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. 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 # don"t use node.setposition(x,y,z) which generates an event
node.position.set(x, y, z) 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) self.session.broadcast_node(node_data)
return True 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, Return True if an EMANE process associated with the given node is running,
False otherwise. False otherwise.
@ -827,7 +835,7 @@ class EmaneGlobalModel:
name = "emane" name = "emane"
bitmap = None bitmap = None
def __init__(self, session): def __init__(self, session: "Session") -> None:
self.session = session self.session = session
self.nem_config = [ self.nem_config = [
Configuration( Configuration(
@ -840,7 +848,7 @@ class EmaneGlobalModel:
self.emulator_config = None self.emulator_config = None
self.parse_config() self.parse_config()
def parse_config(self): def parse_config(self) -> None:
emane_prefix = self.session.options.get_config( emane_prefix = self.session.options.get_config(
"emane_prefix", default=DEFAULT_EMANE_PREFIX "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 return self.emulator_config + self.nem_config
def config_groups(self): def config_groups(self) -> List[ConfigGroup]:
emulator_len = len(self.emulator_config) emulator_len = len(self.emulator_config)
config_len = len(self.configurations()) config_len = len(self.configurations())
return [ return [
@ -873,7 +881,7 @@ class EmaneGlobalModel:
ConfigGroup("NEM Parameters", emulator_len + 1, config_len), ConfigGroup("NEM Parameters", emulator_len + 1, config_len),
] ]
def default_values(self): def default_values(self) -> Dict[str, str]:
return OrderedDict( return OrderedDict(
[(config.id, config.default) for config in self.configurations()] [(config.id, config.default) for config in self.configurations()]
) )

View file

@ -1,4 +1,5 @@
import logging import logging
from typing import Dict, List
from core.config import Configuration from core.config import Configuration
from core.emulator.enumerations import ConfigDataTypes from core.emulator.enumerations import ConfigDataTypes
@ -13,12 +14,12 @@ except ImportError:
logging.debug("compatible emane python bindings not installed") 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. Convert emane configuration type to core configuration value.
:param str config_type: emane configuration type :param config_type: emane configuration type
:return: :return: core config type
""" """
config_type = config_type.upper() config_type = config_type.upper()
if config_type == "DOUBLE": if config_type == "DOUBLE":
@ -28,14 +29,13 @@ def _type_value(config_type):
return ConfigDataTypes[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. Retrieve possible config value options based on emane regexes.
:param str config_type: emane configuration type :param config_type: emane configuration type
:param str config_regex: emane configuration regex :param config_regex: emane configuration regex
:return: a string listing comma delimited values, if needed, empty string otherwise :return: a string listing comma delimited values, if needed, empty string otherwise
:rtype: list
""" """
if config_type == "bool": if config_type == "bool":
return ["On", "Off"] return ["On", "Off"]
@ -47,16 +47,14 @@ def _get_possible(config_type, config_regex):
return [] 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. Convert default configuration values to one used by core.
:param str config_type_name: emane configuration type name :param config_type_name: emane configuration type name
:param list config_value: emane configuration value list :param config_value: emane configuration value list
:return: default core config value :return: default core config value
:rtype: str
""" """
config_default = "" config_default = ""
if config_type_name == "bool": if config_type_name == "bool":
@ -72,14 +70,13 @@ def _get_default(config_type_name, config_value):
return config_default 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. 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 manifest_path: absolute manifest file path
:param dict defaults: used to override default values for configurations :param defaults: used to override default values for configurations
:return: list of core configuration values :return: list of core configuration values
:rtype: list
""" """
# no results when emane bindings are not present # no results when emane bindings are not present

View file

@ -3,12 +3,14 @@ Defines Emane Models used within CORE.
""" """
import logging import logging
import os import os
from typing import Dict, List
from core.config import ConfigGroup, Configuration from core.config import ConfigGroup, Configuration
from core.emane import emanemanifest from core.emane import emanemanifest
from core.emulator.enumerations import ConfigDataTypes from core.emulator.enumerations import ConfigDataTypes
from core.errors import CoreError from core.errors import CoreError
from core.location.mobility import WirelessModel from core.location.mobility import WirelessModel
from core.nodes.interface import CoreInterface
from core.xml import emanexml from core.xml import emanexml
@ -45,12 +47,12 @@ class EmaneModel(WirelessModel):
config_ignore = set() config_ignore = set()
@classmethod @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 Called after being loaded within the EmaneManager. Provides configured emane_prefix for
parsing xml files. parsing xml files.
:param str emane_prefix: configured emane prefix path :param emane_prefix: configured emane prefix path
:return: nothing :return: nothing
""" """
manifest_path = "share/emane/manifest" manifest_path = "share/emane/manifest"
@ -63,22 +65,20 @@ class EmaneModel(WirelessModel):
cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults) cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults)
@classmethod @classmethod
def configurations(cls): def configurations(cls) -> List[Configuration]:
""" """
Returns the combination all all configurations (mac, phy, and external). Returns the combination all all configurations (mac, phy, and external).
:return: all configurations :return: all configurations
:rtype: list[Configuration]
""" """
return cls.mac_config + cls.phy_config + cls.external_config return cls.mac_config + cls.phy_config + cls.external_config
@classmethod @classmethod
def config_groups(cls): def config_groups(cls) -> List[ConfigGroup]:
""" """
Returns the defined configuration groups. Returns the defined configuration groups.
:return: list of configuration groups. :return: list of configuration groups.
:rtype: list[ConfigGroup]
""" """
mac_len = len(cls.mac_config) mac_len = len(cls.mac_config)
phy_len = len(cls.phy_config) + mac_len phy_len = len(cls.phy_config) + mac_len
@ -89,12 +89,14 @@ class EmaneModel(WirelessModel):
ConfigGroup("External Parameters", phy_len + 1, config_len), 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 Builds xml files for this emane model. Creates a nem.xml file that points to
definitions. 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 :param interface: interface for the emane node
:return: nothing :return: nothing
""" """
@ -127,7 +129,7 @@ class EmaneModel(WirelessModel):
phy_file = os.path.join(self.session.session_dir, phy_name) phy_file = os.path.join(self.session.session_dir, phy_name)
emanexml.create_phy_xml(self, config, phy_file, server) 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. 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) 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 Invoked from MobilityModel when nodes are moved; this causes
emane location events to be generated for the nodes in the moved emane location events to be generated for the nodes in the moved
list, making EmaneModels compatible with Ns2ScriptedMobility. list, making EmaneModels compatible with Ns2ScriptedMobility.
:param bool moved: were nodes moved :param moved: were nodes moved
:param list moved_netifs: interfaces that were moved :param moved_netifs: interfaces that were moved
:return: :return: nothing
""" """
try: try:
wlan = self.session.get_node(self.id) wlan = self.session.get_node(self.id)
@ -153,24 +155,24 @@ class EmaneModel(WirelessModel):
def linkconfig( def linkconfig(
self, self,
netif, netif: CoreInterface,
bw=None, bw: float = None,
delay=None, delay: float = None,
loss=None, loss: float = None,
duplicate=None, duplicate: float = None,
jitter=None, jitter: float = None,
netif2=None, netif2: CoreInterface = None,
): ) -> None:
""" """
Invoked when a Link Message is received. Default is unimplemented. 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 bw: bandwidth to set to
:param delay: packet delay to set to :param delay: packet delay to set to
:param loss: packet loss to set to :param loss: packet loss to set to
:param duplicate: duplicate percentage to set to :param duplicate: duplicate percentage to set to
:param jitter: jitter to set to :param jitter: jitter to set to
:param core.netns.vif.Veth netif2: interface two :param netif2: interface two
:return: nothing :return: nothing
""" """
logging.warning( logging.warning(

View file

@ -15,7 +15,7 @@ class EmaneIeee80211abgModel(emanemodel.EmaneModel):
mac_xml = "ieee80211abgmaclayer.xml" mac_xml = "ieee80211abgmaclayer.xml"
@classmethod @classmethod
def load(cls, emane_prefix): def load(cls, emane_prefix: str) -> None:
cls.mac_defaults["pcrcurveuri"] = os.path.join( cls.mac_defaults["pcrcurveuri"] = os.path.join(
emane_prefix, "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml" emane_prefix, "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml"
) )

View file

@ -4,9 +4,18 @@ share the same MAC+PHY model.
""" """
import logging 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.emulator.enumerations import LinkTypes, NodeTypes, RegisterTlvs
from core.nodes.base import CoreNetworkBase 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: try:
from emane.events import LocationEvent from emane.events import LocationEvent
@ -29,7 +38,14 @@ class EmaneNet(CoreNetworkBase):
type = "wlan" type = "wlan"
is_emane = True 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) super().__init__(session, _id, name, start, server)
self.conf = "" self.conf = ""
self.up = False self.up = False
@ -39,20 +55,20 @@ class EmaneNet(CoreNetworkBase):
def linkconfig( def linkconfig(
self, self,
netif, netif: CoreInterface,
bw=None, bw: float = None,
delay=None, delay: float = None,
loss=None, loss: float = None,
duplicate=None, duplicate: float = None,
jitter=None, jitter: float = None,
netif2=None, netif2: CoreInterface = None,
): ) -> None:
""" """
The CommEffect model supports link configuration. The CommEffect model supports link configuration.
""" """
if not self.model: if not self.model:
return return
return self.model.linkconfig( self.model.linkconfig(
netif=netif, netif=netif,
bw=bw, bw=bw,
delay=delay, delay=delay,
@ -62,19 +78,19 @@ class EmaneNet(CoreNetworkBase):
netif2=netif2, netif2=netif2,
) )
def config(self, conf): def config(self, conf: str) -> None:
self.conf = conf self.conf = conf
def shutdown(self): def shutdown(self) -> None:
pass pass
def link(self, netif1, netif2): def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
pass pass
def unlink(self, netif1, netif2): def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
pass pass
def updatemodel(self, config): def updatemodel(self, config: Dict[str, str]) -> None:
if not self.model: if not self.model:
raise ValueError("no model set to update for node(%s)", self.id) raise ValueError("no model set to update for node(%s)", self.id)
logging.info( logging.info(
@ -82,7 +98,7 @@ class EmaneNet(CoreNetworkBase):
) )
self.model.set_configs(config, node_id=self.id) 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 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 = model(session=self.session, _id=self.id)
self.mobility.update_config(config) 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 Record an interface to numerical ID mapping. The Emane controller
object manages and assigns these IDs for all NEMs. object manages and assigns these IDs for all NEMs.
""" """
self.nemidmap[netif] = nemid self.nemidmap[netif] = nemid
def getnemid(self, netif): def getnemid(self, netif: CoreInterface) -> Optional[int]:
""" """
Given an interface, return its numerical ID. Given an interface, return its numerical ID.
""" """
@ -112,7 +128,7 @@ class EmaneNet(CoreNetworkBase):
else: else:
return self.nemidmap[netif] 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 Given a numerical NEM ID, return its interface. This returns the
first interface that matches the given NEM ID. first interface that matches the given NEM ID.
@ -122,13 +138,13 @@ class EmaneNet(CoreNetworkBase):
return netif return netif
return None return None
def netifs(self, sort=True): def netifs(self, sort: bool = True) -> List[CoreInterface]:
""" """
Retrieve list of linked interfaces sorted by node number. Retrieve list of linked interfaces sorted by node number.
""" """
return sorted(self._netif.values(), key=lambda ifc: ifc.node.id) 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 Install TAP devices into their namespaces. This is done after
EMANE daemons have been started, because that is their only chance 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() x, y, z = netif.node.position.get()
self.setnemposition(netif, x, y, z) self.setnemposition(netif, x, y, z)
def deinstallnetifs(self): def deinstallnetifs(self) -> None:
""" """
Uninstall TAP devices. This invokes their shutdown method for Uninstall TAP devices. This invokes their shutdown method for
any required cleanup; the device may be actually removed when any required cleanup; the device may be actually removed when
@ -170,7 +186,9 @@ class EmaneNet(CoreNetworkBase):
netif.shutdown() netif.shutdown()
netif.poshook = None 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. 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) event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.session.emane.service.publish(0, event) self.session.emane.service.publish(0, event)
def setnempositions(self, moved_netifs): def setnempositions(self, moved_netifs: List[CoreInterface]) -> None:
""" """
Several NEMs have moved, from e.g. a WaypointMobilityModel Several NEMs have moved, from e.g. a WaypointMobilityModel
calculation. Generate an EMANE Location Event having several calculation. Generate an EMANE Location Event having several

View file

@ -15,7 +15,7 @@ class EmaneRfPipeModel(emanemodel.EmaneModel):
mac_xml = "rfpipemaclayer.xml" mac_xml = "rfpipemaclayer.xml"
@classmethod @classmethod
def load(cls, emane_prefix): def load(cls, emane_prefix: str) -> None:
cls.mac_defaults["pcrcurveuri"] = os.path.join( cls.mac_defaults["pcrcurveuri"] = os.path.join(
emane_prefix, "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" emane_prefix, "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
) )

View file

@ -27,7 +27,7 @@ class EmaneTdmaModel(emanemodel.EmaneModel):
config_ignore = {schedule_name} config_ignore = {schedule_name}
@classmethod @classmethod
def load(cls, emane_prefix): def load(cls, emane_prefix: str) -> None:
cls.mac_defaults["pcrcurveuri"] = os.path.join( cls.mac_defaults["pcrcurveuri"] = os.path.join(
emane_prefix, emane_prefix,
"share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml", "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. Logic to execute after the emane manager is finished with startup.

View file

@ -3,17 +3,20 @@ import logging
import os import os
import signal import signal
import sys import sys
from typing import Mapping, Type
import core.services import core.services
from core import configservices
from core.configservice.manager import ConfigServiceManager
from core.emulator.session import Session from core.emulator.session import Session
from core.services.coreservices import ServiceManager 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. Handle signals and force an exit with cleanup.
:param int signal_number: signal number :param signal_number: signal number
:param _: ignored :param _: ignored
:return: nothing :return: nothing
""" """
@ -33,11 +36,11 @@ class CoreEmu:
Provides logic for creating and configuring CORE sessions and the nodes within them. 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. Create a CoreEmu object.
:param dict config: configuration options :param config: configuration options
""" """
# set umask 0 # set umask 0
os.umask(0) os.umask(0)
@ -54,10 +57,18 @@ class CoreEmu:
self.service_errors = [] self.service_errors = []
self.load_services() 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 # catch exit event
atexit.register(self.shutdown) atexit.register(self.shutdown)
def load_services(self): def load_services(self) -> None:
# load default services # load default services
self.service_errors = core.services.load() self.service_errors = core.services.load()
@ -70,7 +81,7 @@ class CoreEmu:
custom_service_errors = ServiceManager.add_services(service_path) custom_service_errors = ServiceManager.add_services(service_path)
self.service_errors.extend(custom_service_errors) self.service_errors.extend(custom_service_errors)
def shutdown(self): def shutdown(self) -> None:
""" """
Shutdown all CORE session. Shutdown all CORE session.
@ -83,31 +94,30 @@ class CoreEmu:
session = sessions[_id] session = sessions[_id]
session.shutdown() 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. Create a new CORE session.
:param int _id: session id for new session :param _id: session id for new session
:param class _cls: Session class to use :param _cls: Session class to use
:return: created session :return: created session
:rtype: EmuSession
""" """
if not _id: if not _id:
_id = 1 _id = 1
while _id in self.sessions: while _id in self.sessions:
_id += 1 _id += 1
session = _cls(_id, config=self.config) session = _cls(_id, config=self.config)
session.service_manager = self.service_manager
logging.info("created session: %s", _id) logging.info("created session: %s", _id)
self.sessions[_id] = session self.sessions[_id] = session
return session return session
def delete_session(self, _id): def delete_session(self, _id: int) -> bool:
""" """
Shutdown and delete a CORE session. 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 :return: True if deleted, False otherwise
:rtype: bool
""" """
logging.info("deleting session: %s", _id) logging.info("deleting session: %s", _id)
session = self.sessions.pop(_id, None) session = self.sessions.pop(_id, None)

View file

@ -7,16 +7,20 @@ import os
import threading import threading
from collections import OrderedDict from collections import OrderedDict
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Callable, Dict, Tuple
import netaddr
from fabric import Connection from fabric import Connection
from invoke import UnexpectedExit from invoke import UnexpectedExit
from core import utils from core import utils
from core.errors import CoreCommandError from core.errors import CoreCommandError
from core.nodes.interface import GreTap from core.nodes.interface import GreTap
from core.nodes.ipaddress import IpAddress
from core.nodes.network import CoreNetwork, CtrlNet from core.nodes.network import CoreNetwork, CtrlNet
if TYPE_CHECKING:
from core.emulator.session import Session
LOCK = threading.Lock() LOCK = threading.Lock()
CMD_HIDE = True CMD_HIDE = True
@ -26,29 +30,30 @@ class DistributedServer:
Provides distributed server interactions. Provides distributed server interactions.
""" """
def __init__(self, name, host): def __init__(self, name: str, host: str) -> None:
""" """
Create a DistributedServer instance. Create a DistributedServer instance.
:param str name: convenience name to associate with host :param name: convenience name to associate with host
:param str host: host to connect to :param host: host to connect to
""" """
self.name = name self.name = name
self.host = host self.host = host
self.conn = Connection(host, user="root") self.conn = Connection(host, user="root")
self.lock = threading.Lock() 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. Run command remotely using server connection.
:param str cmd: command to run :param cmd: command to run
:param dict env: environment for remote command, default is None :param env: environment for remote command, default is None
:param str cwd: directory to run command in, defaults to None, which is the :param cwd: directory to run command in, defaults to None, which is the
user's home directory 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 :return: stdout when success
:rtype: str
:raises CoreCommandError: when a non-zero exit status occurs :raises CoreCommandError: when a non-zero exit status occurs
""" """
@ -73,24 +78,24 @@ class DistributedServer:
stdout, stderr = e.streams_for_display() stdout, stderr = e.streams_for_display()
raise CoreCommandError(e.result.exited, cmd, stdout, stderr) 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. Push file to remote server.
:param str source: source file to push :param source: source file to push
:param str destination: destination file location :param destination: destination file location
:return: nothing :return: nothing
""" """
with self.lock: with self.lock:
self.conn.put(source, destination) 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 Remote push file contents to a remote server, using a temp file as an
intermediate step. intermediate step.
:param str destination: file destination for data :param destination: file destination for data
:param str data: data to store in remote file :param data: data to store in remote file
:return: nothing :return: nothing
""" """
with self.lock: with self.lock:
@ -106,11 +111,11 @@ class DistributedController:
Provides logic for dealing with remote tunnels and distributed servers. Provides logic for dealing with remote tunnels and distributed servers.
""" """
def __init__(self, session): def __init__(self, session: "Session") -> None:
""" """
Create Create
:param session: :param session: session
""" """
self.session = session self.session = session
self.servers = OrderedDict() self.servers = OrderedDict()
@ -119,12 +124,12 @@ class DistributedController:
"distributed_address", default=None "distributed_address", default=None
) )
def add_server(self, name, host): def add_server(self, name: str, host: str) -> None:
""" """
Add distributed server configuration. Add distributed server configuration.
:param str name: distributed server name :param name: distributed server name
:param str host: distributed server host address :param host: distributed server host address
:return: nothing :return: nothing
""" """
server = DistributedServer(name, host) server = DistributedServer(name, host)
@ -132,7 +137,7 @@ class DistributedController:
cmd = f"mkdir -p {self.session.session_dir}" cmd = f"mkdir -p {self.session.session_dir}"
server.remote_cmd(cmd) server.remote_cmd(cmd)
def execute(self, func): def execute(self, func: Callable[[DistributedServer], None]) -> None:
""" """
Convenience for executing logic against all distributed servers. Convenience for executing logic against all distributed servers.
@ -143,7 +148,7 @@ class DistributedController:
server = self.servers[name] server = self.servers[name]
func(server) func(server)
def shutdown(self): def shutdown(self) -> None:
""" """
Shutdown logic for dealing with distributed tunnels and server session Shutdown logic for dealing with distributed tunnels and server session
directories. directories.
@ -165,7 +170,7 @@ class DistributedController:
# clear tunnels # clear tunnels
self.tunnels.clear() self.tunnels.clear()
def start(self): def start(self) -> None:
""" """
Start distributed network tunnels. Start distributed network tunnels.
@ -184,19 +189,20 @@ class DistributedController:
server = self.servers[name] server = self.servers[name]
self.create_gre_tunnel(node, server) 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. 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 node: node to create gre tunnel for
:param core.emulator.distributed.DistributedServer server: server to create :param server: server to create
tunnel for tunnel for
:return: local and remote gre taps created for tunnel :return: local and remote gre taps created for tunnel
:rtype: tuple
""" """
host = server.host 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) tunnel = self.tunnels.get(key)
if tunnel is not None: if tunnel is not None:
return tunnel return tunnel
@ -222,16 +228,15 @@ class DistributedController:
self.tunnels[key] = tunnel self.tunnels[key] = tunnel
return 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. Compute a 32-bit key used to uniquely identify a GRE tunnel.
The hash(n1num), hash(n2num) values are used, so node numbers may be The hash(n1num), hash(n2num) values are used, so node numbers may be
None or string values (used for e.g. "ctrlnet"). None or string values (used for e.g. "ctrlnet").
:param int n1_id: node one id :param n1_id: node one id
:param int n2_id: node two id :param n2_id: node two id
:return: tunnel key for the node pair :return: tunnel key for the node pair
:rtype: int
""" """
logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id) logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id)
key = ( key = (
@ -239,12 +244,12 @@ class DistributedController:
) )
return key & 0xFFFFFFFF 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. Return the GreTap between two nodes if it exists.
:param int n1_id: node one id :param n1_id: node one id
:param int n2_id: node two id :param n2_id: node two id
:return: gre tap between nodes or None :return: gre tap between nodes or None
""" """
key = self.tunnel_key(n1_id, n2_id) 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.emane.nodes import EmaneNet
from core.emulator.enumerations import LinkTypes 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 from core.nodes.physical import PhysicalNode
class IdGen: class IdGen:
def __init__(self, _id=0): def __init__(self, _id: int = 0) -> None:
self.id = _id self.id = _id
def next(self): def next(self) -> int:
self.id += 1 self.id += 1
return self.id return self.id
def create_interface(node, network, interface_data): def link_config(
""" network: CoreNetworkBase,
Create an interface for a node on a network using provided interface data. interface: CoreInterface,
link_options: LinkOptions,
:param node: node to create interface for devname: str = None,
:param core.nodes.base.CoreNetworkBase network: network to associate interface with interface_two: CoreInterface = None,
:param core.emulator.emudata.InterfaceData interface_data: interface data ) -> None:
: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):
""" """
Convenience method for configuring a link, Convenience method for configuring a link,
:param network: network to configure link for :param network: network to configure link for
:param interface: interface to configure :param interface: interface to configure
:param core.emulator.emudata.LinkOptions link_options: data to configure link with :param link_options: data to configure link with
:param str devname: device name, default is None :param devname: device name, default is None
:param interface_two: other interface associated, default is None :param interface_two: other interface associated, default is None
:return: nothing :return: nothing
""" """
@ -66,14 +60,14 @@ class NodeOptions:
Options for creating and updating nodes within core. 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. Create a NodeOptions object.
:param str name: name of node, defaults to node class name postfix with its id :param 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 model: defines services for default and physical nodes, defaults to
"router" "router"
:param str image: image to use for docker nodes :param image: image to use for docker nodes
""" """
self.name = name self.name = name
self.model = model self.model = model
@ -81,6 +75,7 @@ class NodeOptions:
self.icon = None self.icon = None
self.opaque = None self.opaque = None
self.services = [] self.services = []
self.config_services = []
self.x = None self.x = None
self.y = None self.y = None
self.lat = None self.lat = None
@ -91,24 +86,24 @@ class NodeOptions:
self.image = image self.image = image
self.emane = None self.emane = None
def set_position(self, x, y): def set_position(self, x: float, y: float) -> None:
""" """
Convenience method for setting position. Convenience method for setting position.
:param float x: x position :param x: x position
:param float y: y position :param y: y position
:return: nothing :return: nothing
""" """
self.x = x self.x = x
self.y = y 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. Convenience method for setting location.
:param float lat: latitude :param lat: latitude
:param float lon: longitude :param lon: longitude
:param float alt: altitude :param alt: altitude
:return: nothing :return: nothing
""" """
self.lat = lat self.lat = lat
@ -121,11 +116,11 @@ class LinkOptions:
Options for creating and updating links within core. 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. Create a LinkOptions object.
:param core.emulator.enumerations.LinkTypes _type: type of link, defaults to :param _type: type of link, defaults to
wired wired
""" """
self.type = _type self.type = _type
@ -146,17 +141,100 @@ class LinkOptions:
self.opaque = None 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: class IpPrefixes:
""" """
Convenience class to help generate IP4 and IP6 addresses for nodes within CORE. 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. Creates an IpPrefixes object.
:param str ip4_prefix: ip4 prefix to use for generation :param ip4_prefix: ip4 prefix to use for generation
:param str ip6_prefix: ip6 prefix to use for generation :param ip6_prefix: ip6 prefix to use for generation
:raises ValueError: when both ip4 and ip6 prefixes have not been provided :raises ValueError: when both ip4 and ip6 prefixes have not been provided
""" """
if not ip4_prefix and not ip6_prefix: if not ip4_prefix and not ip6_prefix:
@ -164,46 +242,45 @@ class IpPrefixes:
self.ip4 = None self.ip4 = None
if ip4_prefix: if ip4_prefix:
self.ip4 = Ipv4Prefix(ip4_prefix) self.ip4 = netaddr.IPNetwork(ip4_prefix)
self.ip6 = None self.ip6 = None
if ip6_prefix: 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. Convenience method to return the IP4 address for a node.
:param node: node to get IP4 address for :param node: node to get IP4 address for
:return: IP4 address or None :return: IP4 address or None
:rtype: str
""" """
if not self.ip4: if not self.ip4:
raise ValueError("ip4 prefixes have not been set") 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. Convenience method to return the IP6 address for a node.
:param node: node to get IP6 address for :param node: node to get IP6 address for
:return: IP4 address or None :return: IP4 address or None
:rtype: str
""" """
if not self.ip6: if not self.ip6:
raise ValueError("ip6 prefixes have not been set") 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 Creates interface data for linking nodes, using the nodes unique id for
generation, along with a random mac address, unless provided. generation, along with a random mac address, unless provided.
:param core.nodes.base.CoreNode node: node to create interface for :param node: node to create interface for
:param str name: name to set for interface, default is eth{id} :param name: name to set for interface, default is eth{id}
:param str mac: mac address to use for this interface, default is random :param mac: mac address to use for this interface, default is random
generation generation
:return: new interface data for the provided node :return: new interface data for the provided node
:rtype: InterfaceData
""" """
# interface id # interface id
inteface_id = node.newifindex() inteface_id = node.newifindex()
@ -212,19 +289,19 @@ class IpPrefixes:
ip4 = None ip4 = None
ip4_mask = None ip4_mask = None
if self.ip4: if self.ip4:
ip4 = str(self.ip4.addr(node.id)) ip4 = self.ip4_address(node)
ip4_mask = self.ip4.prefixlen ip4_mask = self.ip4.prefixlen
# generate ip6 data # generate ip6 data
ip6 = None ip6 = None
ip6_mask = None ip6_mask = None
if self.ip6: if self.ip6:
ip6 = str(self.ip6.addr(node.id)) ip6 = self.ip6_address(node)
ip6_mask = self.ip6.prefixlen ip6_mask = self.ip6.prefixlen
# random mac # random mac
if not mac: if not mac:
mac = MacAddress.random() mac = utils.random_mac()
return InterfaceData( return InterfaceData(
_id=inteface_id, _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
""" """
node.newnetif(
def __init__(self, _id, name, mac, ip4, ip4_mask, ip6, ip6_mask): network,
""" addrlist=interface_data.get_addresses(),
Creates an InterfaceData object. hwaddr=interface_data.mac,
ifindex=interface_data.id,
:param int _id: interface id ifname=interface_data.name,
:param str name: name for interface )
:param core.nodes.ipaddress.MacAddress mac: mac address return node.netif(interface_data.id)
: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]

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.config import ConfigurableManager, ConfigurableOptions, Configuration
from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs
from core.plugins.sdt import Sdt from core.plugins.sdt import Sdt
@ -34,21 +36,18 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
_id="enablerj45", _id="enablerj45",
_type=ConfigDataTypes.BOOL, _type=ConfigDataTypes.BOOL,
default="1", default="1",
options=["On", "Off"],
label="Enable RJ45s", label="Enable RJ45s",
), ),
Configuration( Configuration(
_id="preservedir", _id="preservedir",
_type=ConfigDataTypes.BOOL, _type=ConfigDataTypes.BOOL,
default="0", default="0",
options=["On", "Off"],
label="Preserve session dir", label="Preserve session dir",
), ),
Configuration( Configuration(
_id="enablesdt", _id="enablesdt",
_type=ConfigDataTypes.BOOL, _type=ConfigDataTypes.BOOL,
default="0", default="0",
options=["On", "Off"],
label="Enable SDT3D output", label="Enable SDT3D output",
), ),
Configuration( Configuration(
@ -60,29 +59,52 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
] ]
config_type = RegisterTlvs.UTILITY.value config_type = RegisterTlvs.UTILITY.value
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self.set_configs(self.default_values()) self.set_configs(self.default_values())
def get_config( def get_config(
self, self,
_id, _id: str,
node_id=ConfigurableManager._default_node, node_id: int = ConfigurableManager._default_node,
config_type=ConfigurableManager._default_type, config_type: str = ConfigurableManager._default_type,
default=None, 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) value = super().get_config(_id, node_id, config_type, default)
if value == "": if value == "":
value = default value = default
return value 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) value = self.get_config(name)
if value is None: if value is None:
return default return default
return value.lower() == "true" 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) value = self.get_config(name, default=default)
if value is not None: if value is not None:
value = int(value) value = int(value)

View file

@ -9,7 +9,7 @@ class CoreCommandError(subprocess.CalledProcessError):
Used when encountering internal CORE command errors. Used when encountering internal CORE command errors.
""" """
def __str__(self): def __str__(self) -> str:
return ( return (
f"Command({self.cmd}), Status({self.returncode}):\n" f"Command({self.cmd}), Status({self.returncode}):\n"
f"stdout: {self.output}\nstderr: {self.stderr}" f"stdout: {self.output}\nstderr: {self.stderr}"

View file

@ -17,8 +17,8 @@ HEIGHT = 800
class Application(tk.Frame): class Application(tk.Frame):
def __init__(self, master=None): def __init__(self, proxy: bool):
super().__init__(master) super().__init__(master=None)
# load node icons # load node icons
NodeUtils.setup() NodeUtils.setup()
@ -33,7 +33,7 @@ class Application(tk.Frame):
self.guiconfig = appconfig.read() self.guiconfig = appconfig.read()
self.style = ttk.Style() self.style = ttk.Style()
self.setup_theme() self.setup_theme()
self.core = CoreClient(self) self.core = CoreClient(self, proxy)
self.setup_app() self.setup_app()
self.draw() self.draw()
self.core.set_up() self.core.set_up()

View file

@ -1,4 +1,3 @@
import logging
import os import os
import shutil import shutil
from pathlib import Path from pathlib import Path
@ -16,6 +15,7 @@ ICONS_PATH = HOME_PATH.joinpath("icons")
MOBILITY_PATH = HOME_PATH.joinpath("mobility") MOBILITY_PATH = HOME_PATH.joinpath("mobility")
XMLS_PATH = HOME_PATH.joinpath("xmls") XMLS_PATH = HOME_PATH.joinpath("xmls")
CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") CONFIG_PATH = HOME_PATH.joinpath("gui.yaml")
LOG_PATH = HOME_PATH.joinpath("gui.log")
# local paths # local paths
DATA_PATH = Path(__file__).parent.joinpath("data") DATA_PATH = Path(__file__).parent.joinpath("data")
@ -52,9 +52,7 @@ def copy_files(current_path, new_path):
def check_directory(): def check_directory():
if HOME_PATH.exists(): if HOME_PATH.exists():
logging.info("~/.coretk exists")
return return
logging.info("creating ~/.coretk")
HOME_PATH.mkdir() HOME_PATH.mkdir()
BACKGROUNDS_PATH.mkdir() BACKGROUNDS_PATH.mkdir()
CUSTOM_EMANE_PATH.mkdir() CUSTOM_EMANE_PATH.mkdir()
@ -96,6 +94,7 @@ def check_directory():
}, },
"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}],
"nodes": [], "nodes": [],
"recentfiles": [],
"observers": [{"name": "hello", "cmd": "echo hello"}], "observers": [{"name": "hello", "cmd": "echo hello"}],
} }
save(config) save(config)

View file

@ -5,20 +5,26 @@ import json
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Dict, List
import grpc 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 import appconfig
from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.mobilityplayer import MobilityPlayer
from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.sessions import SessionsDialog
from core.gui.errors import show_grpc_error from core.gui.errors import show_grpc_error
from core.gui.graph import tags 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.shape import AnnotationData, Shape
from core.gui.graph.shapeutils import ShapeType from core.gui.graph.shapeutils import ShapeType
from core.gui.interface import InterfaceManager from core.gui.interface import InterfaceManager
from core.gui.nodeutils import NodeDraw, NodeUtils from core.gui.nodeutils import NodeDraw, NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
GUI_SOURCE = "gui" GUI_SOURCE = "gui"
OBSERVERS = { OBSERVERS = {
"processes": "ps", "processes": "ps",
@ -32,31 +38,44 @@ OBSERVERS = {
"IPSec policies": "setkey -DP", "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: class CoreServer:
def __init__(self, name, address, port): def __init__(self, name: str, address: str, port: int):
self.name = name self.name = name
self.address = address self.address = address
self.port = port self.port = port
class Observer: class Observer:
def __init__(self, name, cmd): def __init__(self, name: str, cmd: str):
self.name = name self.name = name
self.cmd = cmd self.cmd = cmd
class CoreClient: class CoreClient:
def __init__(self, app): def __init__(self, app: "Application", proxy: bool):
""" """
Create a CoreGrpc instance Create a CoreGrpc instance
""" """
self.client = client.CoreGrpcClient() self.client = client.CoreGrpcClient(proxy=proxy)
self.session_id = None self.session_id = None
self.node_ids = [] self.node_ids = []
self.app = app self.app = app
self.master = app.master self.master = app.master
self.services = {} self.services = {}
self.config_services_groups = {}
self.config_services = {}
self.default_services = {} self.default_services = {}
self.emane_models = [] self.emane_models = []
self.observer = None self.observer = None
@ -82,11 +101,17 @@ class CoreClient:
self.emane_model_configs = {} self.emane_model_configs = {}
self.emane_config = None self.emane_config = None
self.service_configs = {} self.service_configs = {}
self.config_service_configs = {}
self.file_configs = {} self.file_configs = {}
self.mobility_players = {} self.mobility_players = {}
self.handling_throughputs = None self.handling_throughputs = None
self.handling_events = None self.handling_events = None
self.xml_dir = None
self.xml_file = None
self.modified_service_nodes = set()
def reset(self): def reset(self):
# helpers # helpers
self.interfaces_manager.reset() self.interfaces_manager.reset()
@ -101,6 +126,9 @@ class CoreClient:
self.emane_config = None self.emane_config = None
self.service_configs.clear() self.service_configs.clear()
self.file_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() self.mobility_players.clear()
# clear streams # clear streams
if self.handling_throughputs: if self.handling_throughputs:
@ -110,7 +138,7 @@ class CoreClient:
self.handling_events.cancel() self.handling_events.cancel()
self.handling_events = None self.handling_events = None
def set_observer(self, value): def set_observer(self, value: str):
self.observer = value self.observer = value
def read_config(self): def read_config(self):
@ -132,9 +160,9 @@ class CoreClient:
observer = Observer(config["name"], config["cmd"]) observer = Observer(config["name"], config["cmd"])
self.custom_observers[observer.name] = observer 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: if event.session_id != self.session_id:
logging.warn( logging.warning(
"ignoring event session(%s) current(%s)", "ignoring event session(%s) current(%s)",
event.session_id, event.session_id,
self.session_id, self.session_id,
@ -142,7 +170,6 @@ class CoreClient:
return return
if event.HasField("link_event"): if event.HasField("link_event"):
logging.info("link event: %s", event)
self.handle_link_event(event.link_event) self.handle_link_event(event.link_event)
elif event.HasField("session_event"): elif event.HasField("session_event"):
logging.info("session event: %s", event) logging.info("session event: %s", event)
@ -170,7 +197,8 @@ class CoreClient:
else: else:
logging.info("unhandled event: %s", event) 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_one_id = event.link.node_one_id
node_two_id = event.link.node_two_id node_two_id = event.link.node_two_id
canvas_node_one = self.canvas_nodes[node_one_id] canvas_node_one = self.canvas_nodes[node_one_id]
@ -183,7 +211,8 @@ class CoreClient:
else: else:
logging.warning("unknown link event: %s", event.message_type) 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: if event.source == GUI_SOURCE:
return return
node_id = event.node.id node_id = event.node.id
@ -201,7 +230,7 @@ class CoreClient:
self.handling_throughputs.cancel() self.handling_throughputs.cancel()
self.handling_throughputs = None self.handling_throughputs = None
def handle_throughputs(self, event): def handle_throughputs(self, event: core_pb2.ThroughputsEvent):
if event.session_id != self.session_id: if event.session_id != self.session_id:
logging.warning( logging.warning(
"ignoring throughput event session(%s) current(%s)", "ignoring throughput event session(%s) current(%s)",
@ -209,14 +238,15 @@ class CoreClient:
self.session_id, self.session_id,
) )
return return
logging.info("handling throughputs event: %s", event) logging.debug("handling throughputs event: %s", event)
self.app.canvas.set_throughputs(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) logging.info("exception event: %s", event)
self.app.statusbar.core_alarms.append(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 # update session and title
self.session_id = session_id self.session_id = session_id
self.master.title(f"CORE Session({self.session_id})") self.master.title(f"CORE Session({self.session_id})")
@ -278,32 +308,45 @@ class CoreClient:
for config in response.configs: for config in response.configs:
service_configs = self.service_configs.setdefault(config.node_id, {}) service_configs = self.service_configs.setdefault(config.node_id, {})
service_configs[config.service] = config.data 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: for file_name in config.files:
file_configs = self.file_configs.setdefault(config.node_id, {}) file_configs = self.file_configs.setdefault(config.node_id, {})
files = file_configs.setdefault(config.service, {}) files = file_configs.setdefault(config.service, {})
data = config.files[file_name] data = config.files[file_name]
files[file_name] = data 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 # draw session
self.app.canvas.reset_and_redraw(session) self.app.canvas.reset_and_redraw(session)
# get metadata # get metadata
response = self.client.get_session_metadata(self.session_id) response = self.client.get_session_metadata(self.session_id)
self.parse_metadata(response.config) self.parse_metadata(response.config)
except grpc.RpcError as e: 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 # update ui to represent current state
self.app.after(0, self.app.joined_session_update) self.app.after(0, self.app.joined_session_update)
def is_runtime(self): def is_runtime(self) -> bool:
return self.state == core_pb2.SessionState.RUNTIME return self.state == core_pb2.SessionState.RUNTIME
def parse_metadata(self, config): def parse_metadata(self, config: Dict[str, str]):
# canvas setting # canvas setting
canvas_config = config.get("canvas") canvas_config = config.get("canvas")
logging.info("canvas metadata: %s", canvas_config) logging.debug("canvas metadata: %s", canvas_config)
if canvas_config: if canvas_config:
canvas_config = json.loads(canvas_config) canvas_config = json.loads(canvas_config)
@ -364,8 +407,6 @@ class CoreClient:
def create_new_session(self): def create_new_session(self):
""" """
Create a new session Create a new session
:return: nothing
""" """
try: try:
response = self.client.create_session() response = self.client.create_session()
@ -382,59 +423,67 @@ class CoreClient:
) )
self.join_session(response.session_id, query_location=False) self.join_session(response.session_id, query_location=False)
except grpc.RpcError as e: 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: if session_id is None:
session_id = self.session_id session_id = self.session_id
try: try:
response = self.client.delete_session(session_id) 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: 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): def set_up(self):
""" """
Query sessions, if there exist any, prompt whether to join one Query sessions, if there exist any, prompt whether to join one
:return: existing sessions
""" """
try: try:
self.client.connect() self.client.connect()
# get service information # get service information
response = self.client.get_services() response = self.client.get_services()
for service in response.services: for service in response.services:
group_services = self.services.setdefault(service.group, set()) group_services = self.services.setdefault(service.group, set())
group_services.add(service.name) 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 # if there are no sessions, create a new session, else join a session
response = self.client.get_sessions() response = self.client.get_sessions()
logging.info("current sessions: %s", response)
sessions = response.sessions sessions = response.sessions
if len(sessions) == 0: if len(sessions) == 0:
self.create_new_session() self.create_new_session()
else: else:
dialog = SessionsDialog(self.app, self.app) dialog = SessionsDialog(self.app, self.app, True)
dialog.show() dialog.show()
response = self.client.get_service_defaults(self.session_id) response = self.client.get_service_defaults(self.session_id)
self.default_services = { self.default_services = {
x.node_type: set(x.services) for x in response.defaults x.node_type: set(x.services) for x in response.defaults
} }
except grpc.RpcError as e: 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() self.app.close()
def edit_node(self, core_node): def edit_node(self, core_node: core_pb2.Node):
try: try:
self.client.edit_node( self.client.edit_node(
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
) )
except grpc.RpcError as e: 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()] nodes = [x.core_node for x in self.canvas_nodes.values()]
links = [x.link for x in self.links.values()] links = [x.link for x in self.links.values()]
wlan_configs = self.get_wlan_configs_proto() wlan_configs = self.get_wlan_configs_proto()
@ -446,6 +495,7 @@ class CoreClient:
asymmetric_links = [ asymmetric_links = [
x.asymmetric_link for x in self.links.values() if x.asymmetric_link 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: if self.emane_config:
emane_config = {x: self.emane_config[x].value for x in self.emane_config} emane_config = {x: self.emane_config[x].value for x in self.emane_config}
else: else:
@ -466,26 +516,27 @@ class CoreClient:
service_configs, service_configs,
file_configs, file_configs,
asymmetric_links, asymmetric_links,
config_service_configs,
) )
logging.debug( logging.info(
"start session(%s), result: %s", self.session_id, response.result "start session(%s), result: %s", self.session_id, response.result
) )
if response.result: if response.result:
self.set_metadata() self.set_metadata()
except grpc.RpcError as e: 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 return response
def stop_session(self, session_id=None): def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse:
if not session_id: if not session_id:
session_id = self.session_id session_id = self.session_id
response = core_pb2.StopSessionResponse(result=False) response = core_pb2.StopSessionResponse(result=False)
try: try:
response = self.client.stop_session(session_id) 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: 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 return response
def show_mobility_players(self): def show_mobility_players(self):
@ -517,81 +568,115 @@ class CoreClient:
metadata = {"canvas": canvas_config, "shapes": shapes} metadata = {"canvas": canvas_config, "shapes": shapes}
response = self.client.set_session_metadata(self.session_id, metadata) response = self.client.set_session_metadata(self.session_id, metadata)
logging.info("set session metadata: %s", 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: try:
terminal = self.app.guiconfig["preferences"]["terminal"] terminal = self.app.guiconfig["preferences"]["terminal"]
response = self.client.get_node_terminal(self.session_id, node_id) response = self.client.get_node_terminal(self.session_id, node_id)
logging.info("get terminal %s", response.terminal) output = os.popen(f"echo {terminal}").read()[:-1]
os.system(f"{terminal} {response.terminal} &") 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: 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 Save core session as to an xml file
:param str file_path: file path that user pick
:return: nothing
""" """
try: try:
if self.state != core_pb2.SessionState.RUNTIME: if self.state != core_pb2.SessionState.RUNTIME:
logging.debug( logging.debug("Send session data to the daemon")
"session state not runtime, send session data to the daemon..."
)
self.send_data() self.send_data()
response = self.client.save_xml(self.session_id, file_path) response = self.client.save_xml(self.session_id, file_path)
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: 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 Open core xml
:param str file_path: file to open
:return: session id
""" """
try: try:
response = self.client.open_xml(file_path) 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) self.join_session(response.session_id)
except grpc.RpcError as e: 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) response = self.client.get_node_service(self.session_id, node_id, service_name)
logging.debug("get node service %s", response) logging.debug(
return response.service "get node(%s) %s service, response: %s", node_id, service_name, response
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("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 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( response = self.client.get_node_service_file(
self.session_id, node_id, service_name, file_name 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 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( response = self.client.set_node_service_file(
self.session_id, node_id, service_name, file_name, data self.session_id, node_id, service_name, file_name, data
) )
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): def create_nodes_and_links(self):
""" """
create nodes and links that have not been created yet create nodes and links that have not been created yet
:return: nothing
""" """
node_protos = [x.core_node for x in self.canvas_nodes.values()] node_protos = [x.core_node for x in self.canvas_nodes.values()]
link_protos = [x.link for x in self.links.values()] link_protos = [x.link for x in self.links.values()]
@ -618,8 +703,6 @@ class CoreClient:
def send_data(self): def send_data(self):
""" """
send to daemon all session info, but don't start the session send to daemon all session info, but don't start the session
:return: nothing
""" """
self.create_nodes_and_links() self.create_nodes_and_links()
for config_proto in self.get_wlan_configs_proto(): for config_proto in self.get_wlan_configs_proto():
@ -635,9 +718,9 @@ class CoreClient:
self.session_id, self.session_id,
config_proto.node_id, config_proto.node_id,
config_proto.service, config_proto.service,
config_proto.startup, startup=config_proto.startup,
config_proto.validate, validate=config_proto.validate,
config_proto.shutdown, shutdown=config_proto.shutdown,
) )
for config_proto in self.get_service_file_configs_proto(): for config_proto in self.get_service_file_configs_proto():
self.client.set_node_service_file( 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} config = {x: self.emane_config[x].value for x in self.emane_config}
self.client.set_emane_config(self.session_id, config) self.client.set_emane_config(self.session_id, config)
self.set_metadata()
def close(self): def close(self):
""" """
Clean ups when done using grpc Clean ups when done using grpc
:return: nothing
""" """
logging.debug("close grpc") logging.debug("close grpc")
self.client.close() self.client.close()
def next_node_id(self): def next_node_id(self) -> int:
""" """
Get the next usable node id. Get the next usable node id.
:return: the next id to be used
:rtype: int
""" """
i = 1 i = 1
while True: while True:
@ -684,15 +764,11 @@ class CoreClient:
i += 1 i += 1
return i 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 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() node_id = self.next_node_id()
position = core_pb2.Position(x=x, y=y) position = core_pb2.Position(x=x, y=y)
@ -702,31 +778,38 @@ class CoreClient:
emane = None emane = None
if node_type == core_pb2.NodeType.EMANE: if node_type == core_pb2.NodeType.EMANE:
emane = self.emane_models[0] 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( node = core_pb2.Node(
id=node_id, id=node_id,
type=node_type, type=node_type,
name=f"n{node_id}", name=name,
model=model, model=model,
position=position, position=position,
image=image, image=image,
emane=emane, emane=emane,
) )
logging.debug( if NodeUtils.is_custom(model):
"adding node to core session: %s, coords: (%s, %s), name: %s", 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, self.session_id,
x, x,
y, y,
node.name,
) )
return node 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 remove the nodes selected by the user and anything related to that node
such as link, configurations, interfaces such as link, configurations, interfaces
:param list canvas_nodes: list of nodes to delete
:return: nothing
""" """
edges = set() edges = set()
for canvas_node in canvas_nodes: for canvas_node in canvas_nodes:
@ -735,6 +818,9 @@ class CoreClient:
logging.error("unknown node: %s", node_id) logging.error("unknown node: %s", node_id)
continue continue
del self.canvas_nodes[node_id] del self.canvas_nodes[node_id]
self.modified_service_nodes.discard(node_id)
if node_id in self.mobility_configs: if node_id in self.mobility_configs:
del self.mobility_configs[node_id] del self.mobility_configs[node_id]
if node_id in self.wlan_configs: if node_id in self.wlan_configs:
@ -748,44 +834,45 @@ class CoreClient:
if edge in edges: if edge in edges:
continue continue
edges.add(edge) edges.add(edge)
#
# if edge.token not in self.links:
# logging.error("unknown edge: %s", edge.token)
self.links.pop(edge.token, None) 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 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) interface_id = len(canvas_node.interfaces)
name = f"eth{interface_id}" name = f"eth{interface_id}"
interface = core_pb2.Interface( 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) canvas_node.interfaces.append(interface)
logging.debug( logging.debug(
"create node(%s) interface IPv4: %s, name: %s", "create node(%s) interface(%s) IPv4(%s) IPv6(%s)",
node.name, node.name,
interface.ip4,
interface.name, interface.name,
interface.ip4,
interface.ip6,
) )
return interface 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 Create core link for a pair of canvas nodes, with token referencing
the canvas edge. 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 src_node = canvas_src_node.core_node
dst_node = canvas_dst_node.core_node dst_node = canvas_dst_node.core_node
# determine subnet # 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 src_interface = None
if NodeUtils.is_container_node(src_node.type): if NodeUtils.is_container_node(src_node.type):
@ -808,8 +895,9 @@ class CoreClient:
) )
edge.set_link(link) edge.set_link(link)
self.links[edge.token] = edge self.links[edge.token] = edge
logging.info("Add link between %s and %s", src_node.name, dst_node.name)
def get_wlan_configs_proto(self): def get_wlan_configs_proto(self) -> List[core_pb2.WlanConfig]:
configs = [] configs = []
for node_id, config in self.wlan_configs.items(): for node_id, config in self.wlan_configs.items():
config = {x: config[x].value for x in config} config = {x: config[x].value for x in config}
@ -817,7 +905,7 @@ class CoreClient:
configs.append(wlan_config) configs.append(wlan_config)
return configs return configs
def get_mobility_configs_proto(self): def get_mobility_configs_proto(self) -> List[core_pb2.MobilityConfig]:
configs = [] configs = []
for node_id, config in self.mobility_configs.items(): for node_id, config in self.mobility_configs.items():
config = {x: config[x].value for x in config} config = {x: config[x].value for x in config}
@ -825,7 +913,7 @@ class CoreClient:
configs.append(mobility_config) configs.append(mobility_config)
return configs return configs
def get_emane_model_configs_proto(self): def get_emane_model_configs_proto(self) -> List[core_pb2.EmaneModelConfig]:
configs = [] configs = []
for key, config in self.emane_model_configs.items(): for key, config in self.emane_model_configs.items():
node_id, model, interface = key node_id, model, interface = key
@ -838,7 +926,7 @@ class CoreClient:
configs.append(config_proto) configs.append(config_proto)
return configs return configs
def get_service_configs_proto(self): def get_service_configs_proto(self) -> List[core_pb2.ServiceConfig]:
configs = [] configs = []
for node_id, services in self.service_configs.items(): for node_id, services in self.service_configs.items():
for name, config in services.items(): for name, config in services.items():
@ -852,7 +940,7 @@ class CoreClient:
configs.append(config_proto) configs.append(config_proto)
return configs return configs
def get_service_file_configs_proto(self): def get_service_file_configs_proto(self) -> List[core_pb2.ServiceFileConfig]:
configs = [] configs = []
for (node_id, file_configs) in self.file_configs.items(): for (node_id, file_configs) in self.file_configs.items():
for service, file_config in file_configs.items(): for service, file_config in file_configs.items():
@ -863,26 +951,53 @@ class CoreClient:
configs.append(config_proto) configs.append(config_proto)
return configs 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) logging.info("running node(%s) cmd: %s", node_id, self.observer)
return self.client.node_command(self.session_id, node_id, self.observer).output return self.client.node_command(self.session_id, node_id, self.observer).output
def get_wlan_config(self, node_id): def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
config = self.wlan_configs.get(node_id) config = self.wlan_configs.get(node_id)
if not config: if not config:
response = self.client.get_wlan_config(self.session_id, node_id) response = self.client.get_wlan_config(self.session_id, node_id)
config = response.config config = response.config
logging.debug(
"get wlan configuration from node %s, result configuration: %s",
node_id,
config,
)
return 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) config = self.mobility_configs.get(node_id)
if not config: if not config:
response = self.client.get_mobility_config(self.session_id, node_id) response = self.client.get_mobility_config(self.session_id, node_id)
config = response.config config = response.config
logging.debug(
"get mobility config from node %s, result configuration: %s",
node_id,
config,
)
return config return config
def get_emane_model_config(self, node_id, model, interface=None): def get_emane_model_config(
logging.info("getting emane model config: %s %s %s", node_id, model, interface) self, node_id: int, model: str, interface: int = None
) -> Dict[str, common_pb2.ConfigOption]:
config = self.emane_model_configs.get((node_id, model, interface)) config = self.emane_model_configs.get((node_id, model, interface))
if not config: if not config:
if interface is None: if interface is None:
@ -891,17 +1006,37 @@ class CoreClient:
self.session_id, node_id, model, interface self.session_id, node_id, model, interface
) )
config = response.config 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 return config
def set_emane_model_config(self, node_id, model, config, interface=None): def set_emane_model_config(
logging.info("setting emane model config: %s %s %s", node_id, model, interface) 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 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 services = self.canvas_nodes[_from].core_node.services
self.canvas_nodes[_to].core_node.services[:] = 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 node_type = self.canvas_nodes[_from].core_node.type
if node_type == core_pb2.NodeType.DEFAULT: if node_type == core_pb2.NodeType.DEFAULT:
services = self.canvas_nodes[_from].core_node.services services = self.canvas_nodes[_from].core_node.services
@ -926,3 +1061,6 @@ class CoreClient:
config = self.emane_model_configs.get(_from) config = self.emane_model_configs.get(_from)
if config: if config:
self.emane_model_configs[_to] = 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 import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.widgets import CodeText from core.gui.widgets import CodeText
if TYPE_CHECKING:
from core.gui.app import Application
LICENSE = """\ LICENSE = """\
Copyright (c) 2005-2020, the Boeing Company. Copyright (c) 2005-2020, the Boeing Company.
@ -31,7 +35,7 @@ THE POSSIBILITY OF SUCH DAMAGE.\
class AboutDialog(Dialog): class AboutDialog(Dialog):
def __init__(self, master, app): def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "About CORE", modal=True) super().__init__(master, app, "About CORE", modal=True)
self.draw() self.draw()

View file

@ -3,15 +3,19 @@ check engine light
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING
from core.api.grpc.core_pb2 import ExceptionLevel from core.api.grpc.core_pb2 import ExceptionLevel
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import CodeText from core.gui.widgets import CodeText
if TYPE_CHECKING:
from core.gui.app import Application
class AlertsDialog(Dialog): class AlertsDialog(Dialog):
def __init__(self, master, app): def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Alerts", modal=True) super().__init__(master, app, "Alerts", modal=True)
self.app = app self.app = app
self.tree = None self.tree = None
@ -110,7 +114,7 @@ class AlertsDialog(Dialog):
dialog = DaemonLog(self, self.app) dialog = DaemonLog(self, self.app)
dialog.show() dialog.show()
def click_select(self, event): def click_select(self, event: tk.Event):
current = self.tree.selection()[0] current = self.tree.selection()[0]
alarm = self.alarm_map[current] alarm = self.alarm_map[current]
self.codetext.text.config(state=tk.NORMAL) self.codetext.text.config(state=tk.NORMAL)
@ -120,7 +124,7 @@ class AlertsDialog(Dialog):
class DaemonLog(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) super().__init__(master, app, "core-daemon log", modal=True)
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.path = tk.StringVar(value="/var/log/core-daemon.log") self.path = tk.StringVar(value="/var/log/core-daemon.log")

View file

@ -3,19 +3,21 @@ size and scale
""" """
import tkinter as tk import tkinter as tk
from tkinter import font, ttk from tkinter import font, ttk
from typing import TYPE_CHECKING
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
from core.gui.app import Application
PIXEL_SCALE = 100 PIXEL_SCALE = 100
class SizeAndScaleDialog(Dialog): class SizeAndScaleDialog(Dialog):
def __init__(self, master, app): def __init__(self, master: "Application", app: "Application"):
""" """
create an instance for size and scale object create an instance for size and scale object
:param app: main application
""" """
super().__init__(master, app, "Canvas Size and Scale", modal=True) super().__init__(master, app, "Canvas Size and Scale", modal=True)
self.canvas = self.app.canvas self.canvas = self.app.canvas

View file

@ -4,6 +4,7 @@ set wallpaper
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.appconfig import BACKGROUNDS_PATH from core.gui.appconfig import BACKGROUNDS_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -11,13 +12,14 @@ from core.gui.images import Images
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import image_chooser from core.gui.widgets import image_chooser
if TYPE_CHECKING:
from core.gui.app import Application
class CanvasWallpaperDialog(Dialog): class CanvasWallpaperDialog(Dialog):
def __init__(self, master, app): def __init__(self, master: "Application", app: "Application"):
""" """
create an instance of CanvasWallpaper object create an instance of CanvasWallpaper object
:param coretk.app.Application app: root application
""" """
super().__init__(master, app, "Canvas Background", modal=True) super().__init__(master, app, "Canvas Background", modal=True)
self.canvas = self.app.canvas self.canvas = self.app.canvas
@ -140,8 +142,6 @@ class CanvasWallpaperDialog(Dialog):
def click_clear(self): def click_clear(self):
""" """
delete like shown in image link entry if there is any delete like shown in image link entry if there is any
:return: nothing
""" """
# delete entry # delete entry
self.filename.set("") self.filename.set("")

View file

@ -1,15 +1,18 @@
""" """
custom color picker custom color picker
""" """
import logging
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Any
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
if TYPE_CHECKING:
from core.gui.app import Application
class ColorPickerDialog(Dialog): 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) super().__init__(master, app, "color picker", modal=True)
self.red_entry = None self.red_entry = None
self.blue_entry = None self.blue_entry = None
@ -31,7 +34,7 @@ class ColorPickerDialog(Dialog):
self.draw() self.draw()
self.set_bindings() self.set_bindings()
def askcolor(self): def askcolor(self) -> str:
self.show() self.show()
return self.color return self.color
@ -171,23 +174,19 @@ class ColorPickerDialog(Dialog):
self.hex.trace_add("write", self.update_color) self.hex.trace_add("write", self.update_color)
def button_ok(self): def button_ok(self):
logging.debug("not implemented")
self.color = self.hex.get() self.color = self.hex.get()
self.destroy() self.destroy()
def get_hex(self): def get_hex(self) -> str:
""" """
convert current RGB values into hex color convert current RGB values into hex color
:rtype: str
:return: hex color
""" """
red = self.red_entry.get() red = self.red_entry.get()
blue = self.blue_entry.get() blue = self.blue_entry.get()
green = self.green_entry.get() green = self.green_entry.get()
return "#%02x%02x%02x" % (int(red), int(green), int(blue)) return "#%02x%02x%02x" % (int(red), int(green), int(blue))
def current_focus(self, focus): def current_focus(self, focus: str):
self.focus = focus self.focus = focus
def update_color(self, arg1=None, arg2=None, arg3=None): def update_color(self, arg1=None, arg2=None, arg3=None):
@ -210,35 +209,31 @@ class ColorPickerDialog(Dialog):
self.set_entry(red, green, blue) self.set_entry(red, green, blue)
self.set_scale(red, green, blue) self.set_scale(red, green, blue)
self.display.config(background=hex_code) 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()) color_var.set(var.get())
self.focus = "rgb" self.focus = "rgb"
self.update_color() 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.red_scale.set(red)
self.green_scale.set(green) self.green_scale.set(green)
self.blue_scale.set(blue) self.blue_scale.set(blue)
def set_entry(self, red, green, blue): def set_entry(self, red: int, green: int, blue: int):
self.red.set(red) self.red.set(red)
self.green.set(green) self.green.set(green)
self.blue.set(blue) self.blue.set(blue)
def set_label(self, red, 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.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0))
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0)) self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue))) self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue)))
def get_rgb(self, hex_code): def get_rgb(self, hex_code: str) -> [int, int, int]:
""" """
convert a valid hex code to RGB values 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: if len(hex_code) == 4:
red = hex_code[1] 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 import tkinter as tk
from pathlib import Path from pathlib import Path
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Any, Set
from core.gui import nodeutils from core.gui import nodeutils
from core.gui.appconfig import ICONS_PATH 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.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser
if TYPE_CHECKING:
from core.gui.app import Application
class ServicesSelectDialog(Dialog): 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) super().__init__(master, app, "Node Services", modal=True)
self.groups = None self.groups = None
self.services = None self.services = None
@ -71,7 +75,7 @@ class ServicesSelectDialog(Dialog):
# trigger group change # trigger group change
self.groups.listbox.event_generate("<<ListboxSelect>>") self.groups.listbox.event_generate("<<ListboxSelect>>")
def handle_group_change(self, event): def handle_group_change(self, event: tk.Event):
selection = self.groups.listbox.curselection() selection = self.groups.listbox.curselection()
if selection: if selection:
index = selection[0] index = selection[0]
@ -81,7 +85,7 @@ class ServicesSelectDialog(Dialog):
checked = name in self.current_services checked = name in self.current_services
self.services.add(name, checked) self.services.add(name, checked)
def service_clicked(self, name, var): def service_clicked(self, name: str, var: tk.BooleanVar):
if var.get() and name not in self.current_services: if var.get() and name not in self.current_services:
self.current_services.add(name) self.current_services.add(name)
elif not var.get() and name in self.current_services: elif not var.get() and name in self.current_services:
@ -96,7 +100,7 @@ class ServicesSelectDialog(Dialog):
class CustomNodesDialog(Dialog): class CustomNodesDialog(Dialog):
def __init__(self, master, app): def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Custom Nodes", modal=True) super().__init__(master, app, "Custom Nodes", modal=True)
self.edit_button = None self.edit_button = None
self.delete_button = None self.delete_button = None
@ -214,6 +218,12 @@ class CustomNodesDialog(Dialog):
if name not in self.app.core.custom_nodes: if name not in self.app.core.custom_nodes:
image_file = Path(self.image_file).stem image_file = Path(self.image_file).stem
node_draw = NodeDraw.from_custom(name, image_file, set(self.services)) 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.app.core.custom_nodes[name] = node_draw
self.nodes_list.listbox.insert(tk.END, name) self.nodes_list.listbox.insert(tk.END, name)
self.reset_values() self.reset_values()
@ -228,6 +238,12 @@ class CustomNodesDialog(Dialog):
node_draw.image_file = Path(self.image_file).stem node_draw.image_file = Path(self.image_file).stem
node_draw.image = self.image node_draw.image = self.image
node_draw.services = self.services 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.app.core.custom_nodes[name] = node_draw
self.nodes_list.listbox.delete(self.selected_index) self.nodes_list.listbox.delete(self.selected_index)
self.nodes_list.listbox.insert(self.selected_index, name) 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.selection_clear(0, tk.END)
self.nodes_list.listbox.event_generate("<<ListboxSelect>>") self.nodes_list.listbox.event_generate("<<ListboxSelect>>")
def handle_node_select(self, event): def handle_node_select(self, event: tk.Event):
selection = self.nodes_list.listbox.curselection() selection = self.nodes_list.listbox.curselection()
if selection: if selection:
self.selected_index = selection[0] self.selected_index = selection[0]

View file

@ -1,12 +1,18 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.themes import DIALOG_PAD from core.gui.themes import DIALOG_PAD
if TYPE_CHECKING:
from core.gui.app import Application
class Dialog(tk.Toplevel): 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) super().__init__(master)
self.withdraw() self.withdraw()
self.app = app self.app = app
@ -30,7 +36,7 @@ class Dialog(tk.Toplevel):
self.grab_set() self.grab_set()
self.wait_window() self.wait_window()
def draw_spacer(self, row=None): def draw_spacer(self, row: int = None):
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(row=row, sticky="nsew") frame.grid(row=row, sticky="nsew")
frame.rowconfigure(0, weight=1) frame.rowconfigure(0, weight=1)

View file

@ -1,22 +1,27 @@
""" """
emane configuration emane configuration
""" """
import logging
import tkinter as tk import tkinter as tk
import webbrowser import webbrowser
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Any
import grpc import grpc
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error from core.gui.errors import show_grpc_error
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame 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): class GlobalEmaneDialog(Dialog):
def __init__(self, master, app): def __init__(self, master: Any, app: "Application"):
super().__init__(master, app, "EMANE Configuration", modal=True) super().__init__(master, app, "EMANE Configuration", modal=True)
self.config_frame = None self.config_frame = None
self.draw() self.draw()
@ -47,20 +52,29 @@ class GlobalEmaneDialog(Dialog):
class EmaneModelDialog(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) super().__init__(master, app, f"{node.name} {model} Configuration", modal=True)
self.node = node self.node = node
self.model = f"emane_{model}" self.model = f"emane_{model}"
self.interface = interface self.interface = interface
self.config_frame = None self.config_frame = None
self.has_error = False
try: try:
self.config = self.app.core.get_emane_model_config( self.config = self.app.core.get_emane_model_config(
self.node.id, self.model, self.interface self.node.id, self.model, self.interface
) )
self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:
show_grpc_error(e) show_grpc_error(e, self.app, self.app)
self.has_error = True
self.destroy() self.destroy()
self.draw()
def draw(self): def draw(self):
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
@ -91,7 +105,9 @@ class EmaneModelDialog(Dialog):
class EmaneConfigDialog(Dialog): class EmaneConfigDialog(Dialog):
def __init__(self, master, app, canvas_node): def __init__(
self, master: "Application", app: "Application", canvas_node: "CanvasNode"
):
super().__init__( super().__init__(
master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True
) )
@ -116,8 +132,6 @@ class EmaneConfigDialog(Dialog):
def draw_emane_configuration(self): def draw_emane_configuration(self):
""" """
draw the main frame for emane configuration draw the main frame for emane configuration
:return: nothing
""" """
label = ttk.Label( label = ttk.Label(
self.top, self.top,
@ -143,8 +157,6 @@ class EmaneConfigDialog(Dialog):
def draw_emane_models(self): def draw_emane_models(self):
""" """
create a combobox that has all the known emane models create a combobox that has all the known emane models
:return: nothing
""" """
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY) frame.grid(sticky="ew", pady=PADY)
@ -210,22 +222,17 @@ class EmaneConfigDialog(Dialog):
def click_model_config(self): def click_model_config(self):
""" """
draw emane model configuration draw emane model configuration
:return: nothing
""" """
model_name = self.emane_model.get() model_name = self.emane_model.get()
logging.info("configuring emane model: %s", model_name)
dialog = EmaneModelDialog( dialog = EmaneModelDialog(
self, self.app, self.canvas_node.core_node, model_name 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 update emane model options button
:param event:
:return: nothing
""" """
model_name = self.emane_model.get() model_name = self.emane_model.get()
self.emane_model_button.config(text=f"{model_name} options") self.emane_model_button.config(text=f"{model_name} options")

View file

@ -1,14 +1,18 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Any
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll from core.gui.widgets import CodeText, ListboxScroll
if TYPE_CHECKING:
from core.gui.app import Application
class HookDialog(Dialog): class HookDialog(Dialog):
def __init__(self, master, app): def __init__(self, master: Any, app: "Application"):
super().__init__(master, app, "Hook", modal=True) super().__init__(master, app, "Hook", modal=True)
self.name = tk.StringVar() self.name = tk.StringVar()
self.codetext = None self.codetext = None
@ -62,11 +66,11 @@ class HookDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def state_change(self, event): def state_change(self, event: tk.Event):
state_name = self.state.get() state_name = self.state.get()
self.name.set(f"{state_name.lower()}_hook.sh") self.name.set(f"{state_name.lower()}_hook.sh")
def set(self, hook): def set(self, hook: core_pb2.Hook):
self.hook = hook self.hook = hook
self.name.set(hook.file) self.name.set(hook.file)
self.codetext.text.delete(1.0, tk.END) self.codetext.text.delete(1.0, tk.END)
@ -84,7 +88,7 @@ class HookDialog(Dialog):
class HooksDialog(Dialog): class HooksDialog(Dialog):
def __init__(self, master, app): def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Hooks", modal=True) super().__init__(master, app, "Hooks", modal=True)
self.listbox = None self.listbox = None
self.edit_button = None self.edit_button = None
@ -140,7 +144,7 @@ class HooksDialog(Dialog):
self.edit_button.config(state=tk.DISABLED) self.edit_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED)
def select(self, event): def select(self, event: tk.Event):
if self.listbox.curselection(): if self.listbox.curselection():
index = self.listbox.curselection()[0] index = self.listbox.curselection()[0]
self.selected = self.listbox.get(index) self.selected = self.listbox.get(index)

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