Compare commits
505 commits
feature/py
...
master
Author | SHA1 | Date | |
---|---|---|---|
3f2db35e68 | |||
56a0511f21 | |||
|
20071eed2e | ||
|
62a09c7570 | ||
|
c37fa33ffe | ||
|
7060a6842f | ||
|
8da068159f | ||
|
e3715e188c | ||
|
722fbde7d0 | ||
|
2cb8ec2fb2 | ||
|
544ad9b638 | ||
|
648f5425b5 | ||
|
359f6940e1 | ||
|
2547c61a7c | ||
|
f8f3f8169c | ||
|
12c07e0175 | ||
|
d63aa03343 | ||
|
db89fbf066 | ||
|
a80796ac72 | ||
|
c76bc2ee8a | ||
|
e60b0f0de1 | ||
|
e0e4b05b7f | ||
|
41e473eefc | ||
|
12d7a1ff2a | ||
|
cbc35b74f8 | ||
|
81230edac3 | ||
|
94f070e0ff | ||
|
d04f8d69d2 | ||
|
0339073868 | ||
|
9d88eba1f5 | ||
|
0b1a44e9b2 | ||
|
2176fcc5a3 | ||
|
c554983436 | ||
|
75a92f3a38 | ||
|
580916f2f0 | ||
|
a5727e3355 | ||
|
01585b6ec5 | ||
|
d2008b1e5a | ||
|
374ddb677c | ||
|
8281382bd6 | ||
|
946d161c11 | ||
|
fac8cfae08 | ||
|
4a02d4bed9 | ||
|
18ac8d5620 | ||
|
d9f2ca8491 | ||
|
49049befc8 | ||
|
32bcd0a345 | ||
|
899800b925 | ||
|
4dba1bbe32 | ||
|
e7351b594d | ||
|
69f05a6712 | ||
|
921bfdf527 | ||
|
7f58224f43 | ||
|
f9505b3173 | ||
|
7ea950f8ec | ||
|
8abf2561bf | ||
|
4c222d1a7a | ||
|
3d722a7721 | ||
|
e770bcd47c | ||
|
6ff2abf0b8 | ||
|
da3cebe1cd | ||
|
b6b300207b | ||
|
cdc8c7360d | ||
|
a6a09d9e56 | ||
|
cafbb15b1e | ||
|
c444304040 | ||
|
2eb29525de | ||
|
fcf1448ab6 | ||
|
15df06d834 | ||
|
0053ddb57d | ||
|
03fe74c195 | ||
|
d52e0c4547 | ||
|
b4ed8bc9c5 | ||
|
5b41b4e5be | ||
|
59f814eac0 | ||
|
078e0df329 | ||
|
785cf82ba3 | ||
|
d09f777645 | ||
|
f8d2b47fa9 | ||
|
c44dc521fb | ||
|
04e778e97f | ||
|
d45eeb6d2e | ||
|
fec400ac2e | ||
|
c1ad39631d | ||
|
b92e4ed6b9 | ||
|
3e5c8c894f | ||
|
f10c7cac45 | ||
|
0ffcc10953 | ||
|
4dca3eac39 | ||
|
93272d6ed7 | ||
|
4c351b0d72 | ||
|
56a287c9c0 | ||
|
d4997bbc04 | ||
|
4f2e20a0a0 | ||
|
5abbc1680a | ||
|
c91facd6e3 | ||
|
d215330426 | ||
|
db2f57ca35 | ||
|
a3892d6b0e | ||
|
f422d05215 | ||
|
9ccb1880a1 | ||
|
88a52f6cd2 | ||
|
a84e689478 | ||
|
a787f46719 | ||
|
b430de226d | ||
|
e176a02460 | ||
|
e96322c9af | ||
|
ecf380c884 | ||
|
6c52029795 | ||
|
41b231b577 | ||
|
f8a20f8dbb | ||
249cdf1dab | |||
|
5ab71377cc | ||
|
50bc4343c3 | ||
|
c6898c7c0e | ||
|
568b1360a2 | ||
|
898a4f7c84 | ||
|
4c9c6e9f8c | ||
|
8e2593c9e0 | ||
|
f43e8f7646 | ||
|
7067b54a00 | ||
|
9c71e0144c | ||
|
0e627afeb0 | ||
|
82739ce3af | ||
|
47991cd35f | ||
|
16b0decde7 | ||
|
d203b7d3ca | ||
|
e0a21fb099 | ||
|
03775c2c3c | ||
|
b5b7b8cdf9 | ||
|
9218fb0b6f | ||
|
0d1fa0049a | ||
|
c067de6792 | ||
|
4b2d33c898 | ||
|
ee2cdf7716 | ||
|
6631fc95ed | ||
|
909142b446 | ||
e8f0ce79d5 | |||
|
5013b53605 | ||
|
7e87ad24fa | ||
|
2f2dbe26d1 | ||
|
33e13e2381 | ||
2a7904e2f1 | |||
47ae24ad59 | |||
3b22c7038f | |||
|
9c4a0fda32 | ||
|
78eb03cc65 | ||
|
5a81283fca | ||
|
281a848bbf | ||
|
e25d1c72b3 | ||
|
88ccd1f194 | ||
|
d045fc0d51 | ||
|
e56d93f0fe | ||
|
382ff6d49b | ||
|
5202b2fa04 | ||
|
e4abefe23b | ||
|
018865b2a2 | ||
|
c91d8df790 | ||
|
a201fd2903 | ||
|
d06659ff82 | ||
|
d8632da96b | ||
|
fcf6f30302 | ||
|
cd6bb319ad | ||
|
d63722c0ed | ||
|
273f68a1ec | ||
|
fe1593b51f | ||
|
3c28ea373a | ||
|
9c69881aad | ||
|
60a48c7084 | ||
|
9c265ab283 | ||
|
9991942e7b | ||
|
469f8f087a | ||
|
1aa9d4bccf | ||
|
1d718aeda2 | ||
|
bb49947550 | ||
|
e5d6299f0a | ||
|
9fa3e77d12 | ||
|
f40de9f838 | ||
|
d77ed9c473 | ||
|
7173e488cb | ||
|
bbcd4664ff | ||
|
31bc0c6497 | ||
|
28d3deeee8 | ||
|
2e3e085522 | ||
|
03e646031c | ||
|
fd3be57f57 | ||
|
25c3c42b40 | ||
|
844404c765 | ||
|
6f60fba18b | ||
|
52b875df36 | ||
|
5f1e722331 | ||
|
33f3eccdcf | ||
|
8ba169c758 | ||
|
2094694ca3 | ||
|
2e4d0e0cea | ||
|
2ab2c27d49 | ||
|
aa8ea40ce6 | ||
|
fe0bc2b405 | ||
|
0a0248d8b2 | ||
|
0b420cfc07 | ||
|
62a060588d | ||
|
7348942a67 | ||
|
964e3aaf39 | ||
|
ce5c155327 | ||
|
d20cb1ef58 | ||
|
d124820a86 | ||
|
e5c06fe47c | ||
|
f811373f0b | ||
|
c40fb2b15d | ||
|
bd6f789cef | ||
|
314eee33f6 | ||
|
443c0e708f | ||
|
7440c1d949 | ||
|
5ee561b210 | ||
|
ea751727b4 | ||
|
440c06c040 | ||
|
409b6809e6 | ||
|
388ae44cf2 | ||
|
543e9982c0 | ||
|
42cd1a1019 | ||
|
559cac18e8 | ||
|
9d97699b1f | ||
|
08637d35b3 | ||
|
5d4642006c | ||
|
ba0e4adb04 | ||
|
ace0183db5 | ||
|
84acb82c18 | ||
|
e4a6ecf3c2 | ||
|
73aaa8ca18 | ||
|
1fa0ac25b5 | ||
|
26f0848cb4 | ||
|
346364d705 | ||
|
d83bfed608 | ||
|
e3466f0669 | ||
|
1841fb1110 | ||
|
d7b2c3cac3 | ||
|
6d8ae4af2e | ||
|
2df8aa4379 | ||
|
b941395100 | ||
|
5398cdd2d5 | ||
|
7ed007496c | ||
|
e5e14ad67b | ||
|
dc9b6adc98 | ||
|
b71272519d | ||
|
8c24e9cfa6 | ||
|
3c8f6a9512 | ||
|
4f58d5d8eb | ||
|
7b16f9cb74 | ||
|
cd7f1a641e | ||
|
e59fc02ec3 | ||
|
d684b8eb5a | ||
|
6cf2793e30 | ||
|
f0750d78b0 | ||
|
41db531e97 | ||
|
dde339cc46 | ||
|
c8a589ef76 | ||
|
dc7bb37252 | ||
|
f324cfbc4a | ||
|
3eed1a09fb | ||
|
f545726ed5 | ||
|
0536747d9a | ||
|
2eef7076f4 | ||
|
a42697ecce | ||
|
b085105a6b | ||
|
074a2263ce | ||
|
e557b402b6 | ||
|
2dd3839396 | ||
|
58ffd045e2 | ||
|
482ad037f4 | ||
|
a7588a2188 | ||
|
bcf7429785 | ||
|
cdde1c89ee | ||
|
a341691d69 | ||
|
458b7f15ce | ||
|
96f2408e01 | ||
|
113be65078 | ||
|
9d32c432db | ||
|
3c64654598 | ||
|
0fcc532c0d | ||
|
e80d22096c | ||
|
a1e9fc02fd | ||
|
47b02c3e7b | ||
|
0e2219f6c8 | ||
|
490a4acf24 | ||
|
efb97d1a5d | ||
|
43737a42e4 | ||
|
44b7b6a27e | ||
|
df5ff02f95 | ||
|
8f767208e0 | ||
|
d5b05a39e8 | ||
|
6791269eeb | ||
|
e9b83b0d28 | ||
|
31e6839ac5 | ||
|
bc8c49c573 | ||
|
e71079c772 | ||
|
8f89488fd5 | ||
|
58b8d1cd24 | ||
|
fdc009699e | ||
|
8cf2b9af08 | ||
|
871b1ae2af | ||
|
0b531d7fd8 | ||
|
5e843f15bb | ||
|
5ff4447528 | ||
|
9da64af79b | ||
|
f98594e927 | ||
|
f090c98c54 | ||
|
2d0732d610 | ||
|
c055103c1b | ||
|
49709261fc | ||
|
22e92111d0 | ||
|
bd3e2f5d0e | ||
|
35a285daa2 | ||
|
750489c2e1 | ||
|
ed90b9bbc3 | ||
|
b78c07bd24 | ||
|
1ce6e51318 | ||
|
2b89503432 | ||
|
04fb3322b5 | ||
|
209303b085 | ||
|
18163c577f | ||
|
0a88851be3 | ||
|
7e18a7a720 | ||
|
bb592cda58 | ||
|
0204472928 | ||
|
4b3693b9cd | ||
|
b1819668a6 | ||
|
35549401eb | ||
|
bd896d1336 | ||
|
d14d10803f | ||
|
629cd13c1b | ||
|
b96dc621cd | ||
|
ac5bbf5c6d | ||
|
8e905b6a37 | ||
|
631cbbc73e | ||
|
150d4bd6ea | ||
|
aa5bb08a16 | ||
|
1347839200 | ||
|
8d303bdc2a | ||
|
eb1a9e2fe4 | ||
|
4879d6e297 | ||
|
54ac807a4f | ||
|
8678922c92 | ||
|
7198d2adc9 | ||
|
425a2ee141 | ||
|
7fcedf527f | ||
|
44d797c633 | ||
|
a63e3e8d96 | ||
|
b508ad6406 | ||
|
f928284fb7 | ||
|
b51200e397 | ||
|
3fcefc4d79 | ||
|
777097c85e | ||
|
795a5f5865 | ||
|
820539191d | ||
|
8d5c3bd212 | ||
|
bcd9cc7ac2 | ||
|
ef0fa8c1a7 | ||
|
aea727ba42 | ||
|
4ff650af67 | ||
|
6b5148566c | ||
|
5bc3345d37 | ||
|
071023b1d9 | ||
|
e2a9f6b1f4 | ||
|
d16f6b234b | ||
|
e5d28b01c6 | ||
|
86cd0a8e18 | ||
|
c2fdbbca32 | ||
|
5e843a7674 | ||
|
5286938e44 | ||
|
11d8bb0674 | ||
|
30291a8438 | ||
|
208c746b67 | ||
|
5ffc3e2aa4 | ||
|
85c5ad22e4 | ||
|
3a08b13d6e | ||
|
ad09bd5504 | ||
|
ca8b4f1f6e | ||
|
13778e1d30 | ||
|
50e3aadc6b | ||
|
1ddb7b7b24 | ||
|
53ae6ac784 | ||
|
598cb0f10d | ||
|
d40435fa68 | ||
|
7e6b87101b | ||
|
8108db545a | ||
|
42dc56c56b | ||
|
805be3f809 | ||
|
e299d3dd16 | ||
|
3e2cb86b6b | ||
|
c574ace9a0 | ||
|
9205fe1764 | ||
|
618d89b8db | ||
|
639b29a134 | ||
|
15acdaa40f | ||
|
924e86da2b | ||
|
0ed30a4feb | ||
|
1cbe891dab | ||
|
917c45e70b | ||
|
38e162aec5 | ||
|
f271b0289e | ||
|
634341dd03 | ||
|
a217c2445c | ||
|
e0fe86bcb2 | ||
|
f891974e3a | ||
|
d8a3f9e78c | ||
|
d4c008e564 | ||
|
3d356272f1 | ||
|
4830538053 | ||
|
dcf402ae04 | ||
|
7938379e6d | ||
|
597834a993 | ||
|
188914ccb1 | ||
|
69652ac577 | ||
|
55d5bb3859 | ||
|
b12aa981d5 | ||
|
25025c00bc | ||
|
cbe1db1215 | ||
|
1daf5778c0 | ||
|
21749502f9 | ||
|
6ce4b425f8 | ||
|
984d28275b | ||
|
44f81391c4 | ||
|
6086d1229b | ||
|
bb3590fbde | ||
|
f7f54d9aa6 | ||
|
be0e0175a2 | ||
|
a2148c6923 | ||
|
1c970bbe00 | ||
|
b2726b627f | ||
|
d0a55dd471 | ||
|
dc9089fcd1 | ||
|
1b025c47da | ||
|
49659976d3 | ||
|
b2626b8d0e | ||
|
f13334cc58 | ||
|
b163b06596 | ||
|
2387812a76 | ||
|
93813358b5 | ||
|
a6fadb76cc | ||
|
47ac4c850d | ||
|
422a1a500e | ||
|
7871a678ca | ||
|
4a21cd5789 | ||
|
b698fed368 | ||
|
c7a62a5743 | ||
|
bce5c2cca3 | ||
|
27f7bdb004 | ||
|
ca004b3e96 | ||
|
e9bf50b3ff | ||
|
fb3d593751 | ||
|
754b8ba91e | ||
|
097f248120 | ||
|
0fee29754d | ||
|
e7c721989f | ||
|
886b56cf8c | ||
|
2b171631c7 | ||
|
b7483c2715 | ||
|
3d958c5d0f | ||
|
cadbf8dd14 | ||
|
a5098263fd | ||
|
4007dc331b | ||
|
878d943ee3 | ||
|
623cc13fca | ||
|
c2a40dbb6b | ||
|
6ef458fc74 | ||
|
cd35b28ead | ||
|
d98a9a5a91 | ||
|
d533083b5f | ||
|
4904f7170f | ||
|
6f3246e329 | ||
|
313768ea56 | ||
|
35cc8fab65 | ||
|
bc540e0669 | ||
|
ebd6bb8a21 | ||
|
b88ec31df6 | ||
|
44ee5308de | ||
|
cba86a3da7 | ||
|
acaef00087 | ||
|
3c97f80614 | ||
|
685b21924c | ||
|
63282134f5 | ||
|
a2d9659fb7 | ||
|
77e2b08d76 | ||
|
f24d5f20b4 | ||
|
a7d7b94215 | ||
|
62d111b74c | ||
|
2af7d595c0 | ||
|
3e2ea42ebd | ||
|
f171c6111a | ||
|
28d22c5800 | ||
|
29ffd64b41 | ||
|
f9a4fe3331 | ||
|
2f9c169e66 | ||
|
b937e316c4 | ||
|
367a2096fa | ||
|
4a8f8557a6 | ||
|
5d436dd94d | ||
|
8eada3d754 | ||
|
b01249bb4e | ||
|
9621df6bc4 | ||
|
6f43d0e88f | ||
|
4363a20ffb | ||
|
886bfc093b | ||
|
e34c00a431 | ||
|
9fa98ae378 |
587 changed files with 21373 additions and 67403 deletions
6
.github/workflows/daemon-checks.yml
vendored
6
.github/workflows/daemon-checks.yml
vendored
|
@ -4,13 +4,13 @@ on: [push]
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.6
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.6
|
||||
python-version: 3.9
|
||||
- name: install poetry
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
|
21
.github/workflows/documentation.yml
vendored
Normal file
21
.github/workflows/documentation.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
name: documentation
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
key: ${{ github.ref }}
|
||||
path: .cache
|
||||
- run: pip install mkdocs-material
|
||||
- run: mkdocs gh-deploy --force
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -14,9 +14,13 @@ config.h.in
|
|||
config.log
|
||||
config.status
|
||||
configure
|
||||
configure~
|
||||
debian
|
||||
stamp-h1
|
||||
|
||||
# python virtual environments
|
||||
venv
|
||||
|
||||
# generated protobuf files
|
||||
*_pb2.py
|
||||
*_pb2_grpc.py
|
||||
|
@ -58,3 +62,6 @@ daemon/setup.py
|
|||
|
||||
# python
|
||||
__pycache__
|
||||
|
||||
# ignore core player files
|
||||
*.core
|
||||
|
|
227
CHANGELOG.md
227
CHANGELOG.md
|
@ -1,3 +1,230 @@
|
|||
## 2023-08-01 CORE 9.0.3
|
||||
|
||||
* Installation
|
||||
* updated various dependencies
|
||||
* Documentation
|
||||
* improved GUI docs to include node interaction and note xhost usage
|
||||
* \#780 - fixed gRPC examples
|
||||
* \#787 - complete documentation revamp to leverage mkdocs material
|
||||
* \#790 - fixed custom emane model example
|
||||
* core-daemon
|
||||
* update type hinting to avoid deprecated imports
|
||||
* updated commands ran within docker based nodes to have proper environment variables
|
||||
* fixed issue improperly setting session options over gRPC
|
||||
* \#668 - add fedora sbin path to frr service
|
||||
* \#774 - fixed pcap configservice
|
||||
* \#805 - fixed radvd configservice template error
|
||||
* core-gui
|
||||
* update type hinting to avoid deprecated imports
|
||||
* fixed issue allowing duplicate named hook scripts
|
||||
* fixed issue joining sessions with RJ45 nodes
|
||||
* utility scripts
|
||||
* fixed issue in core-cleanup for removing devices
|
||||
|
||||
## 2023-03-02 CORE 9.0.2
|
||||
|
||||
* Installation
|
||||
* updated python dependencies, including invoke to resolve python 3.10+ issues
|
||||
* improved example dockerfiles to use less space for built images
|
||||
* Documentation
|
||||
* updated emane install instructions
|
||||
* added Docker related issues to install instructions
|
||||
* core-daemon
|
||||
* fixed issue using invalid device name in sysctl commands
|
||||
* updated PTP nodes to properly disable mac learning for their linux bridge
|
||||
* fixed issue for LXC nodes to properly use a configured image name and write it to XML
|
||||
* \#742 - fixed issue with bad wlan node id being used
|
||||
* \#744 - fixed issue not properly setting broadcast address
|
||||
* core-gui
|
||||
* fixed sample1.xml to remove SSH service
|
||||
* fixed emane demo examples
|
||||
* fixed issue displaying emane configs generally configured for a node
|
||||
|
||||
## 2022-11-28 CORE 9.0.1
|
||||
|
||||
* Installation
|
||||
* updated protobuf and grpcio-tools versions in pyproject.toml to account for bad version mix
|
||||
|
||||
## 2022-11-18 CORE 9.0.0
|
||||
|
||||
* Breaking Changes
|
||||
* removed session nodes file
|
||||
* removed session state file
|
||||
* emane now runs in one process per nem with unique control ports
|
||||
* grpc client has been refactored and updated
|
||||
* removed tcl/legacy gui, imn file support and the tlv api
|
||||
* link configuration is now different, but consistent, for wired links
|
||||
* Installation
|
||||
* added packaging for single file distribution
|
||||
* python3.9 is now the minimum required version
|
||||
* updated Dockerfile examples
|
||||
* updated various python dependencies
|
||||
* virtual environment is now installed to /opt/core/venv
|
||||
* Documentation
|
||||
* updated emane invoke task examples
|
||||
* revamped install documentation
|
||||
* added wireless node notes
|
||||
* core-gui
|
||||
* updated config services to display rendered templated and allow editing
|
||||
* fixed node icon issue when updating preferences
|
||||
* \#89 - throughput widget now works for hubs/switches
|
||||
* \#691 - fixed custom nodes to properly use config services
|
||||
* gRPC API
|
||||
* add linked call to support linking and unlinking interfaces without destroying them
|
||||
* fixed issue during start session clearing out session options
|
||||
* added call to get rendered config service files
|
||||
* removed get_node_links from links from client
|
||||
* nem id and nem port have been added to GetNode and AddLink calls
|
||||
* core-daemon
|
||||
* wired links always create two veth pairs joined by a bridge
|
||||
* node interfaces are now configured within the container to apply to outgoing traffic
|
||||
* session.add_node now uses NodeOptions, allowing for node specific options
|
||||
* fixed issue with xml reading node canvas values
|
||||
* removed Session.add_node_file
|
||||
* fixed get requirements logic
|
||||
* fixed docker/lxd node support terminal commands on remote servers
|
||||
* improved docker node command execution time using nsenter
|
||||
* new wireless node type added to support dynamic loss based on distance
|
||||
* \#513 - add and deleting distributed links during runtime is now supported
|
||||
* \#703 - fixed issue not starting emane event listening service
|
||||
|
||||
## 2022-03-21 CORE 8.2.0
|
||||
|
||||
* core-gui
|
||||
* improved failed starts to trigger runtime to allow node investigation
|
||||
* core-daemon
|
||||
* improved default service loading to use a full import path
|
||||
* updated session instantiation to always set to a runtime state
|
||||
* core-cli
|
||||
* \#672 - fixed xml loading
|
||||
* \#578 - restored json flag and added geo output to session overview
|
||||
* Documentation
|
||||
* updated emane example and documentation
|
||||
* improved table markdown
|
||||
|
||||
## 2022-02-18 CORE 8.1.0
|
||||
|
||||
* Installation
|
||||
* updated dependency versions to account for known vulnerabilities
|
||||
* GUI
|
||||
* fixed issue drawing asymmetric link configurations when joining a session
|
||||
* daemon
|
||||
* fixed issue getting templates and creating files for config services
|
||||
* added by directional support for network to network links
|
||||
* \#647 - fixed issue when creating RJ45 nodes
|
||||
* \#646 - fixed issue when creating files for Docker nodes
|
||||
* \#645 - improved wlan change updates to account for all updates with no delay
|
||||
* services
|
||||
* fixed file generation for OSPFv2 config service
|
||||
|
||||
## 2022-01-12 CORE 8.0.0
|
||||
|
||||
*Breaking Changes
|
||||
* heavily refactored gRPC client, removing some calls, adding others, all using type hinted classes representing their protobuf counterparts
|
||||
* emane adjustments to run each nem in its own process, includes adjustments to configuration, which may cause issues
|
||||
* internal daemon cleanup and refactoring, in a script directly driving a scenario is used
|
||||
* Installation
|
||||
* added options to allow installation without ospf mdr
|
||||
* removed tasks that are no longer needed
|
||||
* updates to properly install/remove example files
|
||||
* pipx/poetry/invoke versions are now locked to help avoid update related issues
|
||||
* install.sh is now setup.sh and is a convenience to get tool setup to run invoke
|
||||
* Documentation
|
||||
* formally added notes for Docker and LXD based node types
|
||||
* added config services
|
||||
* Updated README to have quick notes for installation
|
||||
* \#563 - update to note how to enable core service
|
||||
* Examples
|
||||
* \#598 - update to fix sample1.imn to working order
|
||||
* core-daemon
|
||||
* emane global configuration is now configurable per nem
|
||||
* fixed wlan loss to support float values
|
||||
* improved default service loading to use full core path
|
||||
* improved emane model loading to occur one time
|
||||
* fixed handling rj45 link edits from tlv api
|
||||
* fixed wlan config getting a default value for the promiscuous setting when not provided
|
||||
* ebtables usage has now been replaced with nftables
|
||||
* \#564 - logging is now using module named loggers
|
||||
* \#573 - emane processes are not created 1 to 1 with nems
|
||||
* \#608 - update lxml version
|
||||
* \#609 - update pyyaml version
|
||||
* \#623 - fixed issue with ovs mode and mac learning
|
||||
* core-gui
|
||||
* config services are now the default service type
|
||||
* legacy services are marked as deprecated
|
||||
* fix to properly load session options
|
||||
* logging is now using module named loggers
|
||||
* save as will not update the current session file name as expected
|
||||
* fix to properly clear out removed customized services
|
||||
* adding directories to a service that do not exist, is now valid
|
||||
* added flag to exit after creating gui directory from command line
|
||||
* added new options to enable/disable ip4/ip6 assignment
|
||||
* improved canvas draw order, when joining sessions
|
||||
* improved node copy/paste to avoid issues when pasting text into service config dialogs
|
||||
* each canvas will not correctly save and load their size from xml
|
||||
* gRPC API
|
||||
* session options are now returned for GetSession
|
||||
* fixed issue not properly creating the session directory during start session definition state
|
||||
* updates to separate editing a node and moving a node, new MoveNode call added, EditNode is now used for editing icons
|
||||
* Services
|
||||
* fixed default route config service
|
||||
* config services now have options for shadowing directories, including per node customization
|
||||
|
||||
## 2021-09-17 CORE 7.5.2
|
||||
|
||||
* Installation
|
||||
* \#596 - fixes issue related to installing poetry by pinning version to 1.1.7
|
||||
* updates pipx installation to pinned version 0.16.4
|
||||
* core-daemon
|
||||
* \#600 - fixes known vulnerability for pillow dependency by updating version
|
||||
|
||||
## 2021-04-15 CORE 7.5.1
|
||||
|
||||
* core-pygui
|
||||
* fixed issues creating and drawing custom nodes
|
||||
|
||||
## 2021-03-11 CORE 7.5.0
|
||||
|
||||
* core-daemon
|
||||
* fixed issue setting mobility loop value properly
|
||||
* fixed issue that some states would not properly remove session directories
|
||||
* \#560 - fixed issues with sdt integration for mobility movement and layer creation
|
||||
* core-pygui
|
||||
* added multiple canvas support
|
||||
* added support to hide nodes and restore them visually
|
||||
* update to assign full netmasks to wireless connected nodes by default
|
||||
* update to display services and action controls for nodes during runtime
|
||||
* fixed issues with custom nodes
|
||||
* fixed issue auto assigning macs, avoiding duplication
|
||||
* fixed issue joining session with different netmasks
|
||||
* fixed issues when deleting a session from the sessions dialog
|
||||
* \#550 - fixed issue not sending all service customization data
|
||||
* core-cli
|
||||
* added delete session command
|
||||
|
||||
## 2021-01-11 CORE 7.4.0
|
||||
|
||||
* Installation
|
||||
* fixed issue for automated install assuming ID_LIKE is always present in /etc/os-release
|
||||
* gRPC API
|
||||
* fixed issue stopping session and not properly going to data collect state
|
||||
* fixed issue to have start session properly create a directory before configuration state
|
||||
* core-pygui
|
||||
* fixed issue handling deletion of wired link to a switch
|
||||
* avoid saving edge metadata to xml when values are default
|
||||
* fixed issue editing node mac addresses
|
||||
* added support for configuring interface names
|
||||
* fixed issue with potential node names to allow hyphens and remove under bars
|
||||
* \#531 - fixed issue changing distributed nodes back to local
|
||||
* core-daemon
|
||||
* fixed issue to properly handle deleting links from a network to network node
|
||||
* updated xml to support writing and reading link buffer configurations
|
||||
* reverted change and removed mac learning from wlan, due to promiscuous like behavior
|
||||
* fixed issue creating control interfaces when starting services
|
||||
* fixed deadlock issue when clearing a session using sdt
|
||||
* \#116 - fixed issue for wlans handling multiple mobility scripts at once
|
||||
* \#539 - fixed issue in udp tlv api
|
||||
|
||||
## 2020-12-02 CORE 7.3.0
|
||||
|
||||
* core-daemon
|
||||
|
|
126
Dockerfile
Normal file
126
Dockerfile
Normal file
|
@ -0,0 +1,126 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM ubuntu:22.04
|
||||
LABEL Description="CORE Docker Ubuntu Image"
|
||||
|
||||
ARG PREFIX=/usr/local
|
||||
ARG BRANCH=master
|
||||
ARG PROTOC_VERSION=3.19.6
|
||||
ARG VENV_PATH=/opt/core/venv
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV PATH="$PATH:${VENV_PATH}/bin"
|
||||
WORKDIR /opt
|
||||
|
||||
# install system dependencies
|
||||
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y software-properties-common
|
||||
|
||||
RUN add-apt-repository "deb http://archive.ubuntu.com/ubuntu jammy universe"
|
||||
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
automake \
|
||||
bash \
|
||||
ca-certificates \
|
||||
ethtool \
|
||||
gawk \
|
||||
gcc \
|
||||
g++ \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
libc-dev \
|
||||
libev-dev \
|
||||
libreadline-dev \
|
||||
libtool \
|
||||
nftables \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-tk \
|
||||
pkg-config \
|
||||
tk \
|
||||
xauth \
|
||||
xterm \
|
||||
wireshark \
|
||||
vim \
|
||||
build-essential \
|
||||
nano \
|
||||
firefox \
|
||||
net-tools \
|
||||
rsync \
|
||||
openssh-server \
|
||||
openssh-client \
|
||||
vsftpd \
|
||||
atftpd \
|
||||
atftp \
|
||||
mini-httpd \
|
||||
lynx \
|
||||
tcpdump \
|
||||
iperf \
|
||||
iperf3 \
|
||||
tshark \
|
||||
openssh-sftp-server \
|
||||
bind9 \
|
||||
bind9-utils \
|
||||
openvpn \
|
||||
isc-dhcp-server \
|
||||
isc-dhcp-client \
|
||||
whois \
|
||||
ipcalc \
|
||||
socat \
|
||||
hping3 \
|
||||
libgtk-3-0 \
|
||||
librest-0.7-0 \
|
||||
libgtk-3-common \
|
||||
dconf-gsettings-backend \
|
||||
libsoup-gnome2.4-1 \
|
||||
libsoup2.4-1 \
|
||||
dconf-service \
|
||||
x11-xserver-utils \
|
||||
ftp \
|
||||
git \
|
||||
sudo \
|
||||
wget \
|
||||
tzdata \
|
||||
libpcap-dev \
|
||||
libpcre3-dev \
|
||||
libprotobuf-dev \
|
||||
libxml2-dev \
|
||||
protobuf-compiler \
|
||||
unzip \
|
||||
uuid-dev \
|
||||
iproute2 \
|
||||
vlc \
|
||||
iputils-ping && \
|
||||
apt-get autoremove -y
|
||||
|
||||
# install core
|
||||
RUN git clone https://github.com/coreemu/core && \
|
||||
cd core && \
|
||||
git checkout ${BRANCH} && \
|
||||
./setup.sh && \
|
||||
PATH=/root/.local/bin:$PATH inv install -v -p ${PREFIX} && \
|
||||
cd /opt && \
|
||||
rm -rf ospf-mdr
|
||||
|
||||
# install emane
|
||||
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip && \
|
||||
mkdir protoc && \
|
||||
unzip protoc-${PROTOC_VERSION}-linux-x86_64.zip -d protoc && \
|
||||
git clone https://github.com/adjacentlink/emane.git && \
|
||||
cd emane && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix=/usr && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
cd src/python && \
|
||||
make clean && \
|
||||
PATH=/opt/protoc/bin:$PATH make && \
|
||||
${VENV_PATH}/bin/python -m pip install . && \
|
||||
cd /opt && \
|
||||
rm -rf protoc && \
|
||||
rm -rf emane && \
|
||||
rm -f protoc-${PROTOC_VERSION}-linux-x86_64.zip
|
||||
|
||||
WORKDIR /root
|
||||
|
||||
CMD /opt/core/venv/bin/core-daemon
|
93
Makefile.am
93
Makefile.am
|
@ -6,10 +6,6 @@ if WANT_DOCS
|
|||
DOCS = docs man
|
||||
endif
|
||||
|
||||
if WANT_GUI
|
||||
GUI = gui
|
||||
endif
|
||||
|
||||
if WANT_DAEMON
|
||||
DAEMON = daemon
|
||||
endif
|
||||
|
@ -19,12 +15,13 @@ if WANT_NETNS
|
|||
endif
|
||||
|
||||
# keep docs last due to dependencies on binaries
|
||||
SUBDIRS = $(GUI) $(DAEMON) $(NETNS) $(DOCS)
|
||||
SUBDIRS = $(DAEMON) $(NETNS) $(DOCS)
|
||||
|
||||
ACLOCAL_AMFLAGS = -I config
|
||||
|
||||
# extra files to include with distribution tarball
|
||||
EXTRA_DIST = bootstrap.sh \
|
||||
package \
|
||||
LICENSE \
|
||||
README.md \
|
||||
ASSIGNMENT_OF_COPYRIGHT.pdf \
|
||||
|
@ -51,18 +48,19 @@ fpm -s dir -t deb -n core-distributed \
|
|||
--description "Common Open Research Emulator Distributed Package" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_distributed_VERSION_ARCH.deb \
|
||||
-p core-distributed_VERSION_ARCH.deb \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
-d "ethtool" \
|
||||
-d "procps" \
|
||||
-d "libc6 >= 2.14" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "nftables" \
|
||||
-d "iproute2" \
|
||||
-d "libev4" \
|
||||
-d "openssh-server" \
|
||||
-d "xterm" \
|
||||
-C $(DESTDIR)
|
||||
netns/vnoded=/usr/bin/ \
|
||||
netns/vcmd=/usr/bin/
|
||||
endef
|
||||
|
||||
define fpm-distributed-rpm =
|
||||
|
@ -72,23 +70,86 @@ fpm -s dir -t rpm -n core-distributed \
|
|||
--description "Common Open Research Emulator Distributed Package" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_distributed_VERSION_ARCH.rpm \
|
||||
-p core-distributed_VERSION_ARCH.rpm \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
-d "ethtool" \
|
||||
-d "procps-ng" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "nftables" \
|
||||
-d "iproute" \
|
||||
-d "libev" \
|
||||
-d "net-tools" \
|
||||
-d "openssh-server" \
|
||||
-d "xterm" \
|
||||
netns/vnoded=/usr/bin/ \
|
||||
netns/vcmd=/usr/bin/
|
||||
endef
|
||||
|
||||
define fpm-rpm =
|
||||
fpm -s dir -t rpm -n core \
|
||||
-m "$(PACKAGE_MAINTAINERS)" \
|
||||
--license "BSD" \
|
||||
--description "core vnoded/vcmd and system dependencies" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_VERSION_ARCH.rpm \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
--rpm-init package/core-daemon \
|
||||
--after-install package/after-install.sh \
|
||||
--after-remove package/after-remove.sh \
|
||||
-d "ethtool" \
|
||||
-d "tk" \
|
||||
-d "procps-ng" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "iproute" \
|
||||
-d "libev" \
|
||||
-d "net-tools" \
|
||||
-d "openssh-server" \
|
||||
-d "xterm" \
|
||||
-C $(DESTDIR)
|
||||
-d "nftables" \
|
||||
netns/vnoded=/usr/bin/ \
|
||||
netns/vcmd=/usr/bin/ \
|
||||
package/etc/core.conf=/etc/core/ \
|
||||
package/etc/logging.conf=/etc/core/ \
|
||||
package/examples=/opt/core/ \
|
||||
daemon/dist/core-$(PACKAGE_VERSION)-py3-none-any.whl=/opt/core/
|
||||
endef
|
||||
|
||||
.PHONY: fpm-distributed
|
||||
fpm-distributed: clean-local-fpm
|
||||
$(MAKE) -C netns install DESTDIR=$(DESTDIR)
|
||||
define fpm-deb =
|
||||
fpm -s dir -t deb -n core \
|
||||
-m "$(PACKAGE_MAINTAINERS)" \
|
||||
--license "BSD" \
|
||||
--description "core vnoded/vcmd and system dependencies" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_VERSION_ARCH.deb \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
--deb-systemd package/core-daemon.service \
|
||||
--deb-no-default-config-files \
|
||||
--after-install package/after-install.sh \
|
||||
--after-remove package/after-remove.sh \
|
||||
-d "ethtool" \
|
||||
-d "tk" \
|
||||
-d "libtk-img" \
|
||||
-d "procps" \
|
||||
-d "libc6 >= 2.14" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "iproute2" \
|
||||
-d "libev4" \
|
||||
-d "nftables" \
|
||||
netns/vnoded=/usr/bin/ \
|
||||
netns/vcmd=/usr/bin/ \
|
||||
package/etc/core.conf=/etc/core/ \
|
||||
package/etc/logging.conf=/etc/core/ \
|
||||
package/examples=/opt/core/ \
|
||||
daemon/dist/core-$(PACKAGE_VERSION)-py3-none-any.whl=/opt/core/
|
||||
endef
|
||||
|
||||
.PHONY: fpm
|
||||
fpm: clean-local-fpm
|
||||
cd daemon && poetry build -f wheel
|
||||
$(call fpm-deb)
|
||||
$(call fpm-rpm)
|
||||
$(call fpm-distributed-deb)
|
||||
$(call fpm-distributed-rpm)
|
||||
|
||||
|
@ -115,7 +176,6 @@ $(info creating file $1 from $1.in)
|
|||
-e 's,[@]CORE_STATE_DIR[@],$(CORE_STATE_DIR),g' \
|
||||
-e 's,[@]CORE_DATA_DIR[@],$(CORE_DATA_DIR),g' \
|
||||
-e 's,[@]CORE_CONF_DIR[@],$(CORE_CONF_DIR),g' \
|
||||
-e 's,[@]CORE_GUI_CONF_DIR[@],$(CORE_GUI_CONF_DIR),g' \
|
||||
< $1.in > $1
|
||||
endef
|
||||
|
||||
|
@ -123,7 +183,6 @@ all: change-files
|
|||
|
||||
.PHONY: change-files
|
||||
change-files:
|
||||
$(call change-files,gui/core-gui)
|
||||
$(call change-files,daemon/core/constants.py)
|
||||
$(call change-files,netns/setup.py)
|
||||
|
||||
|
|
109
README.md
109
README.md
|
@ -1,24 +1,107 @@
|
|||
# Index
|
||||
- CORE
|
||||
- Docker Setup
|
||||
- Precompiled container image
|
||||
- Build container image from source
|
||||
- Adding extra packages
|
||||
|
||||
- Useful commands
|
||||
- License
|
||||
|
||||
# CORE
|
||||
|
||||
CORE: Common Open Research Emulator
|
||||
|
||||
Copyright (c)2005-2020 the Boeing Company.
|
||||
Copyright (c)2005-2022 the Boeing Company.
|
||||
|
||||
See the LICENSE file included in this distribution.
|
||||
|
||||
## About
|
||||
# Docker Setup
|
||||
|
||||
The Common Open Research Emulator (CORE) is a tool for emulating
|
||||
networks on one or more machines. You can connect these emulated
|
||||
networks to live networks. CORE consists of a GUI for drawing
|
||||
topologies of lightweight virtual machines, and Python modules for
|
||||
scripting network emulation.
|
||||
Here you have 2 choices
|
||||
|
||||
## Documentation & Support
|
||||
## Precompiled container image
|
||||
|
||||
We are leveraging GitHub hosted documentation and Discord for persistent
|
||||
chat rooms. This allows for more dynamic conversations and the
|
||||
capability to respond faster. Feel free to join us at the link below.
|
||||
```bash
|
||||
|
||||
* [Documentation](https://coreemu.github.io/core/)
|
||||
* [Discord Channel](https://discord.gg/AKd7kmP)
|
||||
# Start container
|
||||
sudo docker run -itd --name core -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix:rw --privileged --restart unless-stopped git.olympuslab.net/afonso/core-extra:latest
|
||||
|
||||
```
|
||||
## Build container image from source
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://gitea.olympuslab.net/afonso/core-extra.git
|
||||
|
||||
# cd into the directory
|
||||
cd core-extra
|
||||
|
||||
# build the docker image
|
||||
sudo docker build -t core-extra .
|
||||
|
||||
# start container
|
||||
sudo docker run -itd --name core -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix:rw --privileged --restart unless-stopped core-extra
|
||||
|
||||
```
|
||||
|
||||
### Adding extra packages
|
||||
|
||||
To add extra packages you must modify the Dockerfile and then compile the docker image.
|
||||
If you install it after starting the container it will, by docker nature, be reverted on the next boot of the container.
|
||||
|
||||
# Useful commands
|
||||
|
||||
I have the following functions on my fish shell
|
||||
to help me better use core
|
||||
|
||||
THIS ONLY WORKS ON FISH, MODIFY FOR BASH OR ZSH
|
||||
|
||||
```fish
|
||||
|
||||
# RUN CORE GUI
|
||||
function core
|
||||
xhost +local:root
|
||||
sudo docker exec -it core core-gui
|
||||
end
|
||||
|
||||
# RUN BASH INSIDE THE CONTAINER
|
||||
function core-bash
|
||||
sudo docker exec -it core /bin/bash
|
||||
end
|
||||
|
||||
|
||||
# LAUNCH NODE BASH ON THE HOST MACHINE
|
||||
function launch-term --argument nodename
|
||||
sudo docker exec -it core xterm -bg black -fg white -fa 'DejaVu Sans Mono' -fs 16 -e vcmd -c /tmp/pycore.1/$nodename -- /bin/bash
|
||||
end
|
||||
|
||||
#TO RUN ANY OTHER COMMAND
|
||||
sudo docker exec -it core COMAND_GOES_HERE
|
||||
|
||||
```
|
||||
|
||||
## LICENSE
|
||||
|
||||
Copyright (c) 2005-2018, the Boeing Company.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# (c)2010-2012 the Boeing Company
|
||||
#
|
||||
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
|
||||
#
|
||||
# Bootstrap the autoconf system.
|
||||
#
|
||||
|
||||
|
|
74
configure.ac
74
configure.ac
|
@ -2,7 +2,7 @@
|
|||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
# this defines the CORE version number, must be static for AC_INIT
|
||||
AC_INIT(core, 7.3.0)
|
||||
AC_INIT(core, 9.0.3)
|
||||
|
||||
# autoconf and automake initialization
|
||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||
|
@ -30,25 +30,14 @@ AC_SUBST(CORE_CONF_DIR)
|
|||
AC_SUBST(CORE_DATA_DIR)
|
||||
AC_SUBST(CORE_STATE_DIR)
|
||||
|
||||
# CORE GUI configuration files and preferences in CORE_GUI_CONF_DIR
|
||||
# scenario files in ~/.core/configs/
|
||||
AC_ARG_WITH([guiconfdir],
|
||||
[AS_HELP_STRING([--with-guiconfdir=dir],
|
||||
[specify GUI configuration directory])],
|
||||
[CORE_GUI_CONF_DIR="$with_guiconfdir"],
|
||||
[CORE_GUI_CONF_DIR="\$\${HOME}/.core"])
|
||||
AC_SUBST(CORE_GUI_CONF_DIR)
|
||||
AC_ARG_ENABLE([gui],
|
||||
[AS_HELP_STRING([--enable-gui[=ARG]],
|
||||
[build and install the GUI (default is yes)])],
|
||||
[], [enable_gui=yes])
|
||||
AC_SUBST(enable_gui)
|
||||
# documentation option
|
||||
AC_ARG_ENABLE([docs],
|
||||
[AS_HELP_STRING([--enable-docs[=ARG]],
|
||||
[build python documentation (default is no)])],
|
||||
[], [enable_docs=no])
|
||||
AC_SUBST(enable_docs)
|
||||
|
||||
# python option
|
||||
AC_ARG_ENABLE([python],
|
||||
[AS_HELP_STRING([--enable-python[=ARG]],
|
||||
[build and install the python bindings (default is yes)])],
|
||||
|
@ -94,28 +83,7 @@ if test "x$enable_daemon" = "xyes"; then
|
|||
want_python=yes
|
||||
want_linux_netns=yes
|
||||
|
||||
# Checks for libraries.
|
||||
AC_CHECK_LIB([netgraph], [NgMkSockNode])
|
||||
|
||||
# Checks for header files.
|
||||
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h stdint.h stdlib.h string.h sys/ioctl.h sys/mount.h sys/socket.h sys/time.h termios.h unistd.h])
|
||||
|
||||
# Checks for typedefs, structures, and compiler characteristics.
|
||||
AC_C_INLINE
|
||||
AC_TYPE_INT32_T
|
||||
AC_TYPE_PID_T
|
||||
AC_TYPE_SIZE_T
|
||||
AC_TYPE_SSIZE_T
|
||||
AC_TYPE_UINT32_T
|
||||
AC_TYPE_UINT8_T
|
||||
|
||||
# Checks for library functions.
|
||||
AC_FUNC_FORK
|
||||
AC_FUNC_MALLOC
|
||||
AC_FUNC_REALLOC
|
||||
AC_CHECK_FUNCS([atexit dup2 gettimeofday memset socket strerror uname])
|
||||
|
||||
AM_PATH_PYTHON(3.6)
|
||||
AM_PATH_PYTHON(3.9)
|
||||
AS_IF([$PYTHON -m grpc_tools.protoc -h &> /dev/null], [], [AC_MSG_ERROR([please install python grpcio-tools])])
|
||||
|
||||
AC_CHECK_PROG(sysctl_path, sysctl, $as_dir, no, $SEARCHPATH)
|
||||
|
@ -123,9 +91,9 @@ if test "x$enable_daemon" = "xyes"; then
|
|||
AC_MSG_ERROR([Could not locate sysctl (from procps package).])
|
||||
fi
|
||||
|
||||
AC_CHECK_PROG(ebtables_path, ebtables, $as_dir, no, $SEARCHPATH)
|
||||
if test "x$ebtables_path" = "xno" ; then
|
||||
AC_MSG_ERROR([Could not locate ebtables (from ebtables package).])
|
||||
AC_CHECK_PROG(nftables_path, nft, $as_dir, no, $SEARCHPATH)
|
||||
if test "x$nftables_path" = "xno" ; then
|
||||
AC_MSG_ERROR([Could not locate nftables (from nftables package).])
|
||||
fi
|
||||
|
||||
AC_CHECK_PROG(ip_path, ip, $as_dir, no, $SEARCHPATH)
|
||||
|
@ -171,6 +139,25 @@ fi
|
|||
|
||||
if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; then
|
||||
want_linux_netns=yes
|
||||
|
||||
# Checks for header files.
|
||||
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h stdint.h stdlib.h string.h sys/ioctl.h sys/mount.h sys/socket.h sys/time.h termios.h unistd.h])
|
||||
|
||||
# Checks for typedefs, structures, and compiler characteristics.
|
||||
AC_C_INLINE
|
||||
AC_TYPE_INT32_T
|
||||
AC_TYPE_PID_T
|
||||
AC_TYPE_SIZE_T
|
||||
AC_TYPE_SSIZE_T
|
||||
AC_TYPE_UINT32_T
|
||||
AC_TYPE_UINT8_T
|
||||
|
||||
# Checks for library functions.
|
||||
AC_FUNC_FORK
|
||||
AC_FUNC_MALLOC
|
||||
AC_FUNC_REALLOC
|
||||
AC_CHECK_FUNCS([atexit dup2 gettimeofday memset socket strerror uname])
|
||||
|
||||
PKG_CHECK_MODULES(libev, libev,
|
||||
AC_MSG_RESULT([found libev using pkgconfig OK])
|
||||
AC_SUBST(libev_CFLAGS)
|
||||
|
@ -209,7 +196,6 @@ if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then
|
|||
fi
|
||||
|
||||
# Variable substitutions
|
||||
AM_CONDITIONAL(WANT_GUI, test x$enable_gui = xyes)
|
||||
AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes)
|
||||
AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes)
|
||||
AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes)
|
||||
|
@ -224,9 +210,6 @@ fi
|
|||
|
||||
# Output files
|
||||
AC_CONFIG_FILES([Makefile
|
||||
gui/version.tcl
|
||||
gui/Makefile
|
||||
gui/icons/Makefile
|
||||
man/Makefile
|
||||
docs/Makefile
|
||||
daemon/Makefile
|
||||
|
@ -248,17 +231,12 @@ Build:
|
|||
Prefix: ${prefix}
|
||||
Exec Prefix: ${exec_prefix}
|
||||
|
||||
GUI:
|
||||
GUI path: ${CORE_LIB_DIR}
|
||||
GUI config: ${CORE_GUI_CONF_DIR}
|
||||
|
||||
Daemon:
|
||||
Daemon path: ${bindir}
|
||||
Daemon config: ${CORE_CONF_DIR}
|
||||
Python: ${PYTHON}
|
||||
|
||||
Features to build:
|
||||
Build GUI: ${enable_gui}
|
||||
Build Daemon: ${enable_daemon}
|
||||
Documentation: ${want_docs}
|
||||
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
# CORE
|
||||
# (c)2010-2012 the Boeing Company.
|
||||
# See the LICENSE file included in this distribution.
|
||||
#
|
||||
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
|
||||
#
|
||||
# Makefile for building netns components.
|
||||
#
|
||||
|
@ -25,10 +21,7 @@ DISTCLEANFILES = Makefile.in
|
|||
|
||||
# files to include with distribution tarball
|
||||
EXTRA_DIST = core \
|
||||
data \
|
||||
doc/conf.py.in \
|
||||
examples \
|
||||
scripts \
|
||||
tests \
|
||||
setup.cfg \
|
||||
poetry.lock \
|
||||
|
|
|
@ -2,6 +2,3 @@ import logging.config
|
|||
|
||||
# setup default null handler
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
|
||||
# disable paramiko logging
|
||||
logging.getLogger("paramiko").setLevel(logging.WARNING)
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,10 @@
|
|||
import logging
|
||||
from collections.abc import Iterable
|
||||
from queue import Empty, Queue
|
||||
from typing import Iterable, Optional
|
||||
from typing import Optional
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc.grpcutils import convert_link
|
||||
from core.api.grpc import core_pb2, grpcutils
|
||||
from core.api.grpc.grpcutils import convert_link_data
|
||||
from core.emulator.data import (
|
||||
ConfigData,
|
||||
EventData,
|
||||
|
@ -14,29 +15,21 @@ from core.emulator.data import (
|
|||
)
|
||||
from core.emulator.session import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def handle_node_event(node_data: NodeData) -> core_pb2.Event:
|
||||
|
||||
def handle_node_event(session: Session, node_data: NodeData) -> core_pb2.Event:
|
||||
"""
|
||||
Handle node event when there is a node event
|
||||
|
||||
:param session: session node is from
|
||||
:param node_data: node data
|
||||
:return: node event that contains node id, name, model, position, and services
|
||||
"""
|
||||
node = node_data.node
|
||||
x, y, _ = node.position.get()
|
||||
position = core_pb2.Position(x=x, y=y)
|
||||
lon, lat, alt = node.position.get_geo()
|
||||
geo = core_pb2.Geo(lon=lon, lat=lat, alt=alt)
|
||||
services = [x.name for x in node.services]
|
||||
node_proto = core_pb2.Node(
|
||||
id=node.id,
|
||||
name=node.name,
|
||||
model=node.type,
|
||||
icon=node.icon,
|
||||
position=position,
|
||||
geo=geo,
|
||||
services=services,
|
||||
)
|
||||
emane_configs = grpcutils.get_emane_model_configs_dict(session)
|
||||
node_emane_configs = emane_configs.get(node.id, [])
|
||||
node_proto = grpcutils.get_node_proto(session, node, node_emane_configs)
|
||||
message_type = node_data.message_type.value
|
||||
node_event = core_pb2.NodeEvent(message_type=message_type, node=node_proto)
|
||||
return core_pb2.Event(node_event=node_event, source=node_data.source)
|
||||
|
@ -49,7 +42,7 @@ def handle_link_event(link_data: LinkData) -> core_pb2.Event:
|
|||
:param link_data: link data
|
||||
:return: link event that has message type and link information
|
||||
"""
|
||||
link = convert_link(link_data)
|
||||
link = convert_link_data(link_data)
|
||||
message_type = link_data.message_type.value
|
||||
link_event = core_pb2.LinkEvent(message_type=message_type, link=link)
|
||||
return core_pb2.Event(link_event=link_event, source=link_data.source)
|
||||
|
@ -187,7 +180,7 @@ class EventStreamer:
|
|||
try:
|
||||
data = self.queue.get(timeout=1)
|
||||
if isinstance(data, NodeData):
|
||||
event = handle_node_event(data)
|
||||
event = handle_node_event(self.session, data)
|
||||
elif isinstance(data, LinkData):
|
||||
event = handle_link_event(data)
|
||||
elif isinstance(data, EventData):
|
||||
|
@ -199,7 +192,7 @@ class EventStreamer:
|
|||
elif isinstance(data, FileData):
|
||||
event = handle_file_event(data)
|
||||
else:
|
||||
logging.error("unknown event: %s", data)
|
||||
logger.error("unknown event: %s", data)
|
||||
except Empty:
|
||||
pass
|
||||
if event:
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Tuple, Type, Union
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import grpc
|
||||
from grpc import ServicerContext
|
||||
|
||||
from core import utils
|
||||
from core.api.grpc import common_pb2, core_pb2
|
||||
from core.api.grpc.common_pb2 import MappedConfig
|
||||
from core.api.grpc import common_pb2, core_pb2, wrappers
|
||||
from core.api.grpc.configservices_pb2 import ConfigServiceConfig
|
||||
from core.api.grpc.emane_pb2 import GetEmaneModelConfig
|
||||
from core.api.grpc.emane_pb2 import NodeEmaneConfig
|
||||
from core.api.grpc.services_pb2 import (
|
||||
NodeServiceConfig,
|
||||
NodeServiceData,
|
||||
|
@ -18,17 +17,30 @@ from core.api.grpc.services_pb2 import (
|
|||
ServiceDefaults,
|
||||
)
|
||||
from core.config import ConfigurableOptions
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
|
||||
from core.emane.nodes import EmaneNet, EmaneOptions
|
||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
||||
from core.emulator.enumerations import LinkTypes, NodeTypes
|
||||
from core.emulator.links import CoreLink
|
||||
from core.emulator.session import Session
|
||||
from core.errors import CoreError
|
||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
||||
from core.nodes.base import (
|
||||
CoreNode,
|
||||
CoreNodeBase,
|
||||
CoreNodeOptions,
|
||||
NodeBase,
|
||||
NodeOptions,
|
||||
Position,
|
||||
)
|
||||
from core.nodes.docker import DockerNode, DockerOptions
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
from core.nodes.lxd import LxcNode, LxcOptions
|
||||
from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode
|
||||
from core.nodes.podman import PodmanNode, PodmanOptions
|
||||
from core.nodes.wireless import WirelessNode
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
WORKERS = 10
|
||||
|
||||
|
||||
|
@ -51,33 +63,33 @@ class CpuUsage:
|
|||
return (total_diff - idle_diff) / total_diff
|
||||
|
||||
|
||||
def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOptions]:
|
||||
def add_node_data(
|
||||
_class: type[NodeBase], node_proto: core_pb2.Node
|
||||
) -> tuple[Position, NodeOptions]:
|
||||
"""
|
||||
Convert node protobuf message to data for creating a node.
|
||||
|
||||
:param _class: node class to create options from
|
||||
:param node_proto: node proto message
|
||||
:return: node type, id, and options
|
||||
"""
|
||||
_id = node_proto.id
|
||||
_type = NodeTypes(node_proto.type)
|
||||
options = NodeOptions(
|
||||
name=node_proto.name,
|
||||
model=node_proto.model,
|
||||
icon=node_proto.icon,
|
||||
image=node_proto.image,
|
||||
services=node_proto.services,
|
||||
config_services=node_proto.config_services,
|
||||
)
|
||||
if node_proto.emane:
|
||||
options.emane = node_proto.emane
|
||||
if node_proto.server:
|
||||
options.server = node_proto.server
|
||||
position = node_proto.position
|
||||
options.set_position(position.x, position.y)
|
||||
options = _class.create_options()
|
||||
options.icon = node_proto.icon
|
||||
options.canvas = node_proto.canvas
|
||||
if isinstance(options, CoreNodeOptions):
|
||||
options.model = node_proto.model
|
||||
options.services = node_proto.services
|
||||
options.config_services = node_proto.config_services
|
||||
if isinstance(options, EmaneOptions):
|
||||
options.emane_model = node_proto.emane
|
||||
if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)):
|
||||
options.image = node_proto.image
|
||||
position = Position()
|
||||
position.set(node_proto.position.x, node_proto.position.y)
|
||||
if node_proto.HasField("geo"):
|
||||
geo = node_proto.geo
|
||||
options.set_location(geo.lat, geo.lon, geo.alt)
|
||||
return _type, _id, options
|
||||
position.set_geo(geo.lon, geo.lat, geo.alt)
|
||||
return position, options
|
||||
|
||||
|
||||
def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
|
||||
|
@ -106,8 +118,8 @@ def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
|
|||
|
||||
|
||||
def add_link_data(
|
||||
link_proto: core_pb2.Link
|
||||
) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]:
|
||||
link_proto: core_pb2.Link,
|
||||
) -> tuple[InterfaceData, InterfaceData, LinkOptions]:
|
||||
"""
|
||||
Convert link proto to link interfaces and options data.
|
||||
|
||||
|
@ -116,7 +128,6 @@ def add_link_data(
|
|||
"""
|
||||
iface1_data = link_iface(link_proto.iface1)
|
||||
iface2_data = link_iface(link_proto.iface2)
|
||||
link_type = LinkTypes(link_proto.type)
|
||||
options = LinkOptions()
|
||||
options_proto = link_proto.options
|
||||
if options_proto:
|
||||
|
@ -131,12 +142,12 @@ def add_link_data(
|
|||
options.buffer = options_proto.buffer
|
||||
options.unidirectional = options_proto.unidirectional
|
||||
options.key = options_proto.key
|
||||
return iface1_data, iface2_data, options, link_type
|
||||
return iface1_data, iface2_data, options
|
||||
|
||||
|
||||
def create_nodes(
|
||||
session: Session, node_protos: List[core_pb2.Node]
|
||||
) -> Tuple[List[NodeBase], List[Exception]]:
|
||||
session: Session, node_protos: list[core_pb2.Node]
|
||||
) -> tuple[list[NodeBase], list[Exception]]:
|
||||
"""
|
||||
Create nodes using a thread pool and wait for completion.
|
||||
|
||||
|
@ -146,20 +157,28 @@ def create_nodes(
|
|||
"""
|
||||
funcs = []
|
||||
for node_proto in node_protos:
|
||||
_type, _id, options = add_node_data(node_proto)
|
||||
_type = NodeTypes(node_proto.type)
|
||||
_class = session.get_node_class(_type)
|
||||
args = (_class, _id, options)
|
||||
position, options = add_node_data(_class, node_proto)
|
||||
args = (
|
||||
_class,
|
||||
node_proto.id or None,
|
||||
node_proto.name or None,
|
||||
node_proto.server or None,
|
||||
position,
|
||||
options,
|
||||
)
|
||||
funcs.append((session.add_node, args, {}))
|
||||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
total = time.monotonic() - start
|
||||
logging.debug("grpc created nodes time: %s", total)
|
||||
logger.debug("grpc created nodes time: %s", total)
|
||||
return results, exceptions
|
||||
|
||||
|
||||
def create_links(
|
||||
session: Session, link_protos: List[core_pb2.Link]
|
||||
) -> Tuple[List[NodeBase], List[Exception]]:
|
||||
session: Session, link_protos: list[core_pb2.Link]
|
||||
) -> tuple[list[NodeBase], list[Exception]]:
|
||||
"""
|
||||
Create links using a thread pool and wait for completion.
|
||||
|
||||
|
@ -171,19 +190,19 @@ def create_links(
|
|||
for link_proto in link_protos:
|
||||
node1_id = link_proto.node1_id
|
||||
node2_id = link_proto.node2_id
|
||||
iface1, iface2, options, link_type = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1, iface2, options, link_type)
|
||||
iface1, iface2, options = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1, iface2, options)
|
||||
funcs.append((session.add_link, args, {}))
|
||||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
total = time.monotonic() - start
|
||||
logging.debug("grpc created links time: %s", total)
|
||||
logger.debug("grpc created links time: %s", total)
|
||||
return results, exceptions
|
||||
|
||||
|
||||
def edit_links(
|
||||
session: Session, link_protos: List[core_pb2.Link]
|
||||
) -> Tuple[List[None], List[Exception]]:
|
||||
session: Session, link_protos: list[core_pb2.Link]
|
||||
) -> tuple[list[None], list[Exception]]:
|
||||
"""
|
||||
Edit links using a thread pool and wait for completion.
|
||||
|
||||
|
@ -195,13 +214,13 @@ def edit_links(
|
|||
for link_proto in link_protos:
|
||||
node1_id = link_proto.node1_id
|
||||
node2_id = link_proto.node2_id
|
||||
iface1, iface2, options, link_type = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type)
|
||||
iface1, iface2, options = add_link_data(link_proto)
|
||||
args = (node1_id, node2_id, iface1.id, iface2.id, options)
|
||||
funcs.append((session.update_link, args, {}))
|
||||
start = time.monotonic()
|
||||
results, exceptions = utils.threadpool(funcs)
|
||||
total = time.monotonic() - start
|
||||
logging.debug("grpc edit links time: %s", total)
|
||||
logger.debug("grpc edit links time: %s", total)
|
||||
return results, exceptions
|
||||
|
||||
|
||||
|
@ -217,10 +236,26 @@ def convert_value(value: Any) -> str:
|
|||
return value
|
||||
|
||||
|
||||
def convert_session_options(session: Session) -> dict[str, common_pb2.ConfigOption]:
|
||||
config_options = {}
|
||||
for option in session.options.options:
|
||||
value = session.options.get(option.id)
|
||||
config_option = common_pb2.ConfigOption(
|
||||
label=option.label,
|
||||
name=option.id,
|
||||
value=value,
|
||||
type=option.type.value,
|
||||
select=option.options,
|
||||
group="Options",
|
||||
)
|
||||
config_options[option.id] = config_option
|
||||
return config_options
|
||||
|
||||
|
||||
def get_config_options(
|
||||
config: Dict[str, str],
|
||||
configurable_options: Union[ConfigurableOptions, Type[ConfigurableOptions]],
|
||||
) -> Dict[str, common_pb2.ConfigOption]:
|
||||
config: dict[str, str],
|
||||
configurable_options: Union[ConfigurableOptions, type[ConfigurableOptions]],
|
||||
) -> dict[str, common_pb2.ConfigOption]:
|
||||
"""
|
||||
Retrieve configuration options in a form that is used by the grpc server.
|
||||
|
||||
|
@ -248,12 +283,15 @@ def get_config_options(
|
|||
return results
|
||||
|
||||
|
||||
def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
|
||||
def get_node_proto(
|
||||
session: Session, node: NodeBase, emane_configs: list[NodeEmaneConfig]
|
||||
) -> core_pb2.Node:
|
||||
"""
|
||||
Convert CORE node to protobuf representation.
|
||||
|
||||
:param session: session containing node
|
||||
:param node: node to convert
|
||||
:param emane_configs: emane configs related to node
|
||||
:return: node proto
|
||||
"""
|
||||
node_type = session.get_node_type(node.__class__)
|
||||
|
@ -263,24 +301,77 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
|
|||
geo = core_pb2.Geo(
|
||||
lat=node.position.lat, lon=node.position.lon, alt=node.position.alt
|
||||
)
|
||||
services = getattr(node, "services", [])
|
||||
if services is None:
|
||||
services = []
|
||||
services = [x.name for x in services]
|
||||
config_services = getattr(node, "config_services", {})
|
||||
config_services = [x for x in config_services]
|
||||
services = [x.name for x in node.services]
|
||||
node_dir = None
|
||||
config_services = []
|
||||
if isinstance(node, CoreNodeBase):
|
||||
node_dir = str(node.directory)
|
||||
config_services = [x for x in node.config_services]
|
||||
channel = None
|
||||
if isinstance(node, CoreNode):
|
||||
channel = str(node.ctrlchnlname)
|
||||
emane_model = None
|
||||
if isinstance(node, EmaneNet):
|
||||
emane_model = node.model.name
|
||||
model = getattr(node, "type", None)
|
||||
node_dir = getattr(node, "nodedir", None)
|
||||
channel = getattr(node, "ctrlchnlname", None)
|
||||
image = getattr(node, "image", None)
|
||||
emane_model = node.wireless_model.name
|
||||
image = None
|
||||
if isinstance(node, (DockerNode, LxcNode, PodmanNode)):
|
||||
image = node.image
|
||||
# check for wlan config
|
||||
wlan_config = session.mobility.get_configs(
|
||||
node.id, config_type=BasicRangeModel.name
|
||||
)
|
||||
if wlan_config:
|
||||
wlan_config = get_config_options(wlan_config, BasicRangeModel)
|
||||
# check for wireless config
|
||||
wireless_config = None
|
||||
if isinstance(node, WirelessNode):
|
||||
configs = node.get_config()
|
||||
wireless_config = {}
|
||||
for config in configs.values():
|
||||
config_option = common_pb2.ConfigOption(
|
||||
label=config.label,
|
||||
name=config.id,
|
||||
value=config.default,
|
||||
type=config.type.value,
|
||||
select=config.options,
|
||||
group=config.group,
|
||||
)
|
||||
wireless_config[config.id] = config_option
|
||||
# check for mobility config
|
||||
mobility_config = session.mobility.get_configs(
|
||||
node.id, config_type=Ns2ScriptedMobility.name
|
||||
)
|
||||
if mobility_config:
|
||||
mobility_config = get_config_options(mobility_config, Ns2ScriptedMobility)
|
||||
# check for service configs
|
||||
custom_services = session.services.custom_services.get(node.id)
|
||||
service_configs = {}
|
||||
if custom_services:
|
||||
for service in custom_services.values():
|
||||
service_proto = get_service_configuration(service)
|
||||
service_configs[service.name] = NodeServiceConfig(
|
||||
node_id=node.id,
|
||||
service=service.name,
|
||||
data=service_proto,
|
||||
files=service.config_data,
|
||||
)
|
||||
# check for config service configs
|
||||
config_service_configs = {}
|
||||
if isinstance(node, CoreNode):
|
||||
for service in node.config_services.values():
|
||||
if not service.custom_templates and not service.custom_config:
|
||||
continue
|
||||
config_service_configs[service.name] = ConfigServiceConfig(
|
||||
node_id=node.id,
|
||||
name=service.name,
|
||||
templates=service.custom_templates,
|
||||
config=service.custom_config,
|
||||
)
|
||||
return core_pb2.Node(
|
||||
id=node.id,
|
||||
name=node.name,
|
||||
emane=emane_model,
|
||||
model=model,
|
||||
model=node.model,
|
||||
type=node_type.value,
|
||||
position=position,
|
||||
geo=geo,
|
||||
|
@ -290,64 +381,94 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
|
|||
config_services=config_services,
|
||||
dir=node_dir,
|
||||
channel=channel,
|
||||
canvas=node.canvas,
|
||||
wlan_config=wlan_config,
|
||||
wireless_config=wireless_config,
|
||||
mobility_config=mobility_config,
|
||||
service_configs=service_configs,
|
||||
config_service_configs=config_service_configs,
|
||||
emane_configs=emane_configs,
|
||||
)
|
||||
|
||||
|
||||
def get_links(node: NodeBase):
|
||||
def get_links(session: Session, node: NodeBase) -> list[core_pb2.Link]:
|
||||
"""
|
||||
Retrieve a list of links for grpc to use.
|
||||
|
||||
:param session: session to get links for node
|
||||
:param node: node to get links from
|
||||
:return: protobuf links
|
||||
"""
|
||||
link_protos = []
|
||||
for core_link in session.link_manager.node_links(node):
|
||||
link_protos.extend(convert_core_link(core_link))
|
||||
if isinstance(node, (WlanNode, EmaneNet)):
|
||||
for link_data in node.links():
|
||||
link_protos.append(convert_link_data(link_data))
|
||||
return link_protos
|
||||
|
||||
|
||||
def convert_iface(iface: CoreInterface) -> core_pb2.Interface:
|
||||
"""
|
||||
Convert interface to protobuf.
|
||||
|
||||
:param iface: interface to convert
|
||||
:return: protobuf interface
|
||||
"""
|
||||
if isinstance(iface.node, CoreNetwork):
|
||||
return core_pb2.Interface(id=iface.id)
|
||||
else:
|
||||
ip4 = iface.get_ip4()
|
||||
ip4_mask = ip4.prefixlen if ip4 else None
|
||||
ip4 = str(ip4.ip) if ip4 else None
|
||||
ip6 = iface.get_ip6()
|
||||
ip6_mask = ip6.prefixlen if ip6 else None
|
||||
ip6 = str(ip6.ip) if ip6 else None
|
||||
mac = str(iface.mac) if iface.mac else None
|
||||
return core_pb2.Interface(
|
||||
id=iface.id,
|
||||
name=iface.name,
|
||||
mac=mac,
|
||||
ip4=ip4,
|
||||
ip4_mask=ip4_mask,
|
||||
ip6=ip6,
|
||||
ip6_mask=ip6_mask,
|
||||
)
|
||||
|
||||
|
||||
def convert_core_link(core_link: CoreLink) -> list[core_pb2.Link]:
|
||||
"""
|
||||
Convert core link to protobuf data.
|
||||
|
||||
:param core_link: core link to convert
|
||||
:return: protobuf link data
|
||||
"""
|
||||
links = []
|
||||
for link in node.links():
|
||||
link_proto = convert_link(link)
|
||||
links.append(link_proto)
|
||||
node1, iface1 = core_link.node1, core_link.iface1
|
||||
node2, iface2 = core_link.node2, core_link.iface2
|
||||
unidirectional = core_link.is_unidirectional()
|
||||
link = convert_link(node1, iface1, node2, iface2, iface1.options, unidirectional)
|
||||
links.append(link)
|
||||
if unidirectional:
|
||||
link = convert_link(
|
||||
node2, iface2, node1, iface1, iface2.options, unidirectional
|
||||
)
|
||||
links.append(link)
|
||||
return links
|
||||
|
||||
|
||||
def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface:
|
||||
return core_pb2.Interface(
|
||||
id=iface_data.id,
|
||||
name=iface_data.name,
|
||||
mac=iface_data.mac,
|
||||
ip4=iface_data.ip4,
|
||||
ip4_mask=iface_data.ip4_mask,
|
||||
ip6=iface_data.ip6,
|
||||
ip6_mask=iface_data.ip6_mask,
|
||||
)
|
||||
|
||||
|
||||
def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions:
|
||||
return core_pb2.LinkOptions(
|
||||
jitter=options_data.jitter,
|
||||
key=options_data.key,
|
||||
mburst=options_data.mburst,
|
||||
mer=options_data.mer,
|
||||
loss=options_data.loss,
|
||||
bandwidth=options_data.bandwidth,
|
||||
burst=options_data.burst,
|
||||
delay=options_data.delay,
|
||||
dup=options_data.dup,
|
||||
buffer=options_data.buffer,
|
||||
unidirectional=options_data.unidirectional,
|
||||
)
|
||||
|
||||
|
||||
def convert_link(link_data: LinkData) -> core_pb2.Link:
|
||||
def convert_link_data(link_data: LinkData) -> core_pb2.Link:
|
||||
"""
|
||||
Convert link_data into core protobuf link.
|
||||
|
||||
:param link_data: link to convert
|
||||
:return: core protobuf Link
|
||||
"""
|
||||
iface1 = None
|
||||
if link_data.iface1 is not None:
|
||||
iface1 = convert_iface(link_data.iface1)
|
||||
iface1 = convert_iface_data(link_data.iface1)
|
||||
iface2 = None
|
||||
if link_data.iface2 is not None:
|
||||
iface2 = convert_iface(link_data.iface2)
|
||||
iface2 = convert_iface_data(link_data.iface2)
|
||||
options = convert_link_options(link_data.options)
|
||||
return core_pb2.Link(
|
||||
type=link_data.type.value,
|
||||
|
@ -362,27 +483,134 @@ def convert_link(link_data: LinkData) -> core_pb2.Link:
|
|||
)
|
||||
|
||||
|
||||
def get_net_stats() -> Dict[str, Dict]:
|
||||
def convert_iface_data(iface_data: InterfaceData) -> core_pb2.Interface:
|
||||
"""
|
||||
Retrieve status about the current interfaces in the system
|
||||
Convert interface data to protobuf.
|
||||
|
||||
:return: send and receive status of the interfaces in the system
|
||||
:param iface_data: interface data to convert
|
||||
:return: interface protobuf
|
||||
"""
|
||||
with open("/proc/net/dev", "r") as f:
|
||||
data = f.readlines()[2:]
|
||||
return core_pb2.Interface(
|
||||
id=iface_data.id,
|
||||
name=iface_data.name,
|
||||
mac=iface_data.mac,
|
||||
ip4=iface_data.ip4,
|
||||
ip4_mask=iface_data.ip4_mask,
|
||||
ip6=iface_data.ip6,
|
||||
ip6_mask=iface_data.ip6_mask,
|
||||
)
|
||||
|
||||
|
||||
def convert_link_options(options: LinkOptions) -> core_pb2.LinkOptions:
|
||||
"""
|
||||
Convert link options to protobuf.
|
||||
|
||||
:param options: link options to convert
|
||||
:return: link options protobuf
|
||||
"""
|
||||
return core_pb2.LinkOptions(
|
||||
jitter=options.jitter,
|
||||
key=options.key,
|
||||
mburst=options.mburst,
|
||||
mer=options.mer,
|
||||
loss=options.loss,
|
||||
bandwidth=options.bandwidth,
|
||||
burst=options.burst,
|
||||
delay=options.delay,
|
||||
dup=options.dup,
|
||||
buffer=options.buffer,
|
||||
unidirectional=options.unidirectional,
|
||||
)
|
||||
|
||||
|
||||
def convert_options_proto(options: core_pb2.LinkOptions) -> LinkOptions:
|
||||
return LinkOptions(
|
||||
delay=options.delay,
|
||||
bandwidth=options.bandwidth,
|
||||
loss=options.loss,
|
||||
dup=options.dup,
|
||||
jitter=options.jitter,
|
||||
mer=options.mer,
|
||||
burst=options.burst,
|
||||
mburst=options.mburst,
|
||||
buffer=options.buffer,
|
||||
unidirectional=options.unidirectional,
|
||||
key=options.key,
|
||||
)
|
||||
|
||||
|
||||
def convert_link(
|
||||
node1: NodeBase,
|
||||
iface1: Optional[CoreInterface],
|
||||
node2: NodeBase,
|
||||
iface2: Optional[CoreInterface],
|
||||
options: LinkOptions,
|
||||
unidirectional: bool,
|
||||
) -> core_pb2.Link:
|
||||
"""
|
||||
Convert link objects to link protobuf.
|
||||
|
||||
:param node1: first node in link
|
||||
:param iface1: node1 interface
|
||||
:param node2: second node in link
|
||||
:param iface2: node2 interface
|
||||
:param options: link options
|
||||
:param unidirectional: if this link is considered unidirectional
|
||||
:return: protobuf link
|
||||
"""
|
||||
if iface1 is not None:
|
||||
iface1 = convert_iface(iface1)
|
||||
if iface2 is not None:
|
||||
iface2 = convert_iface(iface2)
|
||||
is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet))
|
||||
is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet))
|
||||
if not (is_node1_wireless or is_node2_wireless):
|
||||
options = convert_link_options(options)
|
||||
options.unidirectional = unidirectional
|
||||
else:
|
||||
options = None
|
||||
return core_pb2.Link(
|
||||
type=LinkTypes.WIRED.value,
|
||||
node1_id=node1.id,
|
||||
node2_id=node2.id,
|
||||
iface1=iface1,
|
||||
iface2=iface2,
|
||||
options=options,
|
||||
network_id=None,
|
||||
label=None,
|
||||
color=None,
|
||||
)
|
||||
|
||||
|
||||
def parse_proc_net_dev(lines: list[str]) -> dict[str, dict[str, float]]:
|
||||
"""
|
||||
Parse lines of output from /proc/net/dev.
|
||||
|
||||
:param lines: lines of /proc/net/dev
|
||||
:return: parsed device to tx/rx values
|
||||
"""
|
||||
stats = {}
|
||||
for line in data:
|
||||
for line in lines[2:]:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
line = line.split()
|
||||
line[0] = line[0].strip(":")
|
||||
stats[line[0]] = {"rx": float(line[1]), "tx": float(line[9])}
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def get_net_stats() -> dict[str, dict[str, float]]:
|
||||
"""
|
||||
Retrieve status about the current interfaces in the system
|
||||
|
||||
:return: send and receive status of the interfaces in the system
|
||||
"""
|
||||
with open("/proc/net/dev", "r") as f:
|
||||
lines = f.readlines()[2:]
|
||||
return parse_proc_net_dev(lines)
|
||||
|
||||
|
||||
def session_location(session: Session, location: core_pb2.SessionLocation) -> None:
|
||||
"""
|
||||
Set session location based on location proto.
|
||||
|
@ -439,39 +667,14 @@ def get_service_configuration(service: CoreService) -> NodeServiceData:
|
|||
)
|
||||
|
||||
|
||||
def iface_to_data(iface: CoreInterface) -> InterfaceData:
|
||||
ip4 = iface.get_ip4()
|
||||
ip4_addr = str(ip4.ip) if ip4 else None
|
||||
ip4_mask = ip4.prefixlen if ip4 else None
|
||||
ip6 = iface.get_ip6()
|
||||
ip6_addr = str(ip6.ip) if ip6 else None
|
||||
ip6_mask = ip6.prefixlen if ip6 else None
|
||||
return InterfaceData(
|
||||
id=iface.node_id,
|
||||
name=iface.name,
|
||||
mac=str(iface.mac),
|
||||
ip4=ip4_addr,
|
||||
ip4_mask=ip4_mask,
|
||||
ip6=ip6_addr,
|
||||
ip6_mask=ip6_mask,
|
||||
)
|
||||
|
||||
|
||||
def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
|
||||
def iface_to_proto(session: Session, iface: CoreInterface) -> core_pb2.Interface:
|
||||
"""
|
||||
Convenience for converting a core interface to the protobuf representation.
|
||||
|
||||
:param node_id: id of node to convert interface for
|
||||
:param session: session interface belongs to
|
||||
:param iface: interface to convert
|
||||
:return: interface proto
|
||||
"""
|
||||
if iface.node and iface.node.id == node_id:
|
||||
_id = iface.node_id
|
||||
else:
|
||||
_id = iface.net_id
|
||||
net_id = iface.net.id if iface.net else None
|
||||
node_id = iface.node.id if iface.node else None
|
||||
net2_id = iface.othernet.id if iface.othernet else None
|
||||
ip4_net = iface.get_ip4()
|
||||
ip4 = str(ip4_net.ip) if ip4_net else None
|
||||
ip4_mask = ip4_net.prefixlen if ip4_net else None
|
||||
|
@ -479,11 +682,13 @@ def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
|
|||
ip6 = str(ip6_net.ip) if ip6_net else None
|
||||
ip6_mask = ip6_net.prefixlen if ip6_net else None
|
||||
mac = str(iface.mac) if iface.mac else None
|
||||
nem_id = None
|
||||
nem_port = None
|
||||
if isinstance(iface.net, EmaneNet):
|
||||
nem_id = session.emane.get_nem_id(iface)
|
||||
nem_port = session.emane.get_nem_port(iface)
|
||||
return core_pb2.Interface(
|
||||
id=_id,
|
||||
net_id=net_id,
|
||||
net2_id=net2_id,
|
||||
node_id=node_id,
|
||||
id=iface.id,
|
||||
name=iface.name,
|
||||
mac=mac,
|
||||
mtu=iface.mtu,
|
||||
|
@ -492,6 +697,8 @@ def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
|
|||
ip4_mask=ip4_mask,
|
||||
ip6=ip6,
|
||||
ip6_mask=ip6_mask,
|
||||
nem_id=nem_id,
|
||||
nem_port=nem_port,
|
||||
)
|
||||
|
||||
|
||||
|
@ -522,58 +729,36 @@ def get_nem_id(
|
|||
return nem_id
|
||||
|
||||
|
||||
def get_emane_model_configs(session: Session) -> List[GetEmaneModelConfig]:
|
||||
configs = []
|
||||
for _id in session.emane.node_configurations:
|
||||
if _id == -1:
|
||||
continue
|
||||
model_configs = session.emane.node_configurations[_id]
|
||||
def get_emane_model_configs_dict(session: Session) -> dict[int, list[NodeEmaneConfig]]:
|
||||
"""
|
||||
Get emane model configuration protobuf data.
|
||||
|
||||
:param session: session to get emane model configuration for
|
||||
:return: dict of emane model protobuf configurations
|
||||
"""
|
||||
configs = {}
|
||||
for _id, model_configs in session.emane.node_configs.items():
|
||||
for model_name in model_configs:
|
||||
model = session.emane.models[model_name]
|
||||
current_config = session.emane.get_model_config(_id, model_name)
|
||||
config = get_config_options(current_config, model)
|
||||
model_class = session.emane.get_model(model_name)
|
||||
current_config = session.emane.get_config(_id, model_name)
|
||||
config = get_config_options(current_config, model_class)
|
||||
node_id, iface_id = utils.parse_iface_config_id(_id)
|
||||
iface_id = iface_id if iface_id is not None else -1
|
||||
model_config = GetEmaneModelConfig(
|
||||
node_id=node_id, model=model_name, iface_id=iface_id, config=config
|
||||
node_config = NodeEmaneConfig(
|
||||
model=model_name, iface_id=iface_id, config=config
|
||||
)
|
||||
configs.append(model_config)
|
||||
node_configs = configs.setdefault(node_id, [])
|
||||
node_configs.append(node_config)
|
||||
return configs
|
||||
|
||||
|
||||
def get_wlan_configs(session: Session) -> Dict[int, MappedConfig]:
|
||||
configs = {}
|
||||
for node_id in session.mobility.node_configurations:
|
||||
model_config = session.mobility.node_configurations[node_id]
|
||||
if node_id == -1:
|
||||
continue
|
||||
for model_name in model_config:
|
||||
if model_name != BasicRangeModel.name:
|
||||
continue
|
||||
current_config = session.mobility.get_model_config(node_id, model_name)
|
||||
config = get_config_options(current_config, BasicRangeModel)
|
||||
mapped_config = MappedConfig(config=config)
|
||||
configs[node_id] = mapped_config
|
||||
return configs
|
||||
def get_hooks(session: Session) -> list[core_pb2.Hook]:
|
||||
"""
|
||||
Retrieve hook protobuf data for a session.
|
||||
|
||||
|
||||
def get_mobility_configs(session: Session) -> Dict[int, MappedConfig]:
|
||||
configs = {}
|
||||
for node_id in session.mobility.node_configurations:
|
||||
model_config = session.mobility.node_configurations[node_id]
|
||||
if node_id == -1:
|
||||
continue
|
||||
for model_name in model_config:
|
||||
if model_name != Ns2ScriptedMobility.name:
|
||||
continue
|
||||
current_config = session.mobility.get_model_config(node_id, model_name)
|
||||
config = get_config_options(current_config, Ns2ScriptedMobility)
|
||||
mapped_config = MappedConfig(config=config)
|
||||
configs[node_id] = mapped_config
|
||||
return configs
|
||||
|
||||
|
||||
def get_hooks(session: Session) -> List[core_pb2.Hook]:
|
||||
:param session: session to get hooks for
|
||||
:return: list of hook protobufs
|
||||
"""
|
||||
hooks = []
|
||||
for state in session.hooks:
|
||||
state_hooks = session.hooks[state]
|
||||
|
@ -583,65 +768,31 @@ def get_hooks(session: Session) -> List[core_pb2.Hook]:
|
|||
return hooks
|
||||
|
||||
|
||||
def get_emane_models(session: Session) -> List[str]:
|
||||
emane_models = []
|
||||
for model in session.emane.models.keys():
|
||||
if len(model.split("_")) != 2:
|
||||
continue
|
||||
emane_models.append(model)
|
||||
return emane_models
|
||||
def get_default_services(session: Session) -> list[ServiceDefaults]:
|
||||
"""
|
||||
Retrieve the default service sets for a given session.
|
||||
|
||||
|
||||
def get_default_services(session: Session) -> List[ServiceDefaults]:
|
||||
:param session: session to get default service sets for
|
||||
:return: list of default service sets
|
||||
"""
|
||||
default_services = []
|
||||
for name, services in session.services.default_services.items():
|
||||
default_service = ServiceDefaults(node_type=name, services=services)
|
||||
for model, services in session.services.default_services.items():
|
||||
default_service = ServiceDefaults(model=model, services=services)
|
||||
default_services.append(default_service)
|
||||
return default_services
|
||||
|
||||
|
||||
def get_node_service_configs(session: Session) -> List[NodeServiceConfig]:
|
||||
configs = []
|
||||
for node_id, service_configs in session.services.custom_services.items():
|
||||
for name in service_configs:
|
||||
service = session.services.get_service(node_id, name)
|
||||
service_proto = get_service_configuration(service)
|
||||
config = NodeServiceConfig(
|
||||
node_id=node_id,
|
||||
service=name,
|
||||
data=service_proto,
|
||||
files=service.config_data,
|
||||
)
|
||||
configs.append(config)
|
||||
return configs
|
||||
|
||||
|
||||
def get_node_config_service_configs(session: Session) -> List[ConfigServiceConfig]:
|
||||
configs = []
|
||||
for node in session.nodes.values():
|
||||
if not isinstance(node, CoreNodeBase):
|
||||
continue
|
||||
for name, service in node.config_services.items():
|
||||
if not service.custom_templates and not service.custom_config:
|
||||
continue
|
||||
config_proto = ConfigServiceConfig(
|
||||
node_id=node.id,
|
||||
name=name,
|
||||
templates=service.custom_templates,
|
||||
config=service.custom_config,
|
||||
)
|
||||
configs.append(config_proto)
|
||||
return configs
|
||||
|
||||
|
||||
def get_emane_config(session: Session) -> Dict[str, common_pb2.ConfigOption]:
|
||||
current_config = session.emane.get_configs()
|
||||
return get_config_options(current_config, session.emane.emane_config)
|
||||
|
||||
|
||||
def get_mobility_node(
|
||||
session: Session, node_id: int, context: ServicerContext
|
||||
) -> Union[WlanNode, EmaneNet]:
|
||||
"""
|
||||
Get mobility node.
|
||||
|
||||
:param session: session to get node from
|
||||
:param node_id: id of node to get
|
||||
:param context: grpc context
|
||||
:return: wlan or emane node
|
||||
"""
|
||||
try:
|
||||
return session.get_node(node_id, WlanNode)
|
||||
except CoreError:
|
||||
|
@ -649,3 +800,109 @@ def get_mobility_node(
|
|||
return session.get_node(node_id, EmaneNet)
|
||||
except CoreError:
|
||||
context.abort(grpc.StatusCode.NOT_FOUND, "node id is not for wlan or emane")
|
||||
|
||||
|
||||
def convert_session(session: Session) -> wrappers.Session:
|
||||
"""
|
||||
Convert session to its wrapped version.
|
||||
|
||||
:param session: session to convert
|
||||
:return: wrapped session data
|
||||
"""
|
||||
emane_configs = get_emane_model_configs_dict(session)
|
||||
nodes = []
|
||||
links = []
|
||||
for _id in session.nodes:
|
||||
node = session.nodes[_id]
|
||||
if not isinstance(node, (PtpNet, CtrlNet)):
|
||||
node_emane_configs = emane_configs.get(node.id, [])
|
||||
node_proto = get_node_proto(session, node, node_emane_configs)
|
||||
nodes.append(node_proto)
|
||||
if isinstance(node, (WlanNode, EmaneNet)):
|
||||
for link_data in node.links():
|
||||
links.append(convert_link_data(link_data))
|
||||
for core_link in session.link_manager.links():
|
||||
links.extend(convert_core_link(core_link))
|
||||
default_services = get_default_services(session)
|
||||
x, y, z = session.location.refxyz
|
||||
lat, lon, alt = session.location.refgeo
|
||||
location = core_pb2.SessionLocation(
|
||||
x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=session.location.refscale
|
||||
)
|
||||
hooks = get_hooks(session)
|
||||
session_file = str(session.file_path) if session.file_path else None
|
||||
options = convert_session_options(session)
|
||||
servers = [
|
||||
core_pb2.Server(name=x.name, host=x.host)
|
||||
for x in session.distributed.servers.values()
|
||||
]
|
||||
return core_pb2.Session(
|
||||
id=session.id,
|
||||
state=session.state.value,
|
||||
nodes=nodes,
|
||||
links=links,
|
||||
dir=str(session.directory),
|
||||
user=session.user,
|
||||
default_services=default_services,
|
||||
location=location,
|
||||
hooks=hooks,
|
||||
metadata=session.metadata,
|
||||
file=session_file,
|
||||
options=options,
|
||||
servers=servers,
|
||||
)
|
||||
|
||||
|
||||
def configure_node(
|
||||
session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext
|
||||
) -> None:
|
||||
"""
|
||||
Configure a node using all provided protobuf data.
|
||||
|
||||
:param session: session for node
|
||||
:param node: node protobuf data
|
||||
:param core_node: session node
|
||||
:param context: grpc context
|
||||
:return: nothing
|
||||
"""
|
||||
for emane_config in node.emane_configs:
|
||||
_id = utils.iface_config_id(node.id, emane_config.iface_id)
|
||||
config = {k: v.value for k, v in emane_config.config.items()}
|
||||
session.emane.set_config(_id, emane_config.model, config)
|
||||
if node.wlan_config:
|
||||
config = {k: v.value for k, v in node.wlan_config.items()}
|
||||
session.mobility.set_model_config(node.id, BasicRangeModel.name, config)
|
||||
if node.mobility_config:
|
||||
config = {k: v.value for k, v in node.mobility_config.items()}
|
||||
session.mobility.set_model_config(node.id, Ns2ScriptedMobility.name, config)
|
||||
if isinstance(core_node, WirelessNode) and node.wireless_config:
|
||||
config = {k: v.value for k, v in node.wireless_config.items()}
|
||||
core_node.set_config(config)
|
||||
for service_name, service_config in node.service_configs.items():
|
||||
data = service_config.data
|
||||
config = ServiceConfig(
|
||||
node_id=node.id,
|
||||
service=service_name,
|
||||
startup=data.startup,
|
||||
validate=data.validate,
|
||||
shutdown=data.shutdown,
|
||||
files=data.configs,
|
||||
directories=data.dirs,
|
||||
)
|
||||
service_configuration(session, config)
|
||||
for file_name, file_data in service_config.files.items():
|
||||
session.services.set_service_file(
|
||||
node.id, service_name, file_name, file_data
|
||||
)
|
||||
if node.config_service_configs:
|
||||
if not isinstance(core_node, CoreNode):
|
||||
context.abort(
|
||||
grpc.StatusCode.INVALID_ARGUMENT,
|
||||
"invalid node type with config service configs",
|
||||
)
|
||||
for service_name, service_config in node.config_service_configs.items():
|
||||
service = core_node.config_services[service_name]
|
||||
if service_config.config:
|
||||
service.set_config(service_config.config)
|
||||
for name, template in service_config.templates.items():
|
||||
service.set_template(name, template)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,7 @@
|
|||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
from typing import Any, Optional
|
||||
|
||||
from core.api.grpc import (
|
||||
common_pb2,
|
||||
|
@ -67,6 +67,8 @@ class NodeType(Enum):
|
|||
CONTROL_NET = 13
|
||||
DOCKER = 15
|
||||
LXC = 16
|
||||
WIRELESS = 17
|
||||
PODMAN = 18
|
||||
|
||||
|
||||
class LinkType(Enum):
|
||||
|
@ -113,13 +115,13 @@ class EventType:
|
|||
class ConfigService:
|
||||
group: str
|
||||
name: str
|
||||
executables: List[str]
|
||||
dependencies: List[str]
|
||||
directories: List[str]
|
||||
files: List[str]
|
||||
startup: List[str]
|
||||
validate: List[str]
|
||||
shutdown: List[str]
|
||||
executables: list[str]
|
||||
dependencies: list[str]
|
||||
directories: list[str]
|
||||
files: list[str]
|
||||
startup: list[str]
|
||||
validate: list[str]
|
||||
shutdown: list[str]
|
||||
validation_mode: ConfigServiceValidationMode
|
||||
validation_timer: int
|
||||
validation_period: float
|
||||
|
@ -146,8 +148,8 @@ class ConfigService:
|
|||
class ConfigServiceConfig:
|
||||
node_id: int
|
||||
name: str
|
||||
templates: Dict[str, str]
|
||||
config: Dict[str, str]
|
||||
templates: dict[str, str]
|
||||
config: dict[str, str]
|
||||
|
||||
@classmethod
|
||||
def from_proto(
|
||||
|
@ -163,26 +165,40 @@ class ConfigServiceConfig:
|
|||
|
||||
@dataclass
|
||||
class ConfigServiceData:
|
||||
templates: Dict[str, str] = field(default_factory=dict)
|
||||
config: Dict[str, str] = field(default_factory=dict)
|
||||
templates: dict[str, str] = field(default_factory=dict)
|
||||
config: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigServiceDefaults:
|
||||
templates: Dict[str, str]
|
||||
config: Dict[str, "ConfigOption"]
|
||||
modes: List[str]
|
||||
templates: dict[str, str]
|
||||
config: dict[str, "ConfigOption"]
|
||||
modes: dict[str, dict[str, str]]
|
||||
|
||||
@classmethod
|
||||
def from_proto(
|
||||
cls, proto: configservices_pb2.GetConfigServicesResponse
|
||||
cls, proto: configservices_pb2.GetConfigServiceDefaultsResponse
|
||||
) -> "ConfigServiceDefaults":
|
||||
config = ConfigOption.from_dict(proto.config)
|
||||
modes = {x.name: dict(x.config) for x in proto.modes}
|
||||
return ConfigServiceDefaults(
|
||||
templates=dict(proto.templates), config=config, modes=list(proto.modes)
|
||||
templates=dict(proto.templates), config=config, modes=modes
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Server:
|
||||
name: str
|
||||
host: str
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: core_pb2.Server) -> "Server":
|
||||
return Server(name=proto.name, host=proto.host)
|
||||
|
||||
def to_proto(self) -> core_pb2.Server:
|
||||
return core_pb2.Server(name=self.name, host=self.host)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Service:
|
||||
group: str
|
||||
|
@ -195,26 +211,26 @@ class Service:
|
|||
|
||||
@dataclass
|
||||
class ServiceDefault:
|
||||
node_type: str
|
||||
services: List[str]
|
||||
model: str
|
||||
services: list[str]
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: services_pb2.ServiceDefaults) -> "ServiceDefault":
|
||||
return ServiceDefault(node_type=proto.node_type, services=list(proto.services))
|
||||
return ServiceDefault(model=proto.model, services=list(proto.services))
|
||||
|
||||
|
||||
@dataclass
|
||||
class NodeServiceData:
|
||||
executables: List[str]
|
||||
dependencies: List[str]
|
||||
dirs: List[str]
|
||||
configs: List[str]
|
||||
startup: List[str]
|
||||
validate: List[str]
|
||||
validation_mode: ServiceValidationMode
|
||||
validation_timer: int
|
||||
shutdown: List[str]
|
||||
meta: str
|
||||
executables: list[str] = field(default_factory=list)
|
||||
dependencies: list[str] = field(default_factory=list)
|
||||
dirs: list[str] = field(default_factory=list)
|
||||
configs: list[str] = field(default_factory=list)
|
||||
startup: list[str] = field(default_factory=list)
|
||||
validate: list[str] = field(default_factory=list)
|
||||
validation_mode: ServiceValidationMode = ServiceValidationMode.NON_BLOCKING
|
||||
validation_timer: int = 5
|
||||
shutdown: list[str] = field(default_factory=list)
|
||||
meta: str = None
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: services_pb2.NodeServiceData) -> "NodeServiceData":
|
||||
|
@ -225,22 +241,53 @@ class NodeServiceData:
|
|||
configs=proto.configs,
|
||||
startup=proto.startup,
|
||||
validate=proto.validate,
|
||||
validation_mode=proto.validation_mode,
|
||||
validation_mode=ServiceValidationMode(proto.validation_mode),
|
||||
validation_timer=proto.validation_timer,
|
||||
shutdown=proto.shutdown,
|
||||
meta=proto.meta,
|
||||
)
|
||||
|
||||
def to_proto(self) -> services_pb2.NodeServiceData:
|
||||
return services_pb2.NodeServiceData(
|
||||
executables=self.executables,
|
||||
dependencies=self.dependencies,
|
||||
dirs=self.dirs,
|
||||
configs=self.configs,
|
||||
startup=self.startup,
|
||||
validate=self.validate,
|
||||
validation_mode=self.validation_mode.value,
|
||||
validation_timer=self.validation_timer,
|
||||
shutdown=self.shutdown,
|
||||
meta=self.meta,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NodeServiceConfig:
|
||||
node_id: int
|
||||
service: str
|
||||
data: NodeServiceData
|
||||
files: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: services_pb2.NodeServiceConfig) -> "NodeServiceConfig":
|
||||
return NodeServiceConfig(
|
||||
node_id=proto.node_id,
|
||||
service=proto.service,
|
||||
data=NodeServiceData.from_proto(proto.data),
|
||||
files=dict(proto.files),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceConfig:
|
||||
node_id: int
|
||||
service: str
|
||||
files: List[str] = None
|
||||
directories: List[str] = None
|
||||
startup: List[str] = None
|
||||
validate: List[str] = None
|
||||
shutdown: List[str] = None
|
||||
files: list[str] = None
|
||||
directories: list[str] = None
|
||||
startup: list[str] = None
|
||||
validate: list[str] = None
|
||||
shutdown: list[str] = None
|
||||
|
||||
def to_proto(self) -> services_pb2.ServiceConfig:
|
||||
return services_pb2.ServiceConfig(
|
||||
|
@ -254,6 +301,19 @@ class ServiceConfig:
|
|||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceFileConfig:
|
||||
node_id: int
|
||||
service: str
|
||||
file: str
|
||||
data: str = field(repr=False)
|
||||
|
||||
def to_proto(self) -> services_pb2.ServiceFileConfig:
|
||||
return services_pb2.ServiceFileConfig(
|
||||
node_id=self.node_id, service=self.service, file=self.file, data=self.data
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BridgeThroughput:
|
||||
node_id: int
|
||||
|
@ -280,8 +340,8 @@ class InterfaceThroughput:
|
|||
@dataclass
|
||||
class ThroughputsEvent:
|
||||
session_id: int
|
||||
bridge_throughputs: List[BridgeThroughput]
|
||||
iface_throughputs: List[InterfaceThroughput]
|
||||
bridge_throughputs: list[BridgeThroughput]
|
||||
iface_throughputs: list[InterfaceThroughput]
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: core_pb2.ThroughputsEvent) -> "ThroughputsEvent":
|
||||
|
@ -364,37 +424,49 @@ class ExceptionEvent:
|
|||
|
||||
@dataclass
|
||||
class ConfigOption:
|
||||
label: str
|
||||
name: str
|
||||
value: str
|
||||
type: ConfigOptionType
|
||||
group: str
|
||||
select: List[str] = None
|
||||
label: str = None
|
||||
type: ConfigOptionType = None
|
||||
group: str = None
|
||||
select: list[str] = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(
|
||||
cls, config: Dict[str, common_pb2.ConfigOption]
|
||||
) -> Dict[str, "ConfigOption"]:
|
||||
cls, config: dict[str, common_pb2.ConfigOption]
|
||||
) -> dict[str, "ConfigOption"]:
|
||||
d = {}
|
||||
for key, value in config.items():
|
||||
d[key] = ConfigOption.from_proto(value)
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def to_dict(cls, config: Dict[str, "ConfigOption"]) -> Dict[str, str]:
|
||||
def to_dict(cls, config: dict[str, "ConfigOption"]) -> dict[str, str]:
|
||||
return {k: v.value for k, v in config.items()}
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: common_pb2.ConfigOption) -> "ConfigOption":
|
||||
config_type = ConfigOptionType(proto.type) if proto.type is not None else None
|
||||
return ConfigOption(
|
||||
label=proto.label,
|
||||
name=proto.name,
|
||||
value=proto.value,
|
||||
type=ConfigOptionType(proto.type),
|
||||
type=config_type,
|
||||
group=proto.group,
|
||||
select=proto.select,
|
||||
)
|
||||
|
||||
def to_proto(self) -> common_pb2.ConfigOption:
|
||||
config_type = self.type.value if self.type is not None else None
|
||||
return common_pb2.ConfigOption(
|
||||
label=self.label,
|
||||
name=self.name,
|
||||
value=self.value,
|
||||
type=config_type,
|
||||
select=self.select,
|
||||
group=self.group,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Interface:
|
||||
|
@ -410,6 +482,8 @@ class Interface:
|
|||
mtu: int = None
|
||||
node_id: int = None
|
||||
net2_id: int = None
|
||||
nem_id: int = None
|
||||
nem_port: int = None
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: core_pb2.Interface) -> "Interface":
|
||||
|
@ -426,6 +500,8 @@ class Interface:
|
|||
mtu=proto.mtu,
|
||||
node_id=proto.node_id,
|
||||
net2_id=proto.net2_id,
|
||||
nem_id=proto.nem_id,
|
||||
nem_port=proto.nem_port,
|
||||
)
|
||||
|
||||
def to_proto(self) -> core_pb2.Interface:
|
||||
|
@ -567,6 +643,15 @@ class SessionSummary:
|
|||
dir=proto.dir,
|
||||
)
|
||||
|
||||
def to_proto(self) -> core_pb2.SessionSummary:
|
||||
return core_pb2.SessionSummary(
|
||||
id=self.id,
|
||||
state=self.state.value,
|
||||
nodes=self.nodes,
|
||||
file=self.file,
|
||||
dir=self.dir,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Hook:
|
||||
|
@ -587,7 +672,7 @@ class EmaneModelConfig:
|
|||
node_id: int
|
||||
model: str
|
||||
iface_id: int = -1
|
||||
config: Dict[str, ConfigOption] = None
|
||||
config: dict[str, ConfigOption] = None
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: emane_pb2.GetEmaneModelConfig) -> "EmaneModelConfig":
|
||||
|
@ -598,11 +683,12 @@ class EmaneModelConfig:
|
|||
)
|
||||
|
||||
def to_proto(self) -> emane_pb2.EmaneModelConfig:
|
||||
config = ConfigOption.to_dict(self.config)
|
||||
return emane_pb2.EmaneModelConfig(
|
||||
node_id=self.node_id,
|
||||
model=self.model,
|
||||
iface_id=self.iface_id,
|
||||
config=self.config,
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
|
@ -635,13 +721,13 @@ class Geo:
|
|||
|
||||
@dataclass
|
||||
class Node:
|
||||
id: int
|
||||
name: str
|
||||
type: NodeType
|
||||
id: int = None
|
||||
name: str = None
|
||||
type: NodeType = NodeType.DEFAULT
|
||||
model: str = None
|
||||
position: Position = None
|
||||
services: Set[str] = field(default_factory=set)
|
||||
config_services: Set[str] = field(default_factory=set)
|
||||
position: Position = Position(x=0, y=0)
|
||||
services: set[str] = field(default_factory=set)
|
||||
config_services: set[str] = field(default_factory=set)
|
||||
emane: str = None
|
||||
icon: str = None
|
||||
image: str = None
|
||||
|
@ -649,30 +735,49 @@ class Node:
|
|||
geo: Geo = None
|
||||
dir: str = None
|
||||
channel: str = None
|
||||
canvas: int = None
|
||||
|
||||
# configurations
|
||||
emane_model_configs: Dict[
|
||||
Tuple[str, Optional[int]], Dict[str, ConfigOption]
|
||||
emane_model_configs: dict[
|
||||
tuple[str, Optional[int]], dict[str, ConfigOption]
|
||||
] = field(default_factory=dict, repr=False)
|
||||
wlan_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False)
|
||||
mobility_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False)
|
||||
service_configs: Dict[str, NodeServiceData] = field(
|
||||
wlan_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False)
|
||||
wireless_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False)
|
||||
mobility_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False)
|
||||
service_configs: dict[str, NodeServiceData] = field(
|
||||
default_factory=dict, repr=False
|
||||
)
|
||||
service_file_configs: Dict[str, Dict[str, str]] = field(
|
||||
service_file_configs: dict[str, dict[str, str]] = field(
|
||||
default_factory=dict, repr=False
|
||||
)
|
||||
config_service_configs: Dict[str, ConfigServiceData] = field(
|
||||
config_service_configs: dict[str, ConfigServiceData] = field(
|
||||
default_factory=dict, repr=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: core_pb2.Node) -> "Node":
|
||||
service_configs = {}
|
||||
service_file_configs = {}
|
||||
for service, node_config in proto.service_configs.items():
|
||||
service_configs[service] = NodeServiceData.from_proto(node_config.data)
|
||||
service_file_configs[service] = dict(node_config.files)
|
||||
emane_configs = {}
|
||||
for emane_config in proto.emane_configs:
|
||||
iface_id = None if emane_config.iface_id == -1 else emane_config.iface_id
|
||||
model = emane_config.model
|
||||
key = (model, iface_id)
|
||||
emane_configs[key] = ConfigOption.from_dict(emane_config.config)
|
||||
config_service_configs = {}
|
||||
for service, service_config in proto.config_service_configs.items():
|
||||
config_service_configs[service] = ConfigServiceData(
|
||||
templates=dict(service_config.templates),
|
||||
config=dict(service_config.config),
|
||||
)
|
||||
return Node(
|
||||
id=proto.id,
|
||||
name=proto.name,
|
||||
type=NodeType(proto.type),
|
||||
model=proto.model,
|
||||
model=proto.model or None,
|
||||
position=Position.from_proto(proto.position),
|
||||
services=set(proto.services),
|
||||
config_services=set(proto.config_services),
|
||||
|
@ -683,9 +788,45 @@ class Node:
|
|||
geo=Geo.from_proto(proto.geo),
|
||||
dir=proto.dir,
|
||||
channel=proto.channel,
|
||||
canvas=proto.canvas,
|
||||
wlan_config=ConfigOption.from_dict(proto.wlan_config),
|
||||
mobility_config=ConfigOption.from_dict(proto.mobility_config),
|
||||
service_configs=service_configs,
|
||||
service_file_configs=service_file_configs,
|
||||
config_service_configs=config_service_configs,
|
||||
emane_model_configs=emane_configs,
|
||||
wireless_config=ConfigOption.from_dict(proto.wireless_config),
|
||||
)
|
||||
|
||||
def to_proto(self) -> core_pb2.Node:
|
||||
emane_configs = []
|
||||
for key, config in self.emane_model_configs.items():
|
||||
model, iface_id = key
|
||||
if iface_id is None:
|
||||
iface_id = -1
|
||||
config = {k: v.to_proto() for k, v in config.items()}
|
||||
emane_config = emane_pb2.NodeEmaneConfig(
|
||||
iface_id=iface_id, model=model, config=config
|
||||
)
|
||||
emane_configs.append(emane_config)
|
||||
service_configs = {}
|
||||
for service, service_data in self.service_configs.items():
|
||||
service_configs[service] = services_pb2.NodeServiceConfig(
|
||||
service=service, data=service_data.to_proto()
|
||||
)
|
||||
for service, file_configs in self.service_file_configs.items():
|
||||
service_config = service_configs.get(service)
|
||||
if service_config:
|
||||
service_config.files.update(file_configs)
|
||||
else:
|
||||
service_configs[service] = services_pb2.NodeServiceConfig(
|
||||
service=service, files=file_configs
|
||||
)
|
||||
config_service_configs = {}
|
||||
for service, service_config in self.config_service_configs.items():
|
||||
config_service_configs[service] = configservices_pb2.ConfigServiceConfig(
|
||||
templates=service_config.templates, config=service_config.config
|
||||
)
|
||||
return core_pb2.Node(
|
||||
id=self.id,
|
||||
name=self.name,
|
||||
|
@ -700,60 +841,62 @@ class Node:
|
|||
server=self.server,
|
||||
dir=self.dir,
|
||||
channel=self.channel,
|
||||
canvas=self.canvas,
|
||||
wlan_config={k: v.to_proto() for k, v in self.wlan_config.items()},
|
||||
mobility_config={k: v.to_proto() for k, v in self.mobility_config.items()},
|
||||
service_configs=service_configs,
|
||||
config_service_configs=config_service_configs,
|
||||
emane_configs=emane_configs,
|
||||
wireless_config={k: v.to_proto() for k, v in self.wireless_config.items()},
|
||||
)
|
||||
|
||||
def set_wlan(self, config: dict[str, str]) -> None:
|
||||
for key, value in config.items():
|
||||
option = ConfigOption(name=key, value=value)
|
||||
self.wlan_config[key] = option
|
||||
|
||||
def set_mobility(self, config: dict[str, str]) -> None:
|
||||
for key, value in config.items():
|
||||
option = ConfigOption(name=key, value=value)
|
||||
self.mobility_config[key] = option
|
||||
|
||||
def set_emane_model(
|
||||
self, model: str, config: dict[str, str], iface_id: int = None
|
||||
) -> None:
|
||||
key = (model, iface_id)
|
||||
config_options = self.emane_model_configs.setdefault(key, {})
|
||||
for key, value in config.items():
|
||||
option = ConfigOption(name=key, value=value)
|
||||
config_options[key] = option
|
||||
|
||||
|
||||
@dataclass
|
||||
class Session:
|
||||
id: int
|
||||
state: SessionState
|
||||
nodes: Dict[int, Node]
|
||||
links: List[Link]
|
||||
dir: str
|
||||
user: str
|
||||
default_services: Dict[str, Set[str]]
|
||||
location: SessionLocation
|
||||
hooks: Dict[str, Hook]
|
||||
emane_models: List[str]
|
||||
emane_config: Dict[str, ConfigOption]
|
||||
metadata: Dict[str, str]
|
||||
file: Path
|
||||
id: int = None
|
||||
state: SessionState = SessionState.DEFINITION
|
||||
nodes: dict[int, Node] = field(default_factory=dict)
|
||||
links: list[Link] = field(default_factory=list)
|
||||
dir: str = None
|
||||
user: str = None
|
||||
default_services: dict[str, set[str]] = field(default_factory=dict)
|
||||
location: SessionLocation = SessionLocation(
|
||||
x=0.0, y=0.0, z=0.0, lat=47.57917, lon=-122.13232, alt=2.0, scale=150.0
|
||||
)
|
||||
hooks: dict[str, Hook] = field(default_factory=dict)
|
||||
metadata: dict[str, str] = field(default_factory=dict)
|
||||
file: Path = None
|
||||
options: dict[str, ConfigOption] = field(default_factory=dict)
|
||||
servers: list[Server] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: core_pb2.Session) -> "Session":
|
||||
nodes: Dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes}
|
||||
nodes: dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes}
|
||||
links = [Link.from_proto(x) for x in proto.links]
|
||||
default_services = {
|
||||
x.node_type: set(x.services) for x in proto.default_services
|
||||
}
|
||||
default_services = {x.model: set(x.services) for x in proto.default_services}
|
||||
hooks = {x.file: Hook.from_proto(x) for x in proto.hooks}
|
||||
# update nodes with their current configurations
|
||||
for model in proto.emane_model_configs:
|
||||
iface_id = None
|
||||
if model.iface_id != -1:
|
||||
iface_id = model.iface_id
|
||||
node = nodes[model.node_id]
|
||||
key = (model.model, iface_id)
|
||||
node.emane_model_configs[key] = ConfigOption.from_dict(model.config)
|
||||
for node_id, mapped_config in proto.wlan_configs.items():
|
||||
node = nodes[node_id]
|
||||
node.wlan_config = ConfigOption.from_dict(mapped_config.config)
|
||||
for config in proto.service_configs:
|
||||
service = config.service
|
||||
node = nodes[config.node_id]
|
||||
node.service_configs[service] = NodeServiceData.from_proto(config.data)
|
||||
for file, data in config.files.items():
|
||||
files = node.service_file_configs.setdefault(service, {})
|
||||
files[file] = data
|
||||
for config in proto.config_service_configs:
|
||||
node = nodes[config.node_id]
|
||||
node.config_service_configs[config.name] = ConfigServiceData(
|
||||
templates=dict(config.templates), config=dict(config.config)
|
||||
)
|
||||
for node_id, mapped_config in proto.mobility_configs.items():
|
||||
node = nodes[node_id]
|
||||
node.mobility_config = ConfigOption.from_dict(mapped_config.config)
|
||||
file_path = Path(proto.file) if proto.file else None
|
||||
options = ConfigOption.from_dict(proto.options)
|
||||
servers = [Server.from_proto(x) for x in proto.servers]
|
||||
return Session(
|
||||
id=proto.id,
|
||||
state=SessionState(proto.state),
|
||||
|
@ -764,10 +907,107 @@ class Session:
|
|||
default_services=default_services,
|
||||
location=SessionLocation.from_proto(proto.location),
|
||||
hooks=hooks,
|
||||
emane_models=list(proto.emane_models),
|
||||
emane_config=ConfigOption.from_dict(proto.emane_config),
|
||||
metadata=dict(proto.metadata),
|
||||
file=file_path,
|
||||
options=options,
|
||||
servers=servers,
|
||||
)
|
||||
|
||||
def to_proto(self) -> core_pb2.Session:
|
||||
nodes = [x.to_proto() for x in self.nodes.values()]
|
||||
links = [x.to_proto() for x in self.links]
|
||||
hooks = [x.to_proto() for x in self.hooks.values()]
|
||||
options = {k: v.to_proto() for k, v in self.options.items()}
|
||||
servers = [x.to_proto() for x in self.servers]
|
||||
default_services = []
|
||||
for model, services in self.default_services.items():
|
||||
default_service = services_pb2.ServiceDefaults(
|
||||
model=model, services=services
|
||||
)
|
||||
default_services.append(default_service)
|
||||
file = str(self.file) if self.file else None
|
||||
return core_pb2.Session(
|
||||
id=self.id,
|
||||
state=self.state.value,
|
||||
nodes=nodes,
|
||||
links=links,
|
||||
dir=self.dir,
|
||||
user=self.user,
|
||||
default_services=default_services,
|
||||
location=self.location.to_proto(),
|
||||
hooks=hooks,
|
||||
metadata=self.metadata,
|
||||
file=file,
|
||||
options=options,
|
||||
servers=servers,
|
||||
)
|
||||
|
||||
def add_node(
|
||||
self,
|
||||
_id: int,
|
||||
*,
|
||||
name: str = None,
|
||||
_type: NodeType = NodeType.DEFAULT,
|
||||
model: str = "PC",
|
||||
position: Position = None,
|
||||
geo: Geo = None,
|
||||
emane: str = None,
|
||||
image: str = None,
|
||||
server: str = None,
|
||||
) -> Node:
|
||||
node = Node(
|
||||
id=_id,
|
||||
name=name,
|
||||
type=_type,
|
||||
model=model,
|
||||
position=position,
|
||||
geo=geo,
|
||||
emane=emane,
|
||||
image=image,
|
||||
server=server,
|
||||
)
|
||||
self.nodes[node.id] = node
|
||||
return node
|
||||
|
||||
def add_link(
|
||||
self,
|
||||
*,
|
||||
node1: Node,
|
||||
node2: Node,
|
||||
iface1: Interface = None,
|
||||
iface2: Interface = None,
|
||||
options: LinkOptions = None,
|
||||
) -> Link:
|
||||
link = Link(
|
||||
node1_id=node1.id,
|
||||
node2_id=node2.id,
|
||||
iface1=iface1,
|
||||
iface2=iface2,
|
||||
options=options,
|
||||
)
|
||||
self.links.append(link)
|
||||
return link
|
||||
|
||||
def set_options(self, config: dict[str, str]) -> None:
|
||||
for key, value in config.items():
|
||||
option = ConfigOption(name=key, value=value)
|
||||
self.options[key] = option
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoreConfig:
|
||||
services: list[Service] = field(default_factory=list)
|
||||
config_services: list[ConfigService] = field(default_factory=list)
|
||||
emane_models: list[str] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, proto: core_pb2.GetConfigResponse) -> "CoreConfig":
|
||||
services = [Service.from_proto(x) for x in proto.services]
|
||||
config_services = [ConfigService.from_proto(x) for x in proto.config_services]
|
||||
return CoreConfig(
|
||||
services=services,
|
||||
config_services=config_services,
|
||||
emane_models=list(proto.emane_models),
|
||||
)
|
||||
|
||||
|
||||
|
@ -849,7 +1089,7 @@ class ConfigEvent:
|
|||
node_id: int
|
||||
object: str
|
||||
type: int
|
||||
data_types: List[int]
|
||||
data_types: list[int]
|
||||
data_values: str
|
||||
captions: str
|
||||
bitmap: str
|
||||
|
@ -869,7 +1109,6 @@ class ConfigEvent:
|
|||
data_types=list(proto.data_types),
|
||||
data_values=proto.data_values,
|
||||
captions=proto.captions,
|
||||
bitmap=proto.bitmap,
|
||||
possible_values=proto.possible_values,
|
||||
groups=proto.groups,
|
||||
iface_id=proto.iface_id,
|
||||
|
@ -961,13 +1200,13 @@ class EmanePathlossesRequest:
|
|||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class MoveNodesRequest:
|
||||
session_id: int
|
||||
node_id: int
|
||||
source: str = None
|
||||
position: Position = None
|
||||
geo: Geo = None
|
||||
source: str = field(compare=False, default=None)
|
||||
position: Position = field(compare=False, default=None)
|
||||
geo: Geo = field(compare=False, default=None)
|
||||
|
||||
def to_proto(self) -> core_pb2.MoveNodesRequest:
|
||||
position = self.position.to_proto() if self.position else None
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,60 +0,0 @@
|
|||
"""
|
||||
Defines core server for handling TCP connections.
|
||||
"""
|
||||
|
||||
import socketserver
|
||||
|
||||
from core.emulator.coreemu import CoreEmu
|
||||
|
||||
|
||||
class CoreServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
"""
|
||||
TCP server class, manages sessions and spawns request handlers for
|
||||
incoming connections.
|
||||
"""
|
||||
|
||||
daemon_threads = True
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address, handler_class, config=None):
|
||||
"""
|
||||
Server class initialization takes configuration data and calls
|
||||
the socketserver constructor.
|
||||
|
||||
:param tuple[str, int] server_address: server host and port to use
|
||||
:param class handler_class: request handler
|
||||
:param dict config: configuration setting
|
||||
"""
|
||||
self.coreemu = CoreEmu(config)
|
||||
self.config = config
|
||||
socketserver.TCPServer.__init__(self, server_address, handler_class)
|
||||
|
||||
|
||||
class CoreUdpServer(socketserver.ThreadingMixIn, socketserver.UDPServer):
|
||||
"""
|
||||
UDP server class, manages sessions and spawns request handlers for
|
||||
incoming connections.
|
||||
"""
|
||||
|
||||
daemon_threads = True
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address, handler_class, mainserver):
|
||||
"""
|
||||
Server class initialization takes configuration data and calls
|
||||
the SocketServer constructor
|
||||
|
||||
:param server_address:
|
||||
:param class handler_class: request handler
|
||||
:param mainserver:
|
||||
"""
|
||||
self.mainserver = mainserver
|
||||
socketserver.UDPServer.__init__(self, server_address, handler_class)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Thread target to run concurrently with the TCP server.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.serve_forever()
|
|
@ -1,176 +0,0 @@
|
|||
"""
|
||||
Converts CORE data objects into legacy API messages.
|
||||
"""
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, List
|
||||
|
||||
from core.api.tlv import coreapi, structutils
|
||||
from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs
|
||||
from core.config import ConfigGroup, ConfigurableOptions
|
||||
from core.emulator.data import ConfigData, NodeData
|
||||
|
||||
|
||||
def convert_node(node_data: NodeData):
|
||||
"""
|
||||
Convenience method for converting NodeData to a packed TLV message.
|
||||
|
||||
:param core.emulator.data.NodeData node_data: node data to convert
|
||||
:return: packed node message
|
||||
"""
|
||||
node = node_data.node
|
||||
services = None
|
||||
if node.services is not None:
|
||||
services = "|".join([x.name for x in node.services])
|
||||
server = None
|
||||
if node.server is not None:
|
||||
server = node.server.name
|
||||
tlv_data = structutils.pack_values(
|
||||
coreapi.CoreNodeTlv,
|
||||
[
|
||||
(NodeTlvs.NUMBER, node.id),
|
||||
(NodeTlvs.TYPE, node.apitype.value),
|
||||
(NodeTlvs.NAME, node.name),
|
||||
(NodeTlvs.MODEL, node.type),
|
||||
(NodeTlvs.EMULATION_SERVER, server),
|
||||
(NodeTlvs.X_POSITION, int(node.position.x)),
|
||||
(NodeTlvs.Y_POSITION, int(node.position.y)),
|
||||
(NodeTlvs.CANVAS, node.canvas),
|
||||
(NodeTlvs.SERVICES, services),
|
||||
(NodeTlvs.LATITUDE, str(node.position.lat)),
|
||||
(NodeTlvs.LONGITUDE, str(node.position.lon)),
|
||||
(NodeTlvs.ALTITUDE, str(node.position.alt)),
|
||||
(NodeTlvs.ICON, node.icon),
|
||||
],
|
||||
)
|
||||
return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data)
|
||||
|
||||
|
||||
def convert_config(config_data):
|
||||
"""
|
||||
Convenience method for converting ConfigData to a packed TLV message.
|
||||
|
||||
:param core.emulator.data.ConfigData config_data: config data to convert
|
||||
:return: packed message
|
||||
"""
|
||||
session = None
|
||||
if config_data.session is not None:
|
||||
session = str(config_data.session)
|
||||
tlv_data = structutils.pack_values(
|
||||
coreapi.CoreConfigTlv,
|
||||
[
|
||||
(ConfigTlvs.NODE, config_data.node),
|
||||
(ConfigTlvs.OBJECT, config_data.object),
|
||||
(ConfigTlvs.TYPE, config_data.type),
|
||||
(ConfigTlvs.DATA_TYPES, config_data.data_types),
|
||||
(ConfigTlvs.VALUES, config_data.data_values),
|
||||
(ConfigTlvs.CAPTIONS, config_data.captions),
|
||||
(ConfigTlvs.BITMAP, config_data.bitmap),
|
||||
(ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values),
|
||||
(ConfigTlvs.GROUPS, config_data.groups),
|
||||
(ConfigTlvs.SESSION, session),
|
||||
(ConfigTlvs.IFACE_ID, config_data.iface_id),
|
||||
(ConfigTlvs.NETWORK_ID, config_data.network_id),
|
||||
(ConfigTlvs.OPAQUE, config_data.opaque),
|
||||
],
|
||||
)
|
||||
return coreapi.CoreConfMessage.pack(config_data.message_type, tlv_data)
|
||||
|
||||
|
||||
class ConfigShim:
|
||||
"""
|
||||
Provides helper methods for converting newer configuration values into TLV
|
||||
compatible formats.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def str_to_dict(cls, key_values: str) -> Dict[str, str]:
|
||||
"""
|
||||
Converts a TLV key/value string into an ordered mapping.
|
||||
|
||||
:param key_values:
|
||||
:return: ordered mapping of key/value pairs
|
||||
"""
|
||||
key_values = key_values.split("|")
|
||||
values = OrderedDict()
|
||||
for key_value in key_values:
|
||||
key, value = key_value.split("=", 1)
|
||||
values[key] = value
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def groups_to_str(cls, config_groups: List[ConfigGroup]) -> str:
|
||||
"""
|
||||
Converts configuration groups to a TLV formatted string.
|
||||
|
||||
:param config_groups: configuration groups to format
|
||||
:return: TLV configuration group string
|
||||
"""
|
||||
group_strings = []
|
||||
for config_group in config_groups:
|
||||
group_string = (
|
||||
f"{config_group.name}:{config_group.start}-{config_group.stop}"
|
||||
)
|
||||
group_strings.append(group_string)
|
||||
return "|".join(group_strings)
|
||||
|
||||
@classmethod
|
||||
def config_data(
|
||||
cls,
|
||||
flags: int,
|
||||
node_id: int,
|
||||
type_flags: int,
|
||||
configurable_options: ConfigurableOptions,
|
||||
config: Dict[str, str],
|
||||
) -> ConfigData:
|
||||
"""
|
||||
Convert this class to a Config API message. Some TLVs are defined
|
||||
by the class, but node number, conf type flags, and values must
|
||||
be passed in.
|
||||
|
||||
:param flags: message flags
|
||||
:param node_id: node id
|
||||
:param type_flags: type flags
|
||||
:param configurable_options: options to create config data for
|
||||
:param config: configuration values for options
|
||||
:return: configuration data object
|
||||
"""
|
||||
key_values = None
|
||||
captions = None
|
||||
data_types = []
|
||||
possible_values = []
|
||||
logging.debug("configurable: %s", configurable_options)
|
||||
logging.debug("configuration options: %s", configurable_options.configurations)
|
||||
logging.debug("configuration data: %s", config)
|
||||
for configuration in configurable_options.configurations():
|
||||
if not captions:
|
||||
captions = configuration.label
|
||||
else:
|
||||
captions += f"|{configuration.label}"
|
||||
|
||||
data_types.append(configuration.type.value)
|
||||
|
||||
options = ",".join(configuration.options)
|
||||
possible_values.append(options)
|
||||
|
||||
_id = configuration.id
|
||||
config_value = config.get(_id, configuration.default)
|
||||
key_value = f"{_id}={config_value}"
|
||||
if not key_values:
|
||||
key_values = key_value
|
||||
else:
|
||||
key_values += f"|{key_value}"
|
||||
|
||||
groups_str = cls.groups_to_str(configurable_options.config_groups())
|
||||
return ConfigData(
|
||||
message_type=flags,
|
||||
node=node_id,
|
||||
object=configurable_options.name,
|
||||
type=type_flags,
|
||||
data_types=tuple(data_types),
|
||||
data_values=key_values,
|
||||
captions=captions,
|
||||
possible_values="|".join(possible_values),
|
||||
bitmap=configurable_options.bitmap,
|
||||
groups=groups_str,
|
||||
)
|
|
@ -1,212 +0,0 @@
|
|||
"""
|
||||
Enumerations specific to the CORE TLV API.
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
CORE_API_PORT = 4038
|
||||
|
||||
|
||||
class MessageTypes(Enum):
|
||||
"""
|
||||
CORE message types.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
LINK = 0x02
|
||||
EXECUTE = 0x03
|
||||
REGISTER = 0x04
|
||||
CONFIG = 0x05
|
||||
FILE = 0x06
|
||||
INTERFACE = 0x07
|
||||
EVENT = 0x08
|
||||
SESSION = 0x09
|
||||
EXCEPTION = 0x0A
|
||||
|
||||
|
||||
class NodeTlvs(Enum):
|
||||
"""
|
||||
Node type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NUMBER = 0x01
|
||||
TYPE = 0x02
|
||||
NAME = 0x03
|
||||
IP_ADDRESS = 0x04
|
||||
MAC_ADDRESS = 0x05
|
||||
IP6_ADDRESS = 0x06
|
||||
MODEL = 0x07
|
||||
EMULATION_SERVER = 0x08
|
||||
SESSION = 0x0A
|
||||
X_POSITION = 0x20
|
||||
Y_POSITION = 0x21
|
||||
CANVAS = 0x22
|
||||
EMULATION_ID = 0x23
|
||||
NETWORK_ID = 0x24
|
||||
SERVICES = 0x25
|
||||
LATITUDE = 0x30
|
||||
LONGITUDE = 0x31
|
||||
ALTITUDE = 0x32
|
||||
ICON = 0x42
|
||||
OPAQUE = 0x50
|
||||
|
||||
|
||||
class LinkTlvs(Enum):
|
||||
"""
|
||||
Link type, length, value enumerations.
|
||||
"""
|
||||
|
||||
N1_NUMBER = 0x01
|
||||
N2_NUMBER = 0x02
|
||||
DELAY = 0x03
|
||||
BANDWIDTH = 0x04
|
||||
LOSS = 0x05
|
||||
DUP = 0x06
|
||||
JITTER = 0x07
|
||||
MER = 0x08
|
||||
BURST = 0x09
|
||||
SESSION = 0x0A
|
||||
MBURST = 0x10
|
||||
TYPE = 0x20
|
||||
GUI_ATTRIBUTES = 0x21
|
||||
UNIDIRECTIONAL = 0x22
|
||||
EMULATION_ID = 0x23
|
||||
NETWORK_ID = 0x24
|
||||
KEY = 0x25
|
||||
IFACE1_NUMBER = 0x30
|
||||
IFACE1_IP4 = 0x31
|
||||
IFACE1_IP4_MASK = 0x32
|
||||
IFACE1_MAC = 0x33
|
||||
IFACE1_IP6 = 0x34
|
||||
IFACE1_IP6_MASK = 0x35
|
||||
IFACE2_NUMBER = 0x36
|
||||
IFACE2_IP4 = 0x37
|
||||
IFACE2_IP4_MASK = 0x38
|
||||
IFACE2_MAC = 0x39
|
||||
IFACE2_IP6 = 0x40
|
||||
IFACE2_IP6_MASK = 0x41
|
||||
IFACE1_NAME = 0x42
|
||||
IFACE2_NAME = 0x43
|
||||
OPAQUE = 0x50
|
||||
|
||||
|
||||
class ExecuteTlvs(Enum):
|
||||
"""
|
||||
Execute type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
NUMBER = 0x02
|
||||
TIME = 0x03
|
||||
COMMAND = 0x04
|
||||
RESULT = 0x05
|
||||
STATUS = 0x06
|
||||
SESSION = 0x0A
|
||||
|
||||
|
||||
class ConfigTlvs(Enum):
|
||||
"""
|
||||
Configuration type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
OBJECT = 0x02
|
||||
TYPE = 0x03
|
||||
DATA_TYPES = 0x04
|
||||
VALUES = 0x05
|
||||
CAPTIONS = 0x06
|
||||
BITMAP = 0x07
|
||||
POSSIBLE_VALUES = 0x08
|
||||
GROUPS = 0x09
|
||||
SESSION = 0x0A
|
||||
IFACE_ID = 0x0B
|
||||
NETWORK_ID = 0x24
|
||||
OPAQUE = 0x50
|
||||
|
||||
|
||||
class ConfigFlags(Enum):
|
||||
"""
|
||||
Configuration flags.
|
||||
"""
|
||||
|
||||
NONE = 0x00
|
||||
REQUEST = 0x01
|
||||
UPDATE = 0x02
|
||||
RESET = 0x03
|
||||
|
||||
|
||||
class FileTlvs(Enum):
|
||||
"""
|
||||
File type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
NAME = 0x02
|
||||
MODE = 0x03
|
||||
NUMBER = 0x04
|
||||
TYPE = 0x05
|
||||
SOURCE_NAME = 0x06
|
||||
SESSION = 0x0A
|
||||
DATA = 0x10
|
||||
COMPRESSED_DATA = 0x11
|
||||
|
||||
|
||||
class InterfaceTlvs(Enum):
|
||||
"""
|
||||
Interface type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
NUMBER = 0x02
|
||||
NAME = 0x03
|
||||
IP_ADDRESS = 0x04
|
||||
MASK = 0x05
|
||||
MAC_ADDRESS = 0x06
|
||||
IP6_ADDRESS = 0x07
|
||||
IP6_MASK = 0x08
|
||||
TYPE = 0x09
|
||||
SESSION = 0x0A
|
||||
STATE = 0x0B
|
||||
EMULATION_ID = 0x23
|
||||
NETWORK_ID = 0x24
|
||||
|
||||
|
||||
class EventTlvs(Enum):
|
||||
"""
|
||||
Event type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
TYPE = 0x02
|
||||
NAME = 0x03
|
||||
DATA = 0x04
|
||||
TIME = 0x05
|
||||
SESSION = 0x0A
|
||||
|
||||
|
||||
class SessionTlvs(Enum):
|
||||
"""
|
||||
Session type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NUMBER = 0x01
|
||||
NAME = 0x02
|
||||
FILE = 0x03
|
||||
NODE_COUNT = 0x04
|
||||
DATE = 0x05
|
||||
THUMB = 0x06
|
||||
USER = 0x07
|
||||
OPAQUE = 0x0A
|
||||
|
||||
|
||||
class ExceptionTlvs(Enum):
|
||||
"""
|
||||
Exception type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
SESSION = 0x02
|
||||
LEVEL = 0x03
|
||||
SOURCE = 0x04
|
||||
DATE = 0x05
|
||||
TEXT = 0x06
|
||||
OPAQUE = 0x0A
|
|
@ -1,43 +0,0 @@
|
|||
"""
|
||||
Utilities for working with python struct data.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
def pack_values(clazz, packers):
|
||||
"""
|
||||
Pack values for a given legacy class.
|
||||
|
||||
:param class clazz: class that will provide a pack method
|
||||
:param list packers: a list of tuples that are used to pack values and transform them
|
||||
:return: packed data string of all values
|
||||
"""
|
||||
|
||||
# iterate through tuples of values to pack
|
||||
logging.debug("packing: %s", packers)
|
||||
data = b""
|
||||
for packer in packers:
|
||||
# check if a transformer was provided for valid values
|
||||
transformer = None
|
||||
if len(packer) == 2:
|
||||
tlv_type, value = packer
|
||||
elif len(packer) == 3:
|
||||
tlv_type, value, transformer = packer
|
||||
else:
|
||||
raise RuntimeError("packer had more than 3 arguments")
|
||||
|
||||
# only pack actual values and avoid packing empty strings
|
||||
# protobuf defaults to empty strings and does no imply a value to set
|
||||
if value is None or (isinstance(value, str) and not value):
|
||||
continue
|
||||
|
||||
# transform values as needed
|
||||
if transformer:
|
||||
value = transformer(value)
|
||||
|
||||
# pack and add to existing data
|
||||
logging.debug("packing: %s - %s type(%s)", tlv_type, value, type(value))
|
||||
data += clazz.pack(tlv_type.value, value)
|
||||
|
||||
return data
|
|
@ -4,73 +4,112 @@ Common support for configurable CORE objects.
|
|||
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union
|
||||
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
from core.errors import CoreConfigError
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.location.mobility import WirelessModel
|
||||
|
||||
WirelessModelType = Type[WirelessModel]
|
||||
WirelessModelType = type[WirelessModel]
|
||||
|
||||
_BOOL_OPTIONS: set[str] = {"0", "1"}
|
||||
|
||||
|
||||
@dataclass
|
||||
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: str = name
|
||||
self.start: int = start
|
||||
self.stop: int = stop
|
||||
name: str
|
||||
start: int
|
||||
stop: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Configuration:
|
||||
"""
|
||||
Represents a configuration options.
|
||||
Represents a configuration option.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
_id: str,
|
||||
_type: ConfigDataTypes,
|
||||
label: str = None,
|
||||
default: str = "",
|
||||
options: List[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Creates a Configuration object.
|
||||
id: str
|
||||
type: ConfigDataTypes
|
||||
label: str = None
|
||||
default: str = ""
|
||||
options: list[str] = field(default_factory=list)
|
||||
group: str = "Configuration"
|
||||
|
||||
: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: str = _id
|
||||
self.type: ConfigDataTypes = _type
|
||||
self.default: str = default
|
||||
if not options:
|
||||
options = []
|
||||
self.options: List[str] = options
|
||||
if not label:
|
||||
label = _id
|
||||
self.label: str = label
|
||||
def __post_init__(self) -> None:
|
||||
self.label = self.label if self.label else self.id
|
||||
if self.type == ConfigDataTypes.BOOL:
|
||||
if self.default and self.default not in _BOOL_OPTIONS:
|
||||
raise CoreConfigError(
|
||||
f"{self.id} bool value must be one of: {_BOOL_OPTIONS}: "
|
||||
f"{self.default}"
|
||||
)
|
||||
elif self.type == ConfigDataTypes.FLOAT:
|
||||
if self.default:
|
||||
try:
|
||||
float(self.default)
|
||||
except ValueError:
|
||||
raise CoreConfigError(
|
||||
f"{self.id} is not a valid float: {self.default}"
|
||||
)
|
||||
elif self.type != ConfigDataTypes.STRING:
|
||||
if self.default:
|
||||
try:
|
||||
int(self.default)
|
||||
except ValueError:
|
||||
raise CoreConfigError(
|
||||
f"{self.id} is not a valid int: {self.default}"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.__class__.__name__}(id={self.id}, type={self.type}, "
|
||||
f"default={self.default}, options={self.options})"
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class ConfigBool(Configuration):
|
||||
"""
|
||||
Represents a boolean configuration option.
|
||||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.BOOL
|
||||
value: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigFloat(Configuration):
|
||||
"""
|
||||
Represents a float configuration option.
|
||||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.FLOAT
|
||||
value: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigInt(Configuration):
|
||||
"""
|
||||
Represents an integer configuration option.
|
||||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.INT32
|
||||
value: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigString(Configuration):
|
||||
"""
|
||||
Represents a string configuration option.
|
||||
"""
|
||||
|
||||
type: ConfigDataTypes = ConfigDataTypes.STRING
|
||||
value: str = ""
|
||||
|
||||
|
||||
class ConfigurableOptions:
|
||||
|
@ -79,11 +118,10 @@ class ConfigurableOptions:
|
|||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
bitmap: Optional[str] = None
|
||||
options: List[Configuration] = []
|
||||
options: list[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
def configurations(cls) -> list[Configuration]:
|
||||
"""
|
||||
Provides the configurations for this class.
|
||||
|
||||
|
@ -92,7 +130,7 @@ class ConfigurableOptions:
|
|||
return cls.options
|
||||
|
||||
@classmethod
|
||||
def config_groups(cls) -> List[ConfigGroup]:
|
||||
def config_groups(cls) -> list[ConfigGroup]:
|
||||
"""
|
||||
Defines how configurations are grouped.
|
||||
|
||||
|
@ -101,7 +139,7 @@ class ConfigurableOptions:
|
|||
return [ConfigGroup("Options", 1, len(cls.configurations()))]
|
||||
|
||||
@classmethod
|
||||
def default_values(cls) -> Dict[str, str]:
|
||||
def default_values(cls) -> dict[str, str]:
|
||||
"""
|
||||
Provides an ordered mapping of configuration keys to default values.
|
||||
|
||||
|
@ -127,7 +165,7 @@ class ConfigurableManager:
|
|||
"""
|
||||
self.node_configurations = {}
|
||||
|
||||
def nodes(self) -> List[int]:
|
||||
def nodes(self) -> list[int]:
|
||||
"""
|
||||
Retrieves the ids of all node configurations known by this manager.
|
||||
|
||||
|
@ -170,7 +208,7 @@ class ConfigurableManager:
|
|||
|
||||
def set_configs(
|
||||
self,
|
||||
config: Dict[str, str],
|
||||
config: dict[str, str],
|
||||
node_id: int = _default_node,
|
||||
config_type: str = _default_type,
|
||||
) -> None:
|
||||
|
@ -182,7 +220,7 @@ class ConfigurableManager:
|
|||
:param config_type: configuration type to store configuration for
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"setting config for node(%s) type(%s): %s", node_id, config_type, config
|
||||
)
|
||||
node_configs = self.node_configurations.setdefault(node_id, OrderedDict())
|
||||
|
@ -212,7 +250,7 @@ class ConfigurableManager:
|
|||
|
||||
def get_configs(
|
||||
self, node_id: int = _default_node, config_type: str = _default_type
|
||||
) -> Optional[Dict[str, str]]:
|
||||
) -> Optional[dict[str, str]]:
|
||||
"""
|
||||
Retrieve configurations for a node and configuration type.
|
||||
|
||||
|
@ -226,7 +264,7 @@ class ConfigurableManager:
|
|||
result = node_configs.get(config_type)
|
||||
return result
|
||||
|
||||
def get_all_configs(self, node_id: int = _default_node) -> Dict[str, Any]:
|
||||
def get_all_configs(self, node_id: int = _default_node) -> dict[str, Any]:
|
||||
"""
|
||||
Retrieve all current configuration types for a node.
|
||||
|
||||
|
@ -246,11 +284,11 @@ class ModelManager(ConfigurableManager):
|
|||
Creates a ModelManager object.
|
||||
"""
|
||||
super().__init__()
|
||||
self.models: Dict[str, Any] = {}
|
||||
self.node_models: Dict[int, str] = {}
|
||||
self.models: dict[str, Any] = {}
|
||||
self.node_models: dict[int, str] = {}
|
||||
|
||||
def set_model_config(
|
||||
self, node_id: int, model_name: str, config: Dict[str, str] = None
|
||||
self, node_id: int, model_name: str, config: dict[str, str] = None
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration data for a model.
|
||||
|
@ -279,7 +317,7 @@ class ModelManager(ConfigurableManager):
|
|||
# set configuration
|
||||
self.set_configs(model_config, node_id=node_id, config_type=model_name)
|
||||
|
||||
def get_model_config(self, node_id: int, model_name: str) -> Dict[str, str]:
|
||||
def get_model_config(self, node_id: int, model_name: str) -> dict[str, str]:
|
||||
"""
|
||||
Retrieve configuration data for a model.
|
||||
|
||||
|
@ -304,7 +342,7 @@ class ModelManager(ConfigurableManager):
|
|||
self,
|
||||
node: Union[WlanNode, EmaneNet],
|
||||
model_class: "WirelessModelType",
|
||||
config: Dict[str, str] = None,
|
||||
config: dict[str, str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set model and model configuration for node.
|
||||
|
@ -314,7 +352,7 @@ class ModelManager(ConfigurableManager):
|
|||
:param config: model configuration, None for default configuration
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"setting model(%s) for node(%s): %s", model_class.name, node.id, config
|
||||
)
|
||||
self.set_model_config(node.id, model_class.name, config)
|
||||
|
@ -323,7 +361,7 @@ class ModelManager(ConfigurableManager):
|
|||
|
||||
def get_models(
|
||||
self, node: Union[WlanNode, EmaneNet]
|
||||
) -> List[Tuple[Type, Dict[str, str]]]:
|
||||
) -> list[tuple[type, dict[str, str]]]:
|
||||
"""
|
||||
Return a list of model classes and values for a net if one has been
|
||||
configured. This is invoked when exporting a session to XML.
|
||||
|
@ -343,5 +381,5 @@ class ModelManager(ConfigurableManager):
|
|||
model_class = self.models[model_name]
|
||||
models.append((model_class, config))
|
||||
|
||||
logging.debug("models for node(%s): %s", node.id, models)
|
||||
logger.debug("models for node(%s): %s", node.id, models)
|
||||
return models
|
||||
|
|
|
@ -2,9 +2,10 @@ import abc
|
|||
import enum
|
||||
import inspect
|
||||
import logging
|
||||
import pathlib
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from mako import exceptions
|
||||
from mako.lookup import TemplateLookup
|
||||
|
@ -14,9 +15,24 @@ from core.config import Configuration
|
|||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
TEMPLATES_DIR: str = "templates"
|
||||
|
||||
|
||||
def get_template_path(file_path: Path) -> str:
|
||||
"""
|
||||
Utility to convert a given file path to a valid template path format.
|
||||
|
||||
:param file_path: file path to convert
|
||||
:return: template path
|
||||
"""
|
||||
if file_path.is_absolute():
|
||||
template_path = str(file_path.relative_to("/"))
|
||||
else:
|
||||
template_path = str(file_path)
|
||||
return template_path
|
||||
|
||||
|
||||
class ConfigServiceMode(enum.Enum):
|
||||
BLOCKING = 0
|
||||
NON_BLOCKING = 1
|
||||
|
@ -27,6 +43,18 @@ class ConfigServiceBootError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class ConfigServiceTemplateError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShadowDir:
|
||||
path: str
|
||||
src: Optional[str] = None
|
||||
templates: bool = False
|
||||
has_node_paths: bool = False
|
||||
|
||||
|
||||
class ConfigService(abc.ABC):
|
||||
"""
|
||||
Base class for creating configurable services.
|
||||
|
@ -38,6 +66,9 @@ class ConfigService(abc.ABC):
|
|||
# time to wait in seconds for determining if service started successfully
|
||||
validation_timer: int = 5
|
||||
|
||||
# directories to shadow and copy files from
|
||||
shadow_directories: list[ShadowDir] = []
|
||||
|
||||
def __init__(self, node: CoreNode) -> None:
|
||||
"""
|
||||
Create ConfigService instance.
|
||||
|
@ -46,11 +77,11 @@ class ConfigService(abc.ABC):
|
|||
"""
|
||||
self.node: CoreNode = node
|
||||
class_file = inspect.getfile(self.__class__)
|
||||
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
|
||||
templates_path = Path(class_file).parent.joinpath(TEMPLATES_DIR)
|
||||
self.templates: TemplateLookup = TemplateLookup(directories=templates_path)
|
||||
self.config: Dict[str, Configuration] = {}
|
||||
self.custom_templates: Dict[str, str] = {}
|
||||
self.custom_config: Dict[str, str] = {}
|
||||
self.config: dict[str, Configuration] = {}
|
||||
self.custom_templates: dict[str, str] = {}
|
||||
self.custom_config: dict[str, str] = {}
|
||||
configs = self.default_configs[:]
|
||||
self._define_config(configs)
|
||||
|
||||
|
@ -77,47 +108,47 @@ class ConfigService(abc.ABC):
|
|||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def directories(self) -> List[str]:
|
||||
def directories(self) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def files(self) -> List[str]:
|
||||
def files(self) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def default_configs(self) -> List[Configuration]:
|
||||
def default_configs(self) -> list[Configuration]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def modes(self) -> Dict[str, Dict[str, str]]:
|
||||
def modes(self) -> dict[str, dict[str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def executables(self) -> List[str]:
|
||||
def executables(self) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def dependencies(self) -> List[str]:
|
||||
def dependencies(self) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def startup(self) -> List[str]:
|
||||
def startup(self) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def validate(self) -> List[str]:
|
||||
def validate(self) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def shutdown(self) -> List[str]:
|
||||
def shutdown(self) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
|
@ -133,7 +164,8 @@ class ConfigService(abc.ABC):
|
|||
:return: nothing
|
||||
:raises ConfigServiceBootError: when there is an error starting service
|
||||
"""
|
||||
logging.info("node(%s) service(%s) starting...", self.node.name, self.name)
|
||||
logger.info("node(%s) service(%s) starting...", self.node.name, self.name)
|
||||
self.create_shadow_dirs()
|
||||
self.create_dirs()
|
||||
self.create_files()
|
||||
wait = self.validation_mode == ConfigServiceMode.BLOCKING
|
||||
|
@ -154,7 +186,7 @@ class ConfigService(abc.ABC):
|
|||
try:
|
||||
self.node.cmd(cmd)
|
||||
except CoreCommandError:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"failed shutdown: {cmd}"
|
||||
)
|
||||
|
@ -168,6 +200,64 @@ class ConfigService(abc.ABC):
|
|||
self.stop()
|
||||
self.start()
|
||||
|
||||
def create_shadow_dirs(self) -> None:
|
||||
"""
|
||||
Creates a shadow of a host system directory recursively
|
||||
to be mapped and live within a node.
|
||||
|
||||
:return: nothing
|
||||
:raises CoreError: when there is a failure creating a directory or file
|
||||
"""
|
||||
for shadow_dir in self.shadow_directories:
|
||||
# setup shadow and src paths, using node unique paths when configured
|
||||
shadow_path = Path(shadow_dir.path)
|
||||
if shadow_dir.src is None:
|
||||
src_path = shadow_path
|
||||
else:
|
||||
src_path = Path(shadow_dir.src)
|
||||
if shadow_dir.has_node_paths:
|
||||
src_path = src_path / self.node.name
|
||||
# validate shadow and src paths
|
||||
if not shadow_path.is_absolute():
|
||||
raise CoreError(f"shadow dir({shadow_path}) is not absolute")
|
||||
if not src_path.is_absolute():
|
||||
raise CoreError(f"shadow source dir({src_path}) is not absolute")
|
||||
if not src_path.is_dir():
|
||||
raise CoreError(f"shadow source dir({src_path}) does not exist")
|
||||
# create root of the shadow path within node
|
||||
logger.info(
|
||||
"node(%s) creating shadow directory(%s) src(%s) node paths(%s) "
|
||||
"templates(%s)",
|
||||
self.node.name,
|
||||
shadow_path,
|
||||
src_path,
|
||||
shadow_dir.has_node_paths,
|
||||
shadow_dir.templates,
|
||||
)
|
||||
self.node.create_dir(shadow_path)
|
||||
# find all directories and files to create
|
||||
dir_paths = []
|
||||
file_paths = []
|
||||
for path in src_path.rglob("*"):
|
||||
shadow_src_path = shadow_path / path.relative_to(src_path)
|
||||
if path.is_dir():
|
||||
dir_paths.append(shadow_src_path)
|
||||
else:
|
||||
file_paths.append((path, shadow_src_path))
|
||||
# create all directories within node
|
||||
for path in dir_paths:
|
||||
self.node.create_dir(path)
|
||||
# create all files within node, from templates when configured
|
||||
data = self.data()
|
||||
templates = TemplateLookup(directories=src_path)
|
||||
for path, dst_path in file_paths:
|
||||
if shadow_dir.templates:
|
||||
template = templates.get_template(path.name)
|
||||
rendered = self._render(template, data)
|
||||
self.node.create_file(dst_path, rendered)
|
||||
else:
|
||||
self.node.copy_file(path, dst_path)
|
||||
|
||||
def create_dirs(self) -> None:
|
||||
"""
|
||||
Creates directories for service.
|
||||
|
@ -175,16 +265,18 @@ class ConfigService(abc.ABC):
|
|||
:return: nothing
|
||||
:raises CoreError: when there is a failure creating a directory
|
||||
"""
|
||||
for directory in self.directories:
|
||||
logger.debug("creating config service directories")
|
||||
for directory in sorted(self.directories):
|
||||
dir_path = Path(directory)
|
||||
try:
|
||||
self.node.privatedir(directory)
|
||||
except (CoreCommandError, ValueError):
|
||||
self.node.create_dir(dir_path)
|
||||
except (CoreCommandError, CoreError):
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"failure to create service directory: {directory}"
|
||||
)
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
"""
|
||||
Returns key/value data, used when rendering file templates.
|
||||
|
||||
|
@ -211,7 +303,7 @@ class ConfigService(abc.ABC):
|
|||
"""
|
||||
raise CoreError(f"service({self.name}) unknown template({name})")
|
||||
|
||||
def get_templates(self) -> Dict[str, str]:
|
||||
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.
|
||||
|
@ -219,19 +311,53 @@ class ConfigService(abc.ABC):
|
|||
: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]
|
||||
for file in self.files:
|
||||
file_path = Path(file)
|
||||
template_path = get_template_path(file_path)
|
||||
if file in self.custom_templates:
|
||||
template = self.custom_templates[file]
|
||||
template = self.clean_text(template)
|
||||
elif self.templates.has_template(basename):
|
||||
template = self.templates.get_template(basename).source
|
||||
elif self.templates.has_template(template_path):
|
||||
template = self.templates.get_template(template_path).source
|
||||
else:
|
||||
template = self.get_text_template(name)
|
||||
try:
|
||||
template = self.get_text_template(file)
|
||||
except Exception as e:
|
||||
raise ConfigServiceTemplateError(
|
||||
f"node({self.node.name}) service({self.name}) file({file}) "
|
||||
f"failure getting template: {e}"
|
||||
)
|
||||
template = self.clean_text(template)
|
||||
templates[name] = template
|
||||
templates[file] = template
|
||||
return templates
|
||||
|
||||
def get_rendered_templates(self) -> dict[str, str]:
|
||||
templates = {}
|
||||
data = self.data()
|
||||
for file in sorted(self.files):
|
||||
rendered = self._get_rendered_template(file, data)
|
||||
templates[file] = rendered
|
||||
return templates
|
||||
|
||||
def _get_rendered_template(self, file: str, data: dict[str, Any]) -> str:
|
||||
file_path = Path(file)
|
||||
template_path = get_template_path(file_path)
|
||||
if file in self.custom_templates:
|
||||
text = self.custom_templates[file]
|
||||
rendered = self.render_text(text, data)
|
||||
elif self.templates.has_template(template_path):
|
||||
rendered = self.render_template(template_path, data)
|
||||
else:
|
||||
try:
|
||||
text = self.get_text_template(file)
|
||||
except Exception as e:
|
||||
raise ConfigServiceTemplateError(
|
||||
f"node({self.node.name}) service({self.name}) file({file}) "
|
||||
f"failure getting template: {e}"
|
||||
)
|
||||
rendered = self.render_text(text, data)
|
||||
return rendered
|
||||
|
||||
def create_files(self) -> None:
|
||||
"""
|
||||
Creates service files inside associated node.
|
||||
|
@ -239,24 +365,13 @@ class ConfigService(abc.ABC):
|
|||
: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,
|
||||
for file in sorted(self.files):
|
||||
logger.debug(
|
||||
"node(%s) service(%s) template(%s)", self.node.name, self.name, file
|
||||
)
|
||||
self.node.nodefile(name, rendered)
|
||||
rendered = self._get_rendered_template(file, data)
|
||||
file_path = Path(file)
|
||||
self.node.create_file(file_path, rendered)
|
||||
|
||||
def run_startup(self, wait: bool) -> None:
|
||||
"""
|
||||
|
@ -300,7 +415,7 @@ class ConfigService(abc.ABC):
|
|||
del cmds[index]
|
||||
index += 1
|
||||
except CoreCommandError:
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"validate command failed: {cmd}"
|
||||
)
|
||||
|
@ -311,7 +426,7 @@ class ConfigService(abc.ABC):
|
|||
f"node({self.node.name}) service({self.name}) failed to validate"
|
||||
)
|
||||
|
||||
def _render(self, template: Template, data: Dict[str, Any] = None) -> str:
|
||||
def _render(self, template: Template, data: dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders template providing all associated data to template.
|
||||
|
||||
|
@ -325,7 +440,7 @@ class ConfigService(abc.ABC):
|
|||
node=self.node, config=self.render_config(), **data
|
||||
)
|
||||
|
||||
def render_text(self, text: str, data: Dict[str, Any] = None) -> str:
|
||||
def render_text(self, text: str, data: dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders text based template providing all associated data to template.
|
||||
|
||||
|
@ -343,24 +458,24 @@ class ConfigService(abc.ABC):
|
|||
f"{exceptions.text_error_template().render_unicode()}"
|
||||
)
|
||||
|
||||
def render_template(self, basename: str, data: Dict[str, Any] = None) -> str:
|
||||
def render_template(self, template_path: 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 template_path: path of file to render
|
||||
:param data: service specific defined data for template
|
||||
:return: rendered template
|
||||
"""
|
||||
try:
|
||||
template = self.templates.get_template(basename)
|
||||
template = self.templates.get_template(template_path)
|
||||
return self._render(template, data)
|
||||
except Exception:
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"{exceptions.text_error_template().render_template()}"
|
||||
f"node({self.node.name}) service({self.name}) file({template_path})"
|
||||
f"{exceptions.text_error_template().render_unicode()}"
|
||||
)
|
||||
|
||||
def _define_config(self, configs: List[Configuration]) -> None:
|
||||
def _define_config(self, configs: list[Configuration]) -> None:
|
||||
"""
|
||||
Initializes default configuration data.
|
||||
|
||||
|
@ -370,7 +485,7 @@ class ConfigService(abc.ABC):
|
|||
for config in configs:
|
||||
self.config[config.id] = config
|
||||
|
||||
def render_config(self) -> Dict[str, str]:
|
||||
def render_config(self) -> dict[str, str]:
|
||||
"""
|
||||
Returns configuration data key/value pairs for rendering a template.
|
||||
|
||||
|
@ -381,7 +496,7 @@ class ConfigService(abc.ABC):
|
|||
else:
|
||||
return {k: v.default for k, v in self.config.items()}
|
||||
|
||||
def set_config(self, data: Dict[str, str]) -> None:
|
||||
def set_config(self, data: dict[str, str]) -> None:
|
||||
"""
|
||||
Set configuration data from key/value pairs.
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List, Set
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.configservice.base import ConfigService
|
||||
|
@ -10,16 +12,16 @@ class ConfigServiceDependencies:
|
|||
Generates sets of services to start in order of their dependencies.
|
||||
"""
|
||||
|
||||
def __init__(self, services: Dict[str, "ConfigService"]) -> None:
|
||||
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: Dict[str, Set[str]] = {}
|
||||
self.started: Set[str] = set()
|
||||
self.node_services: Dict[str, "ConfigService"] = {}
|
||||
self.dependents: dict[str, set[str]] = {}
|
||||
self.started: set[str] = set()
|
||||
self.node_services: dict[str, "ConfigService"] = {}
|
||||
for service in services.values():
|
||||
self.node_services[service.name] = service
|
||||
for dependency in service.dependencies:
|
||||
|
@ -27,11 +29,11 @@ class ConfigServiceDependencies:
|
|||
dependents.add(service.name)
|
||||
|
||||
# used to find paths
|
||||
self.path: List["ConfigService"] = []
|
||||
self.visited: Set[str] = set()
|
||||
self.visiting: Set[str] = set()
|
||||
self.path: list["ConfigService"] = []
|
||||
self.visited: set[str] = set()
|
||||
self.visiting: set[str] = set()
|
||||
|
||||
def startup_paths(self) -> List[List["ConfigService"]]:
|
||||
def startup_paths(self) -> list[list["ConfigService"]]:
|
||||
"""
|
||||
Find startup path sets based on service dependencies.
|
||||
|
||||
|
@ -41,7 +43,7 @@ class ConfigServiceDependencies:
|
|||
for name in self.node_services:
|
||||
service = self.node_services[name]
|
||||
if service.name in self.started:
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"skipping service that will already be started: %s", service.name
|
||||
)
|
||||
continue
|
||||
|
@ -52,8 +54,8 @@ class ConfigServiceDependencies:
|
|||
|
||||
if self.started != set(self.node_services):
|
||||
raise ValueError(
|
||||
"failure to start all services: %s != %s"
|
||||
% (self.started, self.node_services.keys())
|
||||
f"failure to start all services: {self.started} != "
|
||||
f"{self.node_services.keys()}"
|
||||
)
|
||||
|
||||
return paths
|
||||
|
@ -68,25 +70,25 @@ class ConfigServiceDependencies:
|
|||
self.visited.clear()
|
||||
self.visiting.clear()
|
||||
|
||||
def _start(self, service: "ConfigService") -> List["ConfigService"]:
|
||||
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)
|
||||
logger.debug("starting service dependency check: %s", service.name)
|
||||
self._reset()
|
||||
return self._visit(service)
|
||||
|
||||
def _visit(self, current_service: "ConfigService") -> List["ConfigService"]:
|
||||
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)
|
||||
logger.debug("visiting service(%s): %s", current_service.name, self.path)
|
||||
self.visited.add(current_service.name)
|
||||
self.visiting.add(current_service.name)
|
||||
|
||||
|
@ -94,14 +96,14 @@ class ConfigServiceDependencies:
|
|||
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
|
||||
"required dependency was not included in node "
|
||||
f"services: {service_name}"
|
||||
)
|
||||
|
||||
if service_name in self.visiting:
|
||||
raise ValueError(
|
||||
"cyclic dependency at service(%s): %s"
|
||||
% (current_service.name, service_name)
|
||||
f"cyclic dependency at service({current_service.name}): "
|
||||
f"{service_name}"
|
||||
)
|
||||
|
||||
if service_name not in self.visited:
|
||||
|
@ -109,7 +111,7 @@ class ConfigServiceDependencies:
|
|||
self._visit(service)
|
||||
|
||||
# add service when bottom is found
|
||||
logging.debug("adding service to startup path: %s", current_service.name)
|
||||
logger.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)
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import logging
|
||||
import pathlib
|
||||
from typing import Dict, List, Type
|
||||
import pkgutil
|
||||
from pathlib import Path
|
||||
|
||||
from core import utils
|
||||
from core import configservices, utils
|
||||
from core.configservice.base import ConfigService
|
||||
from core.errors import CoreError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigServiceManager:
|
||||
"""
|
||||
|
@ -16,9 +19,9 @@ class ConfigServiceManager:
|
|||
"""
|
||||
Create a ConfigServiceManager instance.
|
||||
"""
|
||||
self.services: Dict[str, Type[ConfigService]] = {}
|
||||
self.services: dict[str, type[ConfigService]] = {}
|
||||
|
||||
def get_service(self, name: str) -> Type[ConfigService]:
|
||||
def get_service(self, name: str) -> type[ConfigService]:
|
||||
"""
|
||||
Retrieve a service by name.
|
||||
|
||||
|
@ -28,10 +31,10 @@ class ConfigServiceManager:
|
|||
"""
|
||||
service_class = self.services.get(name)
|
||||
if service_class is None:
|
||||
raise CoreError(f"service does not exit {name}")
|
||||
raise CoreError(f"service does not exist {name}")
|
||||
return service_class
|
||||
|
||||
def add(self, service: Type[ConfigService]) -> None:
|
||||
def add(self, service: type[ConfigService]) -> None:
|
||||
"""
|
||||
Add service to manager, checking service requirements have been met.
|
||||
|
||||
|
@ -40,7 +43,7 @@ class ConfigServiceManager:
|
|||
:raises CoreError: when service is a duplicate or has unmet executables
|
||||
"""
|
||||
name = service.name
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"loading service: class(%s) name(%s)", service.__class__.__name__, name
|
||||
)
|
||||
|
||||
|
@ -55,27 +58,46 @@ class ConfigServiceManager:
|
|||
except CoreError as e:
|
||||
raise CoreError(f"config service({service.name}): {e}")
|
||||
|
||||
# make service available
|
||||
# make service available
|
||||
self.services[name] = service
|
||||
|
||||
def load(self, path: str) -> List[str]:
|
||||
def load_locals(self) -> list[str]:
|
||||
"""
|
||||
Search path provided for configurable services and add them for being managed.
|
||||
Search and add config service from local core module.
|
||||
|
||||
:return: list of errors when loading services
|
||||
"""
|
||||
errors = []
|
||||
for module_info in pkgutil.walk_packages(
|
||||
configservices.__path__, f"{configservices.__name__}."
|
||||
):
|
||||
services = utils.load_module(module_info.name, ConfigService)
|
||||
for service in services:
|
||||
try:
|
||||
self.add(service)
|
||||
except CoreError as e:
|
||||
errors.append(service.name)
|
||||
logger.debug("not loading config service(%s): %s", service.name, e)
|
||||
return errors
|
||||
|
||||
def load(self, path: Path) -> list[str]:
|
||||
"""
|
||||
Search path provided for config services and add them for being managed.
|
||||
|
||||
:param path: path to search configurable services
|
||||
:return: list errors when loading and adding services
|
||||
:return: list errors when loading 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)
|
||||
logger.debug("loading config services from: %s", subdir)
|
||||
services = utils.load_classes(subdir, ConfigService)
|
||||
for service in services:
|
||||
try:
|
||||
self.add(service)
|
||||
except CoreError as e:
|
||||
service_errors.append(service.name)
|
||||
logging.debug("not loading service(%s): %s", service.name, e)
|
||||
logger.debug("not loading service(%s): %s", service.name, e)
|
||||
return service_errors
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
import abc
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any
|
||||
|
||||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.nodes.base import CoreNodeBase
|
||||
from core.nodes.base import CoreNodeBase, NodeBase
|
||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
from core.nodes.network import PtpNet, WlanNode
|
||||
from core.nodes.physical import Rj45Node
|
||||
from core.nodes.wireless import WirelessNode
|
||||
|
||||
GROUP: str = "FRR"
|
||||
FRR_STATE_DIR: str = "/var/run/frr"
|
||||
|
||||
|
||||
def is_wireless(node: NodeBase) -> bool:
|
||||
"""
|
||||
Check if the node is a wireless type node.
|
||||
|
||||
:param node: node to check type for
|
||||
:return: True if wireless type, False otherwise
|
||||
"""
|
||||
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
|
||||
|
||||
|
||||
def has_mtu_mismatch(iface: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect MTU mismatch and add the appropriate FRR
|
||||
|
@ -53,32 +65,47 @@ def get_router_id(node: CoreNodeBase) -> str:
|
|||
return "0.0.0.0"
|
||||
|
||||
|
||||
def rj45_check(iface: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect whether interface is connected an external RJ45
|
||||
link.
|
||||
"""
|
||||
if iface.net:
|
||||
for peer_iface in iface.net.get_ifaces():
|
||||
if peer_iface == iface:
|
||||
continue
|
||||
if isinstance(peer_iface.node, Rj45Node):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class FRRZebra(ConfigService):
|
||||
name: str = "FRRzebra"
|
||||
group: str = GROUP
|
||||
directories: List[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
|
||||
files: List[str] = [
|
||||
directories: list[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
|
||||
files: list[str] = [
|
||||
"/usr/local/etc/frr/frr.conf",
|
||||
"frrboot.sh",
|
||||
"/usr/local/etc/frr/vtysh.conf",
|
||||
"/usr/local/etc/frr/daemons",
|
||||
]
|
||||
executables: List[str] = ["zebra"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash frrboot.sh zebra"]
|
||||
validate: List[str] = ["pidof zebra"]
|
||||
shutdown: List[str] = ["killall zebra"]
|
||||
executables: list[str] = ["zebra"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash frrboot.sh zebra"]
|
||||
validate: list[str] = ["pidof zebra"]
|
||||
shutdown: list[str] = ["killall zebra"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
frr_conf = self.files[0]
|
||||
frr_bin_search = self.node.session.options.get_config(
|
||||
frr_bin_search = self.node.session.options.get(
|
||||
"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"
|
||||
frr_sbin_search = self.node.session.options.get(
|
||||
"frr_sbin_search",
|
||||
default="/usr/local/sbin /usr/sbin /usr/lib/frr /usr/libexec/frr",
|
||||
).strip('"')
|
||||
|
||||
services = []
|
||||
|
@ -103,8 +130,7 @@ class FRRZebra(ConfigService):
|
|||
ip4s.append(str(ip4.ip))
|
||||
for ip6 in iface.ip6s:
|
||||
ip6s.append(str(ip6.ip))
|
||||
is_control = getattr(iface, "control", False)
|
||||
ifaces.append((iface, ip4s, ip6s, is_control))
|
||||
ifaces.append((iface, ip4s, ip6s, iface.control))
|
||||
|
||||
return dict(
|
||||
frr_conf=frr_conf,
|
||||
|
@ -120,16 +146,16 @@ class FRRZebra(ConfigService):
|
|||
|
||||
class FrrService(abc.ABC):
|
||||
group: str = GROUP
|
||||
directories: List[str] = []
|
||||
files: List[str] = []
|
||||
executables: List[str] = []
|
||||
dependencies: List[str] = ["FRRzebra"]
|
||||
startup: List[str] = []
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
directories: list[str] = []
|
||||
files: list[str] = []
|
||||
executables: list[str] = []
|
||||
dependencies: list[str] = ["FRRzebra"]
|
||||
startup: list[str] = []
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
ipv4_routing: bool = False
|
||||
ipv6_routing: bool = False
|
||||
|
||||
|
@ -150,8 +176,8 @@ class FRROspfv2(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRROSPFv2"
|
||||
shutdown: List[str] = ["killall ospfd"]
|
||||
validate: List[str] = ["pidof ospfd"]
|
||||
shutdown: list[str] = ["killall ospfd"]
|
||||
validate: list[str] = ["pidof ospfd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
|
@ -159,7 +185,7 @@ class FRROspfv2(FrrService, ConfigService):
|
|||
addresses = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
addresses.append(str(ip4.ip))
|
||||
addresses.append(str(ip4))
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
|
@ -167,15 +193,31 @@ class FRROspfv2(FrrService, ConfigService):
|
|||
% for addr in addresses:
|
||||
network ${addr} area 0
|
||||
% endfor
|
||||
ospf opaque-lsa
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
if has_mtu_mismatch(iface):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
has_mtu = has_mtu_mismatch(iface)
|
||||
has_rj45 = rj45_check(iface)
|
||||
is_ptp = isinstance(iface.net, PtpNet)
|
||||
data = dict(has_mtu=has_mtu, is_ptp=is_ptp, has_rj45=has_rj45)
|
||||
text = """
|
||||
% if has_mtu:
|
||||
ip ospf mtu-ignore
|
||||
% endif
|
||||
% if has_rj45:
|
||||
<% return STOP_RENDERING %>
|
||||
% endif
|
||||
% if is_ptp:
|
||||
ip ospf network point-to-point
|
||||
% endif
|
||||
ip ospf hello-interval 2
|
||||
ip ospf dead-interval 6
|
||||
ip ospf retransmit-interval 5
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
|
||||
class FRROspfv3(FrrService, ConfigService):
|
||||
|
@ -186,8 +228,8 @@ class FRROspfv3(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRROSPFv3"
|
||||
shutdown: List[str] = ["killall ospf6d"]
|
||||
validate: List[str] = ["pidof ospf6d"]
|
||||
shutdown: list[str] = ["killall ospf6d"]
|
||||
validate: list[str] = ["pidof ospf6d"]
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
||||
|
@ -223,8 +265,8 @@ class FRRBgp(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRRBGP"
|
||||
shutdown: List[str] = ["killall bgpd"]
|
||||
validate: List[str] = ["pidof bgpd"]
|
||||
shutdown: list[str] = ["killall bgpd"]
|
||||
validate: list[str] = ["pidof bgpd"]
|
||||
custom_needed: bool = True
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
@ -253,8 +295,8 @@ class FRRRip(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRRRIP"
|
||||
shutdown: List[str] = ["killall ripd"]
|
||||
validate: List[str] = ["pidof ripd"]
|
||||
shutdown: list[str] = ["killall ripd"]
|
||||
validate: list[str] = ["pidof ripd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
|
@ -278,8 +320,8 @@ class FRRRipng(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRRRIPNG"
|
||||
shutdown: List[str] = ["killall ripngd"]
|
||||
validate: List[str] = ["pidof ripngd"]
|
||||
shutdown: list[str] = ["killall ripngd"]
|
||||
validate: list[str] = ["pidof ripngd"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
|
@ -304,8 +346,8 @@ class FRRBabel(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRRBabel"
|
||||
shutdown: List[str] = ["killall babeld"]
|
||||
validate: List[str] = ["pidof babeld"]
|
||||
shutdown: list[str] = ["killall babeld"]
|
||||
validate: list[str] = ["pidof babeld"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
|
@ -325,7 +367,7 @@ class FRRBabel(FrrService, ConfigService):
|
|||
return self.render_text(text, data)
|
||||
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
if is_wireless(iface.net):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
|
@ -344,8 +386,8 @@ class FRRpimd(FrrService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "FRRpimd"
|
||||
shutdown: List[str] = ["killall pimd"]
|
||||
validate: List[str] = ["pidof pimd"]
|
||||
shutdown: list[str] = ["killall pimd"]
|
||||
validate: list[str] = ["pidof pimd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
|
|
|
@ -48,6 +48,10 @@ bootdaemon()
|
|||
flags="$flags -6"
|
||||
fi
|
||||
|
||||
if [ "$1" = "ospfd" ]; then
|
||||
flags="$flags --apiserver"
|
||||
fi
|
||||
|
||||
#force FRR to use CORE generated conf file
|
||||
flags="$flags -d -f $FRR_CONF"
|
||||
$FRR_SBIN_DIR/$1 $flags
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Dict, List
|
||||
from typing import Any
|
||||
|
||||
from core import utils
|
||||
from core.config import Configuration
|
||||
|
@ -10,18 +10,18 @@ GROUP: str = "ProtoSvc"
|
|||
class MgenSinkService(ConfigService):
|
||||
name: str = "MGEN_Sink"
|
||||
group: str = GROUP
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["mgensink.sh", "sink.mgen"]
|
||||
executables: List[str] = ["mgen"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash mgensink.sh"]
|
||||
validate: List[str] = ["pidof mgen"]
|
||||
shutdown: List[str] = ["killall mgen"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["mgensink.sh", "sink.mgen"]
|
||||
executables: list[str] = ["mgen"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash mgensink.sh"]
|
||||
validate: list[str] = ["pidof mgen"]
|
||||
shutdown: list[str] = ["killall mgen"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces():
|
||||
name = utils.sysctl_devname(iface.name)
|
||||
|
@ -32,18 +32,18 @@ class MgenSinkService(ConfigService):
|
|||
class NrlNhdp(ConfigService):
|
||||
name: str = "NHDP"
|
||||
group: str = GROUP
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["nrlnhdp.sh"]
|
||||
executables: List[str] = ["nrlnhdp"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash nrlnhdp.sh"]
|
||||
validate: List[str] = ["pidof nrlnhdp"]
|
||||
shutdown: List[str] = ["killall nrlnhdp"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["nrlnhdp.sh"]
|
||||
executables: list[str] = ["nrlnhdp"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash nrlnhdp.sh"]
|
||||
validate: list[str] = ["pidof nrlnhdp"]
|
||||
shutdown: list[str] = ["killall nrlnhdp"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
|
@ -54,19 +54,18 @@ class NrlNhdp(ConfigService):
|
|||
class NrlSmf(ConfigService):
|
||||
name: str = "SMF"
|
||||
group: str = GROUP
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["startsmf.sh"]
|
||||
executables: List[str] = ["nrlsmf", "killall"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash startsmf.sh"]
|
||||
validate: List[str] = ["pidof nrlsmf"]
|
||||
shutdown: List[str] = ["killall nrlsmf"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["startsmf.sh"]
|
||||
executables: list[str] = ["nrlsmf", "killall"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash startsmf.sh"]
|
||||
validate: list[str] = ["pidof nrlsmf"]
|
||||
shutdown: list[str] = ["killall nrlsmf"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_arouted = "arouted" in self.node.config_services
|
||||
def data(self) -> dict[str, Any]:
|
||||
has_nhdp = "NHDP" in self.node.config_services
|
||||
has_olsr = "OLSR" in self.node.config_services
|
||||
ifnames = []
|
||||
|
@ -78,29 +77,25 @@ class NrlSmf(ConfigService):
|
|||
ip4_prefix = f"{ip4.ip}/{24}"
|
||||
break
|
||||
return dict(
|
||||
has_arouted=has_arouted,
|
||||
has_nhdp=has_nhdp,
|
||||
has_olsr=has_olsr,
|
||||
ifnames=ifnames,
|
||||
ip4_prefix=ip4_prefix,
|
||||
has_nhdp=has_nhdp, has_olsr=has_olsr, ifnames=ifnames, ip4_prefix=ip4_prefix
|
||||
)
|
||||
|
||||
|
||||
class NrlOlsr(ConfigService):
|
||||
name: str = "OLSR"
|
||||
group: str = GROUP
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["nrlolsrd.sh"]
|
||||
executables: List[str] = ["nrlolsrd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash nrlolsrd.sh"]
|
||||
validate: List[str] = ["pidof nrlolsrd"]
|
||||
shutdown: List[str] = ["killall nrlolsrd"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["nrlolsrd.sh"]
|
||||
executables: list[str] = ["nrlolsrd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash nrlolsrd.sh"]
|
||||
validate: list[str] = ["pidof nrlolsrd"]
|
||||
shutdown: list[str] = ["killall nrlolsrd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
has_zebra = "zebra" in self.node.config_services
|
||||
ifname = None
|
||||
|
@ -113,18 +108,18 @@ class NrlOlsr(ConfigService):
|
|||
class NrlOlsrv2(ConfigService):
|
||||
name: str = "OLSRv2"
|
||||
group: str = GROUP
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["nrlolsrv2.sh"]
|
||||
executables: List[str] = ["nrlolsrv2"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash nrlolsrv2.sh"]
|
||||
validate: List[str] = ["pidof nrlolsrv2"]
|
||||
shutdown: List[str] = ["killall nrlolsrv2"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["nrlolsrv2.sh"]
|
||||
executables: list[str] = ["nrlolsrv2"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash nrlolsrv2.sh"]
|
||||
validate: list[str] = ["pidof nrlolsrv2"]
|
||||
shutdown: list[str] = ["killall nrlolsrv2"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
|
@ -135,18 +130,18 @@ class NrlOlsrv2(ConfigService):
|
|||
class OlsrOrg(ConfigService):
|
||||
name: str = "OLSRORG"
|
||||
group: str = GROUP
|
||||
directories: List[str] = ["/etc/olsrd"]
|
||||
files: List[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
|
||||
executables: List[str] = ["olsrd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash olsrd.sh"]
|
||||
validate: List[str] = ["pidof olsrd"]
|
||||
shutdown: List[str] = ["killall olsrd"]
|
||||
directories: list[str] = ["/etc/olsrd"]
|
||||
files: list[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
|
||||
executables: list[str] = ["olsrd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash olsrd.sh"]
|
||||
validate: list[str] = ["pidof olsrd"]
|
||||
shutdown: list[str] = ["killall olsrd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
|
@ -157,37 +152,13 @@ class OlsrOrg(ConfigService):
|
|||
class MgenActor(ConfigService):
|
||||
name: str = "MgenActor"
|
||||
group: str = GROUP
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["start_mgen_actor.sh"]
|
||||
executables: List[str] = ["mgen"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash start_mgen_actor.sh"]
|
||||
validate: List[str] = ["pidof mgen"]
|
||||
shutdown: List[str] = ["killall mgen"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["start_mgen_actor.sh"]
|
||||
executables: list[str] = ["mgen"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash start_mgen_actor.sh"]
|
||||
validate: list[str] = ["pidof mgen"]
|
||||
shutdown: list[str] = ["killall mgen"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
|
||||
class Arouted(ConfigService):
|
||||
name: str = "arouted"
|
||||
group: str = GROUP
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["startarouted.sh"]
|
||||
executables: List[str] = ["arouted"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash startarouted.sh"]
|
||||
validate: List[str] = ["pidof arouted"]
|
||||
shutdown: List[str] = ["pkill arouted"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ip4_prefix = None
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ip4 = iface.get_ip4()
|
||||
if ip4:
|
||||
ip4_prefix = f"{ip4.ip}/{24}"
|
||||
break
|
||||
return dict(ip4_prefix=ip4_prefix)
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
#!/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 &
|
|
@ -1,8 +1,5 @@
|
|||
<%
|
||||
ifaces = ",".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:
|
||||
|
@ -12,4 +9,4 @@
|
|||
%>
|
||||
#!/bin/sh
|
||||
# auto-generated by NrlSmf service
|
||||
nrlsmf instance ${node.name}_smf ${ifaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
|
||||
nrlsmf instance ${node.name}_smf ${flood} ${ifaces} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
|
||||
|
|
|
@ -1,18 +1,31 @@
|
|||
import abc
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any
|
||||
|
||||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.nodes.base import CoreNodeBase
|
||||
from core.nodes.base import CoreNodeBase, NodeBase
|
||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
from core.nodes.network import PtpNet, WlanNode
|
||||
from core.nodes.physical import Rj45Node
|
||||
from core.nodes.wireless import WirelessNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
GROUP: str = "Quagga"
|
||||
QUAGGA_STATE_DIR: str = "/var/run/quagga"
|
||||
|
||||
|
||||
def is_wireless(node: NodeBase) -> bool:
|
||||
"""
|
||||
Check if the node is a wireless type node.
|
||||
|
||||
:param node: node to check type for
|
||||
:return: True if wireless type, False otherwise
|
||||
"""
|
||||
return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
|
||||
|
||||
|
||||
def has_mtu_mismatch(iface: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect MTU mismatch and add the appropriate OSPF
|
||||
|
@ -54,29 +67,43 @@ def get_router_id(node: CoreNodeBase) -> str:
|
|||
return "0.0.0.0"
|
||||
|
||||
|
||||
def rj45_check(iface: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect whether interface is connected an external RJ45
|
||||
link.
|
||||
"""
|
||||
if iface.net:
|
||||
for peer_iface in iface.net.get_ifaces():
|
||||
if peer_iface == iface:
|
||||
continue
|
||||
if isinstance(peer_iface.node, Rj45Node):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Zebra(ConfigService):
|
||||
name: str = "zebra"
|
||||
group: str = GROUP
|
||||
directories: List[str] = ["/usr/local/etc/quagga", "/var/run/quagga"]
|
||||
files: List[str] = [
|
||||
directories: list[str] = ["/usr/local/etc/quagga", "/var/run/quagga"]
|
||||
files: list[str] = [
|
||||
"/usr/local/etc/quagga/Quagga.conf",
|
||||
"quaggaboot.sh",
|
||||
"/usr/local/etc/quagga/vtysh.conf",
|
||||
]
|
||||
executables: List[str] = ["zebra"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash quaggaboot.sh zebra"]
|
||||
validate: List[str] = ["pidof zebra"]
|
||||
shutdown: List[str] = ["killall zebra"]
|
||||
executables: list[str] = ["zebra"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash quaggaboot.sh zebra"]
|
||||
validate: list[str] = ["pidof zebra"]
|
||||
shutdown: list[str] = ["killall zebra"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
quagga_bin_search = self.node.session.options.get_config(
|
||||
def data(self) -> dict[str, Any]:
|
||||
quagga_bin_search = self.node.session.options.get(
|
||||
"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 = self.node.session.options.get(
|
||||
"quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga"
|
||||
).strip('"')
|
||||
quagga_state_dir = QUAGGA_STATE_DIR
|
||||
|
@ -101,11 +128,16 @@ class Zebra(ConfigService):
|
|||
ip4s = []
|
||||
ip6s = []
|
||||
for ip4 in iface.ip4s:
|
||||
ip4s.append(str(ip4.ip))
|
||||
ip4s.append(str(ip4))
|
||||
for ip6 in iface.ip6s:
|
||||
ip6s.append(str(ip6.ip))
|
||||
is_control = getattr(iface, "control", False)
|
||||
ifaces.append((iface, ip4s, ip6s, is_control))
|
||||
ip6s.append(str(ip6))
|
||||
configs = []
|
||||
if not iface.control:
|
||||
for service in services:
|
||||
config = service.quagga_iface_config(iface)
|
||||
if config:
|
||||
configs.append(config.split("\n"))
|
||||
ifaces.append((iface, ip4s, ip6s, configs))
|
||||
|
||||
return dict(
|
||||
quagga_bin_search=quagga_bin_search,
|
||||
|
@ -121,16 +153,16 @@ class Zebra(ConfigService):
|
|||
|
||||
class QuaggaService(abc.ABC):
|
||||
group: str = GROUP
|
||||
directories: List[str] = []
|
||||
files: List[str] = []
|
||||
executables: List[str] = []
|
||||
dependencies: List[str] = ["zebra"]
|
||||
startup: List[str] = []
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
directories: list[str] = []
|
||||
files: list[str] = []
|
||||
executables: list[str] = []
|
||||
dependencies: list[str] = ["zebra"]
|
||||
startup: list[str] = []
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
ipv4_routing: bool = False
|
||||
ipv6_routing: bool = False
|
||||
|
||||
|
@ -151,22 +183,37 @@ class Ospfv2(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "OSPFv2"
|
||||
validate: List[str] = ["pidof ospfd"]
|
||||
shutdown: List[str] = ["killall ospfd"]
|
||||
validate: list[str] = ["pidof ospfd"]
|
||||
shutdown: list[str] = ["killall ospfd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
if has_mtu_mismatch(iface):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
has_mtu = has_mtu_mismatch(iface)
|
||||
has_rj45 = rj45_check(iface)
|
||||
is_ptp = isinstance(iface.net, PtpNet)
|
||||
data = dict(has_mtu=has_mtu, is_ptp=is_ptp, has_rj45=has_rj45)
|
||||
text = """
|
||||
% if has_mtu:
|
||||
ip ospf mtu-ignore
|
||||
% endif
|
||||
% if has_rj45:
|
||||
<% return STOP_RENDERING %>
|
||||
% endif
|
||||
% if is_ptp:
|
||||
ip ospf network point-to-point
|
||||
% endif
|
||||
ip ospf hello-interval 2
|
||||
ip ospf dead-interval 6
|
||||
ip ospf retransmit-interval 5
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
addresses = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
addresses.append(str(ip4.ip))
|
||||
addresses.append(str(ip4))
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
|
@ -187,8 +234,8 @@ class Ospfv3(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "OSPFv3"
|
||||
shutdown: List[str] = ["killall ospf6d"]
|
||||
validate: List[str] = ["pidof ospf6d"]
|
||||
shutdown: list[str] = ["killall ospf6d"]
|
||||
validate: list[str] = ["pidof ospf6d"]
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
||||
|
@ -227,15 +274,9 @@ class Ospfv3mdr(Ospfv3):
|
|||
|
||||
name: str = "OSPFv3MDR"
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
for iface in self.node.get_ifaces():
|
||||
is_wireless = isinstance(iface.net, (WlanNode, EmaneNet))
|
||||
logging.info("MDR wireless: %s", is_wireless)
|
||||
return dict()
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
config = super().quagga_iface_config(iface)
|
||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
if is_wireless(iface.net):
|
||||
config = self.clean_text(
|
||||
f"""
|
||||
{config}
|
||||
|
@ -259,15 +300,12 @@ class Bgp(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "BGP"
|
||||
shutdown: List[str] = ["killall bgpd"]
|
||||
validate: List[str] = ["pidof bgpd"]
|
||||
shutdown: list[str] = ["killall bgpd"]
|
||||
validate: list[str] = ["pidof bgpd"]
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
return ""
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
text = f"""
|
||||
! BGP configuration
|
||||
|
@ -281,6 +319,9 @@ class Bgp(QuaggaService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class Rip(QuaggaService, ConfigService):
|
||||
"""
|
||||
|
@ -288,8 +329,8 @@ class Rip(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "RIP"
|
||||
shutdown: List[str] = ["killall ripd"]
|
||||
validate: List[str] = ["pidof ripd"]
|
||||
shutdown: list[str] = ["killall ripd"]
|
||||
validate: list[str] = ["pidof ripd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
|
@ -313,8 +354,8 @@ class Ripng(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "RIPNG"
|
||||
shutdown: List[str] = ["killall ripngd"]
|
||||
validate: List[str] = ["pidof ripngd"]
|
||||
shutdown: list[str] = ["killall ripngd"]
|
||||
validate: list[str] = ["pidof ripngd"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
|
@ -339,8 +380,8 @@ class Babel(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "Babel"
|
||||
shutdown: List[str] = ["killall babeld"]
|
||||
validate: List[str] = ["pidof babeld"]
|
||||
shutdown: list[str] = ["killall babeld"]
|
||||
validate: list[str] = ["pidof babeld"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
|
@ -360,7 +401,7 @@ class Babel(QuaggaService, ConfigService):
|
|||
return self.render_text(text, data)
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
if isinstance(iface.net, (WlanNode, EmaneNet)):
|
||||
if is_wireless(iface.net):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
|
@ -379,8 +420,8 @@ class Xpimd(QuaggaService, ConfigService):
|
|||
"""
|
||||
|
||||
name: str = "Xpimd"
|
||||
shutdown: List[str] = ["killall xpimd"]
|
||||
validate: List[str] = ["pidof xpimd"]
|
||||
shutdown: list[str] = ["killall xpimd"]
|
||||
validate: list[str] = ["pidof xpimd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
% for iface, ip4s, ip6s, is_control in ifaces:
|
||||
% for iface, ip4s, ip6s, configs in ifaces:
|
||||
interface ${iface.name}
|
||||
% if want_ip4:
|
||||
% for addr in ip4s:
|
||||
|
@ -10,13 +10,11 @@ interface ${iface.name}
|
|||
ipv6 address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% if not is_control:
|
||||
% for service in services:
|
||||
% for line in service.quagga_iface_config(iface).split("\n"):
|
||||
% for config in configs:
|
||||
% for line in config:
|
||||
${line}
|
||||
% endfor
|
||||
% endfor
|
||||
% endif
|
||||
% endfor
|
||||
!
|
||||
% endfor
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
from typing import Any, Dict, List
|
||||
from typing import Any
|
||||
|
||||
from core.config import Configuration
|
||||
from core.config import ConfigString, Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
GROUP_NAME: str = "Security"
|
||||
|
||||
|
@ -10,71 +9,41 @@ GROUP_NAME: str = "Security"
|
|||
class VpnClient(ConfigService):
|
||||
name: str = "VPNClient"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["vpnclient.sh"]
|
||||
executables: List[str] = ["openvpn", "ip", "killall"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash vpnclient.sh"]
|
||||
validate: List[str] = ["pidof openvpn"]
|
||||
shutdown: List[str] = ["killall openvpn"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["vpnclient.sh"]
|
||||
executables: list[str] = ["openvpn", "ip", "killall"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash vpnclient.sh"]
|
||||
validate: list[str] = ["pidof openvpn"]
|
||||
shutdown: list[str] = ["killall openvpn"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="keydir",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Dir",
|
||||
default="/etc/core/keys",
|
||||
),
|
||||
Configuration(
|
||||
_id="keyname",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Name",
|
||||
default="client1",
|
||||
),
|
||||
Configuration(
|
||||
_id="server",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Server",
|
||||
default="10.0.2.10",
|
||||
),
|
||||
default_configs: list[Configuration] = [
|
||||
ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"),
|
||||
ConfigString(id="keyname", label="Key Name", default="client1"),
|
||||
ConfigString(id="server", label="Server", default="10.0.2.10"),
|
||||
]
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
class VpnServer(ConfigService):
|
||||
name: str = "VPNServer"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["vpnserver.sh"]
|
||||
executables: List[str] = ["openvpn", "ip", "killall"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash vpnserver.sh"]
|
||||
validate: List[str] = ["pidof openvpn"]
|
||||
shutdown: List[str] = ["killall openvpn"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["vpnserver.sh"]
|
||||
executables: list[str] = ["openvpn", "ip", "killall"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash vpnserver.sh"]
|
||||
validate: list[str] = ["pidof openvpn"]
|
||||
shutdown: list[str] = ["killall openvpn"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="keydir",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Dir",
|
||||
default="/etc/core/keys",
|
||||
),
|
||||
Configuration(
|
||||
_id="keyname",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Name",
|
||||
default="server",
|
||||
),
|
||||
Configuration(
|
||||
_id="subnet",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Subnet",
|
||||
default="10.0.200.0",
|
||||
),
|
||||
default_configs: list[Configuration] = [
|
||||
ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"),
|
||||
ConfigString(id="keyname", label="Key Name", default="server"),
|
||||
ConfigString(id="subnet", label="Subnet", default="10.0.200.0"),
|
||||
]
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
address = None
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ip4 = iface.get_ip4()
|
||||
|
@ -87,48 +56,48 @@ class VpnServer(ConfigService):
|
|||
class IPsec(ConfigService):
|
||||
name: str = "IPsec"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["ipsec.sh"]
|
||||
executables: List[str] = ["racoon", "ip", "setkey", "killall"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash ipsec.sh"]
|
||||
validate: List[str] = ["pidof racoon"]
|
||||
shutdown: List[str] = ["killall racoon"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["ipsec.sh"]
|
||||
executables: list[str] = ["racoon", "ip", "setkey", "killall"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash ipsec.sh"]
|
||||
validate: list[str] = ["pidof racoon"]
|
||||
shutdown: list[str] = ["killall racoon"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
class Firewall(ConfigService):
|
||||
name: str = "Firewall"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["firewall.sh"]
|
||||
executables: List[str] = ["iptables"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash firewall.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["firewall.sh"]
|
||||
executables: list[str] = ["iptables"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash firewall.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
class Nat(ConfigService):
|
||||
name: str = "NAT"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["nat.sh"]
|
||||
executables: List[str] = ["iptables"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash nat.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["nat.sh"]
|
||||
executables: list[str] = ["iptables"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash nat.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
from typing import Dict, List
|
||||
|
||||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
||||
class SimpleService(ConfigService):
|
||||
name: str = "Simple"
|
||||
group: str = "SimpleGroup"
|
||||
directories: List[str] = ["/etc/quagga", "/usr/local/lib"]
|
||||
files: List[str] = ["test1.sh", "test2.sh"]
|
||||
executables: List[str] = []
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = []
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = [
|
||||
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
|
||||
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
|
||||
Configuration(
|
||||
_id="value3",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Multiple Choice",
|
||||
options=["value1", "value2", "value3"],
|
||||
),
|
||||
]
|
||||
modes: Dict[str, Dict[str, str]] = {
|
||||
"mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
|
||||
"mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
|
||||
"mode3": {"value1": "value3", "value2": "0", "value3": "value1"},
|
||||
}
|
||||
|
||||
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
|
||||
"""
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Dict, List
|
||||
from typing import Any
|
||||
|
||||
import netaddr
|
||||
|
||||
|
@ -12,18 +12,18 @@ GROUP_NAME = "Utility"
|
|||
class DefaultRouteService(ConfigService):
|
||||
name: str = "DefaultRoute"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["defaultroute.sh"]
|
||||
executables: List[str] = ["ip"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash defaultroute.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["defaultroute.sh"]
|
||||
executables: list[str] = ["ip"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash defaultroute.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
# only add default routes for linked routing nodes
|
||||
routes = []
|
||||
ifaces = self.node.get_ifaces()
|
||||
|
@ -40,18 +40,18 @@ class DefaultRouteService(ConfigService):
|
|||
class DefaultMulticastRouteService(ConfigService):
|
||||
name: str = "DefaultMulticastRoute"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["defaultmroute.sh"]
|
||||
executables: List[str] = []
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash defaultmroute.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["defaultmroute.sh"]
|
||||
executables: list[str] = []
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash defaultmroute.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifname = None
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifname = iface.name
|
||||
|
@ -62,18 +62,18 @@ class DefaultMulticastRouteService(ConfigService):
|
|||
class StaticRouteService(ConfigService):
|
||||
name: str = "StaticRoute"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["staticroute.sh"]
|
||||
executables: List[str] = []
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash staticroute.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["staticroute.sh"]
|
||||
executables: list[str] = []
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash staticroute.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
routes = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip in iface.ips():
|
||||
|
@ -90,18 +90,18 @@ class StaticRouteService(ConfigService):
|
|||
class IpForwardService(ConfigService):
|
||||
name: str = "IPForward"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["ipforward.sh"]
|
||||
executables: List[str] = ["sysctl"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash ipforward.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = []
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["ipforward.sh"]
|
||||
executables: list[str] = ["sysctl"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash ipforward.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
devnames = []
|
||||
for iface in self.node.get_ifaces():
|
||||
devname = utils.sysctl_devname(iface.name)
|
||||
|
@ -112,18 +112,18 @@ class IpForwardService(ConfigService):
|
|||
class SshService(ConfigService):
|
||||
name: str = "SSH"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = ["/etc/ssh", "/var/run/sshd"]
|
||||
files: List[str] = ["startsshd.sh", "/etc/ssh/sshd_config"]
|
||||
executables: List[str] = ["sshd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash startsshd.sh"]
|
||||
validate: List[str] = []
|
||||
shutdown: List[str] = ["killall sshd"]
|
||||
directories: list[str] = ["/etc/ssh", "/var/run/sshd"]
|
||||
files: list[str] = ["startsshd.sh", "/etc/ssh/sshd_config"]
|
||||
executables: list[str] = ["sshd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash startsshd.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = ["killall sshd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
return dict(
|
||||
sshcfgdir=self.directories[0],
|
||||
sshstatedir=self.directories[1],
|
||||
|
@ -134,44 +134,46 @@ class SshService(ConfigService):
|
|||
class DhcpService(ConfigService):
|
||||
name: str = "DHCP"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = ["/etc/dhcp", "/var/lib/dhcp"]
|
||||
files: List[str] = ["/etc/dhcp/dhcpd.conf"]
|
||||
executables: List[str] = ["dhcpd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
|
||||
validate: List[str] = ["pidof dhcpd"]
|
||||
shutdown: List[str] = ["killall dhcpd"]
|
||||
directories: list[str] = ["/etc/dhcp", "/var/lib/dhcp"]
|
||||
files: list[str] = ["/etc/dhcp/dhcpd.conf"]
|
||||
executables: list[str] = ["dhcpd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
|
||||
validate: list[str] = ["pidof dhcpd"]
|
||||
shutdown: list[str] = ["killall dhcpd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
subnets = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
if ip4.size == 1:
|
||||
continue
|
||||
# divide the address space in half
|
||||
index = (ip4.size - 2) / 2
|
||||
rangelow = ip4[index]
|
||||
rangehigh = ip4[-2]
|
||||
subnets.append((ip4.ip, ip4.netmask, rangelow, rangehigh, str(ip4.ip)))
|
||||
subnets.append((ip4.cidr.ip, ip4.netmask, rangelow, rangehigh, ip4.ip))
|
||||
return dict(subnets=subnets)
|
||||
|
||||
|
||||
class DhcpClientService(ConfigService):
|
||||
name: str = "DHCPClient"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["startdhcpclient.sh"]
|
||||
executables: List[str] = ["dhclient"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash startdhcpclient.sh"]
|
||||
validate: List[str] = ["pidof dhclient"]
|
||||
shutdown: List[str] = ["killall dhclient"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["startdhcpclient.sh"]
|
||||
executables: list[str] = ["dhclient"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash startdhcpclient.sh"]
|
||||
validate: list[str] = ["pidof dhclient"]
|
||||
shutdown: list[str] = ["killall dhclient"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
|
@ -181,56 +183,56 @@ class DhcpClientService(ConfigService):
|
|||
class FtpService(ConfigService):
|
||||
name: str = "FTP"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = ["/var/run/vsftpd/empty", "/var/ftp"]
|
||||
files: List[str] = ["vsftpd.conf"]
|
||||
executables: List[str] = ["vsftpd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["vsftpd ./vsftpd.conf"]
|
||||
validate: List[str] = ["pidof vsftpd"]
|
||||
shutdown: List[str] = ["killall vsftpd"]
|
||||
directories: list[str] = ["/var/run/vsftpd/empty", "/var/ftp"]
|
||||
files: list[str] = ["vsftpd.conf"]
|
||||
executables: list[str] = ["vsftpd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["vsftpd ./vsftpd.conf"]
|
||||
validate: list[str] = ["pidof vsftpd"]
|
||||
shutdown: list[str] = ["killall vsftpd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
class PcapService(ConfigService):
|
||||
name: str = "pcap"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = []
|
||||
files: List[str] = ["pcap.sh"]
|
||||
executables: List[str] = ["tcpdump"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash pcap.sh start"]
|
||||
validate: List[str] = ["pidof tcpdump"]
|
||||
shutdown: List[str] = ["bash pcap.sh stop"]
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["pcap.sh"]
|
||||
executables: list[str] = ["tcpdump"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash pcap.sh start"]
|
||||
validate: list[str] = ["pidof tcpdump"]
|
||||
shutdown: list[str] = ["bash pcap.sh stop"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
return dict()
|
||||
return dict(ifnames=ifnames)
|
||||
|
||||
|
||||
class RadvdService(ConfigService):
|
||||
name: str = "radvd"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = ["/etc/radvd", "/var/run/radvd"]
|
||||
files: List[str] = ["/etc/radvd/radvd.conf"]
|
||||
executables: List[str] = ["radvd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = [
|
||||
directories: list[str] = ["/etc/radvd", "/var/run/radvd"]
|
||||
files: list[str] = ["/etc/radvd/radvd.conf"]
|
||||
executables: list[str] = ["radvd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = [
|
||||
"radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"
|
||||
]
|
||||
validate: List[str] = ["pidof radvd"]
|
||||
shutdown: List[str] = ["pkill radvd"]
|
||||
validate: list[str] = ["pidof radvd"]
|
||||
shutdown: list[str] = ["pkill radvd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifaces = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
prefixes = []
|
||||
|
@ -245,22 +247,22 @@ class RadvdService(ConfigService):
|
|||
class AtdService(ConfigService):
|
||||
name: str = "atd"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
|
||||
files: List[str] = ["startatd.sh"]
|
||||
executables: List[str] = ["atd"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["bash startatd.sh"]
|
||||
validate: List[str] = ["pidof atd"]
|
||||
shutdown: List[str] = ["pkill atd"]
|
||||
directories: list[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
|
||||
files: list[str] = ["startatd.sh"]
|
||||
executables: list[str] = ["atd"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash startatd.sh"]
|
||||
validate: list[str] = ["pidof atd"]
|
||||
shutdown: list[str] = ["pkill atd"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
class HttpService(ConfigService):
|
||||
name: str = "HTTP"
|
||||
group: str = GROUP_NAME
|
||||
directories: List[str] = [
|
||||
directories: list[str] = [
|
||||
"/etc/apache2",
|
||||
"/var/run/apache2",
|
||||
"/var/log/apache2",
|
||||
|
@ -268,21 +270,21 @@ class HttpService(ConfigService):
|
|||
"/var/lock/apache2",
|
||||
"/var/www",
|
||||
]
|
||||
files: List[str] = [
|
||||
files: list[str] = [
|
||||
"/etc/apache2/apache2.conf",
|
||||
"/etc/apache2/envvars",
|
||||
"/var/www/index.html",
|
||||
]
|
||||
executables: List[str] = ["apache2ctl"]
|
||||
dependencies: List[str] = []
|
||||
startup: List[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"]
|
||||
validate: List[str] = ["pidof apache2"]
|
||||
shutdown: List[str] = ["apache2ctl stop"]
|
||||
executables: list[str] = ["apache2ctl"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"]
|
||||
validate: list[str] = ["pidof apache2"]
|
||||
shutdown: list[str] = ["apache2ctl stop"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: List[Configuration] = []
|
||||
modes: Dict[str, Dict[str, str]] = {}
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifaces = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifaces.append(iface)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# auto-generated by RADVD service (utility.py)
|
||||
% for ifname, prefixes in values:
|
||||
% for ifname, prefixes in ifaces:
|
||||
interface ${ifname}
|
||||
{
|
||||
AdvSendAdvert on;
|
|
@ -13,4 +13,5 @@ sysctl -w net.ipv4.conf.default.rp_filter=0
|
|||
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
|
||||
sysctl -w net.ipv6.conf.${devname}.forwarding=1
|
||||
% endfor
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# (-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 &
|
||||
tcpdump -s 12288 -C 10 -n -w ${node.name}.${ifname}.pcap -i ${ifname} > /dev/null 2>&1 &
|
||||
% endfor
|
||||
elif [ "x$1" = "xstop" ]; then
|
||||
mkdir -p $SESSION_DIR/pcap
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
COREDPY_VERSION = "@PACKAGE_VERSION@"
|
||||
CORE_CONF_DIR = "@CORE_CONF_DIR@"
|
||||
CORE_DATA_DIR = "@CORE_DATA_DIR@"
|
||||
from pathlib import Path
|
||||
|
||||
COREDPY_VERSION: str = "@PACKAGE_VERSION@"
|
||||
CORE_CONF_DIR: Path = Path("@CORE_CONF_DIR@")
|
||||
CORE_DATA_DIR: Path = Path("@CORE_DATA_DIR@")
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,11 @@
|
|||
import logging
|
||||
from typing import Dict, List
|
||||
from pathlib import Path
|
||||
|
||||
from core.config import Configuration
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
manifest = None
|
||||
try:
|
||||
from emane.shell import manifest
|
||||
|
@ -12,7 +14,7 @@ except ImportError:
|
|||
from emanesh import manifest
|
||||
except ImportError:
|
||||
manifest = None
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
def _type_value(config_type: str) -> ConfigDataTypes:
|
||||
|
@ -30,7 +32,7 @@ def _type_value(config_type: str) -> ConfigDataTypes:
|
|||
return ConfigDataTypes[config_type]
|
||||
|
||||
|
||||
def _get_possible(config_type: str, config_regex: str) -> List[str]:
|
||||
def _get_possible(config_type: str, config_regex: str) -> list[str]:
|
||||
"""
|
||||
Retrieve possible config value options based on emane regexes.
|
||||
|
||||
|
@ -48,7 +50,7 @@ def _get_possible(config_type: str, config_regex: str) -> List[str]:
|
|||
return []
|
||||
|
||||
|
||||
def _get_default(config_type_name: str, config_value: List[str]) -> str:
|
||||
def _get_default(config_type_name: str, config_value: list[str]) -> str:
|
||||
"""
|
||||
Convert default configuration values to one used by core.
|
||||
|
||||
|
@ -71,9 +73,10 @@ def _get_default(config_type_name: str, config_value: List[str]) -> str:
|
|||
return config_default
|
||||
|
||||
|
||||
def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]:
|
||||
def parse(manifest_path: Path, 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 manifest_path: absolute manifest file path
|
||||
:param defaults: used to override default values for configurations
|
||||
|
@ -85,7 +88,7 @@ def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]:
|
|||
return []
|
||||
|
||||
# load configuration file
|
||||
manifest_file = manifest.Manifest(manifest_path)
|
||||
manifest_file = manifest.Manifest(str(manifest_path))
|
||||
manifest_configurations = manifest_file.getAllConfiguration()
|
||||
|
||||
configurations = []
|
||||
|
@ -116,8 +119,8 @@ def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]:
|
|||
config_descriptions = f"{config_descriptions} file"
|
||||
|
||||
configuration = Configuration(
|
||||
_id=config_name,
|
||||
_type=config_type_value,
|
||||
id=config_name,
|
||||
type=config_type_value,
|
||||
default=config_default,
|
||||
options=possible,
|
||||
label=config_descriptions,
|
||||
|
|
|
@ -2,19 +2,21 @@
|
|||
Defines Emane Models used within CORE.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, List, Optional, Set
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from core.config import ConfigGroup, Configuration
|
||||
from core.config import ConfigBool, ConfigGroup, ConfigString, Configuration
|
||||
from core.emane import emanemanifest
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import LinkOptions
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
from core.errors import CoreError
|
||||
from core.location.mobility import WirelessModel
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.xml import emanexml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
DEFAULT_DEV: str = "ctrl0"
|
||||
MANIFEST_PATH: str = "share/emane/manifest"
|
||||
|
||||
|
||||
class EmaneModel(WirelessModel):
|
||||
"""
|
||||
|
@ -23,79 +25,104 @@ class EmaneModel(WirelessModel):
|
|||
configurable parameters. Helper functions also live here.
|
||||
"""
|
||||
|
||||
# default platform configuration settings
|
||||
platform_controlport: str = "controlportendpoint"
|
||||
platform_xml: str = "nemmanager.xml"
|
||||
platform_defaults: dict[str, str] = {
|
||||
"eventservicedevice": DEFAULT_DEV,
|
||||
"eventservicegroup": "224.1.2.8:45703",
|
||||
"otamanagerdevice": DEFAULT_DEV,
|
||||
"otamanagergroup": "224.1.2.8:45702",
|
||||
}
|
||||
platform_config: list[Configuration] = []
|
||||
|
||||
# default mac configuration settings
|
||||
mac_library: Optional[str] = None
|
||||
mac_xml: Optional[str] = None
|
||||
mac_defaults: Dict[str, str] = {}
|
||||
mac_config: List[Configuration] = []
|
||||
mac_defaults: dict[str, str] = {}
|
||||
mac_config: list[Configuration] = []
|
||||
|
||||
# default phy configuration settings, using the universal model
|
||||
phy_library: Optional[str] = None
|
||||
phy_xml: str = "emanephy.xml"
|
||||
phy_defaults: Dict[str, str] = {
|
||||
phy_defaults: dict[str, str] = {
|
||||
"subid": "1",
|
||||
"propagationmodel": "2ray",
|
||||
"noisemode": "none",
|
||||
}
|
||||
phy_config: List[Configuration] = []
|
||||
phy_config: list[Configuration] = []
|
||||
|
||||
# support for external configurations
|
||||
external_config: List[Configuration] = [
|
||||
Configuration("external", ConfigDataTypes.BOOL, default="0"),
|
||||
Configuration(
|
||||
"platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001"
|
||||
),
|
||||
Configuration(
|
||||
"transportendpoint", ConfigDataTypes.STRING, default="127.0.0.1:50002"
|
||||
),
|
||||
external_config: list[Configuration] = [
|
||||
ConfigBool(id="external", default="0"),
|
||||
ConfigString(id="platformendpoint", default="127.0.0.1:40001"),
|
||||
ConfigString(id="transportendpoint", default="127.0.0.1:50002"),
|
||||
]
|
||||
|
||||
config_ignore: Set[str] = set()
|
||||
config_ignore: set[str] = set()
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
"""
|
||||
Called after being loaded within the EmaneManager. Provides configured emane_prefix for
|
||||
parsing xml files.
|
||||
Called after being loaded within the EmaneManager. Provides configured
|
||||
emane_prefix for parsing xml files.
|
||||
|
||||
:param emane_prefix: configured emane prefix path
|
||||
:return: nothing
|
||||
"""
|
||||
manifest_path = "share/emane/manifest"
|
||||
cls._load_platform_config(emane_prefix)
|
||||
# load mac configuration
|
||||
mac_xml_path = os.path.join(emane_prefix, manifest_path, cls.mac_xml)
|
||||
mac_xml_path = emane_prefix / MANIFEST_PATH / cls.mac_xml
|
||||
cls.mac_config = emanemanifest.parse(mac_xml_path, cls.mac_defaults)
|
||||
|
||||
# load phy configuration
|
||||
phy_xml_path = os.path.join(emane_prefix, manifest_path, cls.phy_xml)
|
||||
phy_xml_path = emane_prefix / MANIFEST_PATH / cls.phy_xml
|
||||
cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults)
|
||||
|
||||
@classmethod
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
def _load_platform_config(cls, emane_prefix: Path) -> None:
|
||||
platform_xml_path = emane_prefix / MANIFEST_PATH / cls.platform_xml
|
||||
cls.platform_config = emanemanifest.parse(
|
||||
platform_xml_path, cls.platform_defaults
|
||||
)
|
||||
# remove controlport configuration, since core will set this directly
|
||||
controlport_index = None
|
||||
for index, configuration in enumerate(cls.platform_config):
|
||||
if configuration.id == cls.platform_controlport:
|
||||
controlport_index = index
|
||||
break
|
||||
if controlport_index is not None:
|
||||
cls.platform_config.pop(controlport_index)
|
||||
|
||||
@classmethod
|
||||
def configurations(cls) -> list[Configuration]:
|
||||
"""
|
||||
Returns the combination all all configurations (mac, phy, and external).
|
||||
|
||||
:return: all configurations
|
||||
"""
|
||||
return cls.mac_config + cls.phy_config + cls.external_config
|
||||
return (
|
||||
cls.platform_config + cls.mac_config + cls.phy_config + cls.external_config
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def config_groups(cls) -> List[ConfigGroup]:
|
||||
def config_groups(cls) -> list[ConfigGroup]:
|
||||
"""
|
||||
Returns the defined configuration groups.
|
||||
|
||||
:return: list of configuration groups.
|
||||
"""
|
||||
mac_len = len(cls.mac_config)
|
||||
platform_len = len(cls.platform_config)
|
||||
mac_len = len(cls.mac_config) + platform_len
|
||||
phy_len = len(cls.phy_config) + mac_len
|
||||
config_len = len(cls.configurations())
|
||||
return [
|
||||
ConfigGroup("MAC Parameters", 1, mac_len),
|
||||
ConfigGroup("Platform Parameters", 1, platform_len),
|
||||
ConfigGroup("MAC Parameters", platform_len + 1, mac_len),
|
||||
ConfigGroup("PHY Parameters", mac_len + 1, phy_len),
|
||||
ConfigGroup("External Parameters", phy_len + 1, config_len),
|
||||
]
|
||||
|
||||
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
|
||||
def build_xml_files(self, config: dict[str, str], iface: CoreInterface) -> None:
|
||||
"""
|
||||
Builds xml files for this emane model. Creates a nem.xml file that points to
|
||||
both mac.xml and phy.xml definitions.
|
||||
|
@ -110,15 +137,16 @@ class EmaneModel(WirelessModel):
|
|||
emanexml.create_phy_xml(self, iface, config)
|
||||
emanexml.create_transport_xml(iface, config)
|
||||
|
||||
def post_startup(self) -> None:
|
||||
def post_startup(self, iface: CoreInterface) -> None:
|
||||
"""
|
||||
Logic to execute after the emane manager is finished with startup.
|
||||
|
||||
:param iface: interface for post startup
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug("emane model(%s) has no post setup tasks", self.name)
|
||||
logger.debug("emane model(%s) has no post setup tasks", self.name)
|
||||
|
||||
def update(self, moved_ifaces: List[CoreInterface]) -> None:
|
||||
def update(self, moved_ifaces: list[CoreInterface]) -> None:
|
||||
"""
|
||||
Invoked from MobilityModel when nodes are moved; this causes
|
||||
emane location events to be generated for the nodes in the moved
|
||||
|
@ -128,10 +156,9 @@ class EmaneModel(WirelessModel):
|
|||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
emane_net = self.session.get_node(self.id, EmaneNet)
|
||||
emane_net.setnempositions(moved_ifaces)
|
||||
self.session.emane.set_nem_positions(moved_ifaces)
|
||||
except CoreError:
|
||||
logging.exception("error during update")
|
||||
logger.exception("error during update")
|
||||
|
||||
def linkconfig(
|
||||
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||
|
@ -144,4 +171,4 @@ class EmaneModel(WirelessModel):
|
|||
:param iface2: interface two
|
||||
:return: nothing
|
||||
"""
|
||||
logging.warning("emane model(%s) does not support link config", self.name)
|
||||
logger.warning("emane model(%s) does not support link config", self.name)
|
||||
|
|
|
@ -2,14 +2,17 @@ import logging
|
|||
import sched
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import LinkData
|
||||
from core.emulator.enumerations import LinkTypes, MessageFlags
|
||||
from core.nodes.network import CtrlNet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from emane import shell
|
||||
except ImportError:
|
||||
|
@ -17,12 +20,11 @@ except ImportError:
|
|||
from emanesh import shell
|
||||
except ImportError:
|
||||
shell = None
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emane.emanemanager import EmaneManager
|
||||
|
||||
DEFAULT_PORT: int = 47_000
|
||||
MAC_COMPONENT_INDEX: int = 1
|
||||
EMANE_RFPIPE: str = "rfpipemaclayer"
|
||||
EMANE_80211: str = "ieee80211abgmaclayer"
|
||||
|
@ -32,10 +34,10 @@ NEM_SELF: int = 65535
|
|||
|
||||
|
||||
class LossTable:
|
||||
def __init__(self, losses: Dict[float, float]) -> None:
|
||||
self.losses: Dict[float, float] = losses
|
||||
self.sinrs: List[float] = sorted(self.losses.keys())
|
||||
self.loss_lookup: Dict[int, float] = {}
|
||||
def __init__(self, losses: dict[float, float]) -> None:
|
||||
self.losses: dict[float, float] = losses
|
||||
self.sinrs: list[float] = sorted(self.losses.keys())
|
||||
self.loss_lookup: dict[int, float] = {}
|
||||
for index, value in enumerate(self.sinrs):
|
||||
self.loss_lookup[index] = self.losses[value]
|
||||
self.mac_id: Optional[str] = None
|
||||
|
@ -77,12 +79,12 @@ class EmaneLink:
|
|||
|
||||
|
||||
class EmaneClient:
|
||||
def __init__(self, address: str) -> None:
|
||||
def __init__(self, address: str, port: int) -> None:
|
||||
self.address: str = address
|
||||
self.client: shell.ControlPortClient = shell.ControlPortClient(
|
||||
self.address, DEFAULT_PORT
|
||||
self.address, port
|
||||
)
|
||||
self.nems: Dict[int, LossTable] = {}
|
||||
self.nems: dict[int, LossTable] = {}
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
|
@ -91,7 +93,7 @@ class EmaneClient:
|
|||
# get mac config
|
||||
mac_id, _, emane_model = components[MAC_COMPONENT_INDEX]
|
||||
mac_config = self.client.getConfiguration(mac_id)
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"address(%s) nem(%s) emane(%s)", self.address, nem_id, emane_model
|
||||
)
|
||||
|
||||
|
@ -101,14 +103,14 @@ class EmaneClient:
|
|||
elif emane_model == EMANE_RFPIPE:
|
||||
loss_table = self.handle_rfpipe(mac_config)
|
||||
else:
|
||||
logging.warning("unknown emane link model: %s", emane_model)
|
||||
logger.warning("unknown emane link model: %s", emane_model)
|
||||
continue
|
||||
logging.info("monitoring links nem(%s) model(%s)", nem_id, emane_model)
|
||||
logger.info("monitoring links nem(%s) model(%s)", nem_id, emane_model)
|
||||
loss_table.mac_id = mac_id
|
||||
self.nems[nem_id] = loss_table
|
||||
|
||||
def check_links(
|
||||
self, links: Dict[Tuple[int, int], EmaneLink], loss_threshold: int
|
||||
self, links: dict[tuple[int, int], EmaneLink], loss_threshold: int
|
||||
) -> None:
|
||||
for from_nem, loss_table in self.nems.items():
|
||||
tables = self.client.getStatisticTable(loss_table.mac_id, (SINR_TABLE,))
|
||||
|
@ -136,14 +138,14 @@ class EmaneClient:
|
|||
link = EmaneLink(from_nem, to_nem, sinr)
|
||||
links[link_key] = link
|
||||
|
||||
def handle_tdma(self, config: Dict[str, Tuple]):
|
||||
def handle_tdma(self, config: dict[str, tuple]):
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logging.debug("tdma pcr: %s", pcr)
|
||||
logger.debug("tdma pcr: %s", pcr)
|
||||
|
||||
def handle_80211(self, config: Dict[str, Tuple]) -> LossTable:
|
||||
def handle_80211(self, config: dict[str, tuple]) -> LossTable:
|
||||
unicastrate = config["unicastrate"][0][0]
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logging.debug("80211 pcr: %s", pcr)
|
||||
logger.debug("80211 pcr: %s", pcr)
|
||||
tree = etree.parse(pcr)
|
||||
root = tree.getroot()
|
||||
table = root.find("table")
|
||||
|
@ -157,9 +159,9 @@ class EmaneClient:
|
|||
losses[sinr] = por
|
||||
return LossTable(losses)
|
||||
|
||||
def handle_rfpipe(self, config: Dict[str, Tuple]) -> LossTable:
|
||||
def handle_rfpipe(self, config: dict[str, tuple]) -> LossTable:
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logging.debug("rfpipe pcr: %s", pcr)
|
||||
logger.debug("rfpipe pcr: %s", pcr)
|
||||
tree = etree.parse(pcr)
|
||||
root = tree.getroot()
|
||||
table = root.find("table")
|
||||
|
@ -177,9 +179,9 @@ class EmaneClient:
|
|||
class EmaneLinkMonitor:
|
||||
def __init__(self, emane_manager: "EmaneManager") -> None:
|
||||
self.emane_manager: "EmaneManager" = emane_manager
|
||||
self.clients: List[EmaneClient] = []
|
||||
self.links: Dict[Tuple[int, int], EmaneLink] = {}
|
||||
self.complete_links: Set[Tuple[int, int]] = set()
|
||||
self.clients: list[EmaneClient] = []
|
||||
self.links: dict[tuple[int, int], EmaneLink] = {}
|
||||
self.complete_links: set[tuple[int, int]] = set()
|
||||
self.loss_threshold: Optional[int] = None
|
||||
self.link_interval: Optional[int] = None
|
||||
self.link_timeout: Optional[int] = None
|
||||
|
@ -187,12 +189,13 @@ class EmaneLinkMonitor:
|
|||
self.running: bool = False
|
||||
|
||||
def start(self) -> None:
|
||||
self.loss_threshold = int(self.emane_manager.get_config("loss_threshold"))
|
||||
self.link_interval = int(self.emane_manager.get_config("link_interval"))
|
||||
self.link_timeout = int(self.emane_manager.get_config("link_timeout"))
|
||||
options = self.emane_manager.session.options
|
||||
self.loss_threshold = options.get_int("loss_threshold")
|
||||
self.link_interval = options.get_int("link_interval")
|
||||
self.link_timeout = options.get_int("link_timeout")
|
||||
self.initialize()
|
||||
if not self.clients:
|
||||
logging.info("no valid emane models to monitor links")
|
||||
logger.info("no valid emane models to monitor links")
|
||||
return
|
||||
self.scheduler = sched.scheduler()
|
||||
self.scheduler.enter(0, 0, self.check_links)
|
||||
|
@ -202,22 +205,28 @@ class EmaneLinkMonitor:
|
|||
|
||||
def initialize(self) -> None:
|
||||
addresses = self.get_addresses()
|
||||
for address in addresses:
|
||||
client = EmaneClient(address)
|
||||
for address, port in addresses:
|
||||
client = EmaneClient(address, port)
|
||||
if client.nems:
|
||||
self.clients.append(client)
|
||||
|
||||
def get_addresses(self) -> List[str]:
|
||||
def get_addresses(self) -> list[tuple[str, int]]:
|
||||
addresses = []
|
||||
nodes = self.emane_manager.getnodes()
|
||||
for node in nodes:
|
||||
control = None
|
||||
ports = []
|
||||
for iface in node.get_ifaces():
|
||||
if isinstance(iface.net, CtrlNet):
|
||||
ip4 = iface.get_ip4()
|
||||
if ip4:
|
||||
address = str(ip4.ip)
|
||||
addresses.append(address)
|
||||
break
|
||||
control = str(ip4.ip)
|
||||
if isinstance(iface.net, EmaneNet):
|
||||
port = self.emane_manager.get_nem_port(iface)
|
||||
ports.append(port)
|
||||
if control:
|
||||
for port in ports:
|
||||
addresses.append((control, port))
|
||||
return addresses
|
||||
|
||||
def check_links(self) -> None:
|
||||
|
@ -228,7 +237,7 @@ class EmaneLinkMonitor:
|
|||
client.check_links(self.links, self.loss_threshold)
|
||||
except shell.ControlPortException:
|
||||
if self.running:
|
||||
logging.exception("link monitor error")
|
||||
logger.exception("link monitor error")
|
||||
|
||||
# find new links
|
||||
current_links = set(self.links.keys())
|
||||
|
@ -264,25 +273,25 @@ class EmaneLinkMonitor:
|
|||
if self.running:
|
||||
self.scheduler.enter(self.link_interval, 0, self.check_links)
|
||||
|
||||
def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]:
|
||||
def get_complete_id(self, link_id: tuple[int, int]) -> tuple[int, int]:
|
||||
value1, value2 = link_id
|
||||
if value1 < value2:
|
||||
return value1, value2
|
||||
else:
|
||||
return value2, value1
|
||||
|
||||
def is_complete_link(self, link_id: Tuple[int, int]) -> bool:
|
||||
def is_complete_link(self, link_id: tuple[int, int]) -> bool:
|
||||
reverse_id = link_id[1], link_id[0]
|
||||
return link_id in self.links and reverse_id in self.links
|
||||
|
||||
def get_link_label(self, link_id: Tuple[int, int]) -> str:
|
||||
def get_link_label(self, link_id: tuple[int, int]) -> str:
|
||||
source_id = tuple(sorted(link_id))
|
||||
source_link = self.links[source_id]
|
||||
dest_id = link_id[::-1]
|
||||
dest_link = self.links[dest_id]
|
||||
return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}"
|
||||
|
||||
def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None:
|
||||
def send_link(self, message_type: MessageFlags, link_id: tuple[int, int]) -> None:
|
||||
nem1, nem2 = link_id
|
||||
link = self.emane_manager.get_nem_link(nem1, nem2, message_type)
|
||||
if link:
|
||||
|
|
69
daemon/core/emane/modelmanager.py
Normal file
69
daemon/core/emane/modelmanager.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
import logging
|
||||
import pkgutil
|
||||
from pathlib import Path
|
||||
|
||||
from core import utils
|
||||
from core.emane import models as emane_models
|
||||
from core.emane.emanemodel import EmaneModel
|
||||
from core.errors import CoreError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmaneModelManager:
|
||||
models: dict[str, type[EmaneModel]] = {}
|
||||
|
||||
@classmethod
|
||||
def load_locals(cls, emane_prefix: Path) -> list[str]:
|
||||
"""
|
||||
Load local core emane models and make them available.
|
||||
|
||||
:param emane_prefix: installed emane prefix
|
||||
:return: list of errors encountered loading emane models
|
||||
"""
|
||||
errors = []
|
||||
for module_info in pkgutil.walk_packages(
|
||||
emane_models.__path__, f"{emane_models.__name__}."
|
||||
):
|
||||
models = utils.load_module(module_info.name, EmaneModel)
|
||||
for model in models:
|
||||
logger.debug("loading emane model: %s", model.name)
|
||||
try:
|
||||
model.load(emane_prefix)
|
||||
cls.models[model.name] = model
|
||||
except CoreError as e:
|
||||
errors.append(model.name)
|
||||
logger.debug("not loading emane model(%s): %s", model.name, e)
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path, emane_prefix: Path) -> list[str]:
|
||||
"""
|
||||
Search and load custom emane models and make them available.
|
||||
|
||||
:param path: path to search for custom emane models
|
||||
:param emane_prefix: installed emane prefix
|
||||
:return: list of errors encountered loading emane models
|
||||
"""
|
||||
subdirs = [x for x in path.iterdir() if x.is_dir()]
|
||||
subdirs.append(path)
|
||||
errors = []
|
||||
for subdir in subdirs:
|
||||
logger.debug("loading emane models from: %s", subdir)
|
||||
models = utils.load_classes(subdir, EmaneModel)
|
||||
for model in models:
|
||||
logger.debug("loading emane model: %s", model.name)
|
||||
try:
|
||||
model.load(emane_prefix)
|
||||
cls.models[model.name] = model
|
||||
except CoreError as e:
|
||||
errors.append(model.name)
|
||||
logger.debug("not loading emane model(%s): %s", model.name, e)
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def get(cls, name: str) -> type[EmaneModel]:
|
||||
model = cls.models.get(name)
|
||||
if model is None:
|
||||
raise CoreError(f"emame model does not exist {name}")
|
||||
return model
|
|
@ -1,25 +1,23 @@
|
|||
"""
|
||||
EMANE Bypass model for CORE
|
||||
"""
|
||||
from typing import List, Set
|
||||
from pathlib import Path
|
||||
|
||||
from core.config import Configuration
|
||||
from core.config import ConfigBool, Configuration
|
||||
from core.emane import emanemodel
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
||||
class EmaneBypassModel(emanemodel.EmaneModel):
|
||||
name: str = "emane_bypass"
|
||||
|
||||
# values to ignore, when writing xml files
|
||||
config_ignore: Set[str] = {"none"}
|
||||
config_ignore: set[str] = {"none"}
|
||||
|
||||
# mac definitions
|
||||
mac_library: str = "bypassmaclayer"
|
||||
mac_config: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="none",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
mac_config: list[Configuration] = [
|
||||
ConfigBool(
|
||||
id="none",
|
||||
default="0",
|
||||
label="There are no parameters for the bypass model.",
|
||||
)
|
||||
|
@ -27,9 +25,8 @@ class EmaneBypassModel(emanemodel.EmaneModel):
|
|||
|
||||
# phy definitions
|
||||
phy_library: str = "bypassphylayer"
|
||||
phy_config: List[Configuration] = []
|
||||
phy_config: list[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
# ignore default logic
|
||||
pass
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls._load_platform_config(emane_prefix)
|
|
@ -3,8 +3,7 @@ commeffect.py: EMANE CommEffect model for CORE
|
|||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, List
|
||||
from pathlib import Path
|
||||
|
||||
from lxml import etree
|
||||
|
||||
|
@ -14,6 +13,8 @@ from core.emulator.data import LinkOptions
|
|||
from core.nodes.interface import CoreInterface
|
||||
from core.xml import emanexml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from emane.events.commeffectevent import CommEffectEvent
|
||||
except ImportError:
|
||||
|
@ -21,7 +22,7 @@ except ImportError:
|
|||
from emanesh.events.commeffectevent import CommEffectEvent
|
||||
except ImportError:
|
||||
CommEffectEvent = None
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
def convert_none(x: float) -> int:
|
||||
|
@ -40,27 +41,36 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
name: str = "emane_commeffect"
|
||||
shim_library: str = "commeffectshim"
|
||||
shim_xml: str = "commeffectshim.xml"
|
||||
shim_defaults: Dict[str, str] = {}
|
||||
config_shim: List[Configuration] = []
|
||||
shim_defaults: dict[str, str] = {}
|
||||
config_shim: list[Configuration] = []
|
||||
|
||||
# comm effect does not need the default phy and external configurations
|
||||
phy_config: List[Configuration] = []
|
||||
external_config: List[Configuration] = []
|
||||
phy_config: list[Configuration] = []
|
||||
external_config: list[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
shim_xml_path = os.path.join(emane_prefix, "share/emane/manifest", cls.shim_xml)
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls._load_platform_config(emane_prefix)
|
||||
shim_xml_path = emane_prefix / "share/emane/manifest" / cls.shim_xml
|
||||
cls.config_shim = emanemanifest.parse(shim_xml_path, cls.shim_defaults)
|
||||
|
||||
@classmethod
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
return cls.config_shim
|
||||
def configurations(cls) -> list[Configuration]:
|
||||
return cls.platform_config + cls.config_shim
|
||||
|
||||
@classmethod
|
||||
def config_groups(cls) -> List[ConfigGroup]:
|
||||
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
|
||||
def config_groups(cls) -> list[ConfigGroup]:
|
||||
platform_len = len(cls.platform_config)
|
||||
return [
|
||||
ConfigGroup("Platform Parameters", 1, platform_len),
|
||||
ConfigGroup(
|
||||
"CommEffect SHIM Parameters",
|
||||
platform_len + 1,
|
||||
len(cls.configurations()),
|
||||
),
|
||||
]
|
||||
|
||||
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
|
||||
def build_xml_files(self, config: dict[str, str], iface: CoreInterface) -> None:
|
||||
"""
|
||||
Build the necessary nem and commeffect XMLs in the given path.
|
||||
If an individual NEM has a nonstandard config, we need to build
|
||||
|
@ -111,21 +121,15 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
Generate CommEffect events when a Link Message is received having
|
||||
link parameters.
|
||||
"""
|
||||
service = self.session.emane.service
|
||||
if service is None:
|
||||
logging.warning("%s: EMANE event service unavailable", self.name)
|
||||
return
|
||||
|
||||
if iface is None or iface2 is None:
|
||||
logging.warning("%s: missing NEM information", self.name)
|
||||
logger.warning("%s: missing NEM information", self.name)
|
||||
return
|
||||
|
||||
# TODO: batch these into multiple events per transmission
|
||||
# TODO: may want to split out seconds portion of delay and jitter
|
||||
event = CommEffectEvent()
|
||||
nem1 = self.session.emane.get_nem_id(iface)
|
||||
nem2 = self.session.emane.get_nem_id(iface2)
|
||||
logging.info("sending comm effect event")
|
||||
logger.info("sending comm effect event")
|
||||
event.append(
|
||||
nem1,
|
||||
latency=convert_none(options.delay),
|
||||
|
@ -135,4 +139,4 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
unicast=int(convert_none(options.bandwidth)),
|
||||
broadcast=int(convert_none(options.bandwidth)),
|
||||
)
|
||||
service.publish(nem2, event)
|
||||
self.session.emane.publish_event(nem2, event)
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
ieee80211abg.py: EMANE IEEE 802.11abg model for CORE
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
@ -15,8 +15,8 @@ class EmaneIeee80211abgModel(emanemodel.EmaneModel):
|
|||
mac_xml: str = "ieee80211abgmaclayer.xml"
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix, "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml"
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = str(
|
||||
emane_prefix / "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml"
|
||||
)
|
||||
super().load(emane_prefix)
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
rfpipe.py: EMANE RF-PIPE model for CORE
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
@ -15,8 +15,8 @@ class EmaneRfPipeModel(emanemodel.EmaneModel):
|
|||
mac_xml: str = "rfpipemaclayer.xml"
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix, "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = str(
|
||||
emane_prefix / "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"
|
||||
)
|
||||
super().load(emane_prefix)
|
65
daemon/core/emane/models/tdma.py
Normal file
65
daemon/core/emane/models/tdma.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
"""
|
||||
tdma.py: EMANE TDMA model bindings for CORE
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from core import constants, utils
|
||||
from core.config import ConfigString
|
||||
from core.emane import emanemodel
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.nodes.interface import CoreInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmaneTdmaModel(emanemodel.EmaneModel):
|
||||
# model name
|
||||
name: str = "emane_tdma"
|
||||
|
||||
# mac configuration
|
||||
mac_library: str = "tdmaeventschedulerradiomodel"
|
||||
mac_xml: str = "tdmaeventschedulerradiomodel.xml"
|
||||
|
||||
# add custom schedule options and ignore it when writing emane xml
|
||||
schedule_name: str = "schedule"
|
||||
default_schedule: Path = (
|
||||
constants.CORE_DATA_DIR / "examples" / "tdma" / "schedule.xml"
|
||||
)
|
||||
config_ignore: set[str] = {schedule_name}
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = str(
|
||||
emane_prefix
|
||||
/ "share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml"
|
||||
)
|
||||
super().load(emane_prefix)
|
||||
config_item = ConfigString(
|
||||
id=cls.schedule_name,
|
||||
default=str(cls.default_schedule),
|
||||
label="TDMA schedule file (core)",
|
||||
)
|
||||
cls.mac_config.insert(0, config_item)
|
||||
|
||||
def post_startup(self, iface: CoreInterface) -> None:
|
||||
# get configured schedule
|
||||
emane_net = self.session.get_node(self.id, EmaneNet)
|
||||
config = self.session.emane.get_iface_config(emane_net, iface)
|
||||
schedule = Path(config[self.schedule_name])
|
||||
if not schedule.is_file():
|
||||
logger.error("ignoring invalid tdma schedule: %s", schedule)
|
||||
return
|
||||
# initiate tdma schedule
|
||||
nem_id = self.session.emane.get_nem_id(iface)
|
||||
if not nem_id:
|
||||
logger.error("could not find nem for interface")
|
||||
return
|
||||
service = self.session.emane.nem_service.get(nem_id)
|
||||
if service:
|
||||
device = service.device
|
||||
logger.info(
|
||||
"setting up tdma schedule: schedule(%s) device(%s)", schedule, device
|
||||
)
|
||||
utils.cmd(f"emaneevent-tdmaschedule -i {device} {schedule}")
|
|
@ -4,28 +4,23 @@ share the same MAC+PHY model.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Union
|
||||
|
||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions
|
||||
from core.emulator.distributed import DistributedServer
|
||||
from core.emulator.enumerations import (
|
||||
EventTypes,
|
||||
LinkTypes,
|
||||
MessageFlags,
|
||||
NodeTypes,
|
||||
RegisterTlvs,
|
||||
)
|
||||
from core.errors import CoreError
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||
from core.emulator.enumerations import MessageFlags, RegisterTlvs
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode, NodeOptions
|
||||
from core.nodes.interface import CoreInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emane.emanemodel import EmaneModel
|
||||
from core.emulator.session import Session
|
||||
from core.location.mobility import WirelessModel, WayPointMobility
|
||||
|
||||
OptionalEmaneModel = Optional[EmaneModel]
|
||||
WirelessModelType = Type[WirelessModel]
|
||||
from core.location.mobility import WayPointMobility
|
||||
|
||||
try:
|
||||
from emane.events import LocationEvent
|
||||
|
@ -34,7 +29,121 @@ except ImportError:
|
|||
from emanesh.events import LocationEvent
|
||||
except ImportError:
|
||||
LocationEvent = None
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
class TunTap(CoreInterface):
|
||||
"""
|
||||
TUN/TAP virtual device in TAP mode
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
_id: int,
|
||||
name: str,
|
||||
localname: str,
|
||||
use_ovs: bool,
|
||||
node: CoreNode = None,
|
||||
server: "DistributedServer" = None,
|
||||
) -> None:
|
||||
super().__init__(_id, name, localname, use_ovs, node=node, server=server)
|
||||
self.node: CoreNode = node
|
||||
|
||||
def startup(self) -> None:
|
||||
"""
|
||||
Startup logic for a tunnel tap.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.up = True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Shutdown functionality for a tunnel tap.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if not self.up:
|
||||
return
|
||||
self.up = False
|
||||
|
||||
def waitfor(
|
||||
self, func: Callable[[], int], attempts: int = 10, maxretrydelay: float = 0.25
|
||||
) -> bool:
|
||||
"""
|
||||
Wait for func() to return zero with exponential backoff.
|
||||
|
||||
:param func: function to wait for a result of zero
|
||||
:param attempts: number of attempts to wait for a zero result
|
||||
:param maxretrydelay: maximum retry delay
|
||||
:return: True if wait succeeded, False otherwise
|
||||
"""
|
||||
delay = 0.01
|
||||
result = False
|
||||
for i in range(1, attempts + 1):
|
||||
r = func()
|
||||
if r == 0:
|
||||
result = True
|
||||
break
|
||||
msg = f"attempt {i} failed with nonzero exit status {r}"
|
||||
if i < attempts + 1:
|
||||
msg += ", retrying..."
|
||||
logger.info(msg)
|
||||
time.sleep(delay)
|
||||
delay += delay
|
||||
if delay > maxretrydelay:
|
||||
delay = maxretrydelay
|
||||
else:
|
||||
msg += ", giving up"
|
||||
logger.info(msg)
|
||||
return result
|
||||
|
||||
def nodedevexists(self) -> int:
|
||||
"""
|
||||
Checks if device exists.
|
||||
|
||||
:return: 0 if device exists, 1 otherwise
|
||||
"""
|
||||
try:
|
||||
self.node.node_net_client.device_show(self.name)
|
||||
return 0
|
||||
except CoreCommandError:
|
||||
return 1
|
||||
|
||||
def waitfordevicenode(self) -> None:
|
||||
"""
|
||||
Check for presence of a node device - tap device may not appear right away waits.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
logger.debug("waiting for device node: %s", self.name)
|
||||
count = 0
|
||||
while True:
|
||||
result = self.waitfor(self.nodedevexists)
|
||||
if result:
|
||||
break
|
||||
should_retry = count < 5
|
||||
is_emane_running = self.node.session.emane.emanerunning(self.node)
|
||||
if all([should_retry, is_emane_running]):
|
||||
count += 1
|
||||
else:
|
||||
raise RuntimeError("node device failed to exist")
|
||||
|
||||
def set_ips(self) -> None:
|
||||
"""
|
||||
Set interface ip addresses.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.waitfordevicenode()
|
||||
for ip in self.ips():
|
||||
self.node.node_net_client.create_address(self.name, str(ip))
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmaneOptions(NodeOptions):
|
||||
emane_model: str = None
|
||||
"""name of emane model to associate an emane network to"""
|
||||
|
||||
|
||||
class EmaneNet(CoreNetworkBase):
|
||||
|
@ -44,22 +153,26 @@ class EmaneNet(CoreNetworkBase):
|
|||
Emane controller object that exists in a session.
|
||||
"""
|
||||
|
||||
apitype: NodeTypes = NodeTypes.EMANE
|
||||
linktype: LinkTypes = LinkTypes.WIRED
|
||||
type: str = "wlan"
|
||||
has_custom_iface: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
server: DistributedServer = None,
|
||||
options: EmaneOptions = None,
|
||||
) -> None:
|
||||
super().__init__(session, _id, name, server)
|
||||
options = options or EmaneOptions()
|
||||
super().__init__(session, _id, name, server, options)
|
||||
self.conf: str = ""
|
||||
self.model: "OptionalEmaneModel" = None
|
||||
self.mobility: Optional[WayPointMobility] = None
|
||||
model_class = self.session.emane.get_model(options.emane_model)
|
||||
self.wireless_model: Optional["EmaneModel"] = model_class(self.session, self.id)
|
||||
if self.session.is_running():
|
||||
self.session.emane.add_node(self)
|
||||
|
||||
@classmethod
|
||||
def create_options(cls) -> EmaneOptions:
|
||||
return EmaneOptions()
|
||||
|
||||
def linkconfig(
|
||||
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||
|
@ -67,18 +180,15 @@ class EmaneNet(CoreNetworkBase):
|
|||
"""
|
||||
The CommEffect model supports link configuration.
|
||||
"""
|
||||
if not self.model:
|
||||
if not self.wireless_model:
|
||||
return
|
||||
self.model.linkconfig(iface, options, iface2)
|
||||
|
||||
def config(self, conf: str) -> None:
|
||||
self.conf = conf
|
||||
self.wireless_model.linkconfig(iface, options, iface2)
|
||||
|
||||
def startup(self) -> None:
|
||||
pass
|
||||
self.up = True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
pass
|
||||
self.up = False
|
||||
|
||||
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||
pass
|
||||
|
@ -86,93 +196,37 @@ class EmaneNet(CoreNetworkBase):
|
|||
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||
pass
|
||||
|
||||
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
|
||||
raise CoreError("emane networks cannot be linked to other networks")
|
||||
def updatemodel(self, config: dict[str, str]) -> None:
|
||||
"""
|
||||
Update configuration for the current model.
|
||||
|
||||
def updatemodel(self, config: Dict[str, str]) -> None:
|
||||
if not self.model:
|
||||
:param config: configuration to update model with
|
||||
:return: nothing
|
||||
"""
|
||||
if not self.wireless_model:
|
||||
raise CoreError(f"no model set to update for node({self.name})")
|
||||
logging.info(
|
||||
"node(%s) updating model(%s): %s", self.id, self.model.name, config
|
||||
logger.info(
|
||||
"node(%s) updating model(%s): %s", self.id, self.wireless_model.name, config
|
||||
)
|
||||
self.model.update_config(config)
|
||||
self.wireless_model.update_config(config)
|
||||
|
||||
def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None:
|
||||
def setmodel(
|
||||
self,
|
||||
model: Union[type["EmaneModel"], type["WayPointMobility"]],
|
||||
config: dict[str, str],
|
||||
) -> None:
|
||||
"""
|
||||
set the EmaneModel associated with this node
|
||||
"""
|
||||
if model.config_type == RegisterTlvs.WIRELESS:
|
||||
# EmaneModel really uses values from ConfigurableManager
|
||||
# when buildnemxml() is called, not during init()
|
||||
self.model = model(session=self.session, _id=self.id)
|
||||
self.model.update_config(config)
|
||||
self.wireless_model = model(session=self.session, _id=self.id)
|
||||
self.wireless_model.update_config(config)
|
||||
elif model.config_type == RegisterTlvs.MOBILITY:
|
||||
self.mobility = model(session=self.session, _id=self.id)
|
||||
self.mobility.update_config(config)
|
||||
|
||||
def _nem_position(
|
||||
self, iface: CoreInterface
|
||||
) -> Optional[Tuple[int, float, float, float]]:
|
||||
"""
|
||||
Creates nem position for emane event for a given interface.
|
||||
|
||||
:param iface: interface to get nem emane position for
|
||||
:return: nem position tuple, None otherwise
|
||||
"""
|
||||
nem_id = self.session.emane.get_nem_id(iface)
|
||||
ifname = iface.localname
|
||||
if nem_id is None:
|
||||
logging.info("nemid for %s is unknown", ifname)
|
||||
return
|
||||
node = iface.node
|
||||
x, y, z = node.getposition()
|
||||
lat, lon, alt = self.session.location.getgeo(x, y, z)
|
||||
if node.position.alt is not None:
|
||||
alt = node.position.alt
|
||||
node.position.set_geo(lon, lat, alt)
|
||||
# altitude must be an integer or warning is printed
|
||||
alt = int(round(alt))
|
||||
return nem_id, lon, lat, alt
|
||||
|
||||
def setnemposition(self, iface: CoreInterface) -> None:
|
||||
"""
|
||||
Publish a NEM location change event using the EMANE event service.
|
||||
|
||||
:param iface: interface to set nem position for
|
||||
"""
|
||||
if self.session.emane.service is None:
|
||||
logging.info("position service not available")
|
||||
return
|
||||
position = self._nem_position(iface)
|
||||
if position:
|
||||
nemid, lon, lat, alt = position
|
||||
event = LocationEvent()
|
||||
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
||||
self.session.emane.service.publish(0, event)
|
||||
|
||||
def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None:
|
||||
"""
|
||||
Several NEMs have moved, from e.g. a WaypointMobilityModel
|
||||
calculation. Generate an EMANE Location Event having several
|
||||
entries for each interface that has moved.
|
||||
"""
|
||||
if len(moved_ifaces) == 0:
|
||||
return
|
||||
|
||||
if self.session.emane.service is None:
|
||||
logging.info("position service not available")
|
||||
return
|
||||
|
||||
event = LocationEvent()
|
||||
for iface in moved_ifaces:
|
||||
position = self._nem_position(iface)
|
||||
if position:
|
||||
nemid, lon, lat, alt = position
|
||||
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
||||
self.session.emane.service.publish(0, event)
|
||||
|
||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
||||
links = super().links(flags)
|
||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]:
|
||||
links = []
|
||||
emane_manager = self.session.emane
|
||||
# gather current emane links
|
||||
nem_ids = set()
|
||||
|
@ -193,22 +247,44 @@ class EmaneNet(CoreNetworkBase):
|
|||
# ignore incomplete links
|
||||
if (nem2, nem1) not in emane_links:
|
||||
continue
|
||||
link = emane_manager.get_nem_link(nem1, nem2)
|
||||
link = emane_manager.get_nem_link(nem1, nem2, flags)
|
||||
if link:
|
||||
links.append(link)
|
||||
return links
|
||||
|
||||
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
||||
# TUN/TAP is not ready for addressing yet; the device may
|
||||
# take some time to appear, and installing it into a
|
||||
# namespace after it has been bound removes addressing;
|
||||
# save addresses with the interface now
|
||||
iface_id = node.newtuntap(iface_data.id, iface_data.name)
|
||||
node.attachnet(iface_id, self)
|
||||
iface = node.get_iface(iface_id)
|
||||
iface.set_mac(iface_data.mac)
|
||||
for ip in iface_data.get_ips():
|
||||
iface.add_ip(ip)
|
||||
if self.session.state == EventTypes.RUNTIME_STATE:
|
||||
def create_tuntap(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
||||
"""
|
||||
Create a tuntap interface for the provided node.
|
||||
|
||||
:param node: node to create tuntap interface for
|
||||
:param iface_data: interface data to create interface with
|
||||
:return: created tuntap interface
|
||||
"""
|
||||
with node.lock:
|
||||
if iface_data.id is not None and iface_data.id in node.ifaces:
|
||||
raise CoreError(
|
||||
f"node({self.id}) interface({iface_data.id}) already exists"
|
||||
)
|
||||
iface_id = (
|
||||
iface_data.id if iface_data.id is not None else node.next_iface_id()
|
||||
)
|
||||
name = iface_data.name if iface_data.name is not None else f"eth{iface_id}"
|
||||
session_id = self.session.short_session_id()
|
||||
localname = f"tap{node.id}.{iface_id}.{session_id}"
|
||||
iface = TunTap(iface_id, name, localname, self.session.use_ovs(), node=node)
|
||||
if iface_data.mac:
|
||||
iface.set_mac(iface_data.mac)
|
||||
for ip in iface_data.get_ips():
|
||||
iface.add_ip(ip)
|
||||
node.ifaces[iface_id] = iface
|
||||
self.attach(iface)
|
||||
if self.up:
|
||||
iface.startup()
|
||||
if self.session.is_running():
|
||||
self.session.emane.start_iface(self, iface)
|
||||
return iface
|
||||
|
||||
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||
raise CoreError(
|
||||
f"emane network({self.name}) do not support adopting interfaces"
|
||||
)
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
"""
|
||||
tdma.py: EMANE TDMA model bindings for CORE
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Set
|
||||
|
||||
from core import constants, utils
|
||||
from core.config import Configuration
|
||||
from core.emane import emanemodel
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
||||
class EmaneTdmaModel(emanemodel.EmaneModel):
|
||||
# model name
|
||||
name: str = "emane_tdma"
|
||||
|
||||
# mac configuration
|
||||
mac_library: str = "tdmaeventschedulerradiomodel"
|
||||
mac_xml: str = "tdmaeventschedulerradiomodel.xml"
|
||||
|
||||
# add custom schedule options and ignore it when writing emane xml
|
||||
schedule_name: str = "schedule"
|
||||
default_schedule: str = os.path.join(
|
||||
constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml"
|
||||
)
|
||||
config_ignore: Set[str] = {schedule_name}
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
cls.mac_defaults["pcrcurveuri"] = os.path.join(
|
||||
emane_prefix,
|
||||
"share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml",
|
||||
)
|
||||
super().load(emane_prefix)
|
||||
cls.mac_config.insert(
|
||||
0,
|
||||
Configuration(
|
||||
_id=cls.schedule_name,
|
||||
_type=ConfigDataTypes.STRING,
|
||||
default=cls.default_schedule,
|
||||
label="TDMA schedule file (core)",
|
||||
),
|
||||
)
|
||||
|
||||
def post_startup(self) -> None:
|
||||
"""
|
||||
Logic to execute after the emane manager is finished with startup.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
# get configured schedule
|
||||
config = self.session.emane.get_configs(node_id=self.id, config_type=self.name)
|
||||
if not config:
|
||||
return
|
||||
schedule = config[self.schedule_name]
|
||||
|
||||
# get the set event device
|
||||
event_device = self.session.emane.event_device
|
||||
|
||||
# initiate tdma schedule
|
||||
logging.info(
|
||||
"setting up tdma schedule: schedule(%s) device(%s)", schedule, event_device
|
||||
)
|
||||
args = f"emaneevent-tdmaschedule -i {event_device} {schedule}"
|
||||
utils.cmd(args)
|
67
daemon/core/emulator/broadcast.py
Normal file
67
daemon/core/emulator/broadcast.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
from collections.abc import Callable
|
||||
from typing import TypeVar, Union
|
||||
|
||||
from core.emulator.data import (
|
||||
ConfigData,
|
||||
EventData,
|
||||
ExceptionData,
|
||||
FileData,
|
||||
LinkData,
|
||||
NodeData,
|
||||
)
|
||||
from core.errors import CoreError
|
||||
|
||||
T = TypeVar(
|
||||
"T", bound=Union[EventData, ExceptionData, NodeData, LinkData, FileData, ConfigData]
|
||||
)
|
||||
|
||||
|
||||
class BroadcastManager:
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Creates a BroadcastManager instance.
|
||||
"""
|
||||
self.handlers: dict[type[T], set[Callable[[T], None]]] = {}
|
||||
|
||||
def send(self, data: T) -> None:
|
||||
"""
|
||||
Retrieve handlers for data, and run all current handlers.
|
||||
|
||||
:param data: data to provide to handlers
|
||||
:return: nothing
|
||||
"""
|
||||
handlers = self.handlers.get(type(data), set())
|
||||
for handler in handlers:
|
||||
handler(data)
|
||||
|
||||
def add_handler(self, data_type: type[T], handler: Callable[[T], None]) -> None:
|
||||
"""
|
||||
Add a handler for a given data type.
|
||||
|
||||
:param data_type: type of data to add handler for
|
||||
:param handler: handler to add
|
||||
:return: nothing
|
||||
"""
|
||||
handlers = self.handlers.setdefault(data_type, set())
|
||||
if handler in handlers:
|
||||
raise CoreError(
|
||||
f"cannot add data({data_type}) handler({repr(handler)}), "
|
||||
f"already exists"
|
||||
)
|
||||
handlers.add(handler)
|
||||
|
||||
def remove_handler(self, data_type: type[T], handler: Callable[[T], None]) -> None:
|
||||
"""
|
||||
Remove a handler for a given data type.
|
||||
|
||||
:param data_type: type of data to remove handler for
|
||||
:param handler: handler to remove
|
||||
:return: nothing
|
||||
"""
|
||||
handlers = self.handlers.get(data_type, set())
|
||||
if handler not in handlers:
|
||||
raise CoreError(
|
||||
f"cannot remove data({data_type}) handler({repr(handler)}), "
|
||||
f"does not exist"
|
||||
)
|
||||
handlers.remove(handler)
|
239
daemon/core/emulator/controlnets.py
Normal file
239
daemon/core/emulator/controlnets.py
Normal file
|
@ -0,0 +1,239 @@
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core import utils
|
||||
from core.emulator.data import InterfaceData
|
||||
from core.errors import CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
from core.nodes.interface import DEFAULT_MTU
|
||||
from core.nodes.network import CtrlNet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
CTRL_NET_ID: int = 9001
|
||||
ETC_HOSTS_PATH: str = "/etc/hosts"
|
||||
|
||||
|
||||
class ControlNetManager:
|
||||
def __init__(self, session: "Session") -> None:
|
||||
self.session: "Session" = session
|
||||
self.etc_hosts_header: str = f"CORE session {self.session.id} host entries"
|
||||
|
||||
def _etc_hosts_enabled(self) -> bool:
|
||||
"""
|
||||
Determines if /etc/hosts should be configured.
|
||||
|
||||
:return: True if /etc/hosts should be configured, False otherwise
|
||||
"""
|
||||
return self.session.options.get_bool("update_etc_hosts", False)
|
||||
|
||||
def _get_server_ifaces(
|
||||
self,
|
||||
) -> tuple[None, Optional[str], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Retrieve control net server interfaces.
|
||||
|
||||
:return: control net server interfaces
|
||||
"""
|
||||
d0 = self.session.options.get("controlnetif0")
|
||||
if d0:
|
||||
logger.error("controlnet0 cannot be assigned with a host interface")
|
||||
d1 = self.session.options.get("controlnetif1")
|
||||
d2 = self.session.options.get("controlnetif2")
|
||||
d3 = self.session.options.get("controlnetif3")
|
||||
return None, d1, d2, d3
|
||||
|
||||
def _get_prefixes(
|
||||
self,
|
||||
) -> tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Retrieve control net prefixes.
|
||||
|
||||
:return: control net prefixes
|
||||
"""
|
||||
p = self.session.options.get("controlnet")
|
||||
p0 = self.session.options.get("controlnet0")
|
||||
p1 = self.session.options.get("controlnet1")
|
||||
p2 = self.session.options.get("controlnet2")
|
||||
p3 = self.session.options.get("controlnet3")
|
||||
if not p0 and p:
|
||||
p0 = p
|
||||
return p0, p1, p2, p3
|
||||
|
||||
def update_etc_hosts(self) -> None:
|
||||
"""
|
||||
Add the IP addresses of control interfaces to the /etc/hosts file.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if not self._etc_hosts_enabled():
|
||||
return
|
||||
control_net = self.get_control_net(0)
|
||||
entries = ""
|
||||
for iface in control_net.get_ifaces():
|
||||
name = iface.node.name
|
||||
for ip in iface.ips():
|
||||
entries += f"{ip.ip} {name}\n"
|
||||
logger.info("adding entries to /etc/hosts")
|
||||
utils.file_munge(ETC_HOSTS_PATH, self.etc_hosts_header, entries)
|
||||
|
||||
def clear_etc_hosts(self) -> None:
|
||||
"""
|
||||
Clear IP addresses of control interfaces from the /etc/hosts file.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if not self._etc_hosts_enabled():
|
||||
return
|
||||
logger.info("removing /etc/hosts file entries")
|
||||
utils.file_demunge(ETC_HOSTS_PATH, self.etc_hosts_header)
|
||||
|
||||
def get_control_net_index(self, dev: str) -> int:
|
||||
"""
|
||||
Retrieve control net index.
|
||||
|
||||
:param dev: device to get control net index for
|
||||
:return: control net index, -1 otherwise
|
||||
"""
|
||||
if dev[0:4] == "ctrl" and int(dev[4]) in (0, 1, 2, 3):
|
||||
index = int(dev[4])
|
||||
if index == 0:
|
||||
return index
|
||||
if index < 4 and self._get_prefixes()[index] is not None:
|
||||
return index
|
||||
return -1
|
||||
|
||||
def get_control_net(self, index: int) -> Optional[CtrlNet]:
|
||||
"""
|
||||
Retrieve a control net based on index.
|
||||
|
||||
:param index: control net index
|
||||
:return: control net when available, None otherwise
|
||||
"""
|
||||
try:
|
||||
return self.session.get_node(CTRL_NET_ID + index, CtrlNet)
|
||||
except CoreError:
|
||||
return None
|
||||
|
||||
def add_control_net(
|
||||
self, index: int, conf_required: bool = True
|
||||
) -> Optional[CtrlNet]:
|
||||
"""
|
||||
Create a control network bridge as necessary. The conf_reqd flag,
|
||||
when False, causes a control network bridge to be added even if
|
||||
one has not been configured.
|
||||
|
||||
:param index: network index to add
|
||||
:param conf_required: flag to check if conf is required
|
||||
:return: control net node
|
||||
"""
|
||||
logger.info(
|
||||
"checking to add control net index(%s) conf_required(%s)",
|
||||
index,
|
||||
conf_required,
|
||||
)
|
||||
# check for valid index
|
||||
if not (0 <= index <= 3):
|
||||
raise CoreError(f"invalid control net index({index})")
|
||||
# return any existing control net bridge
|
||||
control_net = self.get_control_net(index)
|
||||
if control_net:
|
||||
logger.info("control net index(%s) already exists", index)
|
||||
return control_net
|
||||
# retrieve prefix for current index
|
||||
index_prefix = self._get_prefixes()[index]
|
||||
if not index_prefix:
|
||||
if conf_required:
|
||||
return None
|
||||
else:
|
||||
index_prefix = CtrlNet.DEFAULT_PREFIX_LIST[index]
|
||||
# retrieve valid prefix from old style values
|
||||
prefixes = index_prefix.split()
|
||||
if len(prefixes) > 1:
|
||||
# a list of per-host prefixes is provided
|
||||
try:
|
||||
prefix = prefixes[0].split(":", 1)[1]
|
||||
except IndexError:
|
||||
prefix = prefixes[0]
|
||||
else:
|
||||
prefix = prefixes[0]
|
||||
# use the updown script for control net 0 only
|
||||
updown_script = None
|
||||
if index == 0:
|
||||
updown_script = self.session.options.get("controlnet_updown_script")
|
||||
# build a new controlnet bridge
|
||||
_id = CTRL_NET_ID + index
|
||||
server_iface = self._get_server_ifaces()[index]
|
||||
logger.info(
|
||||
"adding controlnet(%s) prefix(%s) updown(%s) server interface(%s)",
|
||||
_id,
|
||||
prefix,
|
||||
updown_script,
|
||||
server_iface,
|
||||
)
|
||||
options = CtrlNet.create_options()
|
||||
options.prefix = prefix
|
||||
options.updown_script = updown_script
|
||||
options.serverintf = server_iface
|
||||
control_net = self.session.create_node(CtrlNet, False, _id, options=options)
|
||||
control_net.brname = f"ctrl{index}.{self.session.short_session_id()}"
|
||||
control_net.startup()
|
||||
return control_net
|
||||
|
||||
def remove_control_net(self, index: int) -> None:
|
||||
"""
|
||||
Removes control net.
|
||||
|
||||
:param index: index of control net to remove
|
||||
:return: nothing
|
||||
"""
|
||||
control_net = self.get_control_net(index)
|
||||
if control_net:
|
||||
logger.info("removing control net index(%s)", index)
|
||||
self.session.delete_node(control_net.id)
|
||||
|
||||
def add_control_iface(self, node: CoreNode, index: int) -> None:
|
||||
"""
|
||||
Adds a control net interface to a node.
|
||||
|
||||
:param node: node to add control net interface to
|
||||
:param index: index of control net to add interface to
|
||||
:return: nothing
|
||||
:raises CoreError: if control net doesn't exist, interface already exists,
|
||||
or there is an error creating the interface
|
||||
"""
|
||||
control_net = self.get_control_net(index)
|
||||
if not control_net:
|
||||
raise CoreError(f"control net index({index}) does not exist")
|
||||
iface_id = control_net.CTRLIF_IDX_BASE + index
|
||||
if node.ifaces.get(iface_id):
|
||||
raise CoreError(f"control iface({iface_id}) already exists")
|
||||
try:
|
||||
logger.info(
|
||||
"node(%s) adding control net index(%s) interface(%s)",
|
||||
node.name,
|
||||
index,
|
||||
iface_id,
|
||||
)
|
||||
ip4 = control_net.prefix[node.id]
|
||||
ip4_mask = control_net.prefix.prefixlen
|
||||
iface_data = InterfaceData(
|
||||
id=iface_id,
|
||||
name=f"ctrl{index}",
|
||||
mac=utils.random_mac(),
|
||||
ip4=ip4,
|
||||
ip4_mask=ip4_mask,
|
||||
mtu=DEFAULT_MTU,
|
||||
)
|
||||
iface = node.create_iface(iface_data)
|
||||
control_net.attach(iface)
|
||||
iface.control = True
|
||||
except ValueError:
|
||||
raise CoreError(
|
||||
f"error adding control net interface to node({node.id}), "
|
||||
f"invalid control net prefix({control_net.prefix}), "
|
||||
"a longer prefix length may be required"
|
||||
)
|
|
@ -1,35 +1,17 @@
|
|||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import Dict, List, Type
|
||||
from pathlib import Path
|
||||
|
||||
import core.services
|
||||
from core import configservices, utils
|
||||
from core import utils
|
||||
from core.configservice.manager import ConfigServiceManager
|
||||
from core.emane.modelmanager import EmaneModelManager
|
||||
from core.emulator.session import Session
|
||||
from core.executables import get_requirements
|
||||
from core.services.coreservices import ServiceManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def signal_handler(signal_number: int, _) -> None:
|
||||
"""
|
||||
Handle signals and force an exit with cleanup.
|
||||
|
||||
:param signal_number: signal number
|
||||
:param _: ignored
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info("caught signal: %s", signal_number)
|
||||
sys.exit(signal_number)
|
||||
|
||||
|
||||
signal.signal(signal.SIGHUP, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGUSR1, signal_handler)
|
||||
signal.signal(signal.SIGUSR2, signal_handler)
|
||||
DEFAULT_EMANE_PREFIX: str = "/usr"
|
||||
|
||||
|
||||
class CoreEmu:
|
||||
|
@ -37,7 +19,7 @@ class CoreEmu:
|
|||
Provides logic for creating and configuring CORE sessions and the nodes within them.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, str] = None) -> None:
|
||||
def __init__(self, config: dict[str, str] = None) -> None:
|
||||
"""
|
||||
Create a CoreEmu object.
|
||||
|
||||
|
@ -47,31 +29,24 @@ class CoreEmu:
|
|||
os.umask(0)
|
||||
|
||||
# configuration
|
||||
if config is None:
|
||||
config = {}
|
||||
self.config: Dict[str, str] = config
|
||||
config = config if config else {}
|
||||
self.config: dict[str, str] = config
|
||||
|
||||
# session management
|
||||
self.sessions: Dict[int, Session] = {}
|
||||
self.sessions: dict[int, Session] = {}
|
||||
|
||||
# load services
|
||||
self.service_errors: List[str] = []
|
||||
self.load_services()
|
||||
|
||||
# config services
|
||||
self.service_errors: list[str] = []
|
||||
self.service_manager: ConfigServiceManager = 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)
|
||||
self._load_services()
|
||||
|
||||
# check and load emane
|
||||
self.has_emane: bool = False
|
||||
self._load_emane()
|
||||
|
||||
# check executables exist on path
|
||||
self._validate_env()
|
||||
|
||||
# catch exit event
|
||||
atexit.register(self.shutdown)
|
||||
|
||||
def _validate_env(self) -> None:
|
||||
"""
|
||||
Validates executables CORE depends on exist on path.
|
||||
|
@ -83,23 +58,54 @@ class CoreEmu:
|
|||
for requirement in get_requirements(use_ovs):
|
||||
utils.which(requirement, required=True)
|
||||
|
||||
def load_services(self) -> None:
|
||||
def _load_services(self) -> None:
|
||||
"""
|
||||
Loads default and custom services for use within CORE.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
# load default services
|
||||
self.service_errors = core.services.load()
|
||||
|
||||
self.service_errors = ServiceManager.load_locals()
|
||||
# load custom services
|
||||
service_paths = self.config.get("custom_services_dir")
|
||||
logging.debug("custom service paths: %s", service_paths)
|
||||
if service_paths:
|
||||
logger.debug("custom service paths: %s", service_paths)
|
||||
if service_paths is not None:
|
||||
for service_path in service_paths.split(","):
|
||||
service_path = service_path.strip()
|
||||
service_path = Path(service_path.strip())
|
||||
custom_service_errors = ServiceManager.add_services(service_path)
|
||||
self.service_errors.extend(custom_service_errors)
|
||||
# load default config services
|
||||
self.service_manager.load_locals()
|
||||
# load custom config services
|
||||
custom_dir = self.config.get("custom_config_services_dir")
|
||||
if custom_dir is not None:
|
||||
custom_dir = Path(custom_dir)
|
||||
self.service_manager.load(custom_dir)
|
||||
|
||||
def _load_emane(self) -> None:
|
||||
"""
|
||||
Check if emane is installed and load models.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
# check for emane
|
||||
path = utils.which("emane", required=False)
|
||||
self.has_emane = path is not None
|
||||
if not self.has_emane:
|
||||
logger.info("emane is not installed, emane functionality disabled")
|
||||
return
|
||||
# get version
|
||||
emane_version = utils.cmd("emane --version")
|
||||
logger.info("using emane: %s", emane_version)
|
||||
emane_prefix = self.config.get("emane_prefix", DEFAULT_EMANE_PREFIX)
|
||||
emane_prefix = Path(emane_prefix)
|
||||
EmaneModelManager.load_locals(emane_prefix)
|
||||
# load custom models
|
||||
custom_path = self.config.get("emane_models_dir")
|
||||
if custom_path is not None:
|
||||
logger.info("loading custom emane models: %s", custom_path)
|
||||
custom_path = Path(custom_path)
|
||||
EmaneModelManager.load(custom_path, emane_prefix)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
|
@ -107,14 +113,12 @@ class CoreEmu:
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
logging.info("shutting down all sessions")
|
||||
sessions = self.sessions.copy()
|
||||
self.sessions.clear()
|
||||
for _id in sessions:
|
||||
session = sessions[_id]
|
||||
logger.info("shutting down all sessions")
|
||||
while self.sessions:
|
||||
_, session = self.sessions.popitem()
|
||||
session.shutdown()
|
||||
|
||||
def create_session(self, _id: int = None, _cls: Type[Session] = Session) -> Session:
|
||||
def create_session(self, _id: int = None, _cls: type[Session] = Session) -> Session:
|
||||
"""
|
||||
Create a new CORE session.
|
||||
|
||||
|
@ -128,7 +132,7 @@ class CoreEmu:
|
|||
_id += 1
|
||||
session = _cls(_id, config=self.config)
|
||||
session.service_manager = self.service_manager
|
||||
logging.info("created session: %s", _id)
|
||||
logger.info("created session: %s", _id)
|
||||
self.sessions[_id] = session
|
||||
return session
|
||||
|
||||
|
@ -139,14 +143,14 @@ class CoreEmu:
|
|||
:param _id: session id to delete
|
||||
:return: True if deleted, False otherwise
|
||||
"""
|
||||
logging.info("deleting session: %s", _id)
|
||||
logger.info("deleting session: %s", _id)
|
||||
session = self.sessions.pop(_id, None)
|
||||
result = False
|
||||
if session:
|
||||
logging.info("shutting session down: %s", _id)
|
||||
logger.info("shutting session down: %s", _id)
|
||||
session.data_collect()
|
||||
session.shutdown()
|
||||
result = True
|
||||
else:
|
||||
logging.error("session to delete did not exist: %s", _id)
|
||||
logger.error("session to delete did not exist: %s", _id)
|
||||
return result
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
CORE data objects.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import netaddr
|
||||
|
||||
|
@ -24,7 +24,7 @@ class ConfigData:
|
|||
node: int = None
|
||||
object: str = None
|
||||
type: int = None
|
||||
data_types: Tuple[int] = None
|
||||
data_types: tuple[int] = None
|
||||
data_values: str = None
|
||||
captions: str = None
|
||||
bitmap: str = None
|
||||
|
@ -81,8 +81,8 @@ class NodeOptions:
|
|||
model: Optional[str] = "PC"
|
||||
canvas: int = None
|
||||
icon: str = None
|
||||
services: List[str] = field(default_factory=list)
|
||||
config_services: List[str] = field(default_factory=list)
|
||||
services: list[str] = field(default_factory=list)
|
||||
config_services: list[str] = field(default_factory=list)
|
||||
x: float = None
|
||||
y: float = None
|
||||
lat: float = None
|
||||
|
@ -91,6 +91,11 @@ class NodeOptions:
|
|||
server: str = None
|
||||
image: str = None
|
||||
emane: str = None
|
||||
legacy: bool = False
|
||||
# src, dst
|
||||
binds: list[tuple[str, str]] = field(default_factory=list)
|
||||
# src, dst, unique, delete
|
||||
volumes: list[tuple[str, str, bool, bool]] = field(default_factory=list)
|
||||
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
"""
|
||||
|
@ -141,8 +146,9 @@ class InterfaceData:
|
|||
ip4_mask: int = None
|
||||
ip6: str = None
|
||||
ip6_mask: int = None
|
||||
mtu: int = None
|
||||
|
||||
def get_ips(self) -> List[str]:
|
||||
def get_ips(self) -> list[str]:
|
||||
"""
|
||||
Returns a list of ip4 and ip6 addresses when present.
|
||||
|
||||
|
@ -174,6 +180,67 @@ class LinkOptions:
|
|||
key: int = None
|
||||
buffer: int = None
|
||||
|
||||
def update(self, options: "LinkOptions") -> bool:
|
||||
"""
|
||||
Updates current options with values from other options.
|
||||
|
||||
:param options: options to update with
|
||||
:return: True if any value has changed, False otherwise
|
||||
"""
|
||||
changed = False
|
||||
if options.delay is not None and 0 <= options.delay != self.delay:
|
||||
self.delay = options.delay
|
||||
changed = True
|
||||
if options.bandwidth is not None and 0 <= options.bandwidth != self.bandwidth:
|
||||
self.bandwidth = options.bandwidth
|
||||
changed = True
|
||||
if options.loss is not None and 0 <= options.loss != self.loss:
|
||||
self.loss = options.loss
|
||||
changed = True
|
||||
if options.dup is not None and 0 <= options.dup != self.dup:
|
||||
self.dup = options.dup
|
||||
changed = True
|
||||
if options.jitter is not None and 0 <= options.jitter != self.jitter:
|
||||
self.jitter = options.jitter
|
||||
changed = True
|
||||
if options.buffer is not None and 0 <= options.buffer != self.buffer:
|
||||
self.buffer = options.buffer
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
def is_clear(self) -> bool:
|
||||
"""
|
||||
Checks if the current option values represent a clear state.
|
||||
|
||||
:return: True if the current values should clear, False otherwise
|
||||
"""
|
||||
clear = self.delay is None or self.delay <= 0
|
||||
clear &= self.jitter is None or self.jitter <= 0
|
||||
clear &= self.loss is None or self.loss <= 0
|
||||
clear &= self.dup is None or self.dup <= 0
|
||||
clear &= self.bandwidth is None or self.bandwidth <= 0
|
||||
clear &= self.buffer is None or self.buffer <= 0
|
||||
return clear
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""
|
||||
Custom logic to check if this link options is equivalent to another.
|
||||
|
||||
:param other: other object to check
|
||||
:return: True if they are both link options with the same values,
|
||||
False otherwise
|
||||
"""
|
||||
if not isinstance(other, LinkOptions):
|
||||
return False
|
||||
return (
|
||||
self.delay == other.delay
|
||||
and self.jitter == other.jitter
|
||||
and self.loss == other.loss
|
||||
and self.dup == other.dup
|
||||
and self.bandwidth == other.bandwidth
|
||||
and self.buffer == other.buffer
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LinkData:
|
||||
|
|
|
@ -6,19 +6,23 @@ import logging
|
|||
import os
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Tuple
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
import netaddr
|
||||
from fabric import Connection
|
||||
from invoke import UnexpectedExit
|
||||
|
||||
from core import utils
|
||||
from core.emulator.links import CoreLink
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.executables import get_requirements
|
||||
from core.nodes.interface import GreTap
|
||||
from core.nodes.network import CoreNetwork, CtrlNet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
|
@ -44,7 +48,7 @@ class DistributedServer:
|
|||
self.lock: threading.Lock = threading.Lock()
|
||||
|
||||
def remote_cmd(
|
||||
self, cmd: str, env: Dict[str, str] = None, cwd: str = None, wait: bool = True
|
||||
self, cmd: str, env: dict[str, str] = None, cwd: str = None, wait: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Run command remotely using server connection.
|
||||
|
@ -61,7 +65,7 @@ class DistributedServer:
|
|||
replace_env = env is not None
|
||||
if not wait:
|
||||
cmd += " &"
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
"remote cmd server(%s) cwd(%s) wait(%s): %s", self.host, cwd, wait, cmd
|
||||
)
|
||||
try:
|
||||
|
@ -79,31 +83,31 @@ class DistributedServer:
|
|||
stdout, stderr = e.streams_for_display()
|
||||
raise CoreCommandError(e.result.exited, cmd, stdout, stderr)
|
||||
|
||||
def remote_put(self, source: str, destination: str) -> None:
|
||||
def remote_put(self, src_path: Path, dst_path: Path) -> None:
|
||||
"""
|
||||
Push file to remote server.
|
||||
|
||||
:param source: source file to push
|
||||
:param destination: destination file location
|
||||
:param src_path: source file to push
|
||||
:param dst_path: destination file location
|
||||
:return: nothing
|
||||
"""
|
||||
with self.lock:
|
||||
self.conn.put(source, destination)
|
||||
self.conn.put(str(src_path), str(dst_path))
|
||||
|
||||
def remote_put_temp(self, destination: str, data: str) -> None:
|
||||
def remote_put_temp(self, dst_path: Path, data: str) -> None:
|
||||
"""
|
||||
Remote push file contents to a remote server, using a temp file as an
|
||||
intermediate step.
|
||||
|
||||
:param destination: file destination for data
|
||||
:param dst_path: file destination for data
|
||||
:param data: data to store in remote file
|
||||
:return: nothing
|
||||
"""
|
||||
with self.lock:
|
||||
temp = NamedTemporaryFile(delete=False)
|
||||
temp.write(data.encode("utf-8"))
|
||||
temp.write(data.encode())
|
||||
temp.close()
|
||||
self.conn.put(temp.name, destination)
|
||||
self.conn.put(temp.name, str(dst_path))
|
||||
os.unlink(temp.name)
|
||||
|
||||
|
||||
|
@ -119,11 +123,9 @@ class DistributedController:
|
|||
:param session: session
|
||||
"""
|
||||
self.session: "Session" = session
|
||||
self.servers: Dict[str, DistributedServer] = OrderedDict()
|
||||
self.tunnels: Dict[int, Tuple[GreTap, GreTap]] = {}
|
||||
self.address: str = self.session.options.get_config(
|
||||
"distributed_address", default=None
|
||||
)
|
||||
self.servers: dict[str, DistributedServer] = OrderedDict()
|
||||
self.tunnels: dict[int, tuple[GreTap, GreTap]] = {}
|
||||
self.address: str = self.session.options.get("distributed_address")
|
||||
|
||||
def add_server(self, name: str, host: str) -> None:
|
||||
"""
|
||||
|
@ -144,7 +146,7 @@ class DistributedController:
|
|||
f"command({requirement})"
|
||||
)
|
||||
self.servers[name] = server
|
||||
cmd = f"mkdir -p {self.session.session_dir}"
|
||||
cmd = f"mkdir -p {self.session.directory}"
|
||||
server.remote_cmd(cmd)
|
||||
|
||||
def execute(self, func: Callable[[DistributedServer], None]) -> None:
|
||||
|
@ -170,41 +172,55 @@ class DistributedController:
|
|||
tunnels = self.tunnels[key]
|
||||
for tunnel in tunnels:
|
||||
tunnel.shutdown()
|
||||
|
||||
# remove all remote session directories
|
||||
for name in self.servers:
|
||||
server = self.servers[name]
|
||||
cmd = f"rm -rf {self.session.session_dir}"
|
||||
cmd = f"rm -rf {self.session.directory}"
|
||||
server.remote_cmd(cmd)
|
||||
|
||||
# clear tunnels
|
||||
self.tunnels.clear()
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
Start distributed network tunnels.
|
||||
Start distributed network tunnels for control networks.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
for node_id in self.session.nodes:
|
||||
node = self.session.nodes[node_id]
|
||||
if not isinstance(node, CoreNetwork):
|
||||
continue
|
||||
if isinstance(node, CtrlNet) and node.serverintf is not None:
|
||||
mtu = self.session.options.get_int("mtu")
|
||||
for node in self.session.nodes.values():
|
||||
if not isinstance(node, CtrlNet) or node.serverintf is not None:
|
||||
continue
|
||||
for name in self.servers:
|
||||
server = self.servers[name]
|
||||
self.create_gre_tunnel(node, server)
|
||||
self.create_gre_tunnel(node, server, mtu, True)
|
||||
|
||||
def create_gre_tunnels(self, core_link: CoreLink) -> None:
|
||||
"""
|
||||
Creates gre tunnels for a core link with a ptp network connection.
|
||||
|
||||
:param core_link: core link to create gre tunnel for
|
||||
:return: nothing
|
||||
"""
|
||||
if not self.servers:
|
||||
return
|
||||
if not core_link.ptp:
|
||||
raise CoreError(
|
||||
"attempted to create gre tunnel for core link without a ptp network"
|
||||
)
|
||||
mtu = self.session.options.get_int("mtu")
|
||||
for server in self.servers.values():
|
||||
self.create_gre_tunnel(core_link.ptp, server, mtu, True)
|
||||
|
||||
def create_gre_tunnel(
|
||||
self, node: CoreNetwork, server: DistributedServer
|
||||
) -> Tuple[GreTap, GreTap]:
|
||||
self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool
|
||||
) -> tuple[GreTap, GreTap]:
|
||||
"""
|
||||
Create gre tunnel using a pair of gre taps between the local and remote server.
|
||||
|
||||
:param node: node to create gre tunnel for
|
||||
:param server: server to create
|
||||
tunnel for
|
||||
:param server: server to create tunnel for
|
||||
:param mtu: mtu for gre taps
|
||||
:param start: True to start gre taps, False otherwise
|
||||
:return: local and remote gre taps created for tunnel
|
||||
"""
|
||||
host = server.host
|
||||
|
@ -212,23 +228,20 @@ class DistributedController:
|
|||
tunnel = self.tunnels.get(key)
|
||||
if tunnel is not None:
|
||||
return tunnel
|
||||
|
||||
# local to server
|
||||
logging.info(
|
||||
"local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key
|
||||
)
|
||||
local_tap = GreTap(session=self.session, remoteip=host, key=key)
|
||||
local_tap.net_client.set_iface_master(node.brname, local_tap.localname)
|
||||
|
||||
logger.info("local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key)
|
||||
local_tap = GreTap(self.session, host, key=key, mtu=mtu)
|
||||
if start:
|
||||
local_tap.startup()
|
||||
local_tap.net_client.set_iface_master(node.brname, local_tap.localname)
|
||||
# server to local
|
||||
logging.info(
|
||||
logger.info(
|
||||
"remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key
|
||||
)
|
||||
remote_tap = GreTap(
|
||||
session=self.session, remoteip=self.address, key=key, server=server
|
||||
)
|
||||
remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
|
||||
|
||||
remote_tap = GreTap(self.session, self.address, key=key, server=server, mtu=mtu)
|
||||
if start:
|
||||
remote_tap.startup()
|
||||
remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
|
||||
# save tunnels for shutdown
|
||||
tunnel = (local_tap, remote_tap)
|
||||
self.tunnels[key] = tunnel
|
||||
|
@ -244,7 +257,7 @@ class DistributedController:
|
|||
:param node2_id: node two id
|
||||
:return: tunnel key for the node pair
|
||||
"""
|
||||
logging.debug("creating tunnel key for: %s, %s", node1_id, node2_id)
|
||||
logger.debug("creating tunnel key for: %s, %s", node1_id, node2_id)
|
||||
key = (
|
||||
(self.session.id << 16)
|
||||
^ utils.hashkey(node1_id)
|
||||
|
|
|
@ -20,6 +20,17 @@ class MessageFlags(Enum):
|
|||
TTY = 0x40
|
||||
|
||||
|
||||
class ConfigFlags(Enum):
|
||||
"""
|
||||
Configuration flags.
|
||||
"""
|
||||
|
||||
NONE = 0x00
|
||||
REQUEST = 0x01
|
||||
UPDATE = 0x02
|
||||
RESET = 0x03
|
||||
|
||||
|
||||
class NodeTypes(Enum):
|
||||
"""
|
||||
Node types.
|
||||
|
@ -38,6 +49,8 @@ class NodeTypes(Enum):
|
|||
CONTROL_NET = 13
|
||||
DOCKER = 15
|
||||
LXC = 16
|
||||
WIRELESS = 17
|
||||
PODMAN = 18
|
||||
|
||||
|
||||
class LinkTypes(Enum):
|
||||
|
|
145
daemon/core/emulator/hooks.py
Normal file
145
daemon/core/emulator/hooks.py
Normal file
|
@ -0,0 +1,145 @@
|
|||
import logging
|
||||
import subprocess
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from core.emulator.enumerations import EventTypes
|
||||
from core.errors import CoreError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HookManager:
|
||||
"""
|
||||
Provides functionality for managing and running script/callback hooks.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Create a HookManager instance.
|
||||
"""
|
||||
self.script_hooks: dict[EventTypes, dict[str, str]] = {}
|
||||
self.callback_hooks: dict[EventTypes, list[Callable[[], None]]] = {}
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Clear all current hooks.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.script_hooks.clear()
|
||||
self.callback_hooks.clear()
|
||||
|
||||
def add_script_hook(self, state: EventTypes, file_name: str, data: str) -> None:
|
||||
"""
|
||||
Add a hook script to run for a given state.
|
||||
|
||||
:param state: state to run hook on
|
||||
:param file_name: hook file name
|
||||
:param data: file data
|
||||
:return: nothing
|
||||
"""
|
||||
logger.info("setting state hook: %s - %s", state, file_name)
|
||||
state_hooks = self.script_hooks.setdefault(state, {})
|
||||
if file_name in state_hooks:
|
||||
raise CoreError(
|
||||
f"adding duplicate state({state.name}) hook script({file_name})"
|
||||
)
|
||||
state_hooks[file_name] = data
|
||||
|
||||
def delete_script_hook(self, state: EventTypes, file_name: str) -> None:
|
||||
"""
|
||||
Delete a script hook from a given state.
|
||||
|
||||
:param state: state to delete script hook from
|
||||
:param file_name: name of script to delete
|
||||
:return: nothing
|
||||
"""
|
||||
state_hooks = self.script_hooks.get(state, {})
|
||||
if file_name not in state_hooks:
|
||||
raise CoreError(
|
||||
f"deleting state({state.name}) hook script({file_name}) "
|
||||
"that does not exist"
|
||||
)
|
||||
del state_hooks[file_name]
|
||||
|
||||
def add_callback_hook(
|
||||
self, state: EventTypes, hook: Callable[[EventTypes], None]
|
||||
) -> None:
|
||||
"""
|
||||
Add a hook callback to run for a state.
|
||||
|
||||
:param state: state to add hook for
|
||||
:param hook: callback to run
|
||||
:return: nothing
|
||||
"""
|
||||
hooks = self.callback_hooks.setdefault(state, [])
|
||||
if hook in hooks:
|
||||
name = getattr(callable, "__name__", repr(hook))
|
||||
raise CoreError(
|
||||
f"adding duplicate state({state.name}) hook callback({name})"
|
||||
)
|
||||
hooks.append(hook)
|
||||
|
||||
def delete_callback_hook(
|
||||
self, state: EventTypes, hook: Callable[[EventTypes], None]
|
||||
) -> None:
|
||||
"""
|
||||
Delete a state hook.
|
||||
|
||||
:param state: state to delete hook for
|
||||
:param hook: hook to delete
|
||||
:return: nothing
|
||||
"""
|
||||
hooks = self.callback_hooks.get(state, [])
|
||||
if hook not in hooks:
|
||||
name = getattr(callable, "__name__", repr(hook))
|
||||
raise CoreError(
|
||||
f"deleting state({state.name}) hook callback({name}) "
|
||||
"that does not exist"
|
||||
)
|
||||
hooks.remove(hook)
|
||||
|
||||
def run_hooks(
|
||||
self, state: EventTypes, directory: Path, env: dict[str, str]
|
||||
) -> None:
|
||||
"""
|
||||
Run all hooks for the current state.
|
||||
|
||||
:param state: state to run hooks for
|
||||
:param directory: directory to run script hooks within
|
||||
:param env: environment to run script hooks with
|
||||
:return: nothing
|
||||
"""
|
||||
for state_hooks in self.script_hooks.get(state, {}):
|
||||
for file_name, data in state_hooks.items():
|
||||
logger.info("running hook %s", file_name)
|
||||
file_path = directory / file_name
|
||||
log_path = directory / f"{file_name}.log"
|
||||
try:
|
||||
with file_path.open("w") as f:
|
||||
f.write(data)
|
||||
with log_path.open("w") as f:
|
||||
args = ["/bin/sh", file_name]
|
||||
subprocess.check_call(
|
||||
args,
|
||||
stdout=f,
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
cwd=directory,
|
||||
env=env,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as e:
|
||||
raise CoreError(
|
||||
f"failure running state({state.name}) "
|
||||
f"hook script({file_name}): {e}"
|
||||
)
|
||||
for hook in self.callback_hooks.get(state, []):
|
||||
try:
|
||||
hook()
|
||||
except Exception as e:
|
||||
name = getattr(callable, "__name__", repr(hook))
|
||||
raise CoreError(
|
||||
f"failure running state({state.name}) "
|
||||
f"hook callback({name}): {e}"
|
||||
)
|
257
daemon/core/emulator/links.py
Normal file
257
daemon/core/emulator/links.py
Normal file
|
@ -0,0 +1,257 @@
|
|||
"""
|
||||
Provides functionality for maintaining information about known links
|
||||
for a session.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import ValuesView
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from core.emulator.data import LinkData, LinkOptions
|
||||
from core.emulator.enumerations import LinkTypes, MessageFlags
|
||||
from core.errors import CoreError
|
||||
from core.nodes.base import NodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import PtpNet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
LinkKeyType = tuple[int, Optional[int], int, Optional[int]]
|
||||
|
||||
|
||||
def create_key(
|
||||
node1: NodeBase,
|
||||
iface1: Optional[CoreInterface],
|
||||
node2: NodeBase,
|
||||
iface2: Optional[CoreInterface],
|
||||
) -> LinkKeyType:
|
||||
"""
|
||||
Creates a unique key for tracking links.
|
||||
|
||||
:param node1: first node in link
|
||||
:param iface1: node1 interface
|
||||
:param node2: second node in link
|
||||
:param iface2: node2 interface
|
||||
:return: link key
|
||||
"""
|
||||
iface1_id = iface1.id if iface1 else None
|
||||
iface2_id = iface2.id if iface2 else None
|
||||
if node1.id < node2.id:
|
||||
return node1.id, iface1_id, node2.id, iface2_id
|
||||
else:
|
||||
return node2.id, iface2_id, node1.id, iface1_id
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoreLink:
|
||||
"""
|
||||
Provides a core link data structure.
|
||||
"""
|
||||
|
||||
node1: NodeBase
|
||||
iface1: Optional[CoreInterface]
|
||||
node2: NodeBase
|
||||
iface2: Optional[CoreInterface]
|
||||
ptp: PtpNet = None
|
||||
label: str = None
|
||||
color: str = None
|
||||
|
||||
def key(self) -> LinkKeyType:
|
||||
"""
|
||||
Retrieve the key for this link.
|
||||
|
||||
:return: link key
|
||||
"""
|
||||
return create_key(self.node1, self.iface1, self.node2, self.iface2)
|
||||
|
||||
def is_unidirectional(self) -> bool:
|
||||
"""
|
||||
Checks if this link is considered unidirectional, due to current
|
||||
iface configurations.
|
||||
|
||||
:return: True if unidirectional, False otherwise
|
||||
"""
|
||||
unidirectional = False
|
||||
if self.iface1 and self.iface2:
|
||||
unidirectional = self.iface1.options != self.iface2.options
|
||||
return unidirectional
|
||||
|
||||
def options(self) -> LinkOptions:
|
||||
"""
|
||||
Retrieve the options for this link.
|
||||
|
||||
:return: options for this link
|
||||
"""
|
||||
if self.is_unidirectional():
|
||||
options = self.iface1.options
|
||||
else:
|
||||
if self.iface1:
|
||||
options = self.iface1.options
|
||||
else:
|
||||
options = self.iface2.options
|
||||
return options
|
||||
|
||||
def get_data(self, message_type: MessageFlags, source: str = None) -> LinkData:
|
||||
"""
|
||||
Create link data for this link.
|
||||
|
||||
:param message_type: link data message type
|
||||
:param source: source for this data
|
||||
:return: link data
|
||||
"""
|
||||
iface1_data = self.iface1.get_data() if self.iface1 else None
|
||||
iface2_data = self.iface2.get_data() if self.iface2 else None
|
||||
return LinkData(
|
||||
message_type=message_type,
|
||||
type=LinkTypes.WIRED,
|
||||
node1_id=self.node1.id,
|
||||
node2_id=self.node2.id,
|
||||
iface1=iface1_data,
|
||||
iface2=iface2_data,
|
||||
options=self.options(),
|
||||
label=self.label,
|
||||
color=self.color,
|
||||
source=source,
|
||||
)
|
||||
|
||||
def get_data_unidirectional(self, source: str = None) -> LinkData:
|
||||
"""
|
||||
Create other unidirectional link data.
|
||||
|
||||
:param source: source for this data
|
||||
:return: unidirectional link data
|
||||
"""
|
||||
iface1_data = self.iface1.get_data() if self.iface1 else None
|
||||
iface2_data = self.iface2.get_data() if self.iface2 else None
|
||||
return LinkData(
|
||||
message_type=MessageFlags.NONE,
|
||||
type=LinkTypes.WIRED,
|
||||
node1_id=self.node2.id,
|
||||
node2_id=self.node1.id,
|
||||
iface1=iface2_data,
|
||||
iface2=iface1_data,
|
||||
options=self.iface2.options,
|
||||
label=self.label,
|
||||
color=self.color,
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
class LinkManager:
|
||||
"""
|
||||
Provides core link management.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Create a LinkManager instance.
|
||||
"""
|
||||
self._links: dict[LinkKeyType, CoreLink] = {}
|
||||
self._node_links: dict[int, dict[LinkKeyType, CoreLink]] = {}
|
||||
|
||||
def add(self, core_link: CoreLink) -> None:
|
||||
"""
|
||||
Add a core link to be tracked.
|
||||
|
||||
:param core_link: link to track
|
||||
:return: nothing
|
||||
"""
|
||||
node1, iface1 = core_link.node1, core_link.iface1
|
||||
node2, iface2 = core_link.node2, core_link.iface2
|
||||
if core_link.key() in self._links:
|
||||
raise CoreError(
|
||||
f"node1({node1.name}) iface1({iface1.id}) "
|
||||
f"node2({node2.name}) iface2({iface2.id}) link already exists"
|
||||
)
|
||||
logger.info(
|
||||
"adding link from node(%s:%s) to node(%s:%s)",
|
||||
node1.name,
|
||||
iface1.name if iface1 else None,
|
||||
node2.name,
|
||||
iface2.name if iface2 else None,
|
||||
)
|
||||
self._links[core_link.key()] = core_link
|
||||
node1_links = self._node_links.setdefault(node1.id, {})
|
||||
node1_links[core_link.key()] = core_link
|
||||
node2_links = self._node_links.setdefault(node2.id, {})
|
||||
node2_links[core_link.key()] = core_link
|
||||
|
||||
def delete(
|
||||
self,
|
||||
node1: NodeBase,
|
||||
iface1: Optional[CoreInterface],
|
||||
node2: NodeBase,
|
||||
iface2: Optional[CoreInterface],
|
||||
) -> CoreLink:
|
||||
"""
|
||||
Remove a link from being tracked.
|
||||
|
||||
:param node1: first node in link
|
||||
:param iface1: node1 interface
|
||||
:param node2: second node in link
|
||||
:param iface2: node2 interface
|
||||
:return: removed core link
|
||||
"""
|
||||
key = create_key(node1, iface1, node2, iface2)
|
||||
if key not in self._links:
|
||||
raise CoreError(
|
||||
f"node1({node1.name}) iface1({iface1.id}) "
|
||||
f"node2({node2.name}) iface2({iface2.id}) is not linked"
|
||||
)
|
||||
logger.info(
|
||||
"deleting link from node(%s:%s) to node(%s:%s)",
|
||||
node1.name,
|
||||
iface1.name if iface1 else None,
|
||||
node2.name,
|
||||
iface2.name if iface2 else None,
|
||||
)
|
||||
node1_links = self._node_links[node1.id]
|
||||
node1_links.pop(key)
|
||||
node2_links = self._node_links[node2.id]
|
||||
node2_links.pop(key)
|
||||
return self._links.pop(key)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Resets and clears all tracking information.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self._links.clear()
|
||||
self._node_links.clear()
|
||||
|
||||
def get_link(
|
||||
self,
|
||||
node1: NodeBase,
|
||||
iface1: Optional[CoreInterface],
|
||||
node2: NodeBase,
|
||||
iface2: Optional[CoreInterface],
|
||||
) -> Optional[CoreLink]:
|
||||
"""
|
||||
Retrieve a link for provided values.
|
||||
|
||||
:param node1: first node in link
|
||||
:param iface1: interface for node1
|
||||
:param node2: second node in link
|
||||
:param iface2: interface for node2
|
||||
:return: core link if present, None otherwise
|
||||
"""
|
||||
key = create_key(node1, iface1, node2, iface2)
|
||||
return self._links.get(key)
|
||||
|
||||
def links(self) -> ValuesView[CoreLink]:
|
||||
"""
|
||||
Retrieve all known links
|
||||
|
||||
:return: iterator for all known links
|
||||
"""
|
||||
return self._links.values()
|
||||
|
||||
def node_links(self, node: NodeBase) -> ValuesView[CoreLink]:
|
||||
"""
|
||||
Retrieve all links for a given node.
|
||||
|
||||
:param node: node to get links for
|
||||
:return: node links
|
||||
"""
|
||||
return self._node_links.get(node.id, {}).values()
|
File diff suppressed because it is too large
Load diff
|
@ -1,93 +1,87 @@
|
|||
from typing import Any, List
|
||||
from typing import Optional
|
||||
|
||||
from core.config import ConfigurableManager, ConfigurableOptions, Configuration
|
||||
from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs
|
||||
from core.config import ConfigBool, ConfigInt, ConfigString, Configuration
|
||||
from core.errors import CoreError
|
||||
from core.plugins.sdt import Sdt
|
||||
|
||||
|
||||
class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
||||
class SessionConfig:
|
||||
"""
|
||||
Provides session configuration.
|
||||
"""
|
||||
|
||||
name: str = "session"
|
||||
options: List[Configuration] = [
|
||||
Configuration(
|
||||
_id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network"
|
||||
options: list[Configuration] = [
|
||||
ConfigString(id="controlnet", label="Control Network"),
|
||||
ConfigString(id="controlnet0", label="Control Network 0"),
|
||||
ConfigString(id="controlnet1", label="Control Network 1"),
|
||||
ConfigString(id="controlnet2", label="Control Network 2"),
|
||||
ConfigString(id="controlnet3", label="Control Network 3"),
|
||||
ConfigString(id="controlnet_updown_script", label="Control Network Script"),
|
||||
ConfigBool(id="enablerj45", default="1", label="Enable RJ45s"),
|
||||
ConfigBool(id="preservedir", default="0", label="Preserve session dir"),
|
||||
ConfigBool(id="enablesdt", default="0", label="Enable SDT3D output"),
|
||||
ConfigString(id="sdturl", default=Sdt.DEFAULT_SDT_URL, label="SDT3D URL"),
|
||||
ConfigBool(id="ovs", default="0", label="Enable OVS"),
|
||||
ConfigInt(id="platform_id_start", default="1", label="EMANE Platform ID Start"),
|
||||
ConfigInt(id="nem_id_start", default="1", label="EMANE NEM ID Start"),
|
||||
ConfigBool(id="link_enabled", default="1", label="EMANE Links?"),
|
||||
ConfigInt(
|
||||
id="loss_threshold", default="30", label="EMANE Link Loss Threshold (%)"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet0", _type=ConfigDataTypes.STRING, label="Control Network 0"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet1", _type=ConfigDataTypes.STRING, label="Control Network 1"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet2", _type=ConfigDataTypes.STRING, label="Control Network 2"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet3", _type=ConfigDataTypes.STRING, label="Control Network 3"
|
||||
),
|
||||
Configuration(
|
||||
_id="controlnet_updown_script",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Control Network Script",
|
||||
),
|
||||
Configuration(
|
||||
_id="enablerj45",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="1",
|
||||
label="Enable RJ45s",
|
||||
),
|
||||
Configuration(
|
||||
_id="preservedir",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
label="Preserve session dir",
|
||||
),
|
||||
Configuration(
|
||||
_id="enablesdt",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
label="Enable SDT3D output",
|
||||
),
|
||||
Configuration(
|
||||
_id="sdturl",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
default=Sdt.DEFAULT_SDT_URL,
|
||||
label="SDT3D URL",
|
||||
),
|
||||
Configuration(
|
||||
_id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS"
|
||||
ConfigInt(
|
||||
id="link_interval", default="1", label="EMANE Link Check Interval (sec)"
|
||||
),
|
||||
ConfigInt(id="link_timeout", default="4", label="EMANE Link Timeout (sec)"),
|
||||
ConfigInt(id="mtu", default="0", label="MTU for All Devices"),
|
||||
]
|
||||
config_type: RegisterTlvs = RegisterTlvs.UTILITY
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.set_configs(self.default_values())
|
||||
|
||||
def get_config(
|
||||
self,
|
||||
_id: str,
|
||||
node_id: int = ConfigurableManager._default_node,
|
||||
config_type: str = ConfigurableManager._default_type,
|
||||
default: Any = None,
|
||||
) -> str:
|
||||
def __init__(self, config: dict[str, str] = None) -> None:
|
||||
"""
|
||||
Retrieves a specific configuration for a node and configuration type.
|
||||
Create a SessionConfig instance.
|
||||
|
||||
: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
|
||||
:param config: configuration to initialize with
|
||||
"""
|
||||
value = super().get_config(_id, node_id, config_type, default)
|
||||
if value == "":
|
||||
value = default
|
||||
return value
|
||||
self._config: dict[str, str] = {x.id: x.default for x in self.options}
|
||||
self._config.update(config or {})
|
||||
|
||||
def get_config_bool(self, name: str, default: Any = None) -> bool:
|
||||
def update(self, config: dict[str, str]) -> None:
|
||||
"""
|
||||
Update current configuration with provided values.
|
||||
|
||||
:param config: configuration to update with
|
||||
:return: nothing
|
||||
"""
|
||||
self._config.update(config)
|
||||
|
||||
def set(self, name: str, value: str) -> None:
|
||||
"""
|
||||
Set a configuration value.
|
||||
|
||||
:param name: name of configuration to set
|
||||
:param value: value to set
|
||||
:return: nothing
|
||||
"""
|
||||
self._config[name] = value
|
||||
|
||||
def get(self, name: str, default: str = None) -> Optional[str]:
|
||||
"""
|
||||
Retrieve configuration value.
|
||||
|
||||
:param name: name of configuration to get
|
||||
:param default: value to return as default
|
||||
:return: return found configuration value or default
|
||||
"""
|
||||
return self._config.get(name, default)
|
||||
|
||||
def all(self) -> dict[str, str]:
|
||||
"""
|
||||
Retrieve all configuration options.
|
||||
|
||||
:return: configuration value dict
|
||||
"""
|
||||
return self._config
|
||||
|
||||
def get_bool(self, name: str, default: bool = None) -> bool:
|
||||
"""
|
||||
Get configuration value as a boolean.
|
||||
|
||||
|
@ -95,12 +89,15 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
|||
:param default: default value if not found
|
||||
:return: boolean for configuration value
|
||||
"""
|
||||
value = self.get_config(name)
|
||||
value = self._config.get(name)
|
||||
if value is None and default is None:
|
||||
raise CoreError(f"missing session options for {name}")
|
||||
if value is None:
|
||||
return default
|
||||
return value.lower() == "true"
|
||||
else:
|
||||
return value.lower() == "true"
|
||||
|
||||
def get_config_int(self, name: str, default: Any = None) -> int:
|
||||
def get_int(self, name: str, default: int = None) -> int:
|
||||
"""
|
||||
Get configuration value as int.
|
||||
|
||||
|
@ -108,7 +105,10 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
|||
:param default: default value if not found
|
||||
:return: int for configuration value
|
||||
"""
|
||||
value = self.get_config(name, default=default)
|
||||
if value is not None:
|
||||
value = int(value)
|
||||
return value
|
||||
value = self._config.get(name)
|
||||
if value is None and default is None:
|
||||
raise CoreError(f"missing session options for {name}")
|
||||
if value is None:
|
||||
return default
|
||||
else:
|
||||
return int(value)
|
||||
|
|
|
@ -11,7 +11,7 @@ class CoreCommandError(subprocess.CalledProcessError):
|
|||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"Command({self.cmd}), Status({self.returncode}):\n"
|
||||
f"command({self.cmd}), status({self.returncode}):\n"
|
||||
f"stdout: {self.output}\nstderr: {self.stderr}"
|
||||
)
|
||||
|
||||
|
@ -46,3 +46,11 @@ class CoreServiceBootError(Exception):
|
|||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreConfigError(Exception):
|
||||
"""
|
||||
Used when there is an error defining a configurable option.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
@ -1,34 +1,33 @@
|
|||
from typing import List
|
||||
|
||||
BASH: str = "bash"
|
||||
VNODED: str = "vnoded"
|
||||
VCMD: str = "vcmd"
|
||||
SYSCTL: str = "sysctl"
|
||||
IP: str = "ip"
|
||||
ETHTOOL: str = "ethtool"
|
||||
TC: str = "tc"
|
||||
EBTABLES: str = "ebtables"
|
||||
IP: str = "ip"
|
||||
MOUNT: str = "mount"
|
||||
UMOUNT: str = "umount"
|
||||
NFTABLES: str = "nft"
|
||||
OVS_VSCTL: str = "ovs-vsctl"
|
||||
SYSCTL: str = "sysctl"
|
||||
TC: str = "tc"
|
||||
TEST: str = "test"
|
||||
UMOUNT: str = "umount"
|
||||
VCMD: str = "vcmd"
|
||||
VNODED: str = "vnoded"
|
||||
|
||||
COMMON_REQUIREMENTS: List[str] = [
|
||||
COMMON_REQUIREMENTS: list[str] = [
|
||||
BASH,
|
||||
EBTABLES,
|
||||
ETHTOOL,
|
||||
IP,
|
||||
MOUNT,
|
||||
NFTABLES,
|
||||
SYSCTL,
|
||||
TC,
|
||||
UMOUNT,
|
||||
TEST,
|
||||
UMOUNT,
|
||||
VCMD,
|
||||
VNODED,
|
||||
]
|
||||
VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD]
|
||||
OVS_REQUIREMENTS: List[str] = [OVS_VSCTL]
|
||||
OVS_REQUIREMENTS: list[str] = [OVS_VSCTL]
|
||||
|
||||
|
||||
def get_requirements(use_ovs: bool) -> List[str]:
|
||||
def get_requirements(use_ovs: bool) -> list[str]:
|
||||
"""
|
||||
Retrieve executable requirements needed to run CORE.
|
||||
|
||||
|
@ -38,6 +37,4 @@ def get_requirements(use_ovs: bool) -> List[str]:
|
|||
requirements = COMMON_REQUIREMENTS
|
||||
if use_ovs:
|
||||
requirements += OVS_REQUIREMENTS
|
||||
else:
|
||||
requirements += VCMD_REQUIREMENTS
|
||||
return requirements
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
import logging
|
||||
import math
|
||||
import tkinter as tk
|
||||
from tkinter import PhotoImage, font, ttk
|
||||
from tkinter import PhotoImage, font, messagebox, ttk
|
||||
from tkinter.ttk import Progressbar
|
||||
from typing import Any, Dict, Optional, Type
|
||||
from typing import Any, Optional
|
||||
|
||||
import grpc
|
||||
|
||||
from core.gui import appconfig, themes
|
||||
from core.gui import appconfig, images
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui import themes
|
||||
from core.gui.appconfig import GuiConfig
|
||||
from core.gui.coreclient import CoreClient
|
||||
from core.gui.dialogs.error import ErrorDialog
|
||||
from core.gui.frames.base import InfoFrameBase
|
||||
from core.gui.frames.default import DefaultInfoFrame
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.graph.manager import CanvasManager
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.menubar import Menubar
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.statusbar import StatusBar
|
||||
from core.gui.themes import PADY
|
||||
from core.gui.toolbar import Toolbar
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
WIDTH: int = 1000
|
||||
HEIGHT: int = 800
|
||||
|
||||
|
@ -29,13 +31,13 @@ class Application(ttk.Frame):
|
|||
def __init__(self, proxy: bool, session_id: int = None) -> None:
|
||||
super().__init__()
|
||||
# load node icons
|
||||
NodeUtils.setup()
|
||||
nutils.setup()
|
||||
|
||||
# widgets
|
||||
self.menubar: Optional[Menubar] = None
|
||||
self.toolbar: Optional[Toolbar] = None
|
||||
self.right_frame: Optional[ttk.Frame] = None
|
||||
self.canvas: Optional[CanvasGraph] = None
|
||||
self.manager: Optional[CanvasManager] = None
|
||||
self.statusbar: Optional[StatusBar] = None
|
||||
self.progress: Optional[Progressbar] = None
|
||||
self.infobar: Optional[ttk.Frame] = None
|
||||
|
@ -43,7 +45,7 @@ class Application(ttk.Frame):
|
|||
self.show_infobar: tk.BooleanVar = tk.BooleanVar(value=False)
|
||||
|
||||
# fonts
|
||||
self.fonts_size: Dict[str, int] = {}
|
||||
self.fonts_size: dict[str, int] = {}
|
||||
self.icon_text_font: Optional[font.Font] = None
|
||||
self.edge_font: Optional[font.Font] = None
|
||||
|
||||
|
@ -77,7 +79,7 @@ class Application(ttk.Frame):
|
|||
self.master.title("CORE")
|
||||
self.center()
|
||||
self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
image = Images.get(ImageEnum.CORE, 16)
|
||||
image = images.from_enum(ImageEnum.CORE, width=images.DIALOG_SIZE)
|
||||
self.master.tk.call("wm", "iconphoto", self.master._w, image)
|
||||
self.master.option_add("*tearOff", tk.FALSE)
|
||||
self.setup_file_dialogs()
|
||||
|
@ -136,26 +138,14 @@ class Application(ttk.Frame):
|
|||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
def draw_canvas(self) -> None:
|
||||
canvas_frame = ttk.Frame(self.right_frame)
|
||||
canvas_frame.rowconfigure(0, weight=1)
|
||||
canvas_frame.columnconfigure(0, weight=1)
|
||||
canvas_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=1)
|
||||
self.canvas = CanvasGraph(canvas_frame, self, self.core)
|
||||
self.canvas.grid(sticky=tk.NSEW)
|
||||
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
|
||||
scroll_y.grid(row=0, column=1, sticky=tk.NS)
|
||||
scroll_x = ttk.Scrollbar(
|
||||
canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview
|
||||
)
|
||||
scroll_x.grid(row=1, column=0, sticky=tk.EW)
|
||||
self.canvas.configure(xscrollcommand=scroll_x.set)
|
||||
self.canvas.configure(yscrollcommand=scroll_y.set)
|
||||
self.manager = CanvasManager(self.right_frame, self, self.core)
|
||||
self.manager.notebook.grid(sticky=tk.NSEW)
|
||||
|
||||
def draw_status(self) -> None:
|
||||
self.statusbar = StatusBar(self.right_frame, self)
|
||||
self.statusbar.grid(sticky=tk.EW, columnspan=2)
|
||||
|
||||
def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None:
|
||||
def display_info(self, frame_class: type[InfoFrameBase], **kwargs: Any) -> None:
|
||||
if not self.show_infobar.get():
|
||||
return
|
||||
self.clear_info()
|
||||
|
@ -179,17 +169,30 @@ class Application(ttk.Frame):
|
|||
def hide_info(self) -> None:
|
||||
self.infobar.grid_forget()
|
||||
|
||||
def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
|
||||
logging.exception("app grpc exception", exc_info=e)
|
||||
message = e.details()
|
||||
self.show_error(title, message)
|
||||
def show_grpc_exception(
|
||||
self, message: str, e: grpc.RpcError, blocking: bool = False
|
||||
) -> None:
|
||||
logger.exception("app grpc exception", exc_info=e)
|
||||
dialog = ErrorDialog(self, "GRPC Exception", message, e.details())
|
||||
if blocking:
|
||||
dialog.show()
|
||||
else:
|
||||
self.after(0, lambda: dialog.show())
|
||||
|
||||
def show_exception(self, title: str, e: Exception) -> None:
|
||||
logging.exception("app exception", exc_info=e)
|
||||
self.show_error(title, str(e))
|
||||
def show_exception(self, message: str, e: Exception) -> None:
|
||||
logger.exception("app exception", exc_info=e)
|
||||
self.after(
|
||||
0, lambda: ErrorDialog(self, "App Exception", message, str(e)).show()
|
||||
)
|
||||
|
||||
def show_error(self, title: str, message: str) -> None:
|
||||
self.after(0, lambda: ErrorDialog(self, title, message).show())
|
||||
def show_exception_data(self, title: str, message: str, details: str) -> None:
|
||||
self.after(0, lambda: ErrorDialog(self, title, message, details).show())
|
||||
|
||||
def show_error(self, title: str, message: str, blocking: bool = False) -> None:
|
||||
if blocking:
|
||||
messagebox.showerror(title, message, parent=self)
|
||||
else:
|
||||
self.after(0, lambda: messagebox.showerror(title, message, parent=self))
|
||||
|
||||
def on_closing(self) -> None:
|
||||
if self.toolbar.picker:
|
||||
|
@ -201,15 +204,17 @@ class Application(ttk.Frame):
|
|||
|
||||
def joined_session_update(self) -> None:
|
||||
if self.core.is_runtime():
|
||||
self.menubar.set_state(is_runtime=True)
|
||||
self.toolbar.set_runtime()
|
||||
else:
|
||||
self.menubar.set_state(is_runtime=False)
|
||||
self.toolbar.set_design()
|
||||
|
||||
def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage:
|
||||
return Images.get(image_enum, int(width * self.app_scale))
|
||||
def get_enum_icon(self, image_enum: ImageEnum, *, width: int) -> PhotoImage:
|
||||
return images.from_enum(image_enum, width=width, scale=self.app_scale)
|
||||
|
||||
def get_custom_icon(self, image_file: str, width: int) -> PhotoImage:
|
||||
return Images.get_custom(image_file, int(width * self.app_scale))
|
||||
def get_file_icon(self, file_path: str, *, width: int) -> PhotoImage:
|
||||
return images.from_file(file_path, width=width, scale=self.app_scale)
|
||||
|
||||
def close(self) -> None:
|
||||
self.master.destroy()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Type
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
@ -26,7 +26,7 @@ LOCAL_XMLS_PATH: Path = DATA_PATH.joinpath("xmls").absolute()
|
|||
LOCAL_MOBILITY_PATH: Path = DATA_PATH.joinpath("mobility").absolute()
|
||||
|
||||
# configuration data
|
||||
TERMINALS: Dict[str, str] = {
|
||||
TERMINALS: dict[str, str] = {
|
||||
"xterm": "xterm -e",
|
||||
"aterm": "aterm -e",
|
||||
"eterm": "eterm -e",
|
||||
|
@ -36,7 +36,7 @@ TERMINALS: Dict[str, str] = {
|
|||
"xfce4-terminal": "xfce4-terminal -x",
|
||||
"gnome-terminal": "gnome-terminal --window --",
|
||||
}
|
||||
EDITORS: List[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||
EDITORS: list[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||
|
||||
|
||||
class IndentDumper(yaml.Dumper):
|
||||
|
@ -46,17 +46,17 @@ class IndentDumper(yaml.Dumper):
|
|||
|
||||
class CustomNode(yaml.YAMLObject):
|
||||
yaml_tag: str = "!CustomNode"
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(self, name: str, image: str, services: List[str]) -> None:
|
||||
def __init__(self, name: str, image: str, services: list[str]) -> None:
|
||||
self.name: str = name
|
||||
self.image: str = image
|
||||
self.services: List[str] = services
|
||||
self.services: list[str] = services
|
||||
|
||||
|
||||
class CoreServer(yaml.YAMLObject):
|
||||
yaml_tag: str = "!CoreServer"
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(self, name: str, address: str) -> None:
|
||||
self.name: str = name
|
||||
|
@ -65,7 +65,7 @@ class CoreServer(yaml.YAMLObject):
|
|||
|
||||
class Observer(yaml.YAMLObject):
|
||||
yaml_tag: str = "!Observer"
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(self, name: str, cmd: str) -> None:
|
||||
self.name: str = name
|
||||
|
@ -74,7 +74,7 @@ class Observer(yaml.YAMLObject):
|
|||
|
||||
class PreferencesConfig(yaml.YAMLObject):
|
||||
yaml_tag: str = "!PreferencesConfig"
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -95,7 +95,7 @@ class PreferencesConfig(yaml.YAMLObject):
|
|||
|
||||
class LocationConfig(yaml.YAMLObject):
|
||||
yaml_tag: str = "!LocationConfig"
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -118,41 +118,34 @@ class LocationConfig(yaml.YAMLObject):
|
|||
|
||||
class IpConfigs(yaml.YAMLObject):
|
||||
yaml_tag: str = "!IpConfigs"
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ip4: str = None,
|
||||
ip6: str = None,
|
||||
ip4s: List[str] = None,
|
||||
ip6s: List[str] = None,
|
||||
) -> None:
|
||||
if ip4s is None:
|
||||
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
|
||||
self.ip4s: List[str] = ip4s
|
||||
if ip6s is None:
|
||||
ip6s = ["2001::", "2002::", "a::"]
|
||||
self.ip6s: List[str] = ip6s
|
||||
if ip4 is None:
|
||||
ip4 = self.ip4s[0]
|
||||
self.ip4: str = ip4
|
||||
if ip6 is None:
|
||||
ip6 = self.ip6s[0]
|
||||
self.ip6: str = ip6
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__setstate__(kwargs)
|
||||
|
||||
def __setstate__(self, kwargs):
|
||||
self.ip4s: list[str] = kwargs.get(
|
||||
"ip4s", ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
|
||||
)
|
||||
self.ip4: str = kwargs.get("ip4", self.ip4s[0])
|
||||
self.ip6s: list[str] = kwargs.get("ip6s", ["2001::", "2002::", "a::"])
|
||||
self.ip6: str = kwargs.get("ip6", self.ip6s[0])
|
||||
self.enable_ip4: bool = kwargs.get("enable_ip4", True)
|
||||
self.enable_ip6: bool = kwargs.get("enable_ip6", True)
|
||||
|
||||
|
||||
class GuiConfig(yaml.YAMLObject):
|
||||
yaml_tag: str = "!GuiConfig"
|
||||
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
preferences: PreferencesConfig = None,
|
||||
location: LocationConfig = None,
|
||||
servers: List[CoreServer] = None,
|
||||
nodes: List[CustomNode] = None,
|
||||
recentfiles: List[str] = None,
|
||||
observers: List[Observer] = None,
|
||||
servers: list[CoreServer] = None,
|
||||
nodes: list[CustomNode] = None,
|
||||
recentfiles: list[str] = None,
|
||||
observers: list[Observer] = None,
|
||||
scale: float = 1.0,
|
||||
ips: IpConfigs = None,
|
||||
mac: str = "00:00:00:aa:00:00",
|
||||
|
@ -165,16 +158,16 @@ class GuiConfig(yaml.YAMLObject):
|
|||
self.location: LocationConfig = location
|
||||
if servers is None:
|
||||
servers = []
|
||||
self.servers: List[CoreServer] = servers
|
||||
self.servers: list[CoreServer] = servers
|
||||
if nodes is None:
|
||||
nodes = []
|
||||
self.nodes: List[CustomNode] = nodes
|
||||
self.nodes: list[CustomNode] = nodes
|
||||
if recentfiles is None:
|
||||
recentfiles = []
|
||||
self.recentfiles: List[str] = recentfiles
|
||||
self.recentfiles: list[str] = recentfiles
|
||||
if observers is None:
|
||||
observers = []
|
||||
self.observers: List[Observer] = observers
|
||||
self.observers: list[Observer] = observers
|
||||
self.scale: float = scale
|
||||
if ips is None:
|
||||
ips = IpConfigs()
|
||||
|
@ -185,7 +178,8 @@ class GuiConfig(yaml.YAMLObject):
|
|||
def copy_files(current_path: Path, new_path: Path) -> None:
|
||||
for current_file in current_path.glob("*"):
|
||||
new_file = new_path.joinpath(current_file.name)
|
||||
shutil.copy(current_file, new_file)
|
||||
if not new_file.exists():
|
||||
shutil.copy(current_file, new_file)
|
||||
|
||||
|
||||
def find_terminal() -> Optional[str]:
|
||||
|
@ -197,35 +191,32 @@ def find_terminal() -> Optional[str]:
|
|||
|
||||
|
||||
def check_directory() -> None:
|
||||
if HOME_PATH.exists():
|
||||
return
|
||||
HOME_PATH.mkdir()
|
||||
BACKGROUNDS_PATH.mkdir()
|
||||
CUSTOM_EMANE_PATH.mkdir()
|
||||
CUSTOM_SERVICE_PATH.mkdir()
|
||||
ICONS_PATH.mkdir()
|
||||
MOBILITY_PATH.mkdir()
|
||||
XMLS_PATH.mkdir()
|
||||
SCRIPT_PATH.mkdir()
|
||||
|
||||
HOME_PATH.mkdir(exist_ok=True)
|
||||
BACKGROUNDS_PATH.mkdir(exist_ok=True)
|
||||
CUSTOM_EMANE_PATH.mkdir(exist_ok=True)
|
||||
CUSTOM_SERVICE_PATH.mkdir(exist_ok=True)
|
||||
ICONS_PATH.mkdir(exist_ok=True)
|
||||
MOBILITY_PATH.mkdir(exist_ok=True)
|
||||
XMLS_PATH.mkdir(exist_ok=True)
|
||||
SCRIPT_PATH.mkdir(exist_ok=True)
|
||||
copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
|
||||
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)
|
||||
copy_files(LOCAL_XMLS_PATH, XMLS_PATH)
|
||||
copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH)
|
||||
|
||||
terminal = find_terminal()
|
||||
if "EDITOR" in os.environ:
|
||||
editor = EDITORS[0]
|
||||
else:
|
||||
editor = EDITORS[1]
|
||||
preferences = PreferencesConfig(editor, terminal)
|
||||
config = GuiConfig(preferences=preferences)
|
||||
save(config)
|
||||
if not CONFIG_PATH.exists():
|
||||
terminal = find_terminal()
|
||||
if "EDITOR" in os.environ:
|
||||
editor = EDITORS[0]
|
||||
else:
|
||||
editor = EDITORS[1]
|
||||
preferences = PreferencesConfig(editor, terminal)
|
||||
config = GuiConfig(preferences=preferences)
|
||||
save(config)
|
||||
|
||||
|
||||
def read() -> GuiConfig:
|
||||
with CONFIG_PATH.open("r") as f:
|
||||
return yaml.load(f, Loader=yaml.SafeLoader)
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def save(config: GuiConfig) -> None:
|
||||
|
|
File diff suppressed because it is too large
Load diff
Binary file not shown.
Before Width: | Height: | Size: 230 B |
BIN
daemon/core/gui/data/icons/antenna.png
Normal file
BIN
daemon/core/gui/data/icons/antenna.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 385 B |
BIN
daemon/core/gui/data/icons/podman.png
Normal file
BIN
daemon/core/gui/data/icons/podman.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
daemon/core/gui/data/icons/shadow.png
Normal file
BIN
daemon/core/gui/data/icons/shadow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 326 B |
BIN
daemon/core/gui/data/icons/wireless.png
Normal file
BIN
daemon/core/gui/data/icons/wireless.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
|
@ -1,131 +1,77 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<scenario name="/home/developer/.core/configs/emane-demo-antenna.xml">
|
||||
<scenario name="/tmp/tmpd4t2sxy2">
|
||||
<networks>
|
||||
<network id="5" name="wlan5" model="emane_rfpipe" type="EMANE">
|
||||
<position x="388" y="555" lat="47.57412169587584" lon="-122.12709380504643" alt="2.0"/>
|
||||
<network id="5" name="wlan5" icon="" canvas="0" model="emane_rfpipe" type="EMANE">
|
||||
<position x="388.0" y="555.0" lat="47.574121408201655" lon="-122.12709602379641" alt="2.0"/>
|
||||
</network>
|
||||
</networks>
|
||||
<devices>
|
||||
<device id="1" name="n1" type="mdr" class="" image="">
|
||||
<position x="258" y="147" lat="47.577830502987744" lon="-122.12884551985047" alt="2.0"/>
|
||||
<device id="1" name="n1" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="258.0" y="147.0" lat="47.57783021533393" lon="-122.12884773860046" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="2" name="n2" type="mdr" class="" image="">
|
||||
<position x="526" y="147" lat="47.577830502987744" lon="-122.12523429240828" alt="2.0"/>
|
||||
<device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="526.0" y="147.0" lat="47.57783021533393" lon="-122.12523651115826" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="3" name="n3" type="mdr" class="" image="">
|
||||
<position x="241" y="387" lat="47.57564888355958" lon="-122.12907459024791" alt="2.0"/>
|
||||
<device id="3" name="n3" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="241.0" y="387.0" lat="47.575648595893774" lon="-122.1290768089979" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="4" name="n4" type="mdr" class="" image="">
|
||||
<position x="529" y="385" lat="47.57566706409707" lon="-122.1251938682205" alt="2.0"/>
|
||||
<device id="4" name="n4" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="529.0" y="385.0" lat="47.57566677643136" lon="-122.12519608697049" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
</devices>
|
||||
<links>
|
||||
<link node1="5" node2="1">
|
||||
<iface2 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
<link node1="1" node2="5">
|
||||
<iface1 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="5" node2="2">
|
||||
<iface2 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
<link node1="2" node2="5">
|
||||
<iface1 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="5" node2="3">
|
||||
<iface2 nem="3" id="0" name="eth0" mac="02:02:00:00:00:03" ip4="10.0.0.3" ip4_mask="32" ip6="2001::3" ip6_mask="128"/>
|
||||
<link node1="3" node2="5">
|
||||
<iface1 nem="3" id="0" name="eth0" mac="02:02:00:00:00:03" ip4="10.0.0.3" ip4_mask="32" ip6="2001::3" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="5" node2="4">
|
||||
<iface2 nem="4" id="0" name="eth0" mac="02:02:00:00:00:04" ip4="10.0.0.4" ip4_mask="32" ip6="2001::4" ip6_mask="128"/>
|
||||
<link node1="4" node2="5">
|
||||
<iface1 nem="4" id="0" name="eth0" mac="02:02:00:00:00:04" ip4="10.0.0.4" ip4_mask="32" ip6="2001::4" ip6_mask="128"/>
|
||||
</link>
|
||||
</links>
|
||||
<emane_global_configuration>
|
||||
<emulator>
|
||||
<configuration name="antennaprofilemanifesturi" value="/tmp/emane/antennaprofile.xml"/>
|
||||
<configuration name="controlportendpoint" value="0.0.0.0:47000"/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</emulator>
|
||||
<core>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
</core>
|
||||
</emane_global_configuration>
|
||||
<emane_configurations>
|
||||
<emane_configuration node="5" model="emane_rfpipe">
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="0.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="1"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="propagationmodel" value="2ray"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
<emane_configuration node="1" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
|
@ -140,6 +86,17 @@
|
|||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
|
@ -156,7 +113,10 @@
|
|||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="outofband"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
|
@ -169,6 +129,23 @@
|
|||
</external>
|
||||
</emane_configuration>
|
||||
<emane_configuration node="2" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
|
@ -183,6 +160,17 @@
|
|||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
|
@ -199,7 +187,10 @@
|
|||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="outofband"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
|
@ -212,6 +203,23 @@
|
|||
</external>
|
||||
</emane_configuration>
|
||||
<emane_configuration node="3" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
|
@ -226,6 +234,17 @@
|
|||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
|
@ -242,7 +261,10 @@
|
|||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="outofband"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
|
@ -255,6 +277,23 @@
|
|||
</external>
|
||||
</emane_configuration>
|
||||
<emane_configuration node="4" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
|
@ -269,6 +308,17 @@
|
|||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
|
@ -285,7 +335,84 @@
|
|||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="outofband"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
<configuration name="txpower" value="0.000000"/>
|
||||
</phy>
|
||||
<external>
|
||||
<configuration name="external" value="0"/>
|
||||
<configuration name="platformendpoint" value="127.0.0.1:40001"/>
|
||||
<configuration name="transportendpoint" value="127.0.0.1:50002"/>
|
||||
</external>
|
||||
</emane_configuration>
|
||||
<emane_configuration node="5" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
<configuration name="enablepromiscuousmode" value="0"/>
|
||||
<configuration name="flowcontrolenable" value="0"/>
|
||||
<configuration name="flowcontroltokens" value="10"/>
|
||||
<configuration name="jitter" value="0.000000"/>
|
||||
<configuration name="neighbormetricdeletetime" value="60.000000"/>
|
||||
<configuration name="pcrcurveuri" value="/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml"/>
|
||||
<configuration name="radiometricenable" value="0"/>
|
||||
<configuration name="radiometricreportinterval" value="1.000000"/>
|
||||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
<configuration name="fading.nakagami.m0" value="0.750000"/>
|
||||
<configuration name="fading.nakagami.m1" value="1.000000"/>
|
||||
<configuration name="fading.nakagami.m2" value="200.000000"/>
|
||||
<configuration name="fixedantennagain" value="0.000000"/>
|
||||
<configuration name="fixedantennagainenable" value="1"/>
|
||||
<configuration name="frequency" value="2347000000"/>
|
||||
<configuration name="frequencyofinterest" value="2347000000"/>
|
||||
<configuration name="noisebinsize" value="20"/>
|
||||
<configuration name="noisemaxclampenable" value="0"/>
|
||||
<configuration name="noisemaxmessagepropagation" value="200000"/>
|
||||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="2ray"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
|
@ -298,7 +425,7 @@
|
|||
</external>
|
||||
</emane_configuration>
|
||||
</emane_configurations>
|
||||
<session_origin lat="47.5791667" lon="-122.132322" alt="2.0" scale="150.0"/>
|
||||
<session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
|
||||
<session_options>
|
||||
<configuration name="controlnet" value="172.16.0.0/24"/>
|
||||
<configuration name="controlnet0" value=""/>
|
||||
|
@ -311,10 +438,19 @@
|
|||
<configuration name="enablesdt" value="0"/>
|
||||
<configuration name="sdturl" value="tcp://127.0.0.1:50000/"/>
|
||||
<configuration name="ovs" value="0"/>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
<configuration name="mtu" value="0"/>
|
||||
</session_options>
|
||||
<session_metadata>
|
||||
<configuration name="canvas c1" value="{name {Canvas1}}"/>
|
||||
<configuration name="global_options" value="interface_names=no ip_addresses=yes ipv6_addresses=yes node_labels=yes link_labels=yes show_api=no background_images=no annotations=yes grid=yes traffic_start=0"/>
|
||||
<configuration name="shapes" value="[]"/>
|
||||
<configuration name="edges" value="[]"/>
|
||||
<configuration name="hidden" value="[]"/>
|
||||
<configuration name="canvas" value="{"gridlines": true, "canvases": [{"id": 1, "wallpaper": null, "wallpaper_style": 1, "fit_image": false, "dimensions": [1000, 750]}]}"/>
|
||||
</session_metadata>
|
||||
<default_services>
|
||||
<node type="mdr">
|
||||
|
|
|
@ -1,66 +1,55 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<scenario name="/home/developer/.core/configs/emane-demo-eel.xml">
|
||||
<scenario name="/tmp/tmp2mkcwn17">
|
||||
<networks>
|
||||
<network id="3" name="wlan3" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282" y="317" lat="47.57628519861569" lon="-122.12852212634816" alt="2.0"/>
|
||||
<network id="3" name="wlan3" icon="" canvas="0" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282.0" y="317.0" lat="47.5762849109534" lon="-122.12852434509814" alt="2.0"/>
|
||||
</network>
|
||||
</networks>
|
||||
<devices>
|
||||
<device id="1" name="n1" type="mdr" class="" image="">
|
||||
<position x="153" y="172" lat="47.57760325520506" lon="-122.13026036642295" alt="2.0"/>
|
||||
<device id="1" name="n1" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="153.0" y="172.0" lat="47.577602967549986" lon="-122.13026258517293" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="2" name="n2" type="mdr" class="" image="">
|
||||
<position x="393" y="171" lat="47.57761234513531" lon="-122.12702643140011" alt="2.0"/>
|
||||
<device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="393.0" y="171.0" lat="47.57761205748029" lon="-122.1270286501501" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
</devices>
|
||||
<links>
|
||||
<link node1="3" node2="1">
|
||||
<iface2 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
<link node1="1" node2="3">
|
||||
<iface1 id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="3" node2="2">
|
||||
<iface2 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
<link node1="2" node2="3">
|
||||
<iface1 id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
</link>
|
||||
</links>
|
||||
<emane_global_configuration>
|
||||
<emulator>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="controlportendpoint" value="0.0.0.0:47000"/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</emulator>
|
||||
<core>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
</core>
|
||||
</emane_global_configuration>
|
||||
<emane_configurations>
|
||||
<emane_configuration node="3" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
|
@ -75,6 +64,17 @@
|
|||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
|
@ -91,7 +91,10 @@
|
|||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
|
@ -104,7 +107,7 @@
|
|||
</external>
|
||||
</emane_configuration>
|
||||
</emane_configurations>
|
||||
<session_origin lat="47.5791667" lon="-122.132322" alt="2.0" scale="150.0"/>
|
||||
<session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
|
||||
<session_options>
|
||||
<configuration name="controlnet" value="172.16.0.0/24"/>
|
||||
<configuration name="controlnet0" value=""/>
|
||||
|
@ -117,10 +120,19 @@
|
|||
<configuration name="enablesdt" value="0"/>
|
||||
<configuration name="sdturl" value="tcp://127.0.0.1:50000/"/>
|
||||
<configuration name="ovs" value="0"/>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
<configuration name="mtu" value="0"/>
|
||||
</session_options>
|
||||
<session_metadata>
|
||||
<configuration name="canvas c1" value="{name {Canvas1}}"/>
|
||||
<configuration name="global_options" value="interface_names=no ip_addresses=yes ipv6_addresses=yes node_labels=yes link_labels=yes show_api=no background_images=no annotations=yes grid=yes traffic_start=0"/>
|
||||
<configuration name="shapes" value="[]"/>
|
||||
<configuration name="edges" value="[]"/>
|
||||
<configuration name="hidden" value="[]"/>
|
||||
<configuration name="canvas" value="{"gridlines": true, "canvases": [{"id": 1, "wallpaper": null, "wallpaper_style": 1, "fit_image": false, "dimensions": [1000, 750]}]}"/>
|
||||
</session_metadata>
|
||||
<default_services>
|
||||
<node type="mdr">
|
||||
|
|
|
@ -1,66 +1,55 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<scenario name="/home/developer/.core/configs/emane-demo-files.xml">
|
||||
<scenario name="/tmp/tmpsj4dhmce">
|
||||
<networks>
|
||||
<network id="3" name="wlan3" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282" y="317" lat="47.57628519861569" lon="-122.12852212634816" alt="2.0"/>
|
||||
<network id="3" name="wlan3" icon="" canvas="0" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282.0" y="317.0" lat="47.5762849109534" lon="-122.12852434509814" alt="2.0"/>
|
||||
</network>
|
||||
</networks>
|
||||
<devices>
|
||||
<device id="1" name="n1" type="mdr" class="" image="">
|
||||
<position x="153" y="173" lat="47.57759416527324" lon="-122.13026036642295" alt="2.0"/>
|
||||
<device id="1" name="n1" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="153.0" y="173.0" lat="47.57759387761812" lon="-122.13026258517293" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="2" name="n2" type="mdr" class="" image="">
|
||||
<position x="393" y="171" lat="47.57761234513531" lon="-122.12702643140011" alt="2.0"/>
|
||||
<device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="393.0" y="171.0" lat="47.57761205748029" lon="-122.1270286501501" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
</devices>
|
||||
<links>
|
||||
<link node1="3" node2="1">
|
||||
<iface2 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
<link node1="1" node2="3">
|
||||
<iface1 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="3" node2="2">
|
||||
<iface2 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
<link node1="2" node2="3">
|
||||
<iface1 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
</link>
|
||||
</links>
|
||||
<emane_global_configuration>
|
||||
<emulator>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="controlportendpoint" value="0.0.0.0:47000"/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</emulator>
|
||||
<core>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
</core>
|
||||
</emane_global_configuration>
|
||||
<emane_configurations>
|
||||
<emane_configuration node="3" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
|
@ -75,6 +64,17 @@
|
|||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
|
@ -91,7 +91,10 @@
|
|||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="2ray"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
|
@ -104,7 +107,7 @@
|
|||
</external>
|
||||
</emane_configuration>
|
||||
</emane_configurations>
|
||||
<session_origin lat="47.5791667" lon="-122.132322" alt="2.0" scale="150.0"/>
|
||||
<session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
|
||||
<session_options>
|
||||
<configuration name="controlnet" value="172.16.0.0/24"/>
|
||||
<configuration name="controlnet0" value=""/>
|
||||
|
@ -117,10 +120,19 @@
|
|||
<configuration name="enablesdt" value="0"/>
|
||||
<configuration name="sdturl" value="tcp://127.0.0.1:50000/"/>
|
||||
<configuration name="ovs" value="0"/>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
<configuration name="mtu" value="0"/>
|
||||
</session_options>
|
||||
<session_metadata>
|
||||
<configuration name="canvas c1" value="{name {Canvas1}}"/>
|
||||
<configuration name="global_options" value="interface_names=no ip_addresses=yes ipv6_addresses=yes node_labels=yes link_labels=yes show_api=no background_images=no annotations=yes grid=yes traffic_start=0"/>
|
||||
<configuration name="shapes" value="[]"/>
|
||||
<configuration name="edges" value="[]"/>
|
||||
<configuration name="hidden" value="[]"/>
|
||||
<configuration name="canvas" value="{"gridlines": true, "canvases": [{"id": 1, "wallpaper": null, "wallpaper_style": 1, "fit_image": false, "dimensions": [1000, 750]}]}"/>
|
||||
</session_metadata>
|
||||
<default_services>
|
||||
<node type="mdr">
|
||||
|
|
|
@ -1,66 +1,55 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<scenario name="/home/developer/.core/configs/emane-demo-gpsd.xml">
|
||||
<scenario name="/tmp/tmp081pn3j9">
|
||||
<networks>
|
||||
<network id="3" name="wlan3" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282" y="317" lat="47.57628519861569" lon="-122.12852212634816" alt="2.0"/>
|
||||
<network id="3" name="wlan3" icon="" canvas="0" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282.0" y="317.0" lat="47.5762849109534" lon="-122.12852434509814" alt="2.0"/>
|
||||
</network>
|
||||
</networks>
|
||||
<devices>
|
||||
<device id="1" name="n1" type="mdr" class="" image="">
|
||||
<position x="153" y="173" lat="47.57759416527324" lon="-122.13026036642295" alt="2.0"/>
|
||||
<device id="1" name="n1" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="153.0" y="173.0" lat="47.57759387761812" lon="-122.13026258517293" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="2" name="n2" type="mdr" class="" image="">
|
||||
<position x="393" y="171" lat="47.57761234513531" lon="-122.12702643140011" alt="2.0"/>
|
||||
<device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="393.0" y="171.0" lat="47.57761205748029" lon="-122.1270286501501" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
</devices>
|
||||
<links>
|
||||
<link node1="3" node2="1">
|
||||
<iface2 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
<link node1="1" node2="3">
|
||||
<iface1 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="3" node2="2">
|
||||
<iface2 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
<link node1="2" node2="3">
|
||||
<iface1 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
</link>
|
||||
</links>
|
||||
<emane_global_configuration>
|
||||
<emulator>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="controlportendpoint" value="0.0.0.0:47000"/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</emulator>
|
||||
<core>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
</core>
|
||||
</emane_global_configuration>
|
||||
<emane_configurations>
|
||||
<emane_configuration node="3" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
|
@ -75,6 +64,17 @@
|
|||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
|
@ -91,7 +91,10 @@
|
|||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="2ray"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
|
@ -104,7 +107,7 @@
|
|||
</external>
|
||||
</emane_configuration>
|
||||
</emane_configurations>
|
||||
<session_origin lat="47.5791667" lon="-122.132322" alt="2.0" scale="150.0"/>
|
||||
<session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
|
||||
<session_options>
|
||||
<configuration name="controlnet" value="172.16.0.0/24"/>
|
||||
<configuration name="controlnet0" value=""/>
|
||||
|
@ -117,10 +120,19 @@
|
|||
<configuration name="enablesdt" value="0"/>
|
||||
<configuration name="sdturl" value="tcp://127.0.0.1:50000/"/>
|
||||
<configuration name="ovs" value="0"/>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
<configuration name="mtu" value="0"/>
|
||||
</session_options>
|
||||
<session_metadata>
|
||||
<configuration name="canvas c1" value="{name {Canvas1}}"/>
|
||||
<configuration name="global_options" value="interface_names=no ip_addresses=yes ipv6_addresses=yes node_labels=yes link_labels=yes show_api=no background_images=no annotations=yes grid=yes traffic_start=0"/>
|
||||
<configuration name="shapes" value="[]"/>
|
||||
<configuration name="edges" value="[]"/>
|
||||
<configuration name="hidden" value="[]"/>
|
||||
<configuration name="canvas" value="{"gridlines": true, "canvases": [{"id": 1, "wallpaper": null, "wallpaper_style": 1, "fit_image": false, "dimensions": [1000, 750]}]}"/>
|
||||
</session_metadata>
|
||||
<default_services>
|
||||
<node type="mdr">
|
||||
|
|
|
@ -1,66 +1,55 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<scenario name="/home/developer/.core/configs/emane-demo-precomputed.xml">
|
||||
<scenario name="/tmp/tmpfokvqh5k">
|
||||
<networks>
|
||||
<network id="3" name="wlan3" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282" y="317" lat="47.57628519861569" lon="-122.12852212634816" alt="2.0"/>
|
||||
<network id="3" name="wlan3" icon="" canvas="0" model="emane_rfpipe" type="EMANE">
|
||||
<position x="282.0" y="317.0" lat="47.5762849109534" lon="-122.12852434509814" alt="2.0"/>
|
||||
</network>
|
||||
</networks>
|
||||
<devices>
|
||||
<device id="1" name="n1" type="mdr" class="" image="">
|
||||
<position x="153" y="172" lat="47.57760325520506" lon="-122.13026036642295" alt="2.0"/>
|
||||
<device id="1" name="n1" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="153.0" y="172.0" lat="47.577602967549986" lon="-122.13026258517293" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
<device id="2" name="n2" type="mdr" class="" image="">
|
||||
<position x="393" y="171" lat="47.57761234513531" lon="-122.12702643140011" alt="2.0"/>
|
||||
<device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
|
||||
<position x="393.0" y="171.0" lat="47.57761205748029" lon="-122.1270286501501" alt="2.0"/>
|
||||
<services>
|
||||
<service name="zebra"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
<service name="IPForward"/>
|
||||
<service name="OSPFv3MDR"/>
|
||||
</services>
|
||||
</device>
|
||||
</devices>
|
||||
<links>
|
||||
<link node1="3" node2="1">
|
||||
<iface2 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
<link node1="1" node2="3">
|
||||
<iface1 nem="1" id="0" name="eth0" mac="02:02:00:00:00:01" ip4="10.0.0.1" ip4_mask="32" ip6="2001::1" ip6_mask="128"/>
|
||||
</link>
|
||||
<link node1="3" node2="2">
|
||||
<iface2 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
<link node1="2" node2="3">
|
||||
<iface1 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
|
||||
</link>
|
||||
</links>
|
||||
<emane_global_configuration>
|
||||
<emulator>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="controlportendpoint" value="0.0.0.0:47000"/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</emulator>
|
||||
<core>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
</core>
|
||||
</emane_global_configuration>
|
||||
<emane_configurations>
|
||||
<emane_configuration node="3" model="emane_rfpipe">
|
||||
<platform>
|
||||
<configuration name="antennaprofilemanifesturi" value=""/>
|
||||
<configuration name="eventservicedevice" value="ctrl0"/>
|
||||
<configuration name="eventservicegroup" value="224.1.2.8:45703"/>
|
||||
<configuration name="eventservicettl" value="1"/>
|
||||
<configuration name="otamanagerchannelenable" value="1"/>
|
||||
<configuration name="otamanagerdevice" value="ctrl0"/>
|
||||
<configuration name="otamanagergroup" value="224.1.2.8:45702"/>
|
||||
<configuration name="otamanagerloopback" value="0"/>
|
||||
<configuration name="otamanagermtu" value="0"/>
|
||||
<configuration name="otamanagerpartcheckthreshold" value="2"/>
|
||||
<configuration name="otamanagerparttimeoutthreshold" value="5"/>
|
||||
<configuration name="otamanagerttl" value="1"/>
|
||||
<configuration name="stats.event.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxeventcountrows" value="0"/>
|
||||
<configuration name="stats.ota.maxpacketcountrows" value="0"/>
|
||||
</platform>
|
||||
<mac>
|
||||
<configuration name="datarate" value="1000000"/>
|
||||
<configuration name="delay" value="0.000000"/>
|
||||
|
@ -75,6 +64,17 @@
|
|||
</mac>
|
||||
<phy>
|
||||
<configuration name="bandwidth" value="1000000"/>
|
||||
<configuration name="compatibilitymode" value="1"/>
|
||||
<configuration name="dopplershiftenable" value="1"/>
|
||||
<configuration name="excludesamesubidfromfilterenable" value="1"/>
|
||||
<configuration name="fading.lognormal.dlthresh" value="0.250000"/>
|
||||
<configuration name="fading.lognormal.dmu" value="5.000000"/>
|
||||
<configuration name="fading.lognormal.dsigma" value="1.000000"/>
|
||||
<configuration name="fading.lognormal.duthresh" value="0.750000"/>
|
||||
<configuration name="fading.lognormal.lmean" value="0.005000"/>
|
||||
<configuration name="fading.lognormal.lstddev" value="0.001000"/>
|
||||
<configuration name="fading.lognormal.maxpathloss" value="100.000000"/>
|
||||
<configuration name="fading.lognormal.minpathloss" value="0.000000"/>
|
||||
<configuration name="fading.model" value="none"/>
|
||||
<configuration name="fading.nakagami.distance0" value="100.000000"/>
|
||||
<configuration name="fading.nakagami.distance1" value="250.000000"/>
|
||||
|
@ -91,7 +91,10 @@
|
|||
<configuration name="noisemaxsegmentduration" value="1000000"/>
|
||||
<configuration name="noisemaxsegmentoffset" value="300000"/>
|
||||
<configuration name="noisemode" value="none"/>
|
||||
<configuration name="processingpoolsize" value="0"/>
|
||||
<configuration name="propagationmodel" value="precomputed"/>
|
||||
<configuration name="rxsensitivitypromiscuousmodeenable" value="0"/>
|
||||
<configuration name="stats.receivepowertableenable" value="1"/>
|
||||
<configuration name="subid" value="1"/>
|
||||
<configuration name="systemnoisefigure" value="4.000000"/>
|
||||
<configuration name="timesyncthreshold" value="10000"/>
|
||||
|
@ -104,7 +107,7 @@
|
|||
</external>
|
||||
</emane_configuration>
|
||||
</emane_configurations>
|
||||
<session_origin lat="47.5791667" lon="-122.132322" alt="2.0" scale="150.0"/>
|
||||
<session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
|
||||
<session_options>
|
||||
<configuration name="controlnet" value="172.16.0.0/24"/>
|
||||
<configuration name="controlnet0" value=""/>
|
||||
|
@ -117,10 +120,19 @@
|
|||
<configuration name="enablesdt" value="0"/>
|
||||
<configuration name="sdturl" value="tcp://127.0.0.1:50000/"/>
|
||||
<configuration name="ovs" value="0"/>
|
||||
<configuration name="platform_id_start" value="1"/>
|
||||
<configuration name="nem_id_start" value="1"/>
|
||||
<configuration name="link_enabled" value="1"/>
|
||||
<configuration name="loss_threshold" value="30"/>
|
||||
<configuration name="link_interval" value="1"/>
|
||||
<configuration name="link_timeout" value="4"/>
|
||||
<configuration name="mtu" value="0"/>
|
||||
</session_options>
|
||||
<session_metadata>
|
||||
<configuration name="canvas c1" value="{name {Canvas1}}"/>
|
||||
<configuration name="global_options" value="interface_names=no ip_addresses=yes ipv6_addresses=yes node_labels=yes link_labels=yes show_api=no background_images=no annotations=yes grid=yes traffic_start=0"/>
|
||||
<configuration name="shapes" value="[]"/>
|
||||
<configuration name="edges" value="[]"/>
|
||||
<configuration name="hidden" value="[]"/>
|
||||
<configuration name="canvas" value="{"gridlines": true, "canvases": [{"id": 1, "wallpaper": null, "wallpaper_style": 1, "fit_image": false, "dimensions": [1000, 750]}]}"/>
|
||||
</session_metadata>
|
||||
<default_services>
|
||||
<node type="mdr">
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@ check engine light
|
|||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.api.grpc.wrappers import ExceptionEvent, ExceptionLevel
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
|
@ -19,7 +19,7 @@ class AlertsDialog(Dialog):
|
|||
super().__init__(app, "Alerts")
|
||||
self.tree: Optional[ttk.Treeview] = None
|
||||
self.codetext: Optional[CodeText] = None
|
||||
self.alarm_map: Dict[int, ExceptionEvent] = {}
|
||||
self.alarm_map: dict[int, ExceptionEvent] = {}
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from core.gui import validation
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.graph.manager import CanvasManager
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -22,9 +22,9 @@ class SizeAndScaleDialog(Dialog):
|
|||
create an instance for size and scale object
|
||||
"""
|
||||
super().__init__(app, "Canvas Size and Scale")
|
||||
self.canvas: CanvasGraph = self.app.canvas
|
||||
self.manager: CanvasManager = self.app.manager
|
||||
self.section_font: font.Font = font.Font(weight=font.BOLD)
|
||||
width, height = self.canvas.current_dimensions
|
||||
width, height = self.manager.current().current_dimensions
|
||||
self.pixel_width: tk.IntVar = tk.IntVar(value=width)
|
||||
self.pixel_height: tk.IntVar = tk.IntVar(value=height)
|
||||
location = self.app.core.session.location
|
||||
|
@ -189,9 +189,7 @@ class SizeAndScaleDialog(Dialog):
|
|||
|
||||
def click_apply(self) -> None:
|
||||
width, height = self.pixel_width.get(), self.pixel_height.get()
|
||||
self.canvas.redraw_canvas((width, height))
|
||||
if self.canvas.wallpaper:
|
||||
self.canvas.redraw_wallpaper()
|
||||
self.manager.redraw_canvas((width, height))
|
||||
location = self.app.core.session.location
|
||||
location.x = self.x.get()
|
||||
location.y = self.y.get()
|
||||
|
|
|
@ -4,15 +4,17 @@ set wallpaper
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.gui import images
|
||||
from core.gui.appconfig import BACKGROUNDS_PATH
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.images import Images
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import image_chooser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
@ -23,14 +25,14 @@ class CanvasWallpaperDialog(Dialog):
|
|||
create an instance of CanvasWallpaper object
|
||||
"""
|
||||
super().__init__(app, "Canvas Background")
|
||||
self.canvas: CanvasGraph = self.app.canvas
|
||||
self.canvas: CanvasGraph = self.app.manager.current()
|
||||
self.scale_option: tk.IntVar = tk.IntVar(value=self.canvas.scale_option.get())
|
||||
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(
|
||||
value=self.canvas.adjust_to_dim.get()
|
||||
)
|
||||
self.filename: tk.StringVar = tk.StringVar(value=self.canvas.wallpaper_file)
|
||||
self.image_label: Optional[ttk.Label] = None
|
||||
self.options: List[ttk.Radiobutton] = []
|
||||
self.options: list[ttk.Radiobutton] = []
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
@ -132,7 +134,7 @@ class CanvasWallpaperDialog(Dialog):
|
|||
self.draw_preview()
|
||||
|
||||
def draw_preview(self) -> None:
|
||||
image = Images.create(self.filename.get(), 250, 135)
|
||||
image = images.from_file(self.filename.get(), width=250, height=135)
|
||||
self.image_label.config(image=image)
|
||||
self.image_label.image = image
|
||||
|
||||
|
@ -161,12 +163,11 @@ class CanvasWallpaperDialog(Dialog):
|
|||
def click_apply(self) -> None:
|
||||
self.canvas.scale_option.set(self.scale_option.get())
|
||||
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
|
||||
self.canvas.show_grid.click_handler()
|
||||
filename = self.filename.get()
|
||||
if not filename:
|
||||
filename = None
|
||||
try:
|
||||
self.canvas.set_wallpaper(filename)
|
||||
except FileNotFoundError:
|
||||
logging.error("invalid background: %s", filename)
|
||||
logger.error("invalid background: %s", filename)
|
||||
self.destroy()
|
||||
|
|
|
@ -3,7 +3,7 @@ custom color picker
|
|||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.gui import validation
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
|
@ -13,6 +13,36 @@ if TYPE_CHECKING:
|
|||
from core.gui.app import Application
|
||||
|
||||
|
||||
def get_rgb(red: int, green: int, blue: int) -> str:
|
||||
"""
|
||||
Convert rgb integers to an rgb hex code (#<red><green><blue>).
|
||||
|
||||
:param red: red value
|
||||
:param green: green value
|
||||
:param blue: blue value
|
||||
:return: rgb hex code
|
||||
"""
|
||||
return f"#{red:02x}{green:02x}{blue:02x}"
|
||||
|
||||
|
||||
def get_rgb_values(hex_code: str) -> tuple[int, int, int]:
|
||||
"""
|
||||
Convert a valid rgb hex code (#<red><green><blue>) to rgb integers.
|
||||
|
||||
:param hex_code: valid rgb hex code
|
||||
:return: a tuple of red, blue, and green values
|
||||
"""
|
||||
if len(hex_code) == 4:
|
||||
red = hex_code[1]
|
||||
green = hex_code[2]
|
||||
blue = hex_code[3]
|
||||
else:
|
||||
red = hex_code[1:3]
|
||||
green = hex_code[3:5]
|
||||
blue = hex_code[5:]
|
||||
return int(red, 16), int(green, 16), int(blue, 16)
|
||||
|
||||
|
||||
class ColorPickerDialog(Dialog):
|
||||
def __init__(
|
||||
self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000"
|
||||
|
@ -27,7 +57,7 @@ class ColorPickerDialog(Dialog):
|
|||
self.blue_label: Optional[ttk.Label] = None
|
||||
self.display: Optional[tk.Frame] = None
|
||||
self.color: str = initcolor
|
||||
red, green, blue = self.get_rgb(initcolor)
|
||||
red, green, blue = get_rgb_values(initcolor)
|
||||
self.red: tk.IntVar = tk.IntVar(value=red)
|
||||
self.blue: tk.IntVar = tk.IntVar(value=blue)
|
||||
self.green: tk.IntVar = tk.IntVar(value=green)
|
||||
|
@ -66,7 +96,7 @@ class ColorPickerDialog(Dialog):
|
|||
)
|
||||
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
self.red_label = ttk.Label(
|
||||
frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5
|
||||
frame, background=get_rgb(self.red.get(), 0, 0), width=5
|
||||
)
|
||||
self.red_label.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
|
@ -89,7 +119,7 @@ class ColorPickerDialog(Dialog):
|
|||
)
|
||||
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
self.green_label = ttk.Label(
|
||||
frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5
|
||||
frame, background=get_rgb(0, self.green.get(), 0), width=5
|
||||
)
|
||||
self.green_label.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
|
@ -112,7 +142,7 @@ class ColorPickerDialog(Dialog):
|
|||
)
|
||||
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
self.blue_label = ttk.Label(
|
||||
frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5
|
||||
frame, background=get_rgb(0, 0, self.blue.get()), width=5
|
||||
)
|
||||
self.blue_label.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
|
@ -150,39 +180,27 @@ class ColorPickerDialog(Dialog):
|
|||
self.color = self.hex.get()
|
||||
self.destroy()
|
||||
|
||||
def get_hex(self) -> str:
|
||||
"""
|
||||
convert current RGB values into hex color
|
||||
"""
|
||||
red = self.red_entry.get()
|
||||
blue = self.blue_entry.get()
|
||||
green = self.green_entry.get()
|
||||
return "#%02x%02x%02x" % (int(red), int(green), int(blue))
|
||||
|
||||
def current_focus(self, focus: str) -> None:
|
||||
self.focus = focus
|
||||
|
||||
def update_color(self, arg1=None, arg2=None, arg3=None) -> None:
|
||||
if self.focus == "rgb":
|
||||
red = self.red_entry.get()
|
||||
blue = self.blue_entry.get()
|
||||
green = self.green_entry.get()
|
||||
red = int(self.red_entry.get() or 0)
|
||||
blue = int(self.blue_entry.get() or 0)
|
||||
green = int(self.green_entry.get() or 0)
|
||||
self.set_scale(red, green, blue)
|
||||
if red and blue and green:
|
||||
hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue))
|
||||
self.hex.set(hex_code)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(red, green, blue)
|
||||
hex_code = get_rgb(red, green, blue)
|
||||
self.hex.set(hex_code)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(red, green, blue)
|
||||
elif self.focus == "hex":
|
||||
hex_code = self.hex.get()
|
||||
if len(hex_code) == 4 or len(hex_code) == 7:
|
||||
red, green, blue = self.get_rgb(hex_code)
|
||||
else:
|
||||
return
|
||||
self.set_entry(red, green, blue)
|
||||
self.set_scale(red, green, blue)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(str(red), str(green), str(blue))
|
||||
red, green, blue = get_rgb_values(hex_code)
|
||||
self.set_entry(red, green, blue)
|
||||
self.set_scale(red, green, blue)
|
||||
self.display.config(background=hex_code)
|
||||
self.set_label(red, green, blue)
|
||||
|
||||
def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar) -> None:
|
||||
color_var.set(var.get())
|
||||
|
@ -199,21 +217,7 @@ class ColorPickerDialog(Dialog):
|
|||
self.green.set(green)
|
||||
self.blue.set(blue)
|
||||
|
||||
def set_label(self, red: str, green: str, blue: str) -> None:
|
||||
self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0))
|
||||
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
|
||||
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue)))
|
||||
|
||||
def get_rgb(self, hex_code: str) -> Tuple[int, int, int]:
|
||||
"""
|
||||
convert a valid hex code to RGB values
|
||||
"""
|
||||
if len(hex_code) == 4:
|
||||
red = hex_code[1]
|
||||
green = hex_code[2]
|
||||
blue = hex_code[3]
|
||||
else:
|
||||
red = hex_code[1:3]
|
||||
green = hex_code[3:5]
|
||||
blue = hex_code[5:]
|
||||
return int(red, 16), int(green, 16), int(blue, 16)
|
||||
def set_label(self, red: int, green: int, blue: int) -> None:
|
||||
self.red_label.configure(background=get_rgb(red, 0, 0))
|
||||
self.green_label.configure(background=get_rgb(0, green, 0))
|
||||
self.blue_label.configure(background=get_rgb(0, 0, blue))
|
||||
|
|
|
@ -4,7 +4,7 @@ Service configuration dialog
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import grpc
|
||||
|
||||
|
@ -18,6 +18,8 @@ from core.gui.dialogs.dialog import Dialog
|
|||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.coreclient import CoreClient
|
||||
|
@ -32,24 +34,23 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.core: "CoreClient" = app.core
|
||||
self.node: Node = node
|
||||
self.service_name: str = service_name
|
||||
self.radiovar: tk.IntVar = tk.IntVar()
|
||||
self.radiovar.set(2)
|
||||
self.directories: List[str] = []
|
||||
self.templates: List[str] = []
|
||||
self.dependencies: List[str] = []
|
||||
self.executables: List[str] = []
|
||||
self.startup_commands: List[str] = []
|
||||
self.validation_commands: List[str] = []
|
||||
self.shutdown_commands: List[str] = []
|
||||
self.default_startup: List[str] = []
|
||||
self.default_validate: List[str] = []
|
||||
self.default_shutdown: List[str] = []
|
||||
self.radiovar: tk.IntVar = tk.IntVar(value=2)
|
||||
self.directories: list[str] = []
|
||||
self.templates: list[str] = []
|
||||
self.rendered: dict[str, str] = {}
|
||||
self.dependencies: list[str] = []
|
||||
self.executables: list[str] = []
|
||||
self.startup_commands: list[str] = []
|
||||
self.validation_commands: list[str] = []
|
||||
self.shutdown_commands: list[str] = []
|
||||
self.default_startup: list[str] = []
|
||||
self.default_validate: list[str] = []
|
||||
self.default_shutdown: list[str] = []
|
||||
self.validation_mode: Optional[ServiceValidationMode] = None
|
||||
self.validation_time: Optional[int] = None
|
||||
self.validation_period: tk.StringVar = tk.StringVar()
|
||||
self.modes: List[str] = []
|
||||
self.mode_configs: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
self.validation_period: tk.DoubleVar = tk.DoubleVar()
|
||||
self.modes: list[str] = []
|
||||
self.mode_configs: dict[str, dict[str, str]] = {}
|
||||
self.notebook: Optional[ttk.Notebook] = None
|
||||
self.templates_combobox: Optional[ttk.Combobox] = None
|
||||
self.modes_combobox: Optional[ttk.Combobox] = None
|
||||
|
@ -59,13 +60,14 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.validation_time_entry: Optional[ttk.Entry] = None
|
||||
self.validation_mode_entry: Optional[ttk.Entry] = None
|
||||
self.template_text: Optional[CodeText] = None
|
||||
self.rendered_text: Optional[CodeText] = None
|
||||
self.validation_period_entry: Optional[ttk.Entry] = None
|
||||
self.original_service_files: Dict[str, str] = {}
|
||||
self.temp_service_files: Dict[str, str] = {}
|
||||
self.modified_files: Set[str] = set()
|
||||
self.original_service_files: dict[str, str] = {}
|
||||
self.temp_service_files: dict[str, str] = {}
|
||||
self.modified_files: set[str] = set()
|
||||
self.config_frame: Optional[ConfigFrame] = None
|
||||
self.default_config: Dict[str, str] = {}
|
||||
self.config: Dict[str, ConfigOption] = {}
|
||||
self.default_config: dict[str, str] = {}
|
||||
self.config: dict[str, ConfigOption] = {}
|
||||
self.has_error: bool = False
|
||||
self.load()
|
||||
if not self.has_error:
|
||||
|
@ -73,7 +75,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
|
||||
def load(self) -> None:
|
||||
try:
|
||||
self.core.create_nodes_and_links()
|
||||
self.core.start_session(definition=True)
|
||||
service = self.core.config_services[self.service_name]
|
||||
self.dependencies = service.dependencies[:]
|
||||
self.executables = service.executables[:]
|
||||
|
@ -85,19 +87,23 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
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
|
||||
defaults = self.core.get_config_service_defaults(
|
||||
self.node.id, self.service_name
|
||||
)
|
||||
self.original_service_files = defaults.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}
|
||||
self.config = ConfigOption.from_dict(response.config)
|
||||
self.modes = sorted(defaults.modes)
|
||||
self.mode_configs = defaults.modes
|
||||
self.config = ConfigOption.from_dict(defaults.config)
|
||||
self.default_config = {x.name: x.value for x in self.config.values()}
|
||||
self.rendered = self.core.get_config_service_rendered(
|
||||
self.node.id, self.service_name
|
||||
)
|
||||
service_config = self.node.config_service_configs.get(self.service_name)
|
||||
if service_config:
|
||||
for key, value in service_config.config.items():
|
||||
self.config[key].value = value
|
||||
logging.info("default config: %s", self.default_config)
|
||||
logger.info("default config: %s", self.default_config)
|
||||
for file, data in service_config.templates.items():
|
||||
self.modified_files.add(file)
|
||||
self.temp_service_files[file] = data
|
||||
|
@ -108,7 +114,6 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
# draw notebook
|
||||
self.notebook = ttk.Notebook(self.top)
|
||||
self.notebook.grid(sticky=tk.NSEW, pady=PADY)
|
||||
|
@ -123,6 +128,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
tab.rowconfigure(2, weight=1)
|
||||
self.notebook.add(tab, text="Directories/Files")
|
||||
|
||||
label = ttk.Label(
|
||||
|
@ -135,33 +141,54 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Directories")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
directories_combobox = ttk.Combobox(
|
||||
frame, values=self.directories, state="readonly"
|
||||
)
|
||||
state = "readonly" if self.directories else tk.DISABLED
|
||||
directories_combobox = ttk.Combobox(frame, values=self.directories, state=state)
|
||||
directories_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
if self.directories:
|
||||
directories_combobox.current(0)
|
||||
|
||||
label = ttk.Label(frame, text="Templates")
|
||||
label = ttk.Label(frame, text="Files")
|
||||
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
|
||||
state = "readonly" if self.templates else tk.DISABLED
|
||||
self.templates_combobox = ttk.Combobox(
|
||||
frame, values=self.templates, state="readonly"
|
||||
frame, values=self.templates, state=state
|
||||
)
|
||||
self.templates_combobox.bind(
|
||||
"<<ComboboxSelected>>", self.handle_template_changed
|
||||
)
|
||||
self.templates_combobox.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
self.template_text = CodeText(tab)
|
||||
# draw file template tab
|
||||
notebook = ttk.Notebook(tab)
|
||||
notebook.rowconfigure(0, weight=1)
|
||||
notebook.columnconfigure(0, weight=1)
|
||||
notebook.grid(sticky=tk.NSEW, pady=PADY)
|
||||
# draw rendered file tab
|
||||
rendered_tab = ttk.Frame(notebook, padding=FRAME_PAD)
|
||||
rendered_tab.grid(sticky=tk.NSEW)
|
||||
rendered_tab.rowconfigure(0, weight=1)
|
||||
rendered_tab.columnconfigure(0, weight=1)
|
||||
notebook.add(rendered_tab, text="Rendered")
|
||||
self.rendered_text = CodeText(rendered_tab)
|
||||
self.rendered_text.grid(sticky=tk.NSEW)
|
||||
self.rendered_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||
# draw template file tab
|
||||
template_tab = ttk.Frame(notebook, padding=FRAME_PAD)
|
||||
template_tab.grid(sticky=tk.NSEW)
|
||||
template_tab.rowconfigure(0, weight=1)
|
||||
template_tab.columnconfigure(0, weight=1)
|
||||
notebook.add(template_tab, text="Template")
|
||||
self.template_text = CodeText(template_tab)
|
||||
self.template_text.grid(sticky=tk.NSEW)
|
||||
tab.rowconfigure(self.template_text.grid_info()["row"], weight=1)
|
||||
self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||
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)
|
||||
template_name = self.templates[0]
|
||||
temp_data = self.temp_service_files[template_name]
|
||||
self.template_text.set_text(temp_data)
|
||||
rendered_data = self.rendered[template_name]
|
||||
self.rendered_text.set_text(rendered_data)
|
||||
else:
|
||||
self.template_text.text.configure(state=tk.DISABLED)
|
||||
self.rendered_text.text.configure(state=tk.DISABLED)
|
||||
|
||||
def draw_tab_config(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
|
@ -181,7 +208,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.modes_combobox.bind("<<ComboboxSelected>>", self.handle_mode_changed)
|
||||
self.modes_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
logging.info("config service config: %s", self.config)
|
||||
logger.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=tk.NSEW, pady=PADY)
|
||||
|
@ -241,7 +268,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
label = ttk.Label(frame, text="Validation Time")
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
self.validation_time_entry = ttk.Entry(frame)
|
||||
self.validation_time_entry.insert("end", self.validation_time)
|
||||
self.validation_time_entry.insert("end", str(self.validation_time))
|
||||
self.validation_time_entry.config(state=tk.DISABLED)
|
||||
self.validation_time_entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
|
@ -308,9 +335,9 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
|
||||
self.destroy()
|
||||
return
|
||||
service_config = self.node.config_service_configs.get(self.service_name)
|
||||
if not service_config:
|
||||
service_config = ConfigServiceData()
|
||||
service_config = self.node.config_service_configs.setdefault(
|
||||
self.service_name, ConfigServiceData()
|
||||
)
|
||||
if self.config_frame:
|
||||
self.config_frame.parse_config()
|
||||
service_config.config = {x.name: x.value for x in self.config.values()}
|
||||
|
@ -321,20 +348,25 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.destroy()
|
||||
|
||||
def handle_template_changed(self, event: tk.Event) -> None:
|
||||
template = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[template])
|
||||
template_name = self.templates_combobox.get()
|
||||
temp_data = self.temp_service_files[template_name]
|
||||
self.template_text.set_text(temp_data)
|
||||
rendered = self.rendered[template_name]
|
||||
self.rendered_text.set_text(rendered)
|
||||
|
||||
def handle_mode_changed(self, event: tk.Event) -> None:
|
||||
mode = self.modes_combobox.get()
|
||||
config = self.mode_configs[mode]
|
||||
logging.info("mode config: %s", config)
|
||||
logger.info("mode config: %s", config)
|
||||
self.config_frame.set_values(config)
|
||||
|
||||
def update_template_file_data(self, event: tk.Event) -> None:
|
||||
scrolledtext = event.widget
|
||||
def update_template_file_data(self, _event: tk.Event) -> None:
|
||||
template = self.templates_combobox.get()
|
||||
self.temp_service_files[template] = scrolledtext.get(1.0, "end")
|
||||
self.temp_service_files[template] = self.rendered_text.get_text()
|
||||
if self.rendered[template] != self.temp_service_files[template]:
|
||||
self.modified_files.add(template)
|
||||
return
|
||||
self.temp_service_files[template] = self.template_text.get_text()
|
||||
if self.temp_service_files[template] != self.original_service_files[template]:
|
||||
self.modified_files.add(template)
|
||||
else:
|
||||
|
@ -349,23 +381,33 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
return has_custom_templates or has_custom_config
|
||||
|
||||
def click_defaults(self) -> None:
|
||||
# clear all saved state data
|
||||
self.modified_files.clear()
|
||||
self.node.config_service_configs.pop(self.service_name, None)
|
||||
logging.info(
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
# reset session definition and retrieve default rendered templates
|
||||
self.core.start_session(definition=True)
|
||||
self.rendered = self.core.get_config_service_rendered(
|
||||
self.node.id, self.service_name
|
||||
)
|
||||
logger.info(
|
||||
"cleared config service config: %s", self.node.config_service_configs
|
||||
)
|
||||
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])
|
||||
# reset current selected file data and config data, if present
|
||||
template_name = self.templates_combobox.get()
|
||||
temp_data = self.temp_service_files[template_name]
|
||||
self.template_text.set_text(temp_data)
|
||||
rendered_data = self.rendered[template_name]
|
||||
self.rendered_text.set_text(rendered_data)
|
||||
if self.config_frame:
|
||||
logging.info("resetting defaults: %s", self.default_config)
|
||||
logger.info("resetting defaults: %s", self.default_config)
|
||||
self.config_frame.set_values(self.default_config)
|
||||
|
||||
def click_copy(self) -> None:
|
||||
pass
|
||||
|
||||
def append_commands(
|
||||
self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
||||
self, commands: list[str], listbox: tk.Listbox, to_add: list[str]
|
||||
) -> None:
|
||||
for cmd in to_add:
|
||||
commands.append(cmd)
|
||||
|
|
|
@ -4,7 +4,7 @@ copy service config dialog
|
|||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
|
@ -29,7 +29,7 @@ class CopyServiceConfigDialog(Dialog):
|
|||
self.service: str = service
|
||||
self.file_name: str = file_name
|
||||
self.listbox: Optional[tk.Listbox] = None
|
||||
self.nodes: Dict[str, int] = {}
|
||||
self.nodes: dict[str, int] = {}
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
|
|
@ -2,31 +2,32 @@ import logging
|
|||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional, Set
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.gui import nodeutils
|
||||
from core.gui import images
|
||||
from core.gui.appconfig import ICONS_PATH, CustomNode
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import Images
|
||||
from core.gui.nodeutils import NodeDraw
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class ServicesSelectDialog(Dialog):
|
||||
def __init__(
|
||||
self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
|
||||
self, master: tk.BaseWidget, app: "Application", current_services: set[str]
|
||||
) -> None:
|
||||
super().__init__(app, "Node Services", master=master)
|
||||
super().__init__(app, "Node Config Services", master=master)
|
||||
self.groups: Optional[ListboxScroll] = None
|
||||
self.services: Optional[CheckboxList] = None
|
||||
self.current: Optional[ListboxScroll] = None
|
||||
self.current_services: Set[str] = current_services
|
||||
self.current_services: set[str] = current_services
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
@ -44,7 +45,7 @@ class ServicesSelectDialog(Dialog):
|
|||
label_frame.columnconfigure(0, weight=1)
|
||||
self.groups = ListboxScroll(label_frame)
|
||||
self.groups.grid(sticky=tk.NSEW)
|
||||
for group in sorted(self.app.core.services):
|
||||
for group in sorted(self.app.core.config_services_groups):
|
||||
self.groups.listbox.insert(tk.END, group)
|
||||
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
|
||||
self.groups.listbox.selection_set(0)
|
||||
|
@ -77,15 +78,15 @@ class ServicesSelectDialog(Dialog):
|
|||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
# trigger group change
|
||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
||||
self.handle_group_change()
|
||||
|
||||
def handle_group_change(self, event: tk.Event) -> None:
|
||||
def handle_group_change(self, event: tk.Event = None) -> None:
|
||||
selection = self.groups.listbox.curselection()
|
||||
if selection:
|
||||
index = selection[0]
|
||||
group = self.groups.listbox.get(index)
|
||||
self.services.clear()
|
||||
for name in sorted(self.app.core.services[group]):
|
||||
for name in sorted(self.app.core.config_services_groups[group]):
|
||||
checked = name in self.current_services
|
||||
self.services.add(name, checked)
|
||||
|
||||
|
@ -113,7 +114,7 @@ class CustomNodesDialog(Dialog):
|
|||
self.image_button: Optional[ttk.Button] = None
|
||||
self.image: Optional[PhotoImage] = None
|
||||
self.image_file: Optional[str] = None
|
||||
self.services: Set[str] = set()
|
||||
self.services: set[str] = set()
|
||||
self.selected: Optional[str] = None
|
||||
self.selected_index: Optional[int] = None
|
||||
self.draw()
|
||||
|
@ -146,7 +147,7 @@ class CustomNodesDialog(Dialog):
|
|||
frame, text="Icon", compound=tk.LEFT, command=self.click_icon
|
||||
)
|
||||
self.image_button.grid(sticky=tk.EW, pady=PADY)
|
||||
button = ttk.Button(frame, text="Services", command=self.click_services)
|
||||
button = ttk.Button(frame, text="Config Services", command=self.click_services)
|
||||
button.grid(sticky=tk.EW)
|
||||
|
||||
def draw_node_buttons(self) -> None:
|
||||
|
@ -190,13 +191,13 @@ class CustomNodesDialog(Dialog):
|
|||
def click_icon(self) -> None:
|
||||
file_path = image_chooser(self, ICONS_PATH)
|
||||
if file_path:
|
||||
image = Images.create(file_path, nodeutils.ICON_SIZE)
|
||||
image = images.from_file(file_path, width=images.NODE_SIZE)
|
||||
self.image = image
|
||||
self.image_file = file_path
|
||||
self.image_button.config(image=self.image)
|
||||
|
||||
def click_services(self) -> None:
|
||||
dialog = ServicesSelectDialog(self, self.app, self.services)
|
||||
dialog = ServicesSelectDialog(self, self.app, set(self.services))
|
||||
dialog.show()
|
||||
if dialog.current_services is not None:
|
||||
self.services.clear()
|
||||
|
@ -210,17 +211,17 @@ class CustomNodesDialog(Dialog):
|
|||
name, node_draw.image_file, list(node_draw.services)
|
||||
)
|
||||
self.app.guiconfig.nodes.append(custom_node)
|
||||
logging.info("saving custom nodes: %s", self.app.guiconfig.nodes)
|
||||
logger.info("saving custom nodes: %s", self.app.guiconfig.nodes)
|
||||
self.app.save_config()
|
||||
self.destroy()
|
||||
|
||||
def click_create(self) -> None:
|
||||
name = self.name.get()
|
||||
if name not in self.app.core.custom_nodes:
|
||||
image_file = Path(self.image_file).stem
|
||||
image_file = str(Path(self.image_file).absolute())
|
||||
custom_node = CustomNode(name, image_file, list(self.services))
|
||||
node_draw = NodeDraw.from_custom(custom_node)
|
||||
logging.info(
|
||||
logger.info(
|
||||
"created new custom node (%s), image file (%s), services: (%s)",
|
||||
name,
|
||||
image_file,
|
||||
|
@ -237,14 +238,14 @@ class CustomNodesDialog(Dialog):
|
|||
self.selected = name
|
||||
node_draw = self.app.core.custom_nodes.pop(previous_name)
|
||||
node_draw.model = name
|
||||
node_draw.image_file = Path(self.image_file).stem
|
||||
node_draw.image_file = str(Path(self.image_file).absolute())
|
||||
node_draw.image = self.image
|
||||
node_draw.services = self.services
|
||||
logging.debug(
|
||||
node_draw.services = set(self.services)
|
||||
logger.debug(
|
||||
"edit custom node (%s), image: (%s), services (%s)",
|
||||
name,
|
||||
self.image_file,
|
||||
self.services,
|
||||
node_draw.model,
|
||||
node_draw.image_file,
|
||||
node_draw.services,
|
||||
)
|
||||
self.app.core.custom_nodes[name] = node_draw
|
||||
self.nodes_list.listbox.delete(self.selected_index)
|
||||
|
|
|
@ -2,7 +2,8 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui import images
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.themes import DIALOG_PAD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -25,7 +26,7 @@ class Dialog(tk.Toplevel):
|
|||
self.modal: bool = modal
|
||||
self.title(title)
|
||||
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||
image = Images.get(ImageEnum.CORE, 16)
|
||||
image = images.from_enum(ImageEnum.CORE, width=images.DIALOG_SIZE)
|
||||
self.tk.call("wm", "iconphoto", self._w, image)
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
|
|
|
@ -4,13 +4,14 @@ emane configuration
|
|||
import tkinter as tk
|
||||
import webbrowser
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc.wrappers import ConfigOption, Node
|
||||
from core.gui import images
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
|
||||
|
@ -18,40 +19,6 @@ if TYPE_CHECKING:
|
|||
from core.gui.app import Application
|
||||
|
||||
|
||||
class GlobalEmaneDialog(Dialog):
|
||||
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
|
||||
super().__init__(app, "EMANE Configuration", master=master)
|
||||
self.config_frame: Optional[ConfigFrame] = None
|
||||
self.enabled: bool = not self.app.core.is_runtime()
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
session = self.app.core.session
|
||||
self.config_frame = ConfigFrame(
|
||||
self.top, self.app, session.emane_config, self.enabled
|
||||
)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_apply(self) -> None:
|
||||
self.config_frame.parse_config()
|
||||
self.destroy()
|
||||
|
||||
|
||||
class EmaneModelDialog(Dialog):
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -70,11 +37,13 @@ class EmaneModelDialog(Dialog):
|
|||
self.has_error: bool = False
|
||||
try:
|
||||
config = self.node.emane_model_configs.get((self.model, self.iface_id))
|
||||
if not config:
|
||||
config = self.node.emane_model_configs.get((self.model, None))
|
||||
if not config:
|
||||
config = self.app.core.get_emane_model_config(
|
||||
self.node.id, self.model, self.iface_id
|
||||
)
|
||||
self.config: Dict[str, ConfigOption] = config
|
||||
self.config: dict[str, ConfigOption] = config
|
||||
self.draw()
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Get EMANE Config Error", e)
|
||||
|
@ -113,8 +82,8 @@ class EmaneConfigDialog(Dialog):
|
|||
self.node: Node = node
|
||||
self.radiovar: tk.IntVar = tk.IntVar()
|
||||
self.radiovar.set(1)
|
||||
self.emane_models: List[str] = [
|
||||
x.split("_")[1] for x in self.app.core.session.emane_models
|
||||
self.emane_models: list[str] = [
|
||||
x.split("_")[1] for x in self.app.core.emane_models
|
||||
]
|
||||
model = self.node.emane.split("_")[1]
|
||||
self.emane_model: tk.StringVar = tk.StringVar(value=model)
|
||||
|
@ -143,7 +112,7 @@ class EmaneConfigDialog(Dialog):
|
|||
)
|
||||
label.grid(pady=PADY)
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
|
||||
button = ttk.Button(
|
||||
self.top,
|
||||
image=image,
|
||||
|
@ -178,10 +147,8 @@ class EmaneConfigDialog(Dialog):
|
|||
def draw_emane_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
|
||||
self.emane_model_button = ttk.Button(
|
||||
frame,
|
||||
text=f"{self.emane_model.get()} options",
|
||||
|
@ -190,18 +157,7 @@ class EmaneConfigDialog(Dialog):
|
|||
command=self.click_model_config,
|
||||
)
|
||||
self.emane_model_button.image = image
|
||||
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
button = ttk.Button(
|
||||
frame,
|
||||
text="EMANE options",
|
||||
image=image,
|
||||
compound=tk.RIGHT,
|
||||
command=self.click_emane_config,
|
||||
)
|
||||
button.image = image
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
self.emane_model_button.grid(padx=PADX, sticky=tk.EW)
|
||||
|
||||
def draw_apply_and_cancel(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
|
@ -214,10 +170,6 @@ class EmaneConfigDialog(Dialog):
|
|||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_emane_config(self) -> None:
|
||||
dialog = GlobalEmaneDialog(self, self.app)
|
||||
dialog.show()
|
||||
|
||||
def click_model_config(self) -> None:
|
||||
"""
|
||||
draw emane model configuration
|
||||
|
|
|
@ -2,8 +2,9 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.gui import images
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.themes import PADY
|
||||
from core.gui.widgets import CodeText
|
||||
|
||||
|
@ -12,9 +13,11 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class ErrorDialog(Dialog):
|
||||
def __init__(self, app: "Application", title: str, details: str) -> None:
|
||||
super().__init__(app, "CORE Exception")
|
||||
self.title: str = title
|
||||
def __init__(
|
||||
self, app: "Application", title: str, message: str, details: str
|
||||
) -> None:
|
||||
super().__init__(app, title)
|
||||
self.message: str = message
|
||||
self.details: str = details
|
||||
self.error_message: Optional[CodeText] = None
|
||||
self.draw()
|
||||
|
@ -22,15 +25,15 @@ class ErrorDialog(Dialog):
|
|||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(1, weight=1)
|
||||
image = Images.get(ImageEnum.ERROR, 24)
|
||||
image = images.from_enum(ImageEnum.ERROR, width=images.ERROR_SIZE)
|
||||
label = ttk.Label(
|
||||
self.top, text=self.title, image=image, compound=tk.LEFT, anchor=tk.CENTER
|
||||
self.top, text=self.message, image=image, compound=tk.LEFT, anchor=tk.CENTER
|
||||
)
|
||||
label.image = image
|
||||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
label.grid(sticky=tk.W, pady=PADY)
|
||||
self.error_message = CodeText(self.top)
|
||||
self.error_message.text.insert("1.0", self.details)
|
||||
self.error_message.text.config(state=tk.DISABLED)
|
||||
self.error_message.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.error_message.grid(sticky=tk.EW, pady=PADY)
|
||||
button = ttk.Button(self.top, text="Close", command=lambda: self.destroy())
|
||||
button.grid(sticky=tk.EW)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue