Compare commits
1300 commits
coretk-enh
...
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 | ||
|
a660b01e93 | ||
|
ebd6bb8a21 | ||
|
b88ec31df6 | ||
|
44ee5308de | ||
|
cba86a3da7 | ||
|
acaef00087 | ||
|
3c97f80614 | ||
|
f919520058 | ||
|
685b21924c | ||
|
63282134f5 | ||
|
a2d9659fb7 | ||
|
77e2b08d76 | ||
|
f24d5f20b4 | ||
|
a7d7b94215 | ||
|
62d111b74c | ||
|
2af7d595c0 | ||
|
3e2ea42ebd | ||
|
f171c6111a | ||
|
28d22c5800 | ||
|
29ffd64b41 | ||
|
f9a4fe3331 | ||
|
2f9c169e66 | ||
|
b937e316c4 | ||
|
367a2096fa | ||
|
4a8f8557a6 | ||
|
5d436dd94d | ||
|
2e77907d72 | ||
|
8eada3d754 | ||
|
4ec9ea7b16 | ||
|
4b6afe4db7 | ||
|
4a9d16c78c | ||
|
ad839bbc07 | ||
|
b01249bb4e | ||
|
9621df6bc4 | ||
|
6f43d0e88f | ||
|
4363a20ffb | ||
|
d6b95bab24 | ||
|
886bfc093b | ||
|
e34c00a431 | ||
|
e7320a61a6 | ||
|
9fa98ae378 | ||
|
d1c2b1bdb9 | ||
|
02d8a32a50 | ||
|
7308dd50ff | ||
|
d824fbd1c6 | ||
|
836e929fbc | ||
|
5b93c2d7ac | ||
|
6793382f44 | ||
|
a23ef7d603 | ||
|
b762fe664b | ||
|
41222f77c2 | ||
|
664b049bf7 | ||
|
a57b838f19 | ||
|
8297b74524 | ||
|
ed717599c8 | ||
|
66a1c3d426 | ||
|
a35e91aeba | ||
|
d95c2ec05f | ||
|
83ba6cea70 | ||
|
db7d4ca0c1 | ||
|
380d411833 | ||
|
aa40229495 | ||
|
68934da204 | ||
|
e817a24275 | ||
|
fc846272fc | ||
|
961f630acb | ||
|
a1b4279d80 | ||
|
053cd1da65 | ||
|
055029e5c5 | ||
|
29bd6ef7f8 | ||
|
96dddb687d | ||
|
90d2d5f0dc | ||
|
8597c5c1a8 | ||
|
1543dfcc94 | ||
|
dde74f0927 | ||
|
ea44f1b6e7 | ||
|
0c847cfb37 | ||
|
26c2997a42 | ||
|
4e79865035 | ||
|
c25cb3d657 | ||
|
c1864857e1 | ||
|
f2868a9554 | ||
|
8a6fdc69ba | ||
|
93a5d1fb01 | ||
|
ce7736a95e | ||
|
ac781ef3da | ||
|
552d8f60d2 | ||
|
5a07929fde | ||
|
ae336c2cf8 | ||
|
56e98f412c | ||
|
3d3e2271be | ||
|
00433bfd99 | ||
|
3f0993d0a9 | ||
|
b5de4445fe | ||
|
3f1ea0ab86 | ||
|
28daab98dd | ||
|
a3d2e6dfe3 | ||
|
0306f77147 | ||
|
b195891b3b | ||
|
0177b07f03 | ||
|
16495c9008 | ||
|
371ca72900 | ||
|
d43d854314 | ||
|
4dad3f5e9f | ||
|
ee59d6bf8b | ||
|
8ca52bf475 | ||
|
7fdb114375 | ||
|
4538675c90 | ||
|
4bdc454970 | ||
|
2b1b027a11 | ||
|
828a68a0cd | ||
|
9ea1915f48 | ||
|
5fd51e6b2f | ||
|
92090dd659 | ||
|
be8fa1b96d | ||
|
6e059c7446 | ||
|
06e9a04357 | ||
|
7790d4aa00 | ||
|
efdce20afb | ||
|
f83f71c074 | ||
|
0668d0a49b | ||
|
b9a14fbe0c | ||
|
8234a5ed7e | ||
|
646c6b8f01 | ||
|
202e681fff | ||
|
57e6df51d3 | ||
|
9e3d6681c5 | ||
|
d981d88a6f | ||
|
82d87445b6 | ||
|
98a51ce17d | ||
|
c4a724ee10 | ||
|
a80fda11f5 | ||
|
e775ad4c5d | ||
|
ba028a2b00 | ||
|
570ad9522c | ||
|
b0bac1d319 | ||
|
f6992e7545 | ||
|
5300eef27e | ||
|
87ca431e73 | ||
|
534af7cc45 | ||
|
1b05ca0553 | ||
|
4836ac4f2e | ||
|
9e5793ab6e | ||
|
1c2a451fd3 | ||
|
05247524d7 | ||
|
f687115522 | ||
|
936d782e41 | ||
|
3e41d31c6c | ||
|
b98ff0f744 | ||
|
6dd7ce731e | ||
|
b89a19a18e | ||
|
8004be6e7c | ||
|
9352c0eafe | ||
|
5976bca34b | ||
|
cd0351c818 | ||
|
e2b3a2dc6d | ||
|
082677c17b | ||
|
4bcaa32fdb | ||
|
f41ce8e3a6 | ||
|
06e43f619d | ||
|
b7e3d1c877 | ||
|
f0bc3bbc99 | ||
|
2aeb119b04 | ||
|
06563d5953 | ||
|
fc44ad6fe8 | ||
|
04f7bc561b | ||
|
e7a93e7fd6 | ||
|
eb422f5bab | ||
|
63103ab250 | ||
|
f54ddc0912 | ||
|
46f896925c | ||
|
d30778b238 | ||
|
787f02f024 | ||
|
fe36d28522 | ||
|
858e771efd | ||
|
2f2bb06a5b | ||
|
0db1ad1195 | ||
|
1fdc5c5c5a | ||
|
afe434f25c | ||
|
511a3037a8 | ||
|
9e3e0e0326 | ||
|
0d2dd70727 | ||
|
27495cbda1 | ||
|
588afaad13 | ||
|
3bdd6292cd | ||
|
160498336c | ||
|
41a3c5fd7f | ||
|
82a212d1cf | ||
|
a9a2fb8e46 | ||
|
77f6577bce | ||
|
154fa8b77d | ||
|
eb70386238 | ||
|
ba3a247495 | ||
|
fff4bd7963 | ||
|
3544d00431 | ||
|
f8d862a296 | ||
|
e34002b851 | ||
|
165e404184 | ||
|
45bfa9fdad | ||
|
d5d5da7256 | ||
|
5e2ca0f549 | ||
|
6d4434bc12 | ||
|
36123e7aa5 | ||
|
e100defdec | ||
|
fdf00cff0e | ||
|
90d24a1094 | ||
|
35b4c157a0 | ||
|
6219d08416 | ||
|
1c2d7c6d12 | ||
|
6b55061857 | ||
|
db4ef2b42e | ||
|
b50f058374 | ||
|
0be1972a29 | ||
|
1212e5ddf8 | ||
|
d1fd19edc6 | ||
|
c884ee27cd | ||
|
495fbe5632 | ||
|
897ecc6d35 | ||
|
33d100acff | ||
|
1c876819f1 | ||
|
642af4fe47 | ||
|
a1ea762b89 | ||
|
feb81ae876 | ||
|
119a3640e4 | ||
|
1cadf8362f | ||
|
be2f7e1cae | ||
|
80194b3e38 | ||
|
f8b0ab6ec3 | ||
|
fdd2e6f1f1 | ||
|
873fc0e468 | ||
|
0cd3f6115d | ||
|
8c50d08121 | ||
|
7b3f934e95 | ||
|
df01f04444 | ||
|
50f331d93e | ||
|
05830c6830 | ||
|
f00d4aef0b | ||
|
dfb3e0c424 | ||
|
6b5aaa6b19 | ||
|
08105cf4b3 | ||
|
35b6f5297a | ||
|
6c7e760f4e | ||
|
5d23be4a9d | ||
|
e283c5ec7d | ||
|
cb66ba60a6 | ||
|
125d74e7d5 | ||
|
bd87403ae5 | ||
|
79058810c2 | ||
|
63f09e0254 | ||
|
32c7808cab | ||
|
dcf3568098 | ||
|
5c58e99ad4 | ||
|
ec45d7198b | ||
|
4d35e8f968 | ||
|
5a35431bcb | ||
|
28281c6bde | ||
|
68ff7a86c8 | ||
|
e704483527 | ||
|
7398196dcc | ||
|
d0e9cee650 | ||
|
80eaa27469 | ||
|
8cf89fa114 | ||
|
737dae1224 | ||
|
d2fe7fcff0 | ||
|
9c13803e52 | ||
|
a9ec21c604 | ||
|
637f7740d6 | ||
|
ece2f1c43f | ||
|
980ab1526d | ||
|
75acbf4dae | ||
|
8a60a4739f | ||
|
7c3e42396a | ||
|
9b541d0316 | ||
|
fe362a10d6 | ||
|
626b977505 | ||
|
9bf5756a03 | ||
|
7821ffb642 | ||
|
cd9ecd2257 | ||
|
38e6838697 | ||
|
9b7dce0861 | ||
|
85cd31ae52 | ||
|
a2a825e91d | ||
|
51200cf930 | ||
|
7dd2b66680 | ||
|
41f0c8ef95 | ||
|
8357cddbab | ||
|
d4ac9e618f | ||
|
139323146e | ||
|
a236ea2455 | ||
|
9fed908322 | ||
|
7a21affbd4 | ||
|
43b586a1a1 | ||
|
fb21909dad | ||
|
3949bd6d1b | ||
|
bb4514b93e | ||
|
f1ff1a6577 | ||
|
6648dc7825 | ||
|
c761c55ebc | ||
|
0045c8d79c | ||
|
6f7e42d310 | ||
|
3590f2c370 | ||
|
8dc570a98d | ||
|
b3a4b1cb10 | ||
|
5cc4d92760 | ||
|
fcda1f9f14 | ||
|
ac1c27b1c8 | ||
|
ddcb0205f3 | ||
|
2b3e26b7c2 | ||
|
5f676b27ba | ||
|
ce4b61d3b2 | ||
|
bd48e14348 | ||
|
e549830e33 | ||
|
da9c0d0660 | ||
|
a870c15b43 | ||
|
08bbaf463b | ||
|
7a6c602369 | ||
|
3477e84e9d | ||
|
537291b219 | ||
|
f22edd1d25 | ||
|
4a0fdf3307 | ||
|
beaebcfa24 | ||
|
ab17cb1053 | ||
|
d480a1dd4c | ||
|
69721dc129 | ||
|
aef3fe8d50 | ||
|
ec845b920c | ||
|
59e7395a4f | ||
|
c8daeb02d8 | ||
|
5eae67aac5 | ||
|
e79645013b | ||
|
adf28f6b55 | ||
|
f4a3fe6b7b | ||
|
f921fa45c5 | ||
|
aebbff8c22 | ||
|
eac941ce72 | ||
|
f4224d1b80 | ||
|
b94d4d3507 | ||
|
3bfc299bfd | ||
|
98e4baca04 | ||
|
d746cfa935 | ||
|
f582306bb9 | ||
|
bb2ceaf993 | ||
|
6490b5b9cb | ||
|
60d9fe2026 | ||
|
7215f852b8 | ||
|
9649337f18 | ||
|
14573184e0 | ||
|
8e2cfa61c9 | ||
|
29a69e8b40 | ||
|
6dd6bc87ab | ||
|
8f19ad057c | ||
|
e0c9f9c832 | ||
|
c43dd60a42 | ||
|
1ef66181c6 | ||
|
2145c07cb7 | ||
|
f39ab1dee6 | ||
|
27e35a5213 | ||
|
344f35e93e | ||
|
527d34e374 | ||
|
11be40bc90 | ||
|
0356f3b19c | ||
|
adfce52632 | ||
|
0d4a360e89 | ||
|
1829a8e2f8 | ||
|
cfda9509a2 | ||
|
f07176dd43 | ||
|
cd6083aed9 | ||
|
df9216e0f0 | ||
|
88fe860f97 | ||
|
19af9c3f51 | ||
|
3638b05cd1 | ||
|
9e4429fbbc | ||
|
20feea8f12 | ||
|
d88f3a2535 | ||
|
ca2b1c9e4c | ||
|
b2ea8cbbf6 | ||
|
a09910d0bc | ||
|
cd74a44558 | ||
|
250bc6e1f5 | ||
|
e46a072f74 | ||
|
ecc3eb1c89 | ||
|
1702fe256f | ||
|
3d7d775bfb | ||
|
5d34a2b752 | ||
|
b92ff0586a | ||
|
a64047e221 | ||
|
f4671ab2b8 | ||
|
a1734c3bc0 | ||
|
351b99aae0 | ||
|
a29a7a5582 | ||
|
eeca33e722 | ||
|
0725199d6d | ||
|
0462c1b084 | ||
|
f5916fab5b | ||
|
38257de740 | ||
|
cf41948894 | ||
|
c4c667bb74 | ||
|
3243a69afa | ||
|
8587da0621 | ||
|
d94bae6b42 | ||
|
91f1f7f004 | ||
|
8d48393525 | ||
|
5df2e36083 | ||
|
0bcf7c1d83 | ||
|
83f66d33f7 | ||
|
e18ffaafce | ||
|
b74395049a | ||
|
23d957679e | ||
|
178d12b327 | ||
|
876699e8ef | ||
|
76305f7257 | ||
|
b28ef76d65 | ||
|
6201875b78 | ||
|
ef3cf5697d | ||
|
cfaa9397ad | ||
|
e72e332bab | ||
|
00cda5c550 | ||
|
c64094ac1c | ||
|
f626564200 | ||
|
e0842197e3 | ||
|
f2409d0604 | ||
|
e325bcfc55 | ||
|
ccf2646c00 | ||
|
9ed42cfba8 | ||
|
39fd11efb3 | ||
|
a389dc6240 | ||
|
784c4d2419 | ||
|
fd341bd69b | ||
|
6ee9590bdc | ||
|
452e0720f2 | ||
|
32ad8a9b68 | ||
|
a79ba1b8d3 | ||
|
4cc9d3debf | ||
|
f73c617ecf | ||
|
d71d84fae7 | ||
|
21da670698 | ||
|
2965273f58 | ||
|
3be15a1316 | ||
|
3691c6029f | ||
|
7d2034df71 | ||
|
b5e53e573a | ||
|
18044f9474 | ||
|
6ddf1ac9a4 | ||
|
199c4618f5 | ||
|
7ffbf457be | ||
|
bf1bc511e2 | ||
|
75d5bced9c | ||
|
9a5fc94ba2 | ||
|
eaa05c34ba | ||
|
7b2dd59c81 | ||
|
29d09c8397 | ||
|
e74481df34 | ||
|
3b0ca1638c | ||
|
7048aa7867 | ||
|
1884103cb4 | ||
|
0d2bcccf3e | ||
|
e323f8965e | ||
|
c6a06baf29 | ||
|
d31cfedde9 | ||
|
82c33345be | ||
|
13fb5db85d | ||
|
73a5567084 | ||
|
b034ba6cc3 | ||
|
8ad3f7961a | ||
|
10fd844397 | ||
|
048fc1aaf9 | ||
|
b88df84d62 | ||
|
183ffda570 | ||
|
4ab415e37d | ||
|
7e4ef0b280 | ||
|
8fed201fd8 | ||
|
f95a8113c9 | ||
|
2e78025249 | ||
|
ba8b16ec34 | ||
|
964f78f06a | ||
|
c580e15f8e | ||
|
37ff989aa4 | ||
|
26b0868f65 | ||
|
dd13bc8379 | ||
|
fe09b37819 | ||
|
56fbc0e3c5 | ||
|
70bd837f02 | ||
|
bcd9e4ceb1 | ||
|
4b6ba90331 | ||
|
ffc60e5808 | ||
|
d5016bf44f | ||
|
c07766e1eb | ||
|
d5254e6a91 | ||
|
19ee367dc5 | ||
|
0a792f7b3f | ||
|
d14056393b | ||
|
8bae0611a4 | ||
|
773f733cb8 | ||
|
cde053da73 | ||
|
bd897efd05 | ||
|
d0520bf21d | ||
|
34f86174a2 | ||
|
8979c86187 | ||
|
41df8a57b8 | ||
|
06e3d84862 | ||
|
91220078f1 | ||
|
50816b3b80 | ||
|
29fc5acb99 | ||
|
4eaecd6a7b | ||
|
0dcfcbf4ea | ||
|
ee5d5b9864 | ||
|
5e69ea48b3 | ||
|
3b1a9bc3e3 | ||
|
df03f1e173 | ||
|
433fe4ae58 | ||
|
79d7d66bff | ||
|
95d3a6ca8c | ||
|
454dc8091e | ||
|
cc7c1348ce | ||
|
fa163c3ed6 | ||
|
21fdbc48da | ||
|
22d813df63 | ||
|
150db07497 | ||
|
12ed9c8422 | ||
|
124d655dc6 | ||
|
3fac6bc7ec | ||
|
88a98fff82 | ||
|
a36674aba9 | ||
|
f77f37ef86 | ||
|
5d99244596 | ||
|
9b4802a5ae | ||
|
0aba1aa928 | ||
|
4379ef32e9 | ||
|
944040608e | ||
|
32558d15d2 | ||
|
86ae87eafe | ||
|
d9f48d14a7 | ||
|
41b46b7e7a | ||
|
1d620a0b17 | ||
|
185c6736b3 | ||
|
828254dccd | ||
|
0e08242128 | ||
|
4ec6ef25fe | ||
|
1dd45f4424 | ||
|
0999fabb14 | ||
|
835675480b | ||
|
b858e66c49 | ||
|
be70c5383e | ||
|
ac2d60dad6 | ||
|
65466909d3 | ||
|
491f2a8e93 | ||
|
5a8984de10 | ||
|
d158fc99c6 | ||
|
9d1f5cfcc6 | ||
|
ea4271d7cb | ||
|
0ee679d978 | ||
|
686026d9f2 | ||
|
4ae5936bdc | ||
|
28f7d93abd | ||
|
2e9968c306 | ||
|
6ba065e8b6 | ||
|
f7281459ed | ||
|
1f1b1c7b65 | ||
|
4a7abe71e4 | ||
|
9a42368221 | ||
|
ffe6eed2f2 | ||
|
b116d525d9 | ||
|
580641f5d9 | ||
|
7e0ead0766 | ||
|
337c41528a | ||
|
3c49d0676a | ||
|
d945e7c41e | ||
|
4037da49c2 | ||
|
98c9730967 | ||
|
06e145f508 | ||
|
cdde6988e2 | ||
|
47ef5ec14d | ||
|
1f8d16df08 | ||
|
64657b20a8 | ||
|
e9ca4a5b58 | ||
|
a3a9aee7f7 | ||
|
c45202e61b | ||
|
275e8f4c30 | ||
|
62c0011caa | ||
|
b7adbd289c | ||
|
8e8ffb3ffb | ||
|
b5f4571618 | ||
|
01b41b0276 | ||
|
ea99b628fc | ||
|
aa2537753e | ||
|
a662fcc62c | ||
|
7054e606ae | ||
|
f1e0c7245f | ||
|
870d5dc41c | ||
|
f6bd21629d | ||
|
03e291d215 | ||
|
3394f0240a | ||
|
6fe2845051 | ||
|
039cf2a3b9 | ||
|
f521fe4141 | ||
|
ba6a6f06b1 | ||
|
20ecdf70d0 | ||
|
715bae6f74 | ||
|
ec8a15794b | ||
|
219218eebc | ||
|
63b8b7e8a0 | ||
|
b04da98f44 | ||
|
d7ebb90329 | ||
|
85b4a81f8a | ||
|
bd30d0d9ff | ||
|
54eab4576d | ||
|
efa5506c80 | ||
|
b4de016a24 | ||
|
3233d8ab58 | ||
|
d26c4fc4ab | ||
|
f45a11076f | ||
|
be332a2a29 | ||
|
d1f7eafc57 | ||
|
d659a5c139 | ||
|
7da7ea5d62 | ||
|
c43afa4b40 | ||
|
c09e3e90d6 | ||
|
02c8604d6a | ||
|
1117522c21 | ||
|
5dcf2f45c5 | ||
|
78d442b574 | ||
|
ca292cb11e | ||
|
9bd13dce1e | ||
|
7e7bf8c7b7 | ||
|
e2490dee4a | ||
|
42979f1bb3 | ||
|
0203d4178d | ||
|
6f87986364 | ||
|
23562cd294 | ||
|
86b0c07764 | ||
|
18a6442c8e | ||
|
2b97b311ab | ||
|
3c4a908fd5 | ||
|
cd8157eff7 | ||
|
8c8024df10 | ||
|
971a959a19 | ||
|
d5dec12439 | ||
|
04bd3a2f8f | ||
|
3c909d4989 | ||
|
a72b0bb4a5 | ||
|
928bfc73dc | ||
|
27a6c76d57 | ||
|
73a96ffcf5 | ||
|
36b3243a4b | ||
|
0d490344fc | ||
|
ca039946a6 | ||
|
a5c412b594 | ||
|
3a45e9ec7a | ||
|
c3224251b7 | ||
|
41451c29cf | ||
|
0db119a9ae | ||
|
87f90cd8e3 | ||
|
0aa7c6f1f2 | ||
|
0742c08b59 | ||
|
6c9c2cbeb0 | ||
|
2750a69e79 | ||
|
8dfdd6171d | ||
|
29fea7e572 | ||
|
ba18853509 | ||
|
953bd80e2e | ||
|
b6fbedf471 | ||
|
d0c4d4b935 | ||
|
7be7beec42 | ||
|
7d392c43ac | ||
|
72189a5c28 | ||
|
b29f640547 | ||
|
091131fe5c | ||
|
3165bddc92 | ||
|
71196004c8 | ||
|
1252f72220 | ||
|
eccd1f1695 | ||
|
2ce1ef04ae | ||
|
2532f6605d | ||
|
6e8f980cc9 | ||
|
13ef701b6e | ||
|
8186c62b19 | ||
|
5d9b451b1d | ||
|
16764c702b | ||
|
7e0efa7020 | ||
|
7b29f6bb82 | ||
|
16cc73c070 | ||
|
52ff5ce62b | ||
|
6616f104e6 | ||
|
1efa1284bb | ||
|
172c5e7e4e | ||
|
6c5c2c5674 | ||
|
0b30289879 | ||
|
3d59cd0ad8 | ||
|
484d66191d | ||
|
6479afb7b2 | ||
|
fc40c8d7bb | ||
|
e0ce29b25a | ||
|
66e5be7576 | ||
|
6a41012857 | ||
|
38f9f44cdf | ||
|
14e708681c | ||
|
33bcc24d88 | ||
|
fd7db64f6d | ||
|
39499a4ab4 | ||
|
7a5a0f34ea | ||
|
c264634b5f | ||
|
3f17706c28 | ||
|
3be162b0b0 | ||
|
102fa410fe | ||
|
3507b65676 | ||
|
a7790185d4 | ||
|
5cdfd8d8b9 | ||
|
f277e96c9a | ||
|
1e8d1ecd9f | ||
|
0e299d5af4 | ||
|
6b5cd95ac2 | ||
|
595e77a1ef | ||
|
eb030aaca7 | ||
|
81382f2899 | ||
|
d8cf1373da | ||
|
5c52fbbdec | ||
|
e5a446d70f | ||
|
105dd4ad7b | ||
|
f826a4c5e8 | ||
|
c4234d33f0 | ||
|
f2da8dc2c9 | ||
|
be37f0f279 | ||
|
95d36a1792 | ||
|
f50c1e4db4 | ||
|
d076229973 | ||
|
902cfab677 | ||
|
038a83cbfd | ||
|
34895c1f9c | ||
|
7dee59e86e | ||
|
91dae87810 | ||
|
53865e1898 | ||
|
c0d576f26d | ||
|
18d88ab797 | ||
|
fd2a5ec290 | ||
|
b72ce6a66c | ||
|
0d4a86f10e | ||
|
52689bd210 | ||
|
6ce29bea75 | ||
|
4093b2244a | ||
|
539ca5d22c | ||
|
7c8e115bc7 | ||
|
020a13bde6 | ||
|
ea341cbe45 | ||
|
9cd6166b9b | ||
|
58cb5a1a1d | ||
|
933f409498 | ||
|
ff3b20a962 | ||
|
b0a3c85f0e | ||
|
a7fa0bf6d3 | ||
|
dfc24e107f | ||
|
c1f0e385b4 | ||
|
9535d40b70 | ||
|
c630d911fe | ||
|
67da3e5c22 | ||
|
848cda03f7 | ||
|
f8a3f0a2d4 | ||
|
1cba11d9e0 | ||
|
c36f060d44 | ||
|
20e3fbc7d9 | ||
|
e1c9155ba7 | ||
|
21dfaf7d66 | ||
|
377790cff2 | ||
|
7574765305 | ||
|
764a61e89e | ||
|
696fda00ea | ||
|
b5b51794d8 | ||
|
32efc75c64 | ||
|
909f231c13 | ||
|
41bb951531 | ||
|
014707580f | ||
|
177f27372e | ||
|
db0f984401 | ||
|
8a0257d130 | ||
|
e66968dcbb | ||
|
7a50f6ac25 | ||
|
1dca477e6d | ||
|
ddaba7c477 | ||
|
04d7bb6265 | ||
|
8b544d98d8 | ||
|
795e5a0c2c | ||
|
afb0fe8b46 | ||
|
f1a7c10e91 | ||
|
a3c7ed8012 | ||
|
95c32ddd28 | ||
|
3a2da0282f | ||
|
2a8f689ad5 | ||
|
e90eff578e | ||
|
08d4bf98c7 | ||
|
ceb3d072da | ||
|
83842fe9be | ||
|
20be527add | ||
|
b6aef6d560 | ||
|
44bf4e020c | ||
|
d128f2425d | ||
|
2a6c6ac286 | ||
|
8572e153f4 | ||
|
cfeb378287 | ||
|
23aeb40f54 | ||
|
d8f586bd2b | ||
|
08e652633f | ||
|
471f40a0bd | ||
|
b3dabbfe05 | ||
|
87c9492d32 | ||
|
1d911a763f | ||
|
4fd1338cf1 | ||
|
b7116c52ff | ||
|
e8f6ccaa4e | ||
|
1375af51cb | ||
|
ebafa228ff | ||
|
0ea99ca809 | ||
|
71aeb98bb9 | ||
|
55b6cbbd90 | ||
|
0d3460e2ec | ||
|
83e6bbee45 | ||
|
9cd756f2dc | ||
|
3a466fd463 | ||
|
c4f0f62206 | ||
|
13275c59df | ||
|
b4bf3ee391 | ||
|
7fbbfa8c63 | ||
|
8734b9f22f | ||
|
d2fe352797 | ||
|
80f47a5d4c | ||
|
ee0c63e4a1 | ||
|
0407645061 | ||
|
9399218123 | ||
|
7dedaa0344 |
613 changed files with 37897 additions and 74545 deletions
25
.github/workflows/daemon-checks.yml
vendored
25
.github/workflows/daemon-checks.yml
vendored
|
@ -4,39 +4,38 @@ on: [push]
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.7
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install pipenv
|
||||
python-version: 3.9
|
||||
- name: install poetry
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pipenv
|
||||
pip install poetry
|
||||
cd daemon
|
||||
cp setup.py.in setup.py
|
||||
cp core/constants.py.in core/constants.py
|
||||
sed -i 's/True/False/g' core/constants.py
|
||||
pipenv install --dev
|
||||
sed -i 's/required=True/required=False/g' core/emulator/coreemu.py
|
||||
poetry install
|
||||
- name: isort
|
||||
run: |
|
||||
cd daemon
|
||||
pipenv run isort -c
|
||||
poetry run isort -c -df
|
||||
- name: black
|
||||
run: |
|
||||
cd daemon
|
||||
pipenv run black --check --exclude ".+_pb2.*.py|doc|build|utm\.py|setup\.py" .
|
||||
poetry run black --check .
|
||||
- name: flake8
|
||||
run: |
|
||||
cd daemon
|
||||
pipenv run flake8
|
||||
poetry run flake8
|
||||
- name: grpc
|
||||
run: |
|
||||
cd daemon/proto
|
||||
pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto
|
||||
poetry run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto
|
||||
- name: test
|
||||
run: |
|
||||
cd daemon
|
||||
pipenv run test --mock
|
||||
poetry run pytest --mock tests
|
||||
|
|
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
|
11
.gitignore
vendored
11
.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
|
||||
|
@ -39,6 +43,7 @@ coverage.xml
|
|||
|
||||
# python files
|
||||
*.egg-info
|
||||
*.pyc
|
||||
|
||||
# ignore package files
|
||||
*.rpm
|
||||
|
@ -55,8 +60,8 @@ coverage.xml
|
|||
netns/setup.py
|
||||
daemon/setup.py
|
||||
|
||||
# ignore corefx build
|
||||
corefx/target
|
||||
|
||||
# python
|
||||
__pycache__
|
||||
|
||||
# ignore core player files
|
||||
*.core
|
||||
|
|
504
CHANGELOG.md
504
CHANGELOG.md
|
@ -1,3 +1,507 @@
|
|||
## 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
|
||||
* fixed issue where emane global configuration was not being sent to core-gui
|
||||
* updated controlnet names on host to be prefixed with ctrl
|
||||
* fixed RJ45 link shutdown from core-gui causing an error
|
||||
* fixed emane external transport xml generation
|
||||
* \#517 - update to account for radvd required directory
|
||||
* \#514 - support added for session specific environment files
|
||||
* \#529 - updated to configure netem limit based on delay or user specified, requires kernel 3.3+
|
||||
* core-pygui
|
||||
* fixed issue drawing wlan/emane link options when it should not have
|
||||
* edge labels are now placed a set distance from nodes like original gui
|
||||
* link color/width are now saved to xml files
|
||||
* added support to configure buffer size for links
|
||||
* \#525 - added support for multiple wired links between the same nodes
|
||||
* \#526 - added option to hide/show links with 100% loss
|
||||
* Documentation
|
||||
* \#527 - typo in service documentation
|
||||
* \#515 - added examples to docs for using EMANE features within a CORE context
|
||||
|
||||
## 2020-09-29 CORE 7.2.1
|
||||
|
||||
* core-daemon
|
||||
* fixed issue where shutting down sessions may not have removed session directories
|
||||
* fixed issue with multiple emane interfaces on the same node not getting the right configuration
|
||||
* Installation
|
||||
* updated automated install to be a bit more robust for alternative distros
|
||||
* added force install type to try and leverage a redhat/debian like install
|
||||
* locked ospf mdr version installed to older commit to avoid issues with multiple interfaces on same node
|
||||
|
||||
## 2020-09-15 CORE 7.2.0
|
||||
|
||||
* Installation
|
||||
* locked down version of ospf-mdr installed in automated install
|
||||
* locked down version of emane to v1.2.5 in automated emane install
|
||||
* added option to install locally using the -l option
|
||||
* core-daemon
|
||||
* improve error when retrieving services that do not exist, or failed to load
|
||||
* fixed issue with writing/reading emane node interface configurations to xml
|
||||
* fixed issue with not setting the emane model when creating a node
|
||||
* added common utility method for getting a emane node interface config id in core.utils
|
||||
* fixed issue running emane on more than one interface for a node
|
||||
* fixed issue validating paths when creating emane transport xml for a node
|
||||
* fixed issue avoiding multiple calls to shutdown, if already in shutdown state
|
||||
* core-pygui
|
||||
* fixed issue configuring emane for a node interface
|
||||
* gRPC API
|
||||
* added wrapper client that can provide type hinting and a simpler interface at core.api.grpc.clientw
|
||||
* fixed issue creating sessions that default to having a very large reference scale
|
||||
* fixed issue with GetSession returning control net nodes
|
||||
|
||||
## 2020-08-21 CORE 7.1.0
|
||||
|
||||
* Installation
|
||||
* added core-python script that gets installed to help globally reference the virtual environment
|
||||
* gRPC API
|
||||
* GetSession will now return all configuration information for a session and the file it was opened from, if applicable
|
||||
* node update events will now include icon information
|
||||
* fixed issue with getting session throughputs for sessions with a high id
|
||||
* core-daemon
|
||||
* \#503 - EMANE networks will now work with mobility again
|
||||
* \#506 - fixed service dependency resolution issue
|
||||
* fixed issue sending hooks to core-gui when joining session
|
||||
* core-pygui
|
||||
* fixed issues editing hooks
|
||||
* fixed issue with cpu usage when joining a session
|
||||
* fixed mac field not being disabled during runtime when configuring a node
|
||||
* removed unlimited button from link config dialog
|
||||
* fixed issue with copy/paste links and their options
|
||||
* fixed issue with adding nodes/links and editing links during runtime
|
||||
* updated open file dialog in config dialogs to open to ~/.coregui home directory
|
||||
* fixed issue double clicking sessions dialog in invalid areas
|
||||
* added display of asymmetric link options on links
|
||||
* fixed emane config dialog display
|
||||
* fixed issue saving backgrounds in xml files
|
||||
* added view toggle for wired/wireless links
|
||||
* node events will now update icons
|
||||
|
||||
## 2020-07-28 CORE 7.0.1
|
||||
|
||||
* Bugfixes
|
||||
* \#500 - fixed issue running node commands with shell=True
|
||||
* fixed issue for poetry based install not properly vetting requirements for dataclasses dependency
|
||||
|
||||
## 2020-07-23 CORE 7.0.0
|
||||
|
||||
* Breaking Changes
|
||||
* core.emudata and core.data combined and cleaned up into core.data
|
||||
* updates to consistently use mac instead of hwaddr/mac
|
||||
* \#468 - code related to adding/editing/deleting links cleaned up
|
||||
* \#469 - usages of per all changed to loss to be consistent
|
||||
* \#470 - variables with numbered names now use numbers directly
|
||||
* \#471 - node startup is no longer embedded within its constructor
|
||||
* \#472 - code updated to refer to interfaces consistently as iface
|
||||
* \#475 - code updates changing how ip addresses are stored on interfaces
|
||||
* \#476 - executables to check for moved into own module core.executables
|
||||
* \#486 - core will now install into its own python virtual environment managed by poetry
|
||||
* core-daemon
|
||||
* updates to properly save/load distributed servers to xml
|
||||
* \#474 - added type hinting to all service files
|
||||
* \#478 - fixed typo in config service directory
|
||||
* \#479 - opening an xml file will now cycle through states like a normal session
|
||||
* \#480 - ovs configuration will now save/load from xml and display in guis
|
||||
* \#484 - changes to support adding emane links during runtime
|
||||
* core-pygui
|
||||
* fixed issue not displaying services for the default group in service dialogs
|
||||
* fixed issue starting a session when the daemon is not present
|
||||
* fixed issue attempting to open terminals for invalid nodes
|
||||
* fixed issue syncing session location
|
||||
* fixed issue joining a session with mobility, not in runtime
|
||||
* added cpu usage monitor to status bar
|
||||
* emane configurations can now be seen during runtime
|
||||
* rj45 nodes can only have one link
|
||||
* disabling throughputs will clear labels
|
||||
* improvements to custom service copy
|
||||
* link options will now be drawn on as a label
|
||||
* updates to handle runtime link events
|
||||
* \#477 - added optional details pane for a quick view of node/link details
|
||||
* \#485 - pygui fixed observer widget for invalid nodes
|
||||
* \#496 - improved alert handling
|
||||
* core-gui
|
||||
* \#493 - increased frame size to show all emane configuration options
|
||||
* gRPC API
|
||||
* added set session user rpc
|
||||
* added cpu usage stream
|
||||
* interface objects returned from get_node will now provide node_id, net_id, and net2_id data
|
||||
* peer to peer nodes will not be included in get_session calls
|
||||
* pathloss events will now throw an error when nem id not found
|
||||
* \#481 - link rpc calls will broadcast out
|
||||
* \#496 - added alert rpc call
|
||||
* Services
|
||||
* fixed issue reading files in security services
|
||||
* \#494 - add staticd to daemons list for frr services
|
||||
|
||||
## 2020-06-11 CORE 6.5.0
|
||||
* Breaking Changes
|
||||
* CoreNode.newnetif - both parameters are required and now takes an InterfaceData object as its second parameter
|
||||
* CoreNetworkBase.linkconfig - now takes a LinkOptions parameter instead of a subset of some of the options (ie bandwidth, delay, etc)
|
||||
* \#453 - Session.add_node and Session.get_node now requires the node class you expect to create/retrieve
|
||||
* \#458 - rj45 cleanup to only inherit from one class
|
||||
* Enhancements
|
||||
* fixed issues with handling bad commands for TLV execute messages
|
||||
* removed unused boot.sh from CoreNode types
|
||||
* added linkconfig to CoreNetworkBase and cleaned up function signature
|
||||
* emane position hook now saves geo position to node
|
||||
* emane pathloss support
|
||||
* core.emulator.emudata leveraged dataclass and type hinting
|
||||
* \#459 - updated transport type usage to an enum
|
||||
* \#460 - updated network policy type usage to an enum
|
||||
* Python GUI Enhancements
|
||||
* fixed throughput events do not work for joined sessions
|
||||
* fixed exiting app with a toolbar picker showing
|
||||
* fixed issue with creating interfaces and reusing subnets after deletion
|
||||
* fixed issue with moving text shapes
|
||||
* fixed scaling with custom node selected
|
||||
* fixed toolbar state switching issues
|
||||
* enable/disable toolbar when running stop/start
|
||||
* marker config integrated into toolbar
|
||||
* improved color picker layout
|
||||
* shapes can now be moved while drawing shapes
|
||||
* added observers to toolbar in run mode
|
||||
* gRPC API
|
||||
* node events will now have geo positional data
|
||||
* node geo data is now returned in get_session and get_node calls
|
||||
* \#451 - added wlan link api to allow direct linking/unlinking of wireless links between nodes
|
||||
* \#462 - added streaming call for sending node position/geo changes
|
||||
* \#463 - added streaming call for emane pathloss events
|
||||
* Bugfixes
|
||||
* \#454 - fixed issue creating docker nodes, but containers are now required to have networking tools
|
||||
* \#466 - fixed issue in python gui when xml file is loading nodes with no ip4 addresses
|
||||
|
||||
## 2020-05-11 CORE 6.4.0
|
||||
* Enhancements
|
||||
* updates to core-route-monitor, allow specific session, configurable settings, and properly
|
||||
listen on all interfaces
|
||||
* install.sh now has a "-r" option to help with reinstalling from current branch and installing
|
||||
current python dependencies
|
||||
* \#202 - enable OSPFv2 fast convergence
|
||||
* \#178 - added comments to OVS service
|
||||
* Python GUI Enhancements
|
||||
* added initial documentation to help support usage
|
||||
* supports drawing multiple links for wireless connections
|
||||
* supports differentiating wireless networks with different colored links
|
||||
* implemented unlink in node context menu to delete links to other nodes
|
||||
* implemented node run tool dialog
|
||||
* implemented find node dialog
|
||||
* implemented address configuration dialog
|
||||
* implemented mac configuration dialog
|
||||
* updated link address creation to more closely mimic prior behavior
|
||||
* updated configuration to use yaml class based configs
|
||||
* implemented auto grid layout for nodes
|
||||
* fixed drawn wlan ranges during configuration
|
||||
* Bugfixes
|
||||
* no longer writes link option data for WLAN/EMANE links in XML
|
||||
* avoid configuring links for WLAN/EMANE link options in XML, due to them being written to XML prior
|
||||
* updates to allow building python docs again
|
||||
* \#431 - peer to peer node uplink link data was not using an enum properly due to code changes
|
||||
* \#432 - loading XML was not setting EMANE nodes model
|
||||
* \#435 - loading XML was not maintaining existing session options
|
||||
* \#448 - fixed issue sorting hooks being saved to XML
|
||||
|
||||
## 2020-04-13 CORE 6.3.0
|
||||
* Features
|
||||
* \#424 - added FRR IS-IS service
|
||||
* Enhancements
|
||||
* \#414 - update GUI OSPFv2 adjacency widget to work with FRR
|
||||
* \#416 - EMANE links can now be drawn for 80211 and RF Pipe models
|
||||
* \#418 #409 - code cleanup
|
||||
* \#425 - added route monitor script for SDT3D integration
|
||||
* a formal error will now be thrown when EMANE binding are not installed, but attempted to be used
|
||||
* node positions will now default to 0,0 to avoid GUI errors, when one is not provided
|
||||
* improved SDT3D integration, multiple link support and usage of custom layers
|
||||
* Python GUI Enhancements
|
||||
* enabled edit menu delete
|
||||
* cleaned up node context menu and enabled delete
|
||||
* Bugfixes
|
||||
* \#427 - fixed issue in default route service
|
||||
* \#426 - fixed issue reading ipsec template file
|
||||
* \#420 - fixed issue with TLV API udp handler
|
||||
* \#411 - allow wlan to be configured with 0 values
|
||||
* \#415 - general EMANE configuration was not being saved/loaded from XML
|
||||
|
||||
## 2020-03-16 CORE 6.2.0
|
||||
* gRPC API
|
||||
* Added call to execute python script
|
||||
* Enhancements
|
||||
* \#371 - improved coretk gui scaling
|
||||
* \#374 - display range visually for wlan in coretk gui, when configuring
|
||||
* \#377 - improved coretk error dialogs
|
||||
* \#379 - fixed issues with core converting between x,y and lon,lat for values that would cross utm zones
|
||||
* \#384 - sdt integration moved internally to core code allowing it to work for coretk gui as well
|
||||
* \#387 - coretk gui will now auto detect potential valid terminal and command to use for interacting with nodes during runtime
|
||||
* \#389 - coretk gui will now attempt to reconnect to daemon without need to restart
|
||||
* \#395 - coretk gui now has "save" and "save as" menu options
|
||||
* \#402 - coretk will now allow terminal preference to be directly edited
|
||||
* Bugfixes
|
||||
* \#375 - fixed issues with emane event monitor handling data
|
||||
* \#381 - executing a python script will now wait until completion before looking to join a new session
|
||||
* \#391 - fixed configuring node ip addresses in coretk gui
|
||||
* \#392 - fixed coretk link display when addresses are cleared out
|
||||
* \#393 - coretk gui will properly clear marker annotations when switching sessions
|
||||
* \#396 - Docker and LXC nodes will now properly save to XML
|
||||
* \#406- WLAN bridge initialization was not ran when all nodes are disconnected
|
||||
|
||||
## 2020-02-20 CORE 6.1.0
|
||||
* New
|
||||
* config services - these services leverage a proper template engine and have configurable parameters, given enough time may replace existing services
|
||||
* core-imn-to-xml - IMN to XML utility script
|
||||
* replaced internal code for determining ip/mac address with netaddr library
|
||||
* Enhancements
|
||||
* added distributed package for built packages
|
||||
* made use of python type hinting for functions and their return values
|
||||
* updated Quagga zebra service to remove deprecated warning
|
||||
* Removed
|
||||
* removed stale ns3 code
|
||||
* CORETK GUI
|
||||
* added logging
|
||||
* improved error dialog
|
||||
* properly use global ipv6 addresses for nodes
|
||||
* disable proxy usage by default, flag available to enable
|
||||
* gRPC API
|
||||
* add_link - now returns created interface information
|
||||
* set_node_service - can now set files and directories to properly replicate previous usage
|
||||
* get_emane_event_channel - return information related to the currently used emane event channel
|
||||
* Bugfixes
|
||||
* fixed session SDT functionality back to working order, due to python3 changes
|
||||
* avoid shutting down services for nodes that are not up
|
||||
* EMANE bypass model options will now display properly in GUIs
|
||||
* XML scenarios will now properly read in custom node icons
|
||||
* \#372 - fixed mobility waypoint comparisons
|
||||
* \#370 - fixed radvd service
|
||||
* \#368 - updated frr services to properly start staticd when needed
|
||||
* \#358 - fixed systemd service install path
|
||||
* \#350 - fixed frr babel wireless configuration
|
||||
* \#354 - updated frr to reset interfaces to properly take configurations
|
||||
|
||||
## 2020-01-01 CORE 6.0.0
|
||||
* New
|
||||
* beta release of the python based tk GUI, use **coretk-gui** to try it out, plan will be to eventually sunset the old GUI once this is good enough
|
||||
|
|
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
|
148
Makefile.am
148
Makefile.am
|
@ -6,12 +6,8 @@ if WANT_DOCS
|
|||
DOCS = docs man
|
||||
endif
|
||||
|
||||
if WANT_GUI
|
||||
GUI = gui
|
||||
endif
|
||||
|
||||
if WANT_DAEMON
|
||||
DAEMON = scripts daemon
|
||||
DAEMON = daemon
|
||||
endif
|
||||
|
||||
if WANT_NETNS
|
||||
|
@ -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 \
|
||||
|
@ -44,58 +41,6 @@ DISTCLEANFILES = aclocal.m4 \
|
|||
MAINTAINERCLEANFILES = .version \
|
||||
.version.date
|
||||
|
||||
define fpm-rpm =
|
||||
fpm -s dir -t rpm -n core \
|
||||
-m "$(PACKAGE_MAINTAINERS)" \
|
||||
--license "BSD" \
|
||||
--description "Common Open Research Emulator" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_VERSION_ARCH.rpm \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
--rpm-init scripts/core-daemon \
|
||||
--config-files "/etc/core" \
|
||||
-d "ethtool" \
|
||||
-d "tcl" \
|
||||
-d "tk" \
|
||||
-d "procps-ng" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "iproute" \
|
||||
-d "libev" \
|
||||
-d "net-tools" \
|
||||
-d "python3 >= 3.6" \
|
||||
-d "python3-tkinter" \
|
||||
-C $(DESTDIR)
|
||||
endef
|
||||
|
||||
define fpm-deb =
|
||||
fpm -s dir -t deb -n core \
|
||||
-m "$(PACKAGE_MAINTAINERS)" \
|
||||
--license "BSD" \
|
||||
--description "Common Open Research Emulator" \
|
||||
--url https://github.com/coreemu/core \
|
||||
--vendor "$(PACKAGE_VENDOR)" \
|
||||
-p core_VERSION_ARCH.deb \
|
||||
-v $(PACKAGE_VERSION) \
|
||||
--deb-systemd scripts/core-daemon.service \
|
||||
--deb-no-default-config-files \
|
||||
--config-files "/etc/core" \
|
||||
-d "ethtool" \
|
||||
-d "tcl" \
|
||||
-d "tk" \
|
||||
-d "libtk-img" \
|
||||
-d "procps" \
|
||||
-d "libc6 >= 2.14" \
|
||||
-d "bash >= 3.0" \
|
||||
-d "ebtables" \
|
||||
-d "iproute2" \
|
||||
-d "libev4" \
|
||||
-d "python3 >= 3.6" \
|
||||
-d "python3-tk" \
|
||||
-C $(DESTDIR)
|
||||
endef
|
||||
|
||||
define fpm-distributed-deb =
|
||||
fpm -s dir -t deb -n core-distributed \
|
||||
-m "$(PACKAGE_MAINTAINERS)" \
|
||||
|
@ -103,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 =
|
||||
|
@ -124,29 +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
|
||||
|
||||
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
|
||||
$(MAKE) install DESTDIR=$(DESTDIR)
|
||||
cd daemon && poetry build -f wheel
|
||||
$(call fpm-deb)
|
||||
$(call fpm-rpm)
|
||||
|
||||
.PHONY: fpm-distributed
|
||||
fpm-distributed: clean-local-fpm
|
||||
$(MAKE) -C netns install DESTDIR=$(DESTDIR)
|
||||
$(call fpm-distributed-deb)
|
||||
$(call fpm-distributed-rpm)
|
||||
|
||||
|
@ -173,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
|
||||
|
||||
|
@ -181,12 +183,8 @@ all: change-files
|
|||
|
||||
.PHONY: change-files
|
||||
change-files:
|
||||
$(call change-files,gui/core-gui)
|
||||
$(call change-files,scripts/core-daemon.service)
|
||||
$(call change-files,scripts/core-daemon)
|
||||
$(call change-files,daemon/core/constants.py)
|
||||
$(call change-files,netns/setup.py)
|
||||
$(call change-files,daemon/setup.py)
|
||||
|
||||
CORE_DOC_SRC = core-python-$(PACKAGE_VERSION)
|
||||
.PHONY: doc
|
||||
|
|
116
README.md
116
README.md
|
@ -1,41 +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-2019 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 and Examples
|
||||
## Precompiled container image
|
||||
|
||||
* Documentation hosted on GitHub
|
||||
* <http://coreemu.github.io/core/>
|
||||
* Basic Script Examples
|
||||
* [Examples](daemon/examples/python)
|
||||
* Custom Service Example
|
||||
* [sample.py](daemon/examples/myservices/sample.py)
|
||||
* Custom Emane Model Example
|
||||
* [examplemodel.py](daemon/examples/myemane/examplemodel.py)
|
||||
```bash
|
||||
|
||||
## Support
|
||||
# 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
|
||||
|
||||
We are leveraging Discord for persistent chat rooms, voice chat, and
|
||||
GitHub integration. This allows for more dynamic conversations and the
|
||||
capability to respond faster. Feel free to join us at the link below.
|
||||
<https://discord.gg/AKd7kmP>
|
||||
```
|
||||
## Build container image from source
|
||||
|
||||
## Building CORE
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://gitea.olympuslab.net/afonso/core-extra.git
|
||||
|
||||
See [CORE Installation](http://coreemu.github.io/core/install.html) for detailed build instructions.
|
||||
# cd into the directory
|
||||
cd core-extra
|
||||
|
||||
### Running CORE
|
||||
# build the docker image
|
||||
sudo docker build -t core-extra .
|
||||
|
||||
See [Using the CORE GUI](http://coreemu.github.io/core/usage.html) for more details on running CORE.
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
|
121
configure.ac
121
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, 6.0.0)
|
||||
AC_INIT(core, 9.0.3)
|
||||
|
||||
# autoconf and automake initialization
|
||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||
|
@ -30,20 +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)])],
|
||||
|
@ -89,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)
|
||||
|
@ -118,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)
|
||||
|
@ -162,22 +135,29 @@ if test "x$enable_daemon" = "xyes"; then
|
|||
if test "x$ovs_of_path" = "xno" ; then
|
||||
AC_MSG_WARN([Could not locate ovs-ofctl cannot use OVS mode])
|
||||
fi
|
||||
|
||||
CFLAGS_save=$CFLAGS
|
||||
CPPFLAGS_save=$CPPFLAGS
|
||||
if test "x$PYTHON_INCLUDE_DIR" = "x"; then
|
||||
PYTHON_INCLUDE_DIR=`$PYTHON -c "import distutils.sysconfig; print(distutils.sysconfig.get_python_inc())"`
|
||||
fi
|
||||
CFLAGS="-I$PYTHON_INCLUDE_DIR"
|
||||
CPPFLAGS="-I$PYTHON_INCLUDE_DIR"
|
||||
AC_CHECK_HEADERS([Python.h], [],
|
||||
AC_MSG_ERROR([Python bindings require Python development headers (try installing your 'python-devel' or 'python-dev' package)]))
|
||||
CFLAGS=$CFLAGS_save
|
||||
CPPFLAGS=$CPPFLAGS_save
|
||||
fi
|
||||
|
||||
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)
|
||||
|
@ -191,8 +171,7 @@ if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ;
|
|||
fi
|
||||
|
||||
want_docs=no
|
||||
if test "x$enable_docs" = "xyes" ; then
|
||||
|
||||
if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then
|
||||
AC_CHECK_PROG(help2man, help2man, yes, no, $SEARCHPATH)
|
||||
|
||||
if test "x$help2man" = "xno" ; then
|
||||
|
@ -210,37 +189,17 @@ if test "x$enable_docs" = "xyes" ; then
|
|||
# check for sphinx required during make
|
||||
AC_CHECK_PROG(sphinxapi_path, sphinx-apidoc, $as_dir, no, $SEARCHPATH)
|
||||
if test "x$sphinxapi_path" = "xno" ; then
|
||||
AC_MSG_ERROR(["Could not location sphinx-apidoc, from the python-sphinx package"])
|
||||
AC_MSG_ERROR(["Could not locate sphinx-apidoc, install python3 -m pip install sphinx"])
|
||||
want_docs=no
|
||||
fi
|
||||
AS_IF([$PYTHON -c "import sphinx_rtd_theme" &> /dev/null], [], [AC_MSG_ERROR([doc dependency missing, please install python3 -m pip install sphinx-rtd-theme])])
|
||||
fi
|
||||
|
||||
#AC_PATH_PROGS(tcl_path, [tclsh tclsh8.5 tclsh8.4], no)
|
||||
#if test "x$tcl_path" = "xno" ; then
|
||||
# AC_MSG_ERROR([Could not locate tclsh. Please install Tcl/Tk.])
|
||||
#fi
|
||||
|
||||
#AC_PATH_PROGS(wish_path, [wish wish8.5 wish8.4], no)
|
||||
#if test "x$wish_path" = "xno" ; then
|
||||
# AC_MSG_ERROR([Could not locate wish. Please install Tcl/Tk.])
|
||||
#fi
|
||||
|
||||
AC_ARG_WITH([startup],
|
||||
[AS_HELP_STRING([--with-startup=option],
|
||||
[option=systemd,suse,none to install systemd/SUSE init scripts])],
|
||||
[with_startup=$with_startup],
|
||||
[with_startup=initd])
|
||||
AC_SUBST(with_startup)
|
||||
AC_MSG_RESULT([using startup option $with_startup])
|
||||
|
||||
# Variable substitutions
|
||||
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)
|
||||
AM_CONDITIONAL(WANT_NETNS, test x$want_linux_netns = xyes)
|
||||
AM_CONDITIONAL(WANT_INITD, test x$with_startup = xinitd)
|
||||
AM_CONDITIONAL(WANT_SYSTEMD, test x$with_startup = xsystemd)
|
||||
AM_CONDITIONAL(WANT_VNODEDONLY, test x$enable_vnodedonly = xyes)
|
||||
|
||||
if test $cross_compiling = no; then
|
||||
|
@ -251,10 +210,6 @@ fi
|
|||
|
||||
# Output files
|
||||
AC_CONFIG_FILES([Makefile
|
||||
gui/version.tcl
|
||||
gui/Makefile
|
||||
gui/icons/Makefile
|
||||
scripts/Makefile
|
||||
man/Makefile
|
||||
docs/Makefile
|
||||
daemon/Makefile
|
||||
|
@ -276,20 +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}
|
||||
Logs: ${CORE_STATE_DIR}/log
|
||||
|
||||
Startup: ${with_startup}
|
||||
|
||||
Features to build:
|
||||
Build GUI: ${enable_gui}
|
||||
Build Daemon: ${enable_daemon}
|
||||
Documentation: ${want_docs}
|
||||
|
||||
|
|
2
daemon/.gitignore
vendored
2
daemon/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*.pyc
|
||||
build
|
|
@ -5,19 +5,19 @@ repos:
|
|||
name: isort
|
||||
stages: [commit]
|
||||
language: system
|
||||
entry: bash -c 'cd daemon && pipenv run isort --atomic -y'
|
||||
entry: bash -c 'cd daemon && poetry run isort --atomic -y'
|
||||
types: [python]
|
||||
|
||||
- id: black
|
||||
name: black
|
||||
stages: [commit]
|
||||
language: system
|
||||
entry: bash -c 'cd daemon && pipenv run black --exclude ".+_pb2.*.py|doc|build|utm\.py" .'
|
||||
entry: bash -c 'cd daemon && poetry run black .'
|
||||
types: [python]
|
||||
|
||||
- id: flake8
|
||||
name: flake8
|
||||
stages: [commit]
|
||||
language: system
|
||||
entry: bash -c 'cd daemon && pipenv run flake8'
|
||||
entry: bash -c 'cd daemon && poetry run flake8'
|
||||
types: [python]
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
graft core/gui/data
|
||||
graft core/configservices/*/templates
|
|
@ -1,49 +1,14 @@
|
|||
# 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.
|
||||
#
|
||||
|
||||
SETUPPY = setup.py
|
||||
SETUPPYFLAGS = -v
|
||||
|
||||
if WANT_DOCS
|
||||
DOCS = doc
|
||||
endif
|
||||
|
||||
SUBDIRS = proto $(DOCS)
|
||||
|
||||
SCRIPT_FILES := $(notdir $(wildcard scripts/*))
|
||||
MAN_FILES := $(notdir $(wildcard ../man/*.1))
|
||||
|
||||
# Python package build
|
||||
noinst_SCRIPTS = build
|
||||
build:
|
||||
$(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) build
|
||||
|
||||
# Python package install
|
||||
install-exec-hook:
|
||||
$(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) install \
|
||||
--root=/$(DESTDIR) \
|
||||
--prefix=$(prefix) \
|
||||
--single-version-externally-managed
|
||||
|
||||
# Python package uninstall
|
||||
uninstall-hook:
|
||||
rm -rf $(DESTDIR)/etc/core
|
||||
rm -rf $(DESTDIR)/$(datadir)/core
|
||||
rm -f $(addprefix $(DESTDIR)/$(datarootdir)/man/man1/, $(MAN_FILES))
|
||||
rm -f $(addprefix $(DESTDIR)/$(bindir)/,$(SCRIPT_FILES))
|
||||
rm -rf $(DESTDIR)/$(pythondir)/core-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg-info
|
||||
rm -rf $(DESTDIR)/$(pythondir)/core
|
||||
|
||||
# Python package cleanup
|
||||
clean-local:
|
||||
-rm -rf build
|
||||
|
||||
# because we include entire directories with EXTRA_DIST, we need to clean up
|
||||
# the source control files
|
||||
dist-hook:
|
||||
|
@ -52,17 +17,12 @@ dist-hook:
|
|||
distclean-local:
|
||||
-rm -rf core.egg-info
|
||||
|
||||
|
||||
DISTCLEANFILES = Makefile.in
|
||||
|
||||
# files to include with distribution tarball
|
||||
EXTRA_DIST = $(SETUPPY) \
|
||||
core \
|
||||
data \
|
||||
EXTRA_DIST = core \
|
||||
doc/conf.py.in \
|
||||
examples \
|
||||
scripts \
|
||||
tests \
|
||||
test.py \
|
||||
setup.cfg \
|
||||
requirements.txt
|
||||
poetry.lock \
|
||||
pyproject.toml
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[scripts]
|
||||
core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf"
|
||||
coretk = "python scripts/coretk-gui"
|
||||
test = "pytest -v tests"
|
||||
test-mock = "pytest -v --mock tests"
|
||||
test-emane = "pytest -v tests/emane"
|
||||
|
||||
[dev-packages]
|
||||
grpcio-tools = "*"
|
||||
isort = "*"
|
||||
pre-commit = "*"
|
||||
flake8 = "*"
|
||||
black = "==19.3b0"
|
||||
pytest = "*"
|
||||
mock = "*"
|
||||
|
||||
[packages]
|
||||
core = {editable = true,path = "."}
|
709
daemon/Pipfile.lock
generated
709
daemon/Pipfile.lock
generated
|
@ -1,709 +0,0 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "d702e6eed5a1362bf261543572bbffd2e8a87140b8d8cb07b99fb0d25220a2b5"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"bcrypt": {
|
||||
"hashes": [
|
||||
"sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89",
|
||||
"sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42",
|
||||
"sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294",
|
||||
"sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161",
|
||||
"sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752",
|
||||
"sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31",
|
||||
"sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5",
|
||||
"sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c",
|
||||
"sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0",
|
||||
"sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de",
|
||||
"sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e",
|
||||
"sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052",
|
||||
"sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09",
|
||||
"sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105",
|
||||
"sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133",
|
||||
"sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1",
|
||||
"sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7",
|
||||
"sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"
|
||||
],
|
||||
"version": "==3.1.7"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42",
|
||||
"sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04",
|
||||
"sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5",
|
||||
"sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54",
|
||||
"sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba",
|
||||
"sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57",
|
||||
"sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396",
|
||||
"sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12",
|
||||
"sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97",
|
||||
"sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43",
|
||||
"sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db",
|
||||
"sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3",
|
||||
"sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b",
|
||||
"sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579",
|
||||
"sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346",
|
||||
"sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159",
|
||||
"sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652",
|
||||
"sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e",
|
||||
"sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a",
|
||||
"sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506",
|
||||
"sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f",
|
||||
"sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d",
|
||||
"sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c",
|
||||
"sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20",
|
||||
"sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858",
|
||||
"sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc",
|
||||
"sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a",
|
||||
"sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3",
|
||||
"sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e",
|
||||
"sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410",
|
||||
"sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25",
|
||||
"sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b",
|
||||
"sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"
|
||||
],
|
||||
"version": "==1.13.2"
|
||||
},
|
||||
"core": {
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
|
||||
"sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
|
||||
"sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
|
||||
"sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
|
||||
"sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
|
||||
"sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
|
||||
"sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
|
||||
"sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
|
||||
"sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
|
||||
"sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
|
||||
"sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
|
||||
"sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
|
||||
"sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
|
||||
"sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
|
||||
"sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
|
||||
"sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
|
||||
"sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
|
||||
"sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
|
||||
"sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
|
||||
"sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
|
||||
"sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"fabric": {
|
||||
"hashes": [
|
||||
"sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389",
|
||||
"sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"
|
||||
],
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"grpcio": {
|
||||
"hashes": [
|
||||
"sha256:066630f6b62bffa291dacbee56994279a6a3682b8a11967e9ccaf3cc770fc11e",
|
||||
"sha256:07e95762ca6b18afbeb3aa2793e827c841152d5e507089b1db0b18304edda105",
|
||||
"sha256:0a0fb2f8e3a13537106bc77e4c63005bc60124a6203034304d9101921afa4e90",
|
||||
"sha256:0c61b74dcfb302613926e785cb3542a0905b9a3a86e9410d8cf5d25e25e10104",
|
||||
"sha256:13383bd70618da03684a8aafbdd9e3d9a6720bf8c07b85d0bc697afed599d8f0",
|
||||
"sha256:1c6e0f6b9d091e3717e9a58d631c8bb4898be3b261c2a01fe46371fdc271052f",
|
||||
"sha256:1cf710c04689daa5cc1e598efba00b028215700dcc1bf66fcb7b4f64f2ea5d5f",
|
||||
"sha256:2da5cee9faf17bb8daf500cd0d28a17ae881ab5500f070a6aace457f4c08cac4",
|
||||
"sha256:2f78ebf340eaf28fa09aba0f836a8b869af1716078dfe8f3b3f6ff785d8f2b0f",
|
||||
"sha256:33a07a1a8e817d733588dbd18e567caad1a6fe0d440c165619866cd490c7911a",
|
||||
"sha256:3d090c66af9c065b7228b07c3416f93173e9839b1d40bb0ce3dd2aa783645026",
|
||||
"sha256:42b903a3596a10e2a3727bae2a76f8aefd324d498424b843cfa9606847faea7b",
|
||||
"sha256:4fffbb58134c4f23e5a8312ac3412db6f5e39e961dc0eb5e3115ce5aa16bf927",
|
||||
"sha256:57be5a6c509a406fe0ffa6f8b86904314c77b5e2791be8123368ad2ebccec874",
|
||||
"sha256:5b0fa09efb33e2af4e8822b4eb8b2cbc201d562e3e185c439be7eaeee2e8b8aa",
|
||||
"sha256:5ef42dfc18f9a63a06aca938770b69470bb322e4c137cf08cf21703d1ef4ae5c",
|
||||
"sha256:6a43d2f2ff8250f200fdf7aa31fa191a997922aa9ea1182453acd705ad83ab72",
|
||||
"sha256:6d8ab28559be98b02f8b3a154b53239df1aa5b0d28ff865ae5be4f30e7ed4d3f",
|
||||
"sha256:6e47866b7dc14ca3a12d40c1d6082e7bea964670f1c5315ea0fb8b0550244d64",
|
||||
"sha256:6edda1b96541187f73aab11800d25f18ee87e53d5f96bb74473873072bf28a0e",
|
||||
"sha256:7109c8738a8a3c98cfb5dda1c45642a8d6d35dc00d257ab7a175099b2b4daecd",
|
||||
"sha256:8d866aafb08657c456a18c4a31c8526ea62de42427c242b58210b9eae6c64559",
|
||||
"sha256:9939727d9ae01690b24a2b159ac9dbca7b7e8e6edd5af6a6eb709243cae7b52b",
|
||||
"sha256:99fd873699df17cb11c542553270ae2b32c169986e475df0d68a8629b8ef4df7",
|
||||
"sha256:b6fda5674f990e15e1bcaacf026428cf50bce36e708ddcbd1de9673b14aab760",
|
||||
"sha256:bdb2f3dcb664f0c39ef1312cd6acf6bc6375252e4420cf8f36fff4cb4fa55c71",
|
||||
"sha256:bfd7d3130683a1a0a50c456273c21ec8a604f2d043b241a55235a78a0090ee06",
|
||||
"sha256:c6c2db348ac73d73afe14e0833b18abbbe920969bf2c5c03c0922719f8020d06",
|
||||
"sha256:cb7a4b41b5e2611f85c3402ac364f1d689f5d7ecbc24a55ef010eedcd6cf460f",
|
||||
"sha256:cd3d3e328f20f7c807a862620c6ee748e8d57ba2a8fc960d48337ed71c6d9d32",
|
||||
"sha256:d1a481777952e4f99b8a6956581f3ee866d7614100d70ae6d7e07327570b85ce",
|
||||
"sha256:d1d49720ed636920bb3d74cedf549382caa9ad55aea89d1de99d817068d896b2",
|
||||
"sha256:d42433f0086cccd192114343473d7dbd4aae9141794f939e2b7b83efc57543db",
|
||||
"sha256:d44c34463a7c481e076f691d8fa25d080c3486978c2c41dca09a8dd75296c2d7",
|
||||
"sha256:d7e5b7af1350e9c8c17a7baf99d575fbd2de69f7f0b0e6ebd47b57506de6493a",
|
||||
"sha256:d9542366a0917b9b48bab1fee481ac01f56bdffc52437b598c09e7840148a6a9",
|
||||
"sha256:df7cdfb40179acc9790a462c049e0b8e109481164dd7ad1a388dd67ff1528759",
|
||||
"sha256:e1a9d9d2e7224d981aea8da79260c7f6932bf31ce1f99b7ccfa5eceeb30dc5d0",
|
||||
"sha256:ed10e5fad105ecb0b12822f924e62d0deb07f46683a0b64416b17fd143daba1d",
|
||||
"sha256:f0ec5371ce2363b03531ed522bfbe691ec940f51f0e111f0500fc0f44518c69d",
|
||||
"sha256:f6580a8a4f5e701289b45fd62a8f6cb5ec41e4d77082424f8b676806dcd22564",
|
||||
"sha256:f7b83e4b2842d44fce3cdc0d54db7a7e0d169a598751bf393601efaa401c83e0",
|
||||
"sha256:ffec45b0db18a555fdfe0c6fa2d0a3fceb751b22b31e8fcd14ceed7bde05481e"
|
||||
],
|
||||
"version": "==1.26.0"
|
||||
},
|
||||
"invoke": {
|
||||
"hashes": [
|
||||
"sha256:4668a4a594a47f2da2f0672ec2f7b1566f809cebf10bcd95ce2de9ecf39b95d1",
|
||||
"sha256:ae7b4513638bde9afcda0825e9535599637a3f65bd819a27098356027bb17c8a",
|
||||
"sha256:e04faba8ea7cdf6f5c912be42dcafd5c1074b7f2f306998992c4bfb40a9a690b"
|
||||
],
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:00ac0d64949fef6b3693813fe636a2d56d97a5a49b5bbb86e4cc4cc50ebc9ea2",
|
||||
"sha256:0571e607558665ed42e450d7bf0e2941d542c18e117b1ebbf0ba72f287ad841c",
|
||||
"sha256:0e3f04a7615fdac0be5e18b2406529521d6dbdb0167d2a690ee328bef7807487",
|
||||
"sha256:13cf89be53348d1c17b453867da68704802966c433b2bb4fa1f970daadd2ef70",
|
||||
"sha256:217262fcf6a4c2e1c7cb1efa08bd9ebc432502abc6c255c4abab611e8be0d14d",
|
||||
"sha256:223e544828f1955daaf4cefbb4853bc416b2ec3fd56d4f4204a8b17007c21250",
|
||||
"sha256:277cb61fede2f95b9c61912fefb3d43fbd5f18bf18a14fae4911b67984486f5d",
|
||||
"sha256:3213f753e8ae86c396e0e066866e64c6b04618e85c723b32ecb0909885211f74",
|
||||
"sha256:4690984a4dee1033da0af6df0b7a6bde83f74e1c0c870623797cec77964de34d",
|
||||
"sha256:4fcc472ef87f45c429d3b923b925704aa581f875d65bac80f8ab0c3296a63f78",
|
||||
"sha256:61409bd745a265a742f2693e4600e4dbd45cc1daebe1d5fad6fcb22912d44145",
|
||||
"sha256:678f1963f755c5d9f5f6968dded7b245dd1ece8cf53c1aa9d80e6734a8c7f41d",
|
||||
"sha256:6c6d03549d4e2734133badb9ab1c05d9f0ef4bcd31d83e5d2b4747c85cfa21da",
|
||||
"sha256:6e74d5f4d6ecd6942375c52ffcd35f4318a61a02328f6f1bd79fcb4ffedf969e",
|
||||
"sha256:7b4fc7b1ecc987ca7aaf3f4f0e71bbfbd81aaabf87002558f5bc95da3a865bcd",
|
||||
"sha256:7ed386a40e172ddf44c061ad74881d8622f791d9af0b6f5be20023029129bc85",
|
||||
"sha256:8f54f0924d12c47a382c600c880770b5ebfc96c9fd94cf6f6bdc21caf6163ea7",
|
||||
"sha256:ad9b81351fdc236bda538efa6879315448411a81186c836d4b80d6ca8217cdb9",
|
||||
"sha256:bbd00e21ea17f7bcc58dccd13869d68441b32899e89cf6cfa90d624a9198ce85",
|
||||
"sha256:c3c289762cc09735e2a8f8a49571d0e8b4f57ea831ea11558247b5bdea0ac4db",
|
||||
"sha256:cf4650942de5e5685ad308e22bcafbccfe37c54aa7c0e30cd620c2ee5c93d336",
|
||||
"sha256:cfcbc33c9c59c93776aa41ab02e55c288a042211708b72fdb518221cc803abc8",
|
||||
"sha256:e301055deadfedbd80cf94f2f65ff23126b232b0d1fea28f332ce58137bcdb18",
|
||||
"sha256:ebbfe24df7f7b5c6c7620702496b6419f6a9aa2fd7f005eb731cc80d7b4692b9",
|
||||
"sha256:eff69ddbf3ad86375c344339371168640951c302450c5d3e9936e98d6459db06",
|
||||
"sha256:f6ed60a62c5f1c44e789d2cf14009423cb1646b44a43e40a9cf6a21f077678a1"
|
||||
],
|
||||
"version": "==4.4.2"
|
||||
},
|
||||
"mako": {
|
||||
"hashes": [
|
||||
"sha256:2984a6733e1d472796ceef37ad48c26f4a984bb18119bb2dbc37a44d8f6e75a4"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"netaddr": {
|
||||
"hashes": [
|
||||
"sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd",
|
||||
"sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"
|
||||
],
|
||||
"version": "==0.7.19"
|
||||
},
|
||||
"paramiko": {
|
||||
"hashes": [
|
||||
"sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f",
|
||||
"sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f"
|
||||
],
|
||||
"version": "==2.7.1"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be",
|
||||
"sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946",
|
||||
"sha256:54ebae163e8412aff0b9df1e88adab65788f5f5b58e625dc5c7f51eaf14a6837",
|
||||
"sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f",
|
||||
"sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00",
|
||||
"sha256:5f7ae9126d16194f114435ebb79cc536b5682002a4fa57fa7bb2cbcde65f2f4d",
|
||||
"sha256:62a889aeb0a79e50ecf5af272e9e3c164148f4bd9636cc6bcfa182a52c8b0533",
|
||||
"sha256:7406f5a9b2fd966e79e6abdaf700585a4522e98d6559ce37fc52e5c955fade0a",
|
||||
"sha256:8453f914f4e5a3d828281a6628cf517832abfa13ff50679a4848926dac7c0358",
|
||||
"sha256:87269cc6ce1e3dee11f23fa515e4249ae678dbbe2704598a51cee76c52e19cda",
|
||||
"sha256:875358310ed7abd5320f21dd97351d62de4929b0426cdb1eaa904b64ac36b435",
|
||||
"sha256:8ac6ce7ff3892e5deaab7abaec763538ffd011f74dc1801d93d3c5fc541feee2",
|
||||
"sha256:91b710e3353aea6fc758cdb7136d9bbdcb26b53cefe43e2cba953ac3ee1d3313",
|
||||
"sha256:9d2ba4ed13af381233e2d810ff3bab84ef9f18430a9b336ab69eaf3cd24299ff",
|
||||
"sha256:a62ec5e13e227399be73303ff301f2865bf68657d15ea50b038d25fc41097317",
|
||||
"sha256:ab76e5580b0ed647a8d8d2d2daee170e8e9f8aad225ede314f684e297e3643c2",
|
||||
"sha256:bf4003aa538af3f4205c5fac56eacaa67a6dd81e454ffd9e9f055fff9f1bc614",
|
||||
"sha256:bf598d2e37cf8edb1a2f26ed3fb255191f5232badea4003c16301cb94ac5bdd0",
|
||||
"sha256:c18f70dc27cc5d236f10e7834236aff60aadc71346a5bc1f4f83a4b3abee6386",
|
||||
"sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9",
|
||||
"sha256:dc058b7833184970d1248135b8b0ab702e6daa833be14035179f2acb78ff5636",
|
||||
"sha256:ff3797f2f16bf9d17d53257612da84dd0758db33935777149b3334c01ff68865"
|
||||
],
|
||||
"version": "==7.0.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37",
|
||||
"sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36",
|
||||
"sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4",
|
||||
"sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d",
|
||||
"sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574",
|
||||
"sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0",
|
||||
"sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5",
|
||||
"sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f",
|
||||
"sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946",
|
||||
"sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104",
|
||||
"sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e",
|
||||
"sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7",
|
||||
"sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a",
|
||||
"sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507",
|
||||
"sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1",
|
||||
"sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593",
|
||||
"sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c",
|
||||
"sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1"
|
||||
],
|
||||
"version": "==3.11.2"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
|
||||
],
|
||||
"version": "==2.19"
|
||||
},
|
||||
"pynacl": {
|
||||
"hashes": [
|
||||
"sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255",
|
||||
"sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c",
|
||||
"sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e",
|
||||
"sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae",
|
||||
"sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621",
|
||||
"sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56",
|
||||
"sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39",
|
||||
"sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310",
|
||||
"sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1",
|
||||
"sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5",
|
||||
"sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a",
|
||||
"sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786",
|
||||
"sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b",
|
||||
"sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b",
|
||||
"sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f",
|
||||
"sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20",
|
||||
"sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415",
|
||||
"sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715",
|
||||
"sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92",
|
||||
"sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1",
|
||||
"sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
|
||||
"sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
|
||||
"sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
|
||||
"sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
|
||||
"sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
|
||||
"sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
|
||||
"sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
|
||||
"sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
|
||||
"sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
|
||||
"sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
|
||||
"sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
|
||||
],
|
||||
"version": "==5.3"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"aspy.yaml": {
|
||||
"hashes": [
|
||||
"sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc",
|
||||
"sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
|
||||
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.3b0"
|
||||
},
|
||||
"cfgv": {
|
||||
"hashes": [
|
||||
"sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144",
|
||||
"sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289"
|
||||
],
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
|
||||
],
|
||||
"version": "==0.3"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
|
||||
"sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.9"
|
||||
},
|
||||
"grpcio": {
|
||||
"hashes": [
|
||||
"sha256:066630f6b62bffa291dacbee56994279a6a3682b8a11967e9ccaf3cc770fc11e",
|
||||
"sha256:07e95762ca6b18afbeb3aa2793e827c841152d5e507089b1db0b18304edda105",
|
||||
"sha256:0a0fb2f8e3a13537106bc77e4c63005bc60124a6203034304d9101921afa4e90",
|
||||
"sha256:0c61b74dcfb302613926e785cb3542a0905b9a3a86e9410d8cf5d25e25e10104",
|
||||
"sha256:13383bd70618da03684a8aafbdd9e3d9a6720bf8c07b85d0bc697afed599d8f0",
|
||||
"sha256:1c6e0f6b9d091e3717e9a58d631c8bb4898be3b261c2a01fe46371fdc271052f",
|
||||
"sha256:1cf710c04689daa5cc1e598efba00b028215700dcc1bf66fcb7b4f64f2ea5d5f",
|
||||
"sha256:2da5cee9faf17bb8daf500cd0d28a17ae881ab5500f070a6aace457f4c08cac4",
|
||||
"sha256:2f78ebf340eaf28fa09aba0f836a8b869af1716078dfe8f3b3f6ff785d8f2b0f",
|
||||
"sha256:33a07a1a8e817d733588dbd18e567caad1a6fe0d440c165619866cd490c7911a",
|
||||
"sha256:3d090c66af9c065b7228b07c3416f93173e9839b1d40bb0ce3dd2aa783645026",
|
||||
"sha256:42b903a3596a10e2a3727bae2a76f8aefd324d498424b843cfa9606847faea7b",
|
||||
"sha256:4fffbb58134c4f23e5a8312ac3412db6f5e39e961dc0eb5e3115ce5aa16bf927",
|
||||
"sha256:57be5a6c509a406fe0ffa6f8b86904314c77b5e2791be8123368ad2ebccec874",
|
||||
"sha256:5b0fa09efb33e2af4e8822b4eb8b2cbc201d562e3e185c439be7eaeee2e8b8aa",
|
||||
"sha256:5ef42dfc18f9a63a06aca938770b69470bb322e4c137cf08cf21703d1ef4ae5c",
|
||||
"sha256:6a43d2f2ff8250f200fdf7aa31fa191a997922aa9ea1182453acd705ad83ab72",
|
||||
"sha256:6d8ab28559be98b02f8b3a154b53239df1aa5b0d28ff865ae5be4f30e7ed4d3f",
|
||||
"sha256:6e47866b7dc14ca3a12d40c1d6082e7bea964670f1c5315ea0fb8b0550244d64",
|
||||
"sha256:6edda1b96541187f73aab11800d25f18ee87e53d5f96bb74473873072bf28a0e",
|
||||
"sha256:7109c8738a8a3c98cfb5dda1c45642a8d6d35dc00d257ab7a175099b2b4daecd",
|
||||
"sha256:8d866aafb08657c456a18c4a31c8526ea62de42427c242b58210b9eae6c64559",
|
||||
"sha256:9939727d9ae01690b24a2b159ac9dbca7b7e8e6edd5af6a6eb709243cae7b52b",
|
||||
"sha256:99fd873699df17cb11c542553270ae2b32c169986e475df0d68a8629b8ef4df7",
|
||||
"sha256:b6fda5674f990e15e1bcaacf026428cf50bce36e708ddcbd1de9673b14aab760",
|
||||
"sha256:bdb2f3dcb664f0c39ef1312cd6acf6bc6375252e4420cf8f36fff4cb4fa55c71",
|
||||
"sha256:bfd7d3130683a1a0a50c456273c21ec8a604f2d043b241a55235a78a0090ee06",
|
||||
"sha256:c6c2db348ac73d73afe14e0833b18abbbe920969bf2c5c03c0922719f8020d06",
|
||||
"sha256:cb7a4b41b5e2611f85c3402ac364f1d689f5d7ecbc24a55ef010eedcd6cf460f",
|
||||
"sha256:cd3d3e328f20f7c807a862620c6ee748e8d57ba2a8fc960d48337ed71c6d9d32",
|
||||
"sha256:d1a481777952e4f99b8a6956581f3ee866d7614100d70ae6d7e07327570b85ce",
|
||||
"sha256:d1d49720ed636920bb3d74cedf549382caa9ad55aea89d1de99d817068d896b2",
|
||||
"sha256:d42433f0086cccd192114343473d7dbd4aae9141794f939e2b7b83efc57543db",
|
||||
"sha256:d44c34463a7c481e076f691d8fa25d080c3486978c2c41dca09a8dd75296c2d7",
|
||||
"sha256:d7e5b7af1350e9c8c17a7baf99d575fbd2de69f7f0b0e6ebd47b57506de6493a",
|
||||
"sha256:d9542366a0917b9b48bab1fee481ac01f56bdffc52437b598c09e7840148a6a9",
|
||||
"sha256:df7cdfb40179acc9790a462c049e0b8e109481164dd7ad1a388dd67ff1528759",
|
||||
"sha256:e1a9d9d2e7224d981aea8da79260c7f6932bf31ce1f99b7ccfa5eceeb30dc5d0",
|
||||
"sha256:ed10e5fad105ecb0b12822f924e62d0deb07f46683a0b64416b17fd143daba1d",
|
||||
"sha256:f0ec5371ce2363b03531ed522bfbe691ec940f51f0e111f0500fc0f44518c69d",
|
||||
"sha256:f6580a8a4f5e701289b45fd62a8f6cb5ec41e4d77082424f8b676806dcd22564",
|
||||
"sha256:f7b83e4b2842d44fce3cdc0d54db7a7e0d169a598751bf393601efaa401c83e0",
|
||||
"sha256:ffec45b0db18a555fdfe0c6fa2d0a3fceb751b22b31e8fcd14ceed7bde05481e"
|
||||
],
|
||||
"version": "==1.26.0"
|
||||
},
|
||||
"grpcio-tools": {
|
||||
"hashes": [
|
||||
"sha256:0286f704e55e3012fec3910400fe1a4ed11aeb66d3ec4b7f8041845af7fb7206",
|
||||
"sha256:033a4e80dc78d9c11860800bd5a66b65ff385be8f669e96b02e795364c860597",
|
||||
"sha256:0e3b5469912430f19407ebe14cfd1bece1b5a277c4d43e1b65dbff19d9475ccc",
|
||||
"sha256:131aa8c3862a555819428856f872ab9e919e351d7cd60c98012e12d2fb6afc45",
|
||||
"sha256:1783b8fa74f58a643e7780112fc4eb6110789672e852a691fad6af6b94a90c4a",
|
||||
"sha256:1e80f74854bd1c7263942e836d69f95ffc66bb45bf14bf3e1ab61113271b5884",
|
||||
"sha256:27ae784acff3d2fa04e3b4dc72f8d60a55d654f90e410adf08f46a4d2d673dd3",
|
||||
"sha256:33c6bee5a02408018dc10a5737818d2159f14cbb0613df41cc93ba6cbaeea095",
|
||||
"sha256:376a1840d1f5d25e9c3391557d6b3eeb3de17be697b0e55d8247d0262fcbaacf",
|
||||
"sha256:3922dffd8160d54dc00c7d32b30776a974cc098086493c668faffac19e752087",
|
||||
"sha256:4ba7e5afc93b413bbb5f3dd65ba583e078ff5895a5053d825ab793cf7720ae96",
|
||||
"sha256:4e9a1276f8699d06518cec8caceb2c423fc7f971765cab7550d39f281795fd81",
|
||||
"sha256:51ac9c4f8a542cd20c6776fde781c84c0acd8faba55ec14f121c6b4eb4245e89",
|
||||
"sha256:5580b86cf49936c9c74f0def44d3582a7a1bb720eba8a14805c3a61efa790c70",
|
||||
"sha256:58a879208bd84d6819a61c1b0618655574ef9df1d63a0e2f434fdcb5cfa1fb57",
|
||||
"sha256:675918f83fa35bd54f4c29d95d8652c6215d5e95a13b6f14e626cdef6d0fce79",
|
||||
"sha256:68259fd06188951d152665ffe44f9660edd715c102ae4bc4216eca4c4666dadf",
|
||||
"sha256:6cea124cbd9081a587e1954b98e9a27c7cca6ae72babc3046ab6b439a5730679",
|
||||
"sha256:6f356a445ba7afc634b1046d9f51d3ae37afbf4fe1a500285aca37677462a7b9",
|
||||
"sha256:7f7430434bd997584f2136a675559ba0d4afdf7cb71d9bbc429b0cc831e6828c",
|
||||
"sha256:809d60f15a32c21dc221ddb591aff8adfdde4e05095414eb8e015cdfef361615",
|
||||
"sha256:826c19f26b41e99691e77823ad67f04dc0b69e514212907695e330c6f106415c",
|
||||
"sha256:96c6f657b93f49243d083840d27a5a686a1fc26044a80ebf8585734d5152d4ee",
|
||||
"sha256:9a2091371298f04ef350f776365945537d0befa95bad5623d80c4207bdff9d3a",
|
||||
"sha256:9af72b764b41ba939e8e0a7ae9ec8a17d1c46a18797c6342cba6483f29e1790f",
|
||||
"sha256:a209002e3d4787f0e90e29f15cddbe83dc9054238c0da7f539c913002a348cc1",
|
||||
"sha256:a908d5af2f26673e970c7c03703437bf95d10e88dad3322e7e267467db44a04d",
|
||||
"sha256:ab841c69581085b6f9aa54044a13db6ec31183513f7cce0862d29c9b7b4e3c64",
|
||||
"sha256:b1bc78efefb8e085c072add2c02326fdecad9b8644b3be11e715ea4c6102ad87",
|
||||
"sha256:b97e74ffe121dfa9ae7ec94393fce4e95e9e0a343827663e989dc4b7c918d1a5",
|
||||
"sha256:bba8d3b61ec113bb94596599d2568217b22ddfc7baa46c00dec5106cfd4e914b",
|
||||
"sha256:bfe0e33aea60da100b214c72c1746cc0194bb8da910004518c185041cc795543",
|
||||
"sha256:c15f0718cbc3986e747d5b0734198dce0ac07d188ec5e063b1e9889ac947f86e",
|
||||
"sha256:c56d0ac769bf1f01dbb6ec6b6492849e70cd35bdeeb660e206a70ab43917ae92",
|
||||
"sha256:d396fdb7026986e6d3897bb207cc7d5bc536a82a2e50af806a24b3d254c73bc3",
|
||||
"sha256:d62ab00dea7fa0813fc813a6c848da2eeda5cb71893b892a229d23949de0cecd",
|
||||
"sha256:da75e33e185c8be17a82ec4a97f5c75ec05d57e85f8b285f86e2a22484849e4a",
|
||||
"sha256:dcbd1fbb540638c9ad9c3a071b392b654f79666a2bc12808080b0e9f674b9a80",
|
||||
"sha256:e7e90bad5466347a3648358e9f437e72d5f6d6025fe741171a88aca8b9d864df",
|
||||
"sha256:eae371a663ceeef8f930323a120a9d11e13e1c49903a66ddb4ada4830d5bcb7d",
|
||||
"sha256:f290cccc972533a288c2ebc55eb3c0fbe0c6a0d0a9775cb34ce6bfb11fe14a11",
|
||||
"sha256:facb8c588cdd6adc51ae7545f59283565dae8d946c6163e578b70ab6bf161215",
|
||||
"sha256:fb043e45f91634776acdfe4b8dfc96b636c53a458799179041ab633e15c3d833"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.26.0"
|
||||
},
|
||||
"identify": {
|
||||
"hashes": [
|
||||
"sha256:418f3b2313ac0b531139311a6b426854e9cbdfcfb6175447a5039aa6291d8b30",
|
||||
"sha256:8ad99ed1f3a965612dcb881435bf58abcfbeb05e230bb8c352b51e8eac103360"
|
||||
],
|
||||
"version": "==1.4.10"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
||||
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"importlib-resources": {
|
||||
"hashes": [
|
||||
"sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b",
|
||||
"sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078"
|
||||
],
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.21"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"mock": {
|
||||
"hashes": [
|
||||
"sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3",
|
||||
"sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.5"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
|
||||
"sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
|
||||
],
|
||||
"version": "==8.1.0"
|
||||
},
|
||||
"nodeenv": {
|
||||
"hashes": [
|
||||
"sha256:561057acd4ae3809e665a9aaaf214afff110bbb6a6d5c8a96121aea6878408b3"
|
||||
],
|
||||
"version": "==1.3.4"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73",
|
||||
"sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"
|
||||
],
|
||||
"version": "==20.1"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"pre-commit": {
|
||||
"hashes": [
|
||||
"sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850",
|
||||
"sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.21.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37",
|
||||
"sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36",
|
||||
"sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4",
|
||||
"sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d",
|
||||
"sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574",
|
||||
"sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0",
|
||||
"sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5",
|
||||
"sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f",
|
||||
"sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946",
|
||||
"sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104",
|
||||
"sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e",
|
||||
"sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7",
|
||||
"sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a",
|
||||
"sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507",
|
||||
"sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1",
|
||||
"sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593",
|
||||
"sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c",
|
||||
"sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1"
|
||||
],
|
||||
"version": "==3.11.2"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
|
||||
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
|
||||
],
|
||||
"version": "==1.8.1"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
|
||||
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
|
||||
],
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
|
||||
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
|
||||
],
|
||||
"version": "==2.1.1"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
|
||||
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
|
||||
],
|
||||
"version": "==2.4.6"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:1d122e8be54d1a709e56f82e2d85dcba3018313d64647f38a91aec88c239b600",
|
||||
"sha256:c13d1943c63e599b98cf118fcb9703e4d7bde7caa9a432567bcdcae4bf512d20"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.3.4"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
|
||||
"sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
|
||||
"sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
|
||||
"sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
|
||||
"sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
|
||||
"sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
|
||||
"sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
|
||||
"sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
|
||||
"sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
|
||||
"sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
|
||||
"sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
|
||||
],
|
||||
"version": "==5.3"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3",
|
||||
"sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb"
|
||||
],
|
||||
"version": "==16.7.9"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
|
||||
"sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
|
||||
],
|
||||
"version": "==0.1.8"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
|
||||
"sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67"
|
||||
],
|
||||
"version": "==2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -1,9 +1,10 @@
|
|||
import logging
|
||||
from collections.abc import Iterable
|
||||
from queue import Empty, Queue
|
||||
from typing import Iterable
|
||||
from typing import Optional
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc.grpcutils import convert_value
|
||||
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,160 +15,121 @@ from core.emulator.data import (
|
|||
)
|
||||
from core.emulator.session import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def handle_node_event(event: NodeData) -> core_pb2.NodeEvent:
|
||||
|
||||
def handle_node_event(session: Session, node_data: NodeData) -> core_pb2.Event:
|
||||
"""
|
||||
Handle node event when there is a node event
|
||||
|
||||
:param event: node data
|
||||
:param session: session node is from
|
||||
:param node_data: node data
|
||||
:return: node event that contains node id, name, model, position, and services
|
||||
"""
|
||||
position = core_pb2.Position(x=event.x_position, y=event.y_position)
|
||||
services = event.services or ""
|
||||
services = services.split("|")
|
||||
node_proto = core_pb2.Node(
|
||||
id=event.id,
|
||||
name=event.name,
|
||||
model=event.model,
|
||||
position=position,
|
||||
services=services,
|
||||
)
|
||||
return core_pb2.NodeEvent(node=node_proto, source=event.source)
|
||||
node = node_data.node
|
||||
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)
|
||||
|
||||
|
||||
def handle_link_event(event: LinkData) -> core_pb2.LinkEvent:
|
||||
def handle_link_event(link_data: LinkData) -> core_pb2.Event:
|
||||
"""
|
||||
Handle link event when there is a link event
|
||||
|
||||
:param event: link data
|
||||
:param link_data: link data
|
||||
:return: link event that has message type and link information
|
||||
"""
|
||||
interface_one = None
|
||||
if event.interface1_id is not None:
|
||||
interface_one = core_pb2.Interface(
|
||||
id=event.interface1_id,
|
||||
name=event.interface1_name,
|
||||
mac=convert_value(event.interface1_mac),
|
||||
ip4=convert_value(event.interface1_ip4),
|
||||
ip4mask=event.interface1_ip4_mask,
|
||||
ip6=convert_value(event.interface1_ip6),
|
||||
ip6mask=event.interface1_ip6_mask,
|
||||
)
|
||||
|
||||
interface_two = None
|
||||
if event.interface2_id is not None:
|
||||
interface_two = core_pb2.Interface(
|
||||
id=event.interface2_id,
|
||||
name=event.interface2_name,
|
||||
mac=convert_value(event.interface2_mac),
|
||||
ip4=convert_value(event.interface2_ip4),
|
||||
ip4mask=event.interface2_ip4_mask,
|
||||
ip6=convert_value(event.interface2_ip6),
|
||||
ip6mask=event.interface2_ip6_mask,
|
||||
)
|
||||
|
||||
options = core_pb2.LinkOptions(
|
||||
opaque=event.opaque,
|
||||
jitter=event.jitter,
|
||||
key=event.key,
|
||||
mburst=event.mburst,
|
||||
mer=event.mer,
|
||||
per=event.per,
|
||||
bandwidth=event.bandwidth,
|
||||
burst=event.burst,
|
||||
delay=event.delay,
|
||||
dup=event.dup,
|
||||
unidirectional=event.unidirectional,
|
||||
)
|
||||
link = core_pb2.Link(
|
||||
type=event.link_type,
|
||||
node_one_id=event.node1_id,
|
||||
node_two_id=event.node2_id,
|
||||
interface_one=interface_one,
|
||||
interface_two=interface_two,
|
||||
options=options,
|
||||
)
|
||||
return core_pb2.LinkEvent(message_type=event.message_type, link=link)
|
||||
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)
|
||||
|
||||
|
||||
def handle_session_event(event: EventData) -> core_pb2.SessionEvent:
|
||||
def handle_session_event(event_data: EventData) -> core_pb2.Event:
|
||||
"""
|
||||
Handle session event when there is a session event
|
||||
|
||||
:param event: event data
|
||||
:param event_data: event data
|
||||
:return: session event
|
||||
"""
|
||||
event_time = event.time
|
||||
event_time = event_data.time
|
||||
if event_time is not None:
|
||||
event_time = float(event_time)
|
||||
return core_pb2.SessionEvent(
|
||||
node_id=event.node,
|
||||
event=event.event_type,
|
||||
name=event.name,
|
||||
data=event.data,
|
||||
session_event = core_pb2.SessionEvent(
|
||||
node_id=event_data.node,
|
||||
event=event_data.event_type.value,
|
||||
name=event_data.name,
|
||||
data=event_data.data,
|
||||
time=event_time,
|
||||
)
|
||||
return core_pb2.Event(session_event=session_event)
|
||||
|
||||
|
||||
def handle_config_event(event: ConfigData) -> core_pb2.ConfigEvent:
|
||||
def handle_config_event(config_data: ConfigData) -> core_pb2.Event:
|
||||
"""
|
||||
Handle configuration event when there is configuration event
|
||||
|
||||
:param event: configuration data
|
||||
:param config_data: configuration data
|
||||
:return: configuration event
|
||||
"""
|
||||
return core_pb2.ConfigEvent(
|
||||
message_type=event.message_type,
|
||||
node_id=event.node,
|
||||
object=event.object,
|
||||
type=event.type,
|
||||
captions=event.captions,
|
||||
bitmap=event.bitmap,
|
||||
data_values=event.data_values,
|
||||
possible_values=event.possible_values,
|
||||
groups=event.groups,
|
||||
interface=event.interface_number,
|
||||
network_id=event.network_id,
|
||||
opaque=event.opaque,
|
||||
data_types=event.data_types,
|
||||
config_event = core_pb2.ConfigEvent(
|
||||
message_type=config_data.message_type,
|
||||
node_id=config_data.node,
|
||||
object=config_data.object,
|
||||
type=config_data.type,
|
||||
captions=config_data.captions,
|
||||
bitmap=config_data.bitmap,
|
||||
data_values=config_data.data_values,
|
||||
possible_values=config_data.possible_values,
|
||||
groups=config_data.groups,
|
||||
iface_id=config_data.iface_id,
|
||||
network_id=config_data.network_id,
|
||||
opaque=config_data.opaque,
|
||||
data_types=config_data.data_types,
|
||||
)
|
||||
return core_pb2.Event(config_event=config_event)
|
||||
|
||||
|
||||
def handle_exception_event(event: ExceptionData) -> core_pb2.ExceptionEvent:
|
||||
def handle_exception_event(exception_data: ExceptionData) -> core_pb2.Event:
|
||||
"""
|
||||
Handle exception event when there is exception event
|
||||
|
||||
:param event: exception data
|
||||
:param exception_data: exception data
|
||||
:return: exception event
|
||||
"""
|
||||
return core_pb2.ExceptionEvent(
|
||||
node_id=event.node,
|
||||
level=event.level.value,
|
||||
source=event.source,
|
||||
date=event.date,
|
||||
text=event.text,
|
||||
opaque=event.opaque,
|
||||
exception_event = core_pb2.ExceptionEvent(
|
||||
node_id=exception_data.node,
|
||||
level=exception_data.level.value,
|
||||
source=exception_data.source,
|
||||
date=exception_data.date,
|
||||
text=exception_data.text,
|
||||
opaque=exception_data.opaque,
|
||||
)
|
||||
return core_pb2.Event(exception_event=exception_event)
|
||||
|
||||
|
||||
def handle_file_event(event: FileData) -> core_pb2.FileEvent:
|
||||
def handle_file_event(file_data: FileData) -> core_pb2.Event:
|
||||
"""
|
||||
Handle file event
|
||||
|
||||
:param event: file data
|
||||
:param file_data: file data
|
||||
:return: file event
|
||||
"""
|
||||
return core_pb2.FileEvent(
|
||||
message_type=event.message_type,
|
||||
node_id=event.node,
|
||||
name=event.name,
|
||||
mode=event.mode,
|
||||
number=event.number,
|
||||
type=event.type,
|
||||
source=event.source,
|
||||
data=event.data,
|
||||
compressed_data=event.compressed_data,
|
||||
file_event = core_pb2.FileEvent(
|
||||
message_type=file_data.message_type.value,
|
||||
node_id=file_data.node,
|
||||
name=file_data.name,
|
||||
mode=file_data.mode,
|
||||
number=file_data.number,
|
||||
type=file_data.type,
|
||||
source=file_data.source,
|
||||
data=file_data.data,
|
||||
compressed_data=file_data.compressed_data,
|
||||
)
|
||||
return core_pb2.Event(file_event=file_event)
|
||||
|
||||
|
||||
class EventStreamer:
|
||||
|
@ -184,9 +146,9 @@ class EventStreamer:
|
|||
:param session: session to process events for
|
||||
:param event_types: types of events to process
|
||||
"""
|
||||
self.session = session
|
||||
self.event_types = event_types
|
||||
self.queue = Queue()
|
||||
self.session: Session = session
|
||||
self.event_types: Iterable[core_pb2.EventType] = event_types
|
||||
self.queue: Queue = Queue()
|
||||
self.add_handlers()
|
||||
|
||||
def add_handlers(self) -> None:
|
||||
|
@ -208,32 +170,33 @@ class EventStreamer:
|
|||
if core_pb2.EventType.SESSION in self.event_types:
|
||||
self.session.event_handlers.append(self.queue.put)
|
||||
|
||||
def process(self) -> core_pb2.Event:
|
||||
def process(self) -> Optional[core_pb2.Event]:
|
||||
"""
|
||||
Process the next event in the queue.
|
||||
|
||||
:return: grpc event, or None when invalid event or queue timeout
|
||||
"""
|
||||
event = core_pb2.Event(session_id=self.session.id)
|
||||
event = None
|
||||
try:
|
||||
data = self.queue.get(timeout=1)
|
||||
if isinstance(data, NodeData):
|
||||
event.node_event.CopyFrom(handle_node_event(data))
|
||||
event = handle_node_event(self.session, data)
|
||||
elif isinstance(data, LinkData):
|
||||
event.link_event.CopyFrom(handle_link_event(data))
|
||||
event = handle_link_event(data)
|
||||
elif isinstance(data, EventData):
|
||||
event.session_event.CopyFrom(handle_session_event(data))
|
||||
event = handle_session_event(data)
|
||||
elif isinstance(data, ConfigData):
|
||||
event.config_event.CopyFrom(handle_config_event(data))
|
||||
event = handle_config_event(data)
|
||||
elif isinstance(data, ExceptionData):
|
||||
event.exception_event.CopyFrom(handle_exception_event(data))
|
||||
event = handle_exception_event(data)
|
||||
elif isinstance(data, FileData):
|
||||
event.file_event.CopyFrom(handle_file_event(data))
|
||||
event = handle_file_event(data)
|
||||
else:
|
||||
logging.error("unknown event: %s", data)
|
||||
event = None
|
||||
logger.error("unknown event: %s", data)
|
||||
except Empty:
|
||||
event = None
|
||||
pass
|
||||
if event:
|
||||
event.session_id = self.session.id
|
||||
return event
|
||||
|
||||
def remove_handlers(self) -> None:
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1220
daemon/core/api/grpc/wrappers.py
Normal file
1220
daemon/core/api/grpc/wrappers.py
Normal file
File diff suppressed because it is too large
Load diff
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,69 +0,0 @@
|
|||
"""
|
||||
Converts CORE data objects into legacy API messages.
|
||||
"""
|
||||
|
||||
from core.api.tlv import coreapi, structutils
|
||||
from core.emulator.enumerations import ConfigTlvs, NodeTlvs
|
||||
|
||||
|
||||
def convert_node(node_data):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
tlv_data = structutils.pack_values(
|
||||
coreapi.CoreNodeTlv,
|
||||
[
|
||||
(NodeTlvs.NUMBER, node_data.id),
|
||||
(NodeTlvs.TYPE, node_data.node_type),
|
||||
(NodeTlvs.NAME, node_data.name),
|
||||
(NodeTlvs.IP_ADDRESS, node_data.ip_address),
|
||||
(NodeTlvs.MAC_ADDRESS, node_data.mac_address),
|
||||
(NodeTlvs.IP6_ADDRESS, node_data.ip6_address),
|
||||
(NodeTlvs.MODEL, node_data.model),
|
||||
(NodeTlvs.EMULATION_ID, node_data.emulation_id),
|
||||
(NodeTlvs.EMULATION_SERVER, node_data.server),
|
||||
(NodeTlvs.SESSION, node_data.session),
|
||||
(NodeTlvs.X_POSITION, int(node_data.x_position)),
|
||||
(NodeTlvs.Y_POSITION, int(node_data.y_position)),
|
||||
(NodeTlvs.CANVAS, node_data.canvas),
|
||||
(NodeTlvs.NETWORK_ID, node_data.network_id),
|
||||
(NodeTlvs.SERVICES, node_data.services),
|
||||
(NodeTlvs.LATITUDE, node_data.latitude),
|
||||
(NodeTlvs.LONGITUDE, node_data.longitude),
|
||||
(NodeTlvs.ALTITUDE, node_data.altitude),
|
||||
(NodeTlvs.ICON, node_data.icon),
|
||||
(NodeTlvs.OPAQUE, node_data.opaque),
|
||||
],
|
||||
)
|
||||
return coreapi.CoreNodeMessage.pack(node_data.message_type, 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
|
||||
"""
|
||||
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, config_data.session),
|
||||
(ConfigTlvs.INTERFACE_NUMBER, config_data.interface_number),
|
||||
(ConfigTlvs.NETWORK_ID, config_data.network_id),
|
||||
(ConfigTlvs.OPAQUE, config_data.opaque),
|
||||
],
|
||||
)
|
||||
return coreapi.CoreConfMessage.pack(config_data.message_type, tlv_data)
|
|
@ -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,71 +4,112 @@ Common support for configurable CORE objects.
|
|||
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING, Dict, List, 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.data import ConfigData
|
||||
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 = name
|
||||
self.start = start
|
||||
self.stop = 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 = _id
|
||||
self.type = _type
|
||||
self.default = default
|
||||
if not options:
|
||||
options = []
|
||||
self.options = options
|
||||
if not label:
|
||||
label = _id
|
||||
self.label = 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}, 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:
|
||||
|
@ -76,12 +117,11 @@ class ConfigurableOptions:
|
|||
Provides a base for defining configuration options within CORE.
|
||||
"""
|
||||
|
||||
name = None
|
||||
bitmap = None
|
||||
options = []
|
||||
name: Optional[str] = None
|
||||
options: list[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
def configurations(cls) -> List[Configuration]:
|
||||
def configurations(cls) -> list[Configuration]:
|
||||
"""
|
||||
Provides the configurations for this class.
|
||||
|
||||
|
@ -90,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.
|
||||
|
||||
|
@ -99,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.
|
||||
|
||||
|
@ -110,112 +150,14 @@ class ConfigurableOptions:
|
|||
)
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class ConfigurableManager:
|
||||
"""
|
||||
Provides convenience methods for storing and retrieving configuration options for
|
||||
nodes.
|
||||
"""
|
||||
|
||||
_default_node = -1
|
||||
_default_type = _default_node
|
||||
_default_node: int = -1
|
||||
_default_type: int = _default_node
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
|
@ -223,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.
|
||||
|
||||
|
@ -235,7 +177,8 @@ class ConfigurableManager:
|
|||
"""
|
||||
Clears all configurations or configuration for a specific node.
|
||||
|
||||
:param node_id: node id to clear configurations for, default is None and clears all configurations
|
||||
:param node_id: node id to clear configurations for, default is None and clears
|
||||
all configurations
|
||||
:return: nothing
|
||||
"""
|
||||
if not node_id:
|
||||
|
@ -265,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:
|
||||
|
@ -277,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())
|
||||
|
@ -307,7 +250,7 @@ class ConfigurableManager:
|
|||
|
||||
def get_configs(
|
||||
self, node_id: int = _default_node, config_type: str = _default_type
|
||||
) -> Dict[str, str]:
|
||||
) -> Optional[dict[str, str]]:
|
||||
"""
|
||||
Retrieve configurations for a node and configuration type.
|
||||
|
||||
|
@ -321,7 +264,7 @@ class ConfigurableManager:
|
|||
result = node_configs.get(config_type)
|
||||
return result
|
||||
|
||||
def get_all_configs(self, node_id: int = _default_node) -> List[Dict[str, str]]:
|
||||
def get_all_configs(self, node_id: int = _default_node) -> dict[str, Any]:
|
||||
"""
|
||||
Retrieve all current configuration types for a node.
|
||||
|
||||
|
@ -341,11 +284,11 @@ class ModelManager(ConfigurableManager):
|
|||
Creates a ModelManager object.
|
||||
"""
|
||||
super().__init__()
|
||||
self.models = {}
|
||||
self.node_models = {}
|
||||
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.
|
||||
|
@ -374,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.
|
||||
|
||||
|
@ -399,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.
|
||||
|
@ -409,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)
|
||||
|
@ -418,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.
|
||||
|
@ -438,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,7 +15,22 @@ from core.config import Configuration
|
|||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
|
||||
TEMPLATES_DIR = "templates"
|
||||
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):
|
||||
|
@ -27,16 +43,31 @@ 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.
|
||||
"""
|
||||
|
||||
# validation period in seconds, how frequent validation is attempted
|
||||
validation_period = 0.5
|
||||
validation_period: float = 0.5
|
||||
|
||||
# time to wait in seconds for determining if service started successfully
|
||||
validation_timer = 5
|
||||
validation_timer: int = 5
|
||||
|
||||
# directories to shadow and copy files from
|
||||
shadow_directories: list[ShadowDir] = []
|
||||
|
||||
def __init__(self, node: CoreNode) -> None:
|
||||
"""
|
||||
|
@ -44,13 +75,13 @@ class ConfigService(abc.ABC):
|
|||
|
||||
:param node: node this service is assigned to
|
||||
"""
|
||||
self.node = node
|
||||
self.node: CoreNode = node
|
||||
class_file = inspect.getfile(self.__class__)
|
||||
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
|
||||
self.templates = TemplateLookup(directories=templates_path)
|
||||
self.config = {}
|
||||
self.custom_templates = {}
|
||||
self.custom_config = {}
|
||||
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] = {}
|
||||
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
|
||||
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 = {}
|
||||
self.started = set()
|
||||
self.node_services = {}
|
||||
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 = []
|
||||
self.visited = set()
|
||||
self.visiting = 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 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 = {}
|
||||
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: ConfigService) -> None:
|
||||
def add(self, service: type[ConfigService]) -> None:
|
||||
"""
|
||||
Add service to manager, checking service requirements have been met.
|
||||
|
||||
|
@ -40,7 +43,9 @@ class ConfigServiceManager:
|
|||
:raises CoreError: when service is a duplicate or has unmet executables
|
||||
"""
|
||||
name = service.name
|
||||
logging.debug("loading service: class(%s) name(%s)", service.__class__, name)
|
||||
logger.debug(
|
||||
"loading service: class(%s) name(%s)", service.__class__.__name__, name
|
||||
)
|
||||
|
||||
# avoid duplicate services
|
||||
if name in self.services:
|
||||
|
@ -50,33 +55,49 @@ class ConfigServiceManager:
|
|||
for executable in service.executables:
|
||||
try:
|
||||
utils.which(executable, required=True)
|
||||
except ValueError:
|
||||
raise CoreError(
|
||||
f"service({service.name}) missing executable {executable}"
|
||||
)
|
||||
except CoreError as e:
|
||||
raise CoreError(f"config service({service.name}): {e}")
|
||||
|
||||
# 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:
|
||||
logging.debug("found service: %s", service)
|
||||
try:
|
||||
self.add(service)
|
||||
except CoreError as e:
|
||||
service_errors.append(service.name)
|
||||
logging.debug("not loading service(%s): %s", service.name, e)
|
||||
logger.debug("not loading service(%s): %s", service.name, e)
|
||||
return service_errors
|
||||
|
|
|
@ -1,45 +1,56 @@
|
|||
import abc
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import constants
|
||||
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.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
from core.nodes.base import CoreNodeBase, NodeBase
|
||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.nodes.network import PtpNet, WlanNode
|
||||
from core.nodes.physical import Rj45Node
|
||||
from core.nodes.wireless import WirelessNode
|
||||
|
||||
GROUP = "FRR"
|
||||
GROUP: str = "FRR"
|
||||
FRR_STATE_DIR: str = "/var/run/frr"
|
||||
|
||||
|
||||
def has_mtu_mismatch(ifc: CoreInterface) -> bool:
|
||||
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
|
||||
mtu-ignore command. This is needed when e.g. a node is linked via a
|
||||
GreTap device.
|
||||
"""
|
||||
if ifc.mtu != 1500:
|
||||
if iface.mtu != DEFAULT_MTU:
|
||||
return True
|
||||
if not ifc.net:
|
||||
if not iface.net:
|
||||
return False
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu != ifc.mtu:
|
||||
for iface in iface.net.get_ifaces():
|
||||
if iface.mtu != iface.mtu:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_min_mtu(ifc):
|
||||
def get_min_mtu(iface: CoreInterface) -> int:
|
||||
"""
|
||||
Helper to discover the minimum MTU of interfaces linked with the
|
||||
given interface.
|
||||
"""
|
||||
mtu = ifc.mtu
|
||||
if not ifc.net:
|
||||
mtu = iface.mtu
|
||||
if not iface.net:
|
||||
return mtu
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu < mtu:
|
||||
mtu = i.mtu
|
||||
for iface in iface.net.get_ifaces():
|
||||
if iface.mtu < mtu:
|
||||
mtu = iface.mtu
|
||||
return mtu
|
||||
|
||||
|
||||
|
@ -47,42 +58,54 @@ def get_router_id(node: CoreNodeBase) -> str:
|
|||
"""
|
||||
Helper to return the first IPv4 address of a node as its router ID.
|
||||
"""
|
||||
for ifc in node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
return a
|
||||
for iface in node.get_ifaces(control=False):
|
||||
ip4 = iface.get_ip4()
|
||||
if ip4:
|
||||
return str(ip4.ip)
|
||||
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 = "FRRzebra"
|
||||
group = GROUP
|
||||
directories = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
|
||||
files = [
|
||||
name: str = "FRRzebra"
|
||||
group: str = GROUP
|
||||
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 = ["zebra"]
|
||||
dependencies = []
|
||||
startup = ["sh frrboot.sh zebra"]
|
||||
validate = ["pidof zebra"]
|
||||
shutdown = ["killall zebra"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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]] = {}
|
||||
|
||||
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 = []
|
||||
|
@ -91,31 +114,30 @@ class FRRZebra(ConfigService):
|
|||
for service in self.node.config_services.values():
|
||||
if self.name not in service.dependencies:
|
||||
continue
|
||||
if not isinstance(service, FrrService):
|
||||
continue
|
||||
if service.ipv4_routing:
|
||||
want_ip4 = True
|
||||
if service.ipv6_routing:
|
||||
want_ip6 = True
|
||||
services.append(service)
|
||||
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
ifaces = []
|
||||
for iface in self.node.get_ifaces():
|
||||
ip4s = []
|
||||
ip6s = []
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
ip4s.append(x)
|
||||
else:
|
||||
ip6s.append(x)
|
||||
is_control = getattr(ifc, "control", False)
|
||||
interfaces.append((ifc, ip4s, ip6s, is_control))
|
||||
for ip4 in iface.ip4s:
|
||||
ip4s.append(str(ip4.ip))
|
||||
for ip6 in iface.ip6s:
|
||||
ip6s.append(str(ip6.ip))
|
||||
ifaces.append((iface, ip4s, ip6s, iface.control))
|
||||
|
||||
return dict(
|
||||
frr_conf=frr_conf,
|
||||
frr_sbin_search=frr_sbin_search,
|
||||
frr_bin_search=frr_bin_search,
|
||||
frr_state_dir=constants.FRR_STATE_DIR,
|
||||
interfaces=interfaces,
|
||||
frr_state_dir=FRR_STATE_DIR,
|
||||
ifaces=ifaces,
|
||||
want_ip4=want_ip4,
|
||||
want_ip6=want_ip6,
|
||||
services=services,
|
||||
|
@ -123,22 +145,22 @@ class FRRZebra(ConfigService):
|
|||
|
||||
|
||||
class FrrService(abc.ABC):
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = []
|
||||
executables = []
|
||||
dependencies = ["FRRzebra"]
|
||||
startup = []
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
ipv4_routing = False
|
||||
ipv6_routing = False
|
||||
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] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
ipv4_routing: bool = False
|
||||
ipv6_routing: bool = False
|
||||
|
||||
@abc.abstractmethod
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -153,22 +175,17 @@ class FRROspfv2(FrrService, ConfigService):
|
|||
unified frr.conf file.
|
||||
"""
|
||||
|
||||
name = "FRROSPFv2"
|
||||
startup = ()
|
||||
shutdown = ["killall ospfd"]
|
||||
validate = ["pidof ospfd"]
|
||||
ipv4_routing = True
|
||||
name: str = "FRROSPFv2"
|
||||
shutdown: list[str] = ["killall ospfd"]
|
||||
validate: list[str] = ["pidof ospfd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
addresses = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
addr = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
addresses.append(a)
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
addresses.append(str(ip4))
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
|
@ -176,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_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if has_mtu_mismatch(ifc):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
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):
|
||||
|
@ -194,19 +227,17 @@ class FRROspfv3(FrrService, ConfigService):
|
|||
unified frr.conf file.
|
||||
"""
|
||||
|
||||
name = "FRROSPFv3"
|
||||
shutdown = ["killall ospf6d"]
|
||||
validate = ["pidof ospf6d"]
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
name: str = "FRROSPFv3"
|
||||
shutdown: list[str] = ["killall ospf6d"]
|
||||
validate: list[str] = ["pidof ospf6d"]
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
data = dict(router_id=router_id, ifnames=ifnames)
|
||||
text = """
|
||||
router ospf6
|
||||
|
@ -218,9 +249,9 @@ class FRROspfv3(FrrService, ConfigService):
|
|||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
mtu = get_min_mtu(ifc)
|
||||
if mtu < ifc.mtu:
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
mtu = get_min_mtu(iface)
|
||||
if mtu < iface.mtu:
|
||||
return f"ipv6 ospf6 ifmtu {mtu}"
|
||||
else:
|
||||
return ""
|
||||
|
@ -233,12 +264,12 @@ class FRRBgp(FrrService, ConfigService):
|
|||
having the same AS number.
|
||||
"""
|
||||
|
||||
name = "FRRBGP"
|
||||
shutdown = ["killall bgpd"]
|
||||
validate = ["pidof bgpd"]
|
||||
custom_needed = True
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
name: str = "FRRBGP"
|
||||
shutdown: list[str] = ["killall bgpd"]
|
||||
validate: list[str] = ["pidof bgpd"]
|
||||
custom_needed: bool = True
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
|
@ -254,7 +285,7 @@ class FRRBgp(FrrService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
|
@ -263,10 +294,10 @@ class FRRRip(FrrService, ConfigService):
|
|||
The RIP service provides IPv4 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "FRRRIP"
|
||||
shutdown = ["killall ripd"]
|
||||
validate = ["pidof ripd"]
|
||||
ipv4_routing = True
|
||||
name: str = "FRRRIP"
|
||||
shutdown: list[str] = ["killall ripd"]
|
||||
validate: list[str] = ["pidof ripd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
text = """
|
||||
|
@ -279,7 +310,7 @@ class FRRRip(FrrService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
|
@ -288,10 +319,10 @@ class FRRRipng(FrrService, ConfigService):
|
|||
The RIP NG service provides IPv6 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "FRRRIPNG"
|
||||
shutdown = ["killall ripngd"]
|
||||
validate = ["pidof ripngd"]
|
||||
ipv6_routing = True
|
||||
name: str = "FRRRIPNG"
|
||||
shutdown: list[str] = ["killall ripngd"]
|
||||
validate: list[str] = ["pidof ripngd"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
text = """
|
||||
|
@ -304,7 +335,7 @@ class FRRRipng(FrrService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
|
@ -314,17 +345,15 @@ class FRRBabel(FrrService, ConfigService):
|
|||
protocol for IPv6 and IPv4 with fast convergence properties.
|
||||
"""
|
||||
|
||||
name = "FRRBabel"
|
||||
shutdown = ["killall babeld"]
|
||||
validate = ["pidof babeld"]
|
||||
ipv6_routing = True
|
||||
name: str = "FRRBabel"
|
||||
shutdown: list[str] = ["killall babeld"]
|
||||
validate: list[str] = ["pidof babeld"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
text = """
|
||||
router babel
|
||||
% for ifname in ifnames:
|
||||
|
@ -337,8 +366,8 @@ class FRRBabel(FrrService, ConfigService):
|
|||
data = dict(ifnames=ifnames)
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
if is_wireless(iface.net):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
|
@ -356,16 +385,16 @@ class FRRpimd(FrrService, ConfigService):
|
|||
PIM multicast routing based on XORP.
|
||||
"""
|
||||
|
||||
name = "FRRpimd"
|
||||
shutdown = ["killall pimd"]
|
||||
validate = ["pidof pimd"]
|
||||
ipv4_routing = True
|
||||
name: str = "FRRpimd"
|
||||
shutdown: list[str] = ["killall pimd"]
|
||||
validate: list[str] = ["pidof pimd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
ifname = "eth0"
|
||||
for ifc in self.node.netifs():
|
||||
if ifc.name != "lo":
|
||||
ifname = ifc.name
|
||||
for iface in self.node.get_ifaces():
|
||||
if iface.name != "lo":
|
||||
ifname = iface.name
|
||||
break
|
||||
|
||||
text = f"""
|
||||
|
@ -382,7 +411,7 @@ class FRRpimd(FrrService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
def frr_iface_config(self, iface: CoreInterface) -> str:
|
||||
text = """
|
||||
ip mfea
|
||||
ip igmp
|
||||
|
|
|
@ -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
|
||||
|
@ -74,6 +78,9 @@ bootfrr()
|
|||
fi
|
||||
|
||||
bootdaemon "zebra"
|
||||
if grep -q "^ip route " $FRR_CONF; then
|
||||
bootdaemon "staticd"
|
||||
fi
|
||||
for r in rip ripng ospf6 ospf bgp babel; do
|
||||
if grep -q "^router \\<$${}{r}\\>" $FRR_CONF; then
|
||||
bootdaemon "$${}{r}d"
|
||||
|
@ -93,3 +100,10 @@ if [ "$1" != "zebra" ]; then
|
|||
fi
|
||||
confcheck
|
||||
bootfrr
|
||||
|
||||
# reset interfaces
|
||||
% for iface, _, _ , _ in ifaces:
|
||||
ip link set dev ${iface.name} down
|
||||
sleep 1
|
||||
ip link set dev ${iface.name} up
|
||||
% endfor
|
||||
|
|
|
@ -20,6 +20,7 @@ nhrpd=yes
|
|||
eigrpd=yes
|
||||
babeld=yes
|
||||
sharpd=yes
|
||||
staticd=yes
|
||||
pbrd=yes
|
||||
bfdd=yes
|
||||
fabricd=yes
|
|
@ -1,5 +1,5 @@
|
|||
% for ifc, ip4s, ip6s, is_control in interfaces:
|
||||
interface ${ifc.name}
|
||||
% for iface, ip4s, ip6s, is_control in ifaces:
|
||||
interface ${iface.name}
|
||||
% if want_ip4:
|
||||
% for addr in ip4s:
|
||||
ip address ${addr}
|
||||
|
@ -12,7 +12,7 @@ interface ${ifc.name}
|
|||
% endif
|
||||
% if not is_control:
|
||||
% for service in services:
|
||||
% for line in service.frr_interface_config(ifc).split("\n"):
|
||||
% for line in service.frr_iface_config(iface).split("\n"):
|
||||
${line}
|
||||
% endfor
|
||||
% endfor
|
|
@ -1,212 +1,164 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
import netaddr
|
||||
from typing import Any
|
||||
|
||||
from core import utils
|
||||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
|
||||
GROUP = "ProtoSvc"
|
||||
GROUP: str = "ProtoSvc"
|
||||
|
||||
|
||||
class MgenSinkService(ConfigService):
|
||||
name = "MGEN_Sink"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["mgensink.sh", "sink.mgen"]
|
||||
executables = ["mgen"]
|
||||
dependencies = []
|
||||
startup = ["sh mgensink.sh"]
|
||||
validate = ["pidof mgen"]
|
||||
shutdown = ["killall mgen"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
name = utils.sysctl_devname(ifc.name)
|
||||
for iface in self.node.get_ifaces():
|
||||
name = utils.sysctl_devname(iface.name)
|
||||
ifnames.append(name)
|
||||
return dict(ifnames=ifnames)
|
||||
|
||||
|
||||
class NrlNhdp(ConfigService):
|
||||
name = "NHDP"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["nrlnhdp.sh"]
|
||||
executables = ["nrlnhdp"]
|
||||
dependencies = []
|
||||
startup = ["sh nrlnhdp.sh"]
|
||||
validate = ["pidof nrlnhdp"]
|
||||
shutdown = ["killall nrlnhdp"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||
|
||||
|
||||
class NrlSmf(ConfigService):
|
||||
name = "SMF"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["startsmf.sh"]
|
||||
executables = ["nrlsmf", "killall"]
|
||||
dependencies = []
|
||||
startup = ["sh startsmf.sh"]
|
||||
validate = ["pidof nrlsmf"]
|
||||
shutdown = ["killall nrlsmf"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
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 = []
|
||||
ip4_prefix = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
if ip4_prefix:
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
ip4_prefix = f"{a}/{24}"
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
ip4 = iface.get_ip4()
|
||||
if ip4:
|
||||
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 = "OLSR"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["nrlolsrd.sh"]
|
||||
executables = ["nrlolsrd"]
|
||||
dependencies = []
|
||||
startup = ["sh nrlolsrd.sh"]
|
||||
validate = ["pidof nrlolsrd"]
|
||||
shutdown = ["killall nrlolsrd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
has_zebra = "zebra" in self.node.config_services
|
||||
ifname = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifname = ifc.name
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifname = iface.name
|
||||
break
|
||||
return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname)
|
||||
|
||||
|
||||
class NrlOlsrv2(ConfigService):
|
||||
name = "OLSRv2"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["nrlolsrv2.sh"]
|
||||
executables = ["nrlolsrv2"]
|
||||
dependencies = []
|
||||
startup = ["sh nrlolsrv2.sh"]
|
||||
validate = ["pidof nrlolsrv2"]
|
||||
shutdown = ["killall nrlolsrv2"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||
|
||||
|
||||
class OlsrOrg(ConfigService):
|
||||
name = "OLSRORG"
|
||||
group = GROUP
|
||||
directories = ["/etc/olsrd"]
|
||||
files = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
|
||||
executables = ["olsrd"]
|
||||
dependencies = []
|
||||
startup = ["sh olsrd.sh"]
|
||||
validate = ["pidof olsrd"]
|
||||
shutdown = ["killall olsrd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||
|
||||
|
||||
class MgenActor(ConfigService):
|
||||
name = "MgenActor"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["start_mgen_actor.sh"]
|
||||
executables = ["mgen"]
|
||||
dependencies = []
|
||||
startup = ["sh start_mgen_actor.sh"]
|
||||
validate = ["pidof mgen"]
|
||||
shutdown = ["killall mgen"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class Arouted(ConfigService):
|
||||
name = "arouted"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["startarouted.sh"]
|
||||
executables = ["arouted"]
|
||||
dependencies = []
|
||||
startup = ["sh startarouted.sh"]
|
||||
validate = ["pidof arouted"]
|
||||
shutdown = ["pkill arouted"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ip4_prefix = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
if ip4_prefix:
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
ip4_prefix = f"{a}/{24}"
|
||||
break
|
||||
return dict(ip4_prefix=ip4_prefix)
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<%
|
||||
interfaces = "-i " + " -i ".join(ifnames)
|
||||
ifaces = "-i " + " -i ".join(ifnames)
|
||||
smf = ""
|
||||
if has_smf:
|
||||
smf = "-flooding ecds -smfClient %s_smf" % node.name
|
||||
%>
|
||||
nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${interfaces}
|
||||
nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${ifaces}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<%
|
||||
interfaces = "-i " + " -i ".join(ifnames)
|
||||
ifaces = "-i " + " -i ".join(ifnames)
|
||||
smf = ""
|
||||
if has_smf:
|
||||
smf = "-flooding ecds -smfClient %s_smf" % node.name
|
||||
%>
|
||||
nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${interfaces}
|
||||
nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${ifaces}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%
|
||||
interfaces = "-i " + " -i ".join(ifnames)
|
||||
ifaces = "-i " + " -i ".join(ifnames)
|
||||
%>
|
||||
olsrd ${interfaces}
|
||||
olsrd ${ifaces}
|
||||
|
|
|
@ -1,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 @@
|
|||
<%
|
||||
interfaces = ",".join(ifnames)
|
||||
arouted = ""
|
||||
if has_arouted:
|
||||
arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0])
|
||||
ifaces = ",".join(ifnames)
|
||||
if has_nhdp:
|
||||
flood = "ecds"
|
||||
elif has_olsr:
|
||||
|
@ -12,4 +9,4 @@
|
|||
%>
|
||||
#!/bin/sh
|
||||
# auto-generated by NrlSmf service
|
||||
nrlsmf instance ${node.name}_smf ${interfaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
|
||||
nrlsmf instance ${node.name}_smf ${flood} ${ifaces} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
|
||||
|
|
|
@ -1,46 +1,58 @@
|
|||
import abc
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import constants
|
||||
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.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
from core.nodes.base import CoreNodeBase, NodeBase
|
||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.nodes.network import PtpNet, WlanNode
|
||||
from core.nodes.physical import Rj45Node
|
||||
from core.nodes.wireless import WirelessNode
|
||||
|
||||
GROUP = "Quagga"
|
||||
logger = logging.getLogger(__name__)
|
||||
GROUP: str = "Quagga"
|
||||
QUAGGA_STATE_DIR: str = "/var/run/quagga"
|
||||
|
||||
|
||||
def has_mtu_mismatch(ifc: CoreInterface) -> bool:
|
||||
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
|
||||
mtu-ignore command. This is needed when e.g. a node is linked via a
|
||||
GreTap device.
|
||||
"""
|
||||
if ifc.mtu != 1500:
|
||||
if iface.mtu != DEFAULT_MTU:
|
||||
return True
|
||||
if not ifc.net:
|
||||
if not iface.net:
|
||||
return False
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu != ifc.mtu:
|
||||
for iface in iface.net.get_ifaces():
|
||||
if iface.mtu != iface.mtu:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_min_mtu(ifc):
|
||||
def get_min_mtu(iface: CoreInterface):
|
||||
"""
|
||||
Helper to discover the minimum MTU of interfaces linked with the
|
||||
given interface.
|
||||
"""
|
||||
mtu = ifc.mtu
|
||||
if not ifc.net:
|
||||
mtu = iface.mtu
|
||||
if not iface.net:
|
||||
return mtu
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu < mtu:
|
||||
mtu = i.mtu
|
||||
for iface in iface.net.get_ifaces():
|
||||
if iface.mtu < mtu:
|
||||
mtu = iface.mtu
|
||||
return mtu
|
||||
|
||||
|
||||
|
@ -48,42 +60,53 @@ def get_router_id(node: CoreNodeBase) -> str:
|
|||
"""
|
||||
Helper to return the first IPv4 address of a node as its router ID.
|
||||
"""
|
||||
for ifc in node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
return a
|
||||
for iface in node.get_ifaces(control=False):
|
||||
ip4 = iface.get_ip4()
|
||||
if ip4:
|
||||
return str(ip4.ip)
|
||||
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 = "zebra"
|
||||
group = GROUP
|
||||
directories = ["/usr/local/etc/quagga", "/var/run/quagga"]
|
||||
files = [
|
||||
name: str = "zebra"
|
||||
group: str = GROUP
|
||||
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 = ["zebra"]
|
||||
dependencies = []
|
||||
startup = ["sh quaggaboot.sh zebra"]
|
||||
validate = ["pidof zebra"]
|
||||
shutdown = ["killall zebra"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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]] = {}
|
||||
|
||||
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 = constants.QUAGGA_STATE_DIR
|
||||
quagga_state_dir = QUAGGA_STATE_DIR
|
||||
quagga_conf = self.files[0]
|
||||
|
||||
services = []
|
||||
|
@ -92,31 +115,36 @@ class Zebra(ConfigService):
|
|||
for service in self.node.config_services.values():
|
||||
if self.name not in service.dependencies:
|
||||
continue
|
||||
if not isinstance(service, QuaggaService):
|
||||
continue
|
||||
if service.ipv4_routing:
|
||||
want_ip4 = True
|
||||
if service.ipv6_routing:
|
||||
want_ip6 = True
|
||||
services.append(service)
|
||||
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
ifaces = []
|
||||
for iface in self.node.get_ifaces():
|
||||
ip4s = []
|
||||
ip6s = []
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
ip4s.append(x)
|
||||
else:
|
||||
ip6s.append(x)
|
||||
is_control = getattr(ifc, "control", False)
|
||||
interfaces.append((ifc, ip4s, ip6s, is_control))
|
||||
for ip4 in iface.ip4s:
|
||||
ip4s.append(str(ip4))
|
||||
for ip6 in iface.ip6s:
|
||||
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,
|
||||
quagga_sbin_search=quagga_sbin_search,
|
||||
quagga_state_dir=quagga_state_dir,
|
||||
quagga_conf=quagga_conf,
|
||||
interfaces=interfaces,
|
||||
ifaces=ifaces,
|
||||
want_ip4=want_ip4,
|
||||
want_ip6=want_ip6,
|
||||
services=services,
|
||||
|
@ -124,22 +152,22 @@ class Zebra(ConfigService):
|
|||
|
||||
|
||||
class QuaggaService(abc.ABC):
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = []
|
||||
executables = []
|
||||
dependencies = ["zebra"]
|
||||
startup = []
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
ipv4_routing = False
|
||||
ipv6_routing = False
|
||||
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] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
ipv4_routing: bool = False
|
||||
ipv6_routing: bool = False
|
||||
|
||||
@abc.abstractmethod
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -154,27 +182,38 @@ class Ospfv2(QuaggaService, ConfigService):
|
|||
unified Quagga.conf file.
|
||||
"""
|
||||
|
||||
name = "OSPFv2"
|
||||
validate = ["pidof ospfd"]
|
||||
shutdown = ["killall ospfd"]
|
||||
ipv4_routing = True
|
||||
name: str = "OSPFv2"
|
||||
validate: list[str] = ["pidof ospfd"]
|
||||
shutdown: list[str] = ["killall ospfd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if has_mtu_mismatch(ifc):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
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 ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
addr = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
addresses.append(a)
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
addresses.append(str(ip4))
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
|
@ -194,15 +233,15 @@ class Ospfv3(QuaggaService, ConfigService):
|
|||
unified Quagga.conf file.
|
||||
"""
|
||||
|
||||
name = "OSPFv3"
|
||||
shutdown = ("killall ospf6d",)
|
||||
validate = ("pidof ospf6d",)
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
name: str = "OSPFv3"
|
||||
shutdown: list[str] = ["killall ospf6d"]
|
||||
validate: list[str] = ["pidof ospf6d"]
|
||||
ipv4_routing: bool = True
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
mtu = get_min_mtu(ifc)
|
||||
if mtu < ifc.mtu:
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
mtu = get_min_mtu(iface)
|
||||
if mtu < iface.mtu:
|
||||
return f"ipv6 ospf6 ifmtu {mtu}"
|
||||
else:
|
||||
return ""
|
||||
|
@ -210,10 +249,8 @@ class Ospfv3(QuaggaService, ConfigService):
|
|||
def quagga_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
data = dict(router_id=router_id, ifnames=ifnames)
|
||||
text = """
|
||||
router ospf6
|
||||
|
@ -235,17 +272,11 @@ class Ospfv3mdr(Ospfv3):
|
|||
unified Quagga.conf file.
|
||||
"""
|
||||
|
||||
name = "OSPFv3MDR"
|
||||
name: str = "OSPFv3MDR"
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
for ifc in self.node.netifs():
|
||||
is_wireless = isinstance(ifc.net, (WlanNode, EmaneNet))
|
||||
logging.info("MDR wireless: %s", is_wireless)
|
||||
return dict()
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
config = super().quagga_interface_config(ifc)
|
||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
config = super().quagga_iface_config(iface)
|
||||
if is_wireless(iface.net):
|
||||
config = self.clean_text(
|
||||
f"""
|
||||
{config}
|
||||
|
@ -268,16 +299,13 @@ class Bgp(QuaggaService, ConfigService):
|
|||
having the same AS number.
|
||||
"""
|
||||
|
||||
name = "BGP"
|
||||
shutdown = ["killall bgpd"]
|
||||
validate = ["pidof bgpd"]
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
name: str = "BGP"
|
||||
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_interface_config(self, ifc: CoreInterface) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
text = f"""
|
||||
! BGP configuration
|
||||
|
@ -291,16 +319,19 @@ class Bgp(QuaggaService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class Rip(QuaggaService, ConfigService):
|
||||
"""
|
||||
The RIP service provides IPv4 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "RIP"
|
||||
shutdown = ["killall ripd"]
|
||||
validate = ["pidof ripd"]
|
||||
ipv4_routing = True
|
||||
name: str = "RIP"
|
||||
shutdown: list[str] = ["killall ripd"]
|
||||
validate: list[str] = ["pidof ripd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
text = """
|
||||
|
@ -313,7 +344,7 @@ class Rip(QuaggaService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
|
@ -322,10 +353,10 @@ class Ripng(QuaggaService, ConfigService):
|
|||
The RIP NG service provides IPv6 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "RIPNG"
|
||||
shutdown = ["killall ripngd"]
|
||||
validate = ["pidof ripngd"]
|
||||
ipv6_routing = True
|
||||
name: str = "RIPNG"
|
||||
shutdown: list[str] = ["killall ripngd"]
|
||||
validate: list[str] = ["pidof ripngd"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
text = """
|
||||
|
@ -338,7 +369,7 @@ class Ripng(QuaggaService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
|
@ -348,17 +379,15 @@ class Babel(QuaggaService, ConfigService):
|
|||
protocol for IPv6 and IPv4 with fast convergence properties.
|
||||
"""
|
||||
|
||||
name = "Babel"
|
||||
shutdown = ["killall babeld"]
|
||||
validate = ["pidof babeld"]
|
||||
ipv6_routing = True
|
||||
name: str = "Babel"
|
||||
shutdown: list[str] = ["killall babeld"]
|
||||
validate: list[str] = ["pidof babeld"]
|
||||
ipv6_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
text = """
|
||||
router babel
|
||||
% for ifname in ifnames:
|
||||
|
@ -371,8 +400,8 @@ class Babel(QuaggaService, ConfigService):
|
|||
data = dict(ifnames=ifnames)
|
||||
return self.render_text(text, data)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
if is_wireless(iface.net):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
|
@ -390,16 +419,16 @@ class Xpimd(QuaggaService, ConfigService):
|
|||
PIM multicast routing based on XORP.
|
||||
"""
|
||||
|
||||
name = "Xpimd"
|
||||
shutdown = ["killall xpimd"]
|
||||
validate = ["pidof xpimd"]
|
||||
ipv4_routing = True
|
||||
name: str = "Xpimd"
|
||||
shutdown: list[str] = ["killall xpimd"]
|
||||
validate: list[str] = ["pidof xpimd"]
|
||||
ipv4_routing: bool = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
ifname = "eth0"
|
||||
for ifc in self.node.netifs():
|
||||
if ifc.name != "lo":
|
||||
ifname = ifc.name
|
||||
for iface in self.node.get_ifaces():
|
||||
if iface.name != "lo":
|
||||
ifname = iface.name
|
||||
break
|
||||
|
||||
text = f"""
|
||||
|
@ -416,7 +445,7 @@ class Xpimd(QuaggaService, ConfigService):
|
|||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
def quagga_iface_config(self, iface: CoreInterface) -> str:
|
||||
text = """
|
||||
ip mfea
|
||||
ip pim
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
% for ifc, ip4s, ip6s, is_control in interfaces:
|
||||
interface ${ifc.name}
|
||||
% for iface, ip4s, ip6s, configs in ifaces:
|
||||
interface ${iface.name}
|
||||
% if want_ip4:
|
||||
% for addr in ip4s:
|
||||
ip address ${addr}
|
||||
|
@ -10,13 +10,11 @@ interface ${ifc.name}
|
|||
ipv6 address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% if not is_control:
|
||||
% for service in services:
|
||||
% for line in service.quagga_interface_config(ifc).split("\n"):
|
||||
% for config in configs:
|
||||
% for line in config:
|
||||
${line}
|
||||
% endfor
|
||||
% endfor
|
||||
% endif
|
||||
!
|
||||
% endfor
|
||||
|
104
daemon/core/configservices/securityservices/services.py
Normal file
104
daemon/core/configservices/securityservices/services.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
from typing import Any
|
||||
|
||||
from core.config import ConfigString, Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
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]] = {}
|
||||
|
||||
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
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]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
address = None
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ip4 = iface.get_ip4()
|
||||
if ip4:
|
||||
address = str(ip4.ip)
|
||||
break
|
||||
return dict(address=address)
|
||||
|
||||
|
||||
class IPsec(ConfigService):
|
||||
name: str = "IPsec"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["ipsec.sh"]
|
||||
executables: list[str] = ["racoon", "ip", "setkey", "killall"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["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]] = {}
|
||||
|
||||
|
||||
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] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
class Nat(ConfigService):
|
||||
name: str = "NAT"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = []
|
||||
files: list[str] = ["nat.sh"]
|
||||
executables: list[str] = ["iptables"]
|
||||
dependencies: list[str] = []
|
||||
startup: list[str] = ["bash nat.sh"]
|
||||
validate: list[str] = []
|
||||
shutdown: list[str] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifnames = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
return dict(ifnames=ifnames)
|
|
@ -1,141 +0,0 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
import netaddr
|
||||
|
||||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
GROUP_NAME = "Security"
|
||||
|
||||
|
||||
class VpnClient(ConfigService):
|
||||
name = "VPNClient"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["vpnclient.sh"]
|
||||
executables = ["openvpn", "ip", "killall"]
|
||||
dependencies = []
|
||||
startup = ["sh vpnclient.sh"]
|
||||
validate = ["pidof openvpn"]
|
||||
shutdown = ["killall openvpn"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = [
|
||||
Configuration(
|
||||
_id="keydir",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Dir",
|
||||
default="/etc/core/keys",
|
||||
),
|
||||
Configuration(
|
||||
_id="keyname",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Name",
|
||||
default="client1",
|
||||
),
|
||||
Configuration(
|
||||
_id="server",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Server",
|
||||
default="10.0.2.10",
|
||||
),
|
||||
]
|
||||
modes = {}
|
||||
|
||||
|
||||
class VpnServer(ConfigService):
|
||||
name = "VPNServer"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["vpnserver.sh"]
|
||||
executables = ["openvpn", "ip", "killall"]
|
||||
dependencies = []
|
||||
startup = ["sh vpnserver.sh"]
|
||||
validate = ["pidof openvpn"]
|
||||
shutdown = ["killall openvpn"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = [
|
||||
Configuration(
|
||||
_id="keydir",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Dir",
|
||||
default="/etc/core/keys",
|
||||
),
|
||||
Configuration(
|
||||
_id="keyname",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Key Name",
|
||||
default="server",
|
||||
),
|
||||
Configuration(
|
||||
_id="subnet",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Subnet",
|
||||
default="10.0.200.0",
|
||||
),
|
||||
]
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
address = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
address = addr
|
||||
return dict(address=address)
|
||||
|
||||
|
||||
class IPsec(ConfigService):
|
||||
name = "IPsec"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["ipsec.sh"]
|
||||
executables = ["racoon", "ip", "setkey", "killall"]
|
||||
dependencies = []
|
||||
startup = ["sh ipsec.sh"]
|
||||
validate = ["pidof racoon"]
|
||||
shutdown = ["killall racoon"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class Firewall(ConfigService):
|
||||
name = "Firewall"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["firewall.sh"]
|
||||
executables = ["iptables"]
|
||||
dependencies = []
|
||||
startup = ["sh firewall.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class Nat(ConfigService):
|
||||
name = "NAT"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["nat.sh"]
|
||||
executables = ["iptables"]
|
||||
dependencies = []
|
||||
startup = ["sh nat.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict(ifnames=ifnames)
|
|
@ -1,47 +0,0 @@
|
|||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
||||
class SimpleService(ConfigService):
|
||||
name = "Simple"
|
||||
group = "SimpleGroup"
|
||||
directories = ["/etc/quagga", "/usr/local/lib"]
|
||||
files = ["test1.sh", "test2.sh"]
|
||||
executables = []
|
||||
dependencies = []
|
||||
startup = []
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = [
|
||||
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
|
||||
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
|
||||
Configuration(
|
||||
_id="value3",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
label="Multiple Choice",
|
||||
options=["value1", "value2", "value3"],
|
||||
),
|
||||
]
|
||||
modes = {
|
||||
"mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
|
||||
"mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
|
||||
"mode3": {"value1": "value3", "value2": "0", "value3": "value1"},
|
||||
}
|
||||
|
||||
def get_text_template(self, name: str) -> str:
|
||||
if name == "test1.sh":
|
||||
return """
|
||||
# sample script 1
|
||||
# node id(${node.id}) name(${node.name})
|
||||
# config: ${config}
|
||||
echo hello
|
||||
"""
|
||||
elif name == "test2.sh":
|
||||
return """
|
||||
# sample script 2
|
||||
# node id(${node.id}) name(${node.name})
|
||||
# config: ${config}
|
||||
echo hello2
|
||||
"""
|
|
@ -1,133 +1,129 @@
|
|||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
|
||||
GROUP_NAME = "Utility"
|
||||
|
||||
|
||||
class DefaultRouteService(ConfigService):
|
||||
name = "DefaultRoute"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["defaultroute.sh"]
|
||||
executables = ["ip"]
|
||||
dependencies = []
|
||||
startup = ["sh defaultroute.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
addresses = []
|
||||
for netif in self.node.netifs():
|
||||
if getattr(netif, "control", False):
|
||||
continue
|
||||
for addr in netif.addrlist:
|
||||
logging.info("default route address: %s", addr)
|
||||
net = netaddr.IPNetwork(addr)
|
||||
if net[1] != net[-2]:
|
||||
addresses.append(net[1])
|
||||
return dict(addresses=addresses)
|
||||
def data(self) -> dict[str, Any]:
|
||||
# only add default routes for linked routing nodes
|
||||
routes = []
|
||||
ifaces = self.node.get_ifaces()
|
||||
if ifaces:
|
||||
iface = ifaces[0]
|
||||
for ip in iface.ips():
|
||||
net = ip.cidr
|
||||
if net.size > 1:
|
||||
router = net[1]
|
||||
routes.append(str(router))
|
||||
return dict(routes=routes)
|
||||
|
||||
|
||||
class DefaultMulticastRouteService(ConfigService):
|
||||
name = "DefaultMulticastRoute"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["defaultmroute.sh"]
|
||||
executables = []
|
||||
dependencies = []
|
||||
startup = ["sh defaultmroute.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifname = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifname = ifc.name
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifname = iface.name
|
||||
break
|
||||
return dict(ifname=ifname)
|
||||
|
||||
|
||||
class StaticRouteService(ConfigService):
|
||||
name = "StaticRoute"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["staticroute.sh"]
|
||||
executables = []
|
||||
dependencies = []
|
||||
startup = ["sh staticroute.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
routes = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv6(addr):
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip in iface.ips():
|
||||
address = str(ip.ip)
|
||||
if netaddr.valid_ipv6(address):
|
||||
dst = "3ffe:4::/64"
|
||||
else:
|
||||
dst = "10.9.8.0/24"
|
||||
net = netaddr.IPNetwork(x)
|
||||
if net[-2] != net[1]:
|
||||
routes.append((dst, net[1]))
|
||||
if ip[-2] != ip[1]:
|
||||
routes.append((dst, ip[1]))
|
||||
return dict(routes=routes)
|
||||
|
||||
|
||||
class IpForwardService(ConfigService):
|
||||
name = "IPForward"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["ipforward.sh"]
|
||||
executables = ["sysctl"]
|
||||
dependencies = []
|
||||
startup = ["sh ipforward.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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] = []
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
devnames = []
|
||||
for ifc in self.node.netifs():
|
||||
devname = utils.sysctl_devname(ifc.name)
|
||||
for iface in self.node.get_ifaces():
|
||||
devname = utils.sysctl_devname(iface.name)
|
||||
devnames.append(devname)
|
||||
return dict(devnames=devnames)
|
||||
|
||||
|
||||
class SshService(ConfigService):
|
||||
name = "SSH"
|
||||
group = GROUP_NAME
|
||||
directories = ["/etc/ssh", "/var/run/sshd"]
|
||||
files = ["startsshd.sh", "/etc/ssh/sshd_config"]
|
||||
executables = ["sshd"]
|
||||
dependencies = []
|
||||
startup = ["sh startsshd.sh"]
|
||||
validate = []
|
||||
shutdown = ["killall sshd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
return dict(
|
||||
sshcfgdir=self.directories[0],
|
||||
sshstatedir=self.directories[1],
|
||||
|
@ -136,146 +132,137 @@ class SshService(ConfigService):
|
|||
|
||||
|
||||
class DhcpService(ConfigService):
|
||||
name = "DHCP"
|
||||
group = GROUP_NAME
|
||||
directories = ["/etc/dhcp", "/var/lib/dhcp"]
|
||||
files = ["/etc/dhcp/dhcpd.conf"]
|
||||
executables = ["dhcpd"]
|
||||
dependencies = []
|
||||
startup = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
|
||||
validate = ["pidof dhcpd"]
|
||||
shutdown = ["killall dhcpd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
subnets = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
for ip4 in iface.ip4s:
|
||||
if ip4.size == 1:
|
||||
continue
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
net = netaddr.IPNetwork(x)
|
||||
# divide the address space in half
|
||||
index = (net.size - 2) / 2
|
||||
rangelow = net[index]
|
||||
rangehigh = net[-2]
|
||||
subnets.append((net.ip, net.netmask, rangelow, rangehigh, addr))
|
||||
index = (ip4.size - 2) / 2
|
||||
rangelow = ip4[index]
|
||||
rangehigh = ip4[-2]
|
||||
subnets.append((ip4.cidr.ip, ip4.netmask, rangelow, rangehigh, ip4.ip))
|
||||
return dict(subnets=subnets)
|
||||
|
||||
|
||||
class DhcpClientService(ConfigService):
|
||||
name = "DHCPClient"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["startdhcpclient.sh"]
|
||||
executables = ["dhclient"]
|
||||
dependencies = []
|
||||
startup = ["sh startdhcpclient.sh"]
|
||||
validate = ["pidof dhclient"]
|
||||
shutdown = ["killall dhclient"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
return dict(ifnames=ifnames)
|
||||
|
||||
|
||||
class FtpService(ConfigService):
|
||||
name = "FTP"
|
||||
group = GROUP_NAME
|
||||
directories = ["/var/run/vsftpd/empty", "/var/ftp"]
|
||||
files = ["vsftpd.conf"]
|
||||
executables = ["vsftpd"]
|
||||
dependencies = []
|
||||
startup = ["vsftpd ./vsftpd.conf"]
|
||||
validate = ["pidof vsftpd"]
|
||||
shutdown = ["killall vsftpd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
class PcapService(ConfigService):
|
||||
name = "pcap"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["pcap.sh"]
|
||||
executables = ["tcpdump"]
|
||||
dependencies = []
|
||||
startup = ["sh pcap.sh start"]
|
||||
validate = ["pidof tcpdump"]
|
||||
shutdown = ["sh pcap.sh stop"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict()
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifnames.append(iface.name)
|
||||
return dict(ifnames=ifnames)
|
||||
|
||||
|
||||
class RadvdService(ConfigService):
|
||||
name = "radvd"
|
||||
group = GROUP_NAME
|
||||
directories = ["/etc/radvd"]
|
||||
files = ["/etc/radvd/radvd.conf"]
|
||||
executables = ["radvd"]
|
||||
dependencies = []
|
||||
startup = ["radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"]
|
||||
validate = ["pidof radvd"]
|
||||
shutdown = ["pkill radvd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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] = [
|
||||
"radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"
|
||||
]
|
||||
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]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifaces = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
prefixes = []
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv6(addr):
|
||||
prefixes.append(x)
|
||||
for ip6 in iface.ip6s:
|
||||
prefixes.append(str(ip6))
|
||||
if not prefixes:
|
||||
continue
|
||||
interfaces.append((ifc.name, prefixes))
|
||||
return dict(interfaces=interfaces)
|
||||
ifaces.append((iface.name, prefixes))
|
||||
return dict(ifaces=ifaces)
|
||||
|
||||
|
||||
class AtdService(ConfigService):
|
||||
name = "atd"
|
||||
group = GROUP_NAME
|
||||
directories = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
|
||||
files = ["startatd.sh"]
|
||||
executables = ["atd"]
|
||||
dependencies = []
|
||||
startup = ["sh startatd.sh"]
|
||||
validate = ["pidof atd"]
|
||||
shutdown = ["pkill atd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
class HttpService(ConfigService):
|
||||
name = "HTTP"
|
||||
group = GROUP_NAME
|
||||
directories = [
|
||||
name: str = "HTTP"
|
||||
group: str = GROUP_NAME
|
||||
directories: list[str] = [
|
||||
"/etc/apache2",
|
||||
"/var/run/apache2",
|
||||
"/var/log/apache2",
|
||||
|
@ -283,20 +270,22 @@ class HttpService(ConfigService):
|
|||
"/var/lock/apache2",
|
||||
"/var/www",
|
||||
]
|
||||
files = ["/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html"]
|
||||
executables = ["apache2ctl"]
|
||||
dependencies = []
|
||||
startup = ["chown www-data /var/lock/apache2", "apache2ctl start"]
|
||||
validate = ["pidof apache2"]
|
||||
shutdown = ["apache2ctl stop"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
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"]
|
||||
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
|
||||
default_configs: list[Configuration] = []
|
||||
modes: dict[str, dict[str, str]] = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
interfaces.append(ifc)
|
||||
return dict(interfaces=interfaces)
|
||||
def data(self) -> dict[str, Any]:
|
||||
ifaces = []
|
||||
for iface in self.node.get_ifaces(control=False):
|
||||
ifaces.append(iface)
|
||||
return dict(ifaces=ifaces)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by DefaultRoute service
|
||||
% for address in addresses:
|
||||
ip route add default via ${address}
|
||||
% for route in routes:
|
||||
ip route add default via ${route}
|
||||
% endfor
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
<p>This is the default web page for this server.</p>
|
||||
<p>The web server software is running but no content has been added, yet.</p>
|
||||
<ul>
|
||||
% for ifc in interfaces:
|
||||
<li>${ifc.name} - ${ifc.addrlist}</li>
|
||||
% for iface in ifaces:
|
||||
<li>${iface.name} - ${iface.addrlist}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</body>
|
|
@ -1,19 +1,5 @@
|
|||
from core.utils import which
|
||||
from pathlib import Path
|
||||
|
||||
COREDPY_VERSION = "@PACKAGE_VERSION@"
|
||||
CORE_CONF_DIR = "@CORE_CONF_DIR@"
|
||||
CORE_DATA_DIR = "@CORE_DATA_DIR@"
|
||||
QUAGGA_STATE_DIR = "@CORE_STATE_DIR@/run/quagga"
|
||||
FRR_STATE_DIR = "@CORE_STATE_DIR@/run/frr"
|
||||
|
||||
VNODED_BIN = which("vnoded", required=True)
|
||||
VCMD_BIN = which("vcmd", required=True)
|
||||
SYSCTL_BIN = which("sysctl", required=True)
|
||||
IP_BIN = which("ip", required=True)
|
||||
ETHTOOL_BIN = which("ethtool", required=True)
|
||||
TC_BIN = which("tc", required=True)
|
||||
EBTABLES_BIN = which("ebtables", required=True)
|
||||
MOUNT_BIN = which("mount", required=True)
|
||||
UMOUNT_BIN = which("umount", required=True)
|
||||
OVS_BIN = which("ovs-vsctl", required=False)
|
||||
OVS_FLOW_BIN = which("ovs-ofctl", required=False)
|
||||
COREDPY_VERSION: str = "@PACKAGE_VERSION@"
|
||||
CORE_CONF_DIR: Path = Path("@CORE_CONF_DIR@")
|
||||
CORE_DATA_DIR: Path = Path("@CORE_DATA_DIR@")
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
"""
|
||||
EMANE Bypass model for CORE
|
||||
"""
|
||||
|
||||
from core.config import Configuration
|
||||
from core.emane import emanemodel
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
||||
class EmaneBypassModel(emanemodel.EmaneModel):
|
||||
name = "emane_bypass"
|
||||
|
||||
# values to ignore, when writing xml files
|
||||
config_ignore = {"none"}
|
||||
|
||||
# mac definitions
|
||||
mac_library = "bypassmaclayer"
|
||||
mac_config = [
|
||||
Configuration(
|
||||
_id="none",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
label="There are no parameters for the bypass model.",
|
||||
)
|
||||
]
|
||||
|
||||
# phy definitions
|
||||
phy_library = "bypassphylayer"
|
||||
phy_config = []
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: str) -> None:
|
||||
# ignore default logic
|
||||
pass
|
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
|
||||
|
@ -11,7 +13,8 @@ except ImportError:
|
|||
try:
|
||||
from emanesh import manifest
|
||||
except ImportError:
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
manifest = None
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
def _type_value(config_type: str) -> ConfigDataTypes:
|
||||
|
@ -29,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.
|
||||
|
||||
|
@ -47,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.
|
||||
|
||||
|
@ -70,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
|
||||
|
@ -84,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 = []
|
||||
|
@ -115,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,17 +2,21 @@
|
|||
Defines Emane Models used within CORE.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, List
|
||||
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.emulator.enumerations import ConfigDataTypes
|
||||
from core.emulator.data import LinkOptions
|
||||
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):
|
||||
"""
|
||||
|
@ -21,160 +25,150 @@ 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 = None
|
||||
mac_xml = None
|
||||
mac_defaults = {}
|
||||
mac_config = []
|
||||
mac_library: Optional[str] = None
|
||||
mac_xml: Optional[str] = None
|
||||
mac_defaults: dict[str, str] = {}
|
||||
mac_config: list[Configuration] = []
|
||||
|
||||
# default phy configuration settings, using the universal model
|
||||
phy_library = None
|
||||
phy_xml = "emanephy.xml"
|
||||
phy_defaults = {"subid": "1", "propagationmodel": "2ray", "noisemode": "none"}
|
||||
phy_config = []
|
||||
phy_library: Optional[str] = None
|
||||
phy_xml: str = "emanephy.xml"
|
||||
phy_defaults: dict[str, str] = {
|
||||
"subid": "1",
|
||||
"propagationmodel": "2ray",
|
||||
"noisemode": "none",
|
||||
}
|
||||
phy_config: list[Configuration] = []
|
||||
|
||||
# support for external configurations
|
||||
external_config = [
|
||||
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()
|
||||
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], interface: CoreInterface = None
|
||||
) -> 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.
|
||||
|
||||
:param config: emane model configuration for the node and interface
|
||||
:param interface: interface for the emane node
|
||||
:param iface: interface to run emane for
|
||||
:return: nothing
|
||||
"""
|
||||
nem_name = emanexml.nem_file_name(self, interface)
|
||||
mac_name = emanexml.mac_file_name(self, interface)
|
||||
phy_name = emanexml.phy_file_name(self, interface)
|
||||
# create nem, mac, and phy xml files
|
||||
emanexml.create_nem_xml(self, iface, config)
|
||||
emanexml.create_mac_xml(self, iface, config)
|
||||
emanexml.create_phy_xml(self, iface, config)
|
||||
emanexml.create_transport_xml(iface, config)
|
||||
|
||||
# remote server for file
|
||||
server = None
|
||||
if interface is not None:
|
||||
server = interface.node.server
|
||||
|
||||
# check if this is external
|
||||
transport_type = "virtual"
|
||||
if interface and interface.transport_type == "raw":
|
||||
transport_type = "raw"
|
||||
transport_name = emanexml.transport_file_name(self.id, transport_type)
|
||||
|
||||
# create nem xml file
|
||||
nem_file = os.path.join(self.session.session_dir, nem_name)
|
||||
emanexml.create_nem_xml(
|
||||
self, config, nem_file, transport_name, mac_name, phy_name, server
|
||||
)
|
||||
|
||||
# create mac xml file
|
||||
mac_file = os.path.join(self.session.session_dir, mac_name)
|
||||
emanexml.create_mac_xml(self, config, mac_file, server)
|
||||
|
||||
# create phy xml file
|
||||
phy_file = os.path.join(self.session.session_dir, phy_name)
|
||||
emanexml.create_phy_xml(self, config, phy_file, server)
|
||||
|
||||
def post_startup(self) -> None:
|
||||
def post_startup(self, 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: bool, moved_netifs: 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
|
||||
list, making EmaneModels compatible with Ns2ScriptedMobility.
|
||||
|
||||
:param moved: were nodes moved
|
||||
:param moved_netifs: interfaces that were moved
|
||||
:param moved_ifaces: interfaces that were moved
|
||||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
wlan = self.session.get_node(self.id)
|
||||
wlan.setnempositions(moved_netifs)
|
||||
self.session.emane.set_nem_positions(moved_ifaces)
|
||||
except CoreError:
|
||||
logging.exception("error during update")
|
||||
logger.exception("error during update")
|
||||
|
||||
def linkconfig(
|
||||
self,
|
||||
netif: CoreInterface,
|
||||
bw: float = None,
|
||||
delay: float = None,
|
||||
loss: float = None,
|
||||
duplicate: float = None,
|
||||
jitter: float = None,
|
||||
netif2: CoreInterface = None,
|
||||
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||
) -> None:
|
||||
"""
|
||||
Invoked when a Link Message is received. Default is unimplemented.
|
||||
|
||||
:param netif: interface one
|
||||
:param bw: bandwidth to set to
|
||||
:param delay: packet delay to set to
|
||||
:param loss: packet loss to set to
|
||||
:param duplicate: duplicate percentage to set to
|
||||
:param jitter: jitter to set to
|
||||
:param netif2: interface two
|
||||
:param iface: interface one
|
||||
:param options: options for configuring link
|
||||
:param iface2: interface two
|
||||
:return: nothing
|
||||
"""
|
||||
logging.warning(
|
||||
"emane model(%s) does not support link configuration", self.name
|
||||
)
|
||||
logger.warning("emane model(%s) does not support link config", self.name)
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
"""
|
||||
ieee80211abg.py: EMANE IEEE 802.11abg model for CORE
|
||||
"""
|
||||
import os
|
||||
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
||||
class EmaneIeee80211abgModel(emanemodel.EmaneModel):
|
||||
# model name
|
||||
name = "emane_ieee80211abg"
|
||||
|
||||
# mac configuration
|
||||
mac_library = "ieee80211abgmaclayer"
|
||||
mac_xml = "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"
|
||||
)
|
||||
super().load(emane_prefix)
|
328
daemon/core/emane/linkmonitor.py
Normal file
328
daemon/core/emane/linkmonitor.py
Normal file
|
@ -0,0 +1,328 @@
|
|||
import logging
|
||||
import sched
|
||||
import threading
|
||||
import time
|
||||
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:
|
||||
try:
|
||||
from emanesh import shell
|
||||
except ImportError:
|
||||
shell = None
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emane.emanemanager import EmaneManager
|
||||
|
||||
MAC_COMPONENT_INDEX: int = 1
|
||||
EMANE_RFPIPE: str = "rfpipemaclayer"
|
||||
EMANE_80211: str = "ieee80211abgmaclayer"
|
||||
EMANE_TDMA: str = "tdmaeventschedulerradiomodel"
|
||||
SINR_TABLE: str = "NeighborStatusTable"
|
||||
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] = {}
|
||||
for index, value in enumerate(self.sinrs):
|
||||
self.loss_lookup[index] = self.losses[value]
|
||||
self.mac_id: Optional[str] = None
|
||||
|
||||
def get_loss(self, sinr: float) -> float:
|
||||
index = self._get_index(sinr)
|
||||
loss = 100.0 - self.loss_lookup[index]
|
||||
return loss
|
||||
|
||||
def _get_index(self, current_sinr: float) -> int:
|
||||
for index, sinr in enumerate(self.sinrs):
|
||||
if current_sinr <= sinr:
|
||||
return index
|
||||
return len(self.sinrs) - 1
|
||||
|
||||
|
||||
class EmaneLink:
|
||||
def __init__(self, from_nem: int, to_nem: int, sinr: float) -> None:
|
||||
self.from_nem: int = from_nem
|
||||
self.to_nem: int = to_nem
|
||||
self.sinr: float = sinr
|
||||
self.last_seen: Optional[float] = None
|
||||
self.updated: bool = False
|
||||
self.touch()
|
||||
|
||||
def update(self, sinr: float) -> None:
|
||||
self.updated = self.sinr != sinr
|
||||
self.sinr = sinr
|
||||
self.touch()
|
||||
|
||||
def touch(self) -> None:
|
||||
self.last_seen = time.monotonic()
|
||||
|
||||
def is_dead(self, timeout: int) -> bool:
|
||||
return (time.monotonic() - self.last_seen) >= timeout
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"EmaneLink({self.from_nem}, {self.to_nem}, {self.sinr})"
|
||||
|
||||
|
||||
class EmaneClient:
|
||||
def __init__(self, address: str, port: int) -> None:
|
||||
self.address: str = address
|
||||
self.client: shell.ControlPortClient = shell.ControlPortClient(
|
||||
self.address, port
|
||||
)
|
||||
self.nems: dict[int, LossTable] = {}
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
manifest = self.client.getManifest()
|
||||
for nem_id, components in manifest.items():
|
||||
# get mac config
|
||||
mac_id, _, emane_model = components[MAC_COMPONENT_INDEX]
|
||||
mac_config = self.client.getConfiguration(mac_id)
|
||||
logger.debug(
|
||||
"address(%s) nem(%s) emane(%s)", self.address, nem_id, emane_model
|
||||
)
|
||||
|
||||
# create loss table based on current configuration
|
||||
if emane_model == EMANE_80211:
|
||||
loss_table = self.handle_80211(mac_config)
|
||||
elif emane_model == EMANE_RFPIPE:
|
||||
loss_table = self.handle_rfpipe(mac_config)
|
||||
else:
|
||||
logger.warning("unknown emane link model: %s", emane_model)
|
||||
continue
|
||||
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
|
||||
) -> None:
|
||||
for from_nem, loss_table in self.nems.items():
|
||||
tables = self.client.getStatisticTable(loss_table.mac_id, (SINR_TABLE,))
|
||||
table = tables[SINR_TABLE][1:][0]
|
||||
for row in table:
|
||||
row = row
|
||||
to_nem = row[0][0]
|
||||
sinr = row[5][0]
|
||||
age = row[-1][0]
|
||||
|
||||
# exclude invalid links
|
||||
is_self = to_nem == NEM_SELF
|
||||
has_valid_age = 0 <= age <= 1
|
||||
if is_self or not has_valid_age:
|
||||
continue
|
||||
|
||||
# check if valid link loss
|
||||
link_key = (from_nem, to_nem)
|
||||
loss = loss_table.get_loss(sinr)
|
||||
if loss < loss_threshold:
|
||||
link = links.get(link_key)
|
||||
if link:
|
||||
link.update(sinr)
|
||||
else:
|
||||
link = EmaneLink(from_nem, to_nem, sinr)
|
||||
links[link_key] = link
|
||||
|
||||
def handle_tdma(self, config: dict[str, tuple]):
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logger.debug("tdma pcr: %s", pcr)
|
||||
|
||||
def handle_80211(self, config: dict[str, tuple]) -> LossTable:
|
||||
unicastrate = config["unicastrate"][0][0]
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logger.debug("80211 pcr: %s", pcr)
|
||||
tree = etree.parse(pcr)
|
||||
root = tree.getroot()
|
||||
table = root.find("table")
|
||||
losses = {}
|
||||
for rate in table.iter("datarate"):
|
||||
index = int(rate.get("index"))
|
||||
if index == unicastrate:
|
||||
for row in rate.iter("row"):
|
||||
sinr = float(row.get("sinr"))
|
||||
por = float(row.get("por"))
|
||||
losses[sinr] = por
|
||||
return LossTable(losses)
|
||||
|
||||
def handle_rfpipe(self, config: dict[str, tuple]) -> LossTable:
|
||||
pcr = config["pcrcurveuri"][0][0]
|
||||
logger.debug("rfpipe pcr: %s", pcr)
|
||||
tree = etree.parse(pcr)
|
||||
root = tree.getroot()
|
||||
table = root.find("table")
|
||||
losses = {}
|
||||
for row in table.iter("row"):
|
||||
sinr = float(row.get("sinr"))
|
||||
por = float(row.get("por"))
|
||||
losses[sinr] = por
|
||||
return LossTable(losses)
|
||||
|
||||
def stop(self) -> None:
|
||||
self.client.stop()
|
||||
|
||||
|
||||
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.loss_threshold: Optional[int] = None
|
||||
self.link_interval: Optional[int] = None
|
||||
self.link_timeout: Optional[int] = None
|
||||
self.scheduler: Optional[sched.scheduler] = None
|
||||
self.running: bool = False
|
||||
|
||||
def start(self) -> None:
|
||||
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:
|
||||
logger.info("no valid emane models to monitor links")
|
||||
return
|
||||
self.scheduler = sched.scheduler()
|
||||
self.scheduler.enter(0, 0, self.check_links)
|
||||
self.running = True
|
||||
thread = threading.Thread(target=self.scheduler.run, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def initialize(self) -> None:
|
||||
addresses = self.get_addresses()
|
||||
for address, port in addresses:
|
||||
client = EmaneClient(address, port)
|
||||
if client.nems:
|
||||
self.clients.append(client)
|
||||
|
||||
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:
|
||||
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:
|
||||
# check for new links
|
||||
previous_links = set(self.links.keys())
|
||||
for client in self.clients:
|
||||
try:
|
||||
client.check_links(self.links, self.loss_threshold)
|
||||
except shell.ControlPortException:
|
||||
if self.running:
|
||||
logger.exception("link monitor error")
|
||||
|
||||
# find new links
|
||||
current_links = set(self.links.keys())
|
||||
new_links = current_links - previous_links
|
||||
|
||||
# find updated and dead links
|
||||
dead_links = []
|
||||
for link_id, link in self.links.items():
|
||||
complete_id = self.get_complete_id(link_id)
|
||||
if link.is_dead(self.link_timeout):
|
||||
dead_links.append(link_id)
|
||||
elif link.updated and complete_id in self.complete_links:
|
||||
link.updated = False
|
||||
self.send_link(MessageFlags.NONE, complete_id)
|
||||
|
||||
# announce dead links
|
||||
for link_id in dead_links:
|
||||
complete_id = self.get_complete_id(link_id)
|
||||
if complete_id in self.complete_links:
|
||||
self.complete_links.remove(complete_id)
|
||||
self.send_link(MessageFlags.DELETE, complete_id)
|
||||
del self.links[link_id]
|
||||
|
||||
# announce new links
|
||||
for link_id in new_links:
|
||||
complete_id = self.get_complete_id(link_id)
|
||||
if complete_id in self.complete_links:
|
||||
continue
|
||||
if self.is_complete_link(link_id):
|
||||
self.complete_links.add(complete_id)
|
||||
self.send_link(MessageFlags.ADD, complete_id)
|
||||
|
||||
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]:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
nem1, nem2 = link_id
|
||||
link = self.emane_manager.get_nem_link(nem1, nem2, message_type)
|
||||
if link:
|
||||
label = self.get_link_label(link_id)
|
||||
link.label = label
|
||||
self.emane_manager.session.broadcast_link(link)
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
message_type: MessageFlags,
|
||||
label: str,
|
||||
node1: int,
|
||||
node2: int,
|
||||
emane_id: int,
|
||||
) -> None:
|
||||
color = self.emane_manager.session.get_link_color(emane_id)
|
||||
link_data = LinkData(
|
||||
message_type=message_type,
|
||||
type=LinkTypes.WIRELESS,
|
||||
label=label,
|
||||
node1_id=node1,
|
||||
node2_id=node2,
|
||||
network_id=emane_id,
|
||||
color=color,
|
||||
)
|
||||
self.emane_manager.session.broadcast_link(link_data)
|
||||
|
||||
def stop(self) -> None:
|
||||
self.running = False
|
||||
for client in self.clients:
|
||||
client.stop()
|
||||
self.clients.clear()
|
||||
self.links.clear()
|
||||
self.complete_links.clear()
|
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
|
32
daemon/core/emane/models/bypass.py
Normal file
32
daemon/core/emane/models/bypass.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
"""
|
||||
EMANE Bypass model for CORE
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from core.config import ConfigBool, Configuration
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
||||
class EmaneBypassModel(emanemodel.EmaneModel):
|
||||
name: str = "emane_bypass"
|
||||
|
||||
# values to ignore, when writing xml files
|
||||
config_ignore: set[str] = {"none"}
|
||||
|
||||
# mac definitions
|
||||
mac_library: str = "bypassmaclayer"
|
||||
mac_config: list[Configuration] = [
|
||||
ConfigBool(
|
||||
id="none",
|
||||
default="0",
|
||||
label="There are no parameters for the bypass model.",
|
||||
)
|
||||
]
|
||||
|
||||
# phy definitions
|
||||
phy_library: str = "bypassphylayer"
|
||||
phy_config: list[Configuration] = []
|
||||
|
||||
@classmethod
|
||||
def load(cls, emane_prefix: Path) -> None:
|
||||
cls._load_platform_config(emane_prefix)
|
|
@ -3,23 +3,26 @@ commeffect.py: EMANE CommEffect model for CORE
|
|||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, List
|
||||
from pathlib import Path
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from core.config import ConfigGroup, Configuration
|
||||
from core.emane import emanemanifest, emanemodel
|
||||
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:
|
||||
try:
|
||||
from emanesh.events.commeffectevent import CommEffectEvent
|
||||
except ImportError:
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
CommEffectEvent = None
|
||||
logger.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
def convert_none(x: float) -> int:
|
||||
|
@ -35,33 +38,39 @@ def convert_none(x: float) -> int:
|
|||
|
||||
|
||||
class EmaneCommEffectModel(emanemodel.EmaneModel):
|
||||
name = "emane_commeffect"
|
||||
|
||||
shim_library = "commeffectshim"
|
||||
shim_xml = "commeffectshim.xml"
|
||||
shim_defaults = {}
|
||||
config_shim = []
|
||||
name: str = "emane_commeffect"
|
||||
shim_library: str = "commeffectshim"
|
||||
shim_xml: str = "commeffectshim.xml"
|
||||
shim_defaults: dict[str, str] = {}
|
||||
config_shim: list[Configuration] = []
|
||||
|
||||
# comm effect does not need the default phy and external configurations
|
||||
phy_config = []
|
||||
external_config = []
|
||||
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], interface: CoreInterface = None
|
||||
) -> 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
|
||||
|
@ -69,26 +78,19 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used.
|
||||
|
||||
:param config: emane model configuration for the node and interface
|
||||
:param interface: interface for the emane node
|
||||
:param iface: interface for the emane node
|
||||
:return: nothing
|
||||
"""
|
||||
# retrieve xml names
|
||||
nem_name = emanexml.nem_file_name(self, interface)
|
||||
shim_name = emanexml.shim_file_name(self, interface)
|
||||
|
||||
# create and write nem document
|
||||
nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured")
|
||||
transport_type = "virtual"
|
||||
if interface and interface.transport_type == "raw":
|
||||
transport_type = "raw"
|
||||
transport_file = emanexml.transport_file_name(self.id, transport_type)
|
||||
etree.SubElement(nem_element, "transport", definition=transport_file)
|
||||
transport_name = emanexml.transport_file_name(iface)
|
||||
etree.SubElement(nem_element, "transport", definition=transport_name)
|
||||
|
||||
# set shim configuration
|
||||
nem_name = emanexml.nem_file_name(iface)
|
||||
shim_name = emanexml.shim_file_name(iface)
|
||||
etree.SubElement(nem_element, "shim", definition=shim_name)
|
||||
|
||||
nem_file = os.path.join(self.session.session_dir, nem_name)
|
||||
emanexml.create_file(nem_element, "nem", nem_file)
|
||||
emanexml.create_node_file(iface.node, nem_element, "nem", nem_name)
|
||||
|
||||
# create and write shim document
|
||||
shim_element = etree.Element(
|
||||
|
@ -107,48 +109,34 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
|
|||
ff = config["filterfile"]
|
||||
if ff.strip() != "":
|
||||
emanexml.add_param(shim_element, "filterfile", ff)
|
||||
emanexml.create_node_file(iface.node, shim_element, "shim", shim_name)
|
||||
|
||||
shim_file = os.path.join(self.session.session_dir, shim_name)
|
||||
emanexml.create_file(shim_element, "shim", shim_file)
|
||||
# create transport xml
|
||||
emanexml.create_transport_xml(iface, config)
|
||||
|
||||
def linkconfig(
|
||||
self,
|
||||
netif: CoreInterface,
|
||||
bw: float = None,
|
||||
delay: float = None,
|
||||
loss: float = None,
|
||||
duplicate: float = None,
|
||||
jitter: float = None,
|
||||
netif2: CoreInterface = None,
|
||||
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||
) -> None:
|
||||
"""
|
||||
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)
|
||||
if iface is None or iface2 is None:
|
||||
logger.warning("%s: missing NEM information", self.name)
|
||||
return
|
||||
|
||||
if netif is None or netif2 is None:
|
||||
logging.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()
|
||||
emane_node = self.session.get_node(self.id)
|
||||
nemid = emane_node.getnemid(netif)
|
||||
nemid2 = emane_node.getnemid(netif2)
|
||||
mbw = bw
|
||||
logging.info("sending comm effect event")
|
||||
nem1 = self.session.emane.get_nem_id(iface)
|
||||
nem2 = self.session.emane.get_nem_id(iface2)
|
||||
logger.info("sending comm effect event")
|
||||
event.append(
|
||||
nemid,
|
||||
latency=convert_none(delay),
|
||||
jitter=convert_none(jitter),
|
||||
loss=convert_none(loss),
|
||||
duplicate=convert_none(duplicate),
|
||||
unicast=int(convert_none(bw)),
|
||||
broadcast=int(convert_none(mbw)),
|
||||
nem1,
|
||||
latency=convert_none(options.delay),
|
||||
jitter=convert_none(options.jitter),
|
||||
loss=convert_none(options.loss),
|
||||
duplicate=convert_none(options.dup),
|
||||
unicast=int(convert_none(options.bandwidth)),
|
||||
broadcast=int(convert_none(options.bandwidth)),
|
||||
)
|
||||
service.publish(nemid2, event)
|
||||
self.session.emane.publish_event(nem2, event)
|
22
daemon/core/emane/models/ieee80211abg.py
Normal file
22
daemon/core/emane/models/ieee80211abg.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""
|
||||
ieee80211abg.py: EMANE IEEE 802.11abg model for CORE
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
||||
class EmaneIeee80211abgModel(emanemodel.EmaneModel):
|
||||
# model name
|
||||
name: str = "emane_ieee80211abg"
|
||||
|
||||
# mac configuration
|
||||
mac_library: str = "ieee80211abgmaclayer"
|
||||
mac_xml: str = "ieee80211abgmaclayer.xml"
|
||||
|
||||
@classmethod
|
||||
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)
|
22
daemon/core/emane/models/rfpipe.py
Normal file
22
daemon/core/emane/models/rfpipe.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""
|
||||
rfpipe.py: EMANE RF-PIPE model for CORE
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
||||
class EmaneRfPipeModel(emanemodel.EmaneModel):
|
||||
# model name
|
||||
name: str = "emane_rfpipe"
|
||||
|
||||
# mac configuration
|
||||
mac_library: str = "rfpipemaclayer"
|
||||
mac_xml: str = "rfpipemaclayer.xml"
|
||||
|
||||
@classmethod
|
||||
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,18 +4,23 @@ share the same MAC+PHY model.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, 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 LinkTypes, NodeTypes, RegisterTlvs
|
||||
from core.nodes.base import CoreNetworkBase
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
from core.location.mobility import WirelessModel
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WirelessModelType = Type[WirelessModel]
|
||||
if TYPE_CHECKING:
|
||||
from core.emane.emanemodel import EmaneModel
|
||||
from core.emulator.session import Session
|
||||
from core.location.mobility import WayPointMobility
|
||||
|
||||
try:
|
||||
from emane.events import LocationEvent
|
||||
|
@ -23,7 +28,122 @@ except ImportError:
|
|||
try:
|
||||
from emanesh.events import LocationEvent
|
||||
except ImportError:
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
LocationEvent = None
|
||||
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):
|
||||
|
@ -33,208 +153,138 @@ class EmaneNet(CoreNetworkBase):
|
|||
Emane controller object that exists in a session.
|
||||
"""
|
||||
|
||||
apitype = NodeTypes.EMANE.value
|
||||
linktype = LinkTypes.WIRED.value
|
||||
type = "wlan"
|
||||
is_emane = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int = None,
|
||||
name: str = None,
|
||||
start: bool = True,
|
||||
server: DistributedServer = None,
|
||||
options: EmaneOptions = None,
|
||||
) -> None:
|
||||
super().__init__(session, _id, name, start, server)
|
||||
self.conf = ""
|
||||
self.up = False
|
||||
self.nemidmap = {}
|
||||
self.model = None
|
||||
self.mobility = None
|
||||
options = options or EmaneOptions()
|
||||
super().__init__(session, _id, name, server, options)
|
||||
self.conf: str = ""
|
||||
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,
|
||||
netif: CoreInterface,
|
||||
bw: float = None,
|
||||
delay: float = None,
|
||||
loss: float = None,
|
||||
duplicate: float = None,
|
||||
jitter: float = None,
|
||||
netif2: CoreInterface = None,
|
||||
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
|
||||
) -> None:
|
||||
"""
|
||||
The CommEffect model supports link configuration.
|
||||
"""
|
||||
if not self.model:
|
||||
if not self.wireless_model:
|
||||
return
|
||||
self.model.linkconfig(
|
||||
netif=netif,
|
||||
bw=bw,
|
||||
delay=delay,
|
||||
loss=loss,
|
||||
duplicate=duplicate,
|
||||
jitter=jitter,
|
||||
netif2=netif2,
|
||||
)
|
||||
self.wireless_model.linkconfig(iface, options, iface2)
|
||||
|
||||
def config(self, conf: str) -> None:
|
||||
self.conf = conf
|
||||
def startup(self) -> None:
|
||||
self.up = True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.up = False
|
||||
|
||||
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||
pass
|
||||
|
||||
def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
|
||||
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
|
||||
pass
|
||||
|
||||
def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
|
||||
pass
|
||||
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:
|
||||
raise ValueError("no model set to update for node(%s)", self.id)
|
||||
logging.info(
|
||||
"node(%s) updating model(%s): %s", self.id, self.model.name, config
|
||||
: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})")
|
||||
logger.info(
|
||||
"node(%s) updating model(%s): %s", self.id, self.wireless_model.name, config
|
||||
)
|
||||
self.model.set_configs(config, node_id=self.id)
|
||||
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
|
||||
"""
|
||||
logging.info("adding model: %s", model.name)
|
||||
if model.config_type == RegisterTlvs.WIRELESS.value:
|
||||
# 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)
|
||||
elif model.config_type == RegisterTlvs.MOBILITY.value:
|
||||
if model.config_type == RegisterTlvs.WIRELESS:
|
||||
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 setnemid(self, netif: CoreInterface, nemid: int) -> None:
|
||||
"""
|
||||
Record an interface to numerical ID mapping. The Emane controller
|
||||
object manages and assigns these IDs for all NEMs.
|
||||
"""
|
||||
self.nemidmap[netif] = nemid
|
||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> list[LinkData]:
|
||||
links = []
|
||||
emane_manager = self.session.emane
|
||||
# gather current emane links
|
||||
nem_ids = set()
|
||||
for iface in self.get_ifaces():
|
||||
nem_id = emane_manager.get_nem_id(iface)
|
||||
nem_ids.add(nem_id)
|
||||
emane_links = emane_manager.link_monitor.links
|
||||
considered = set()
|
||||
for link_key in emane_links:
|
||||
considered_key = tuple(sorted(link_key))
|
||||
if considered_key in considered:
|
||||
continue
|
||||
considered.add(considered_key)
|
||||
nem1, nem2 = considered_key
|
||||
# ignore links not related to this node
|
||||
if nem1 not in nem_ids and nem2 not in nem_ids:
|
||||
continue
|
||||
# ignore incomplete links
|
||||
if (nem2, nem1) not in emane_links:
|
||||
continue
|
||||
link = emane_manager.get_nem_link(nem1, nem2, flags)
|
||||
if link:
|
||||
links.append(link)
|
||||
return links
|
||||
|
||||
def getnemid(self, netif: CoreInterface) -> Optional[int]:
|
||||
def create_tuntap(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
||||
"""
|
||||
Given an interface, return its numerical ID.
|
||||
"""
|
||||
if netif not in self.nemidmap:
|
||||
return None
|
||||
else:
|
||||
return self.nemidmap[netif]
|
||||
Create a tuntap interface for the provided node.
|
||||
|
||||
def getnemnetif(self, nemid: int) -> Optional[CoreInterface]:
|
||||
:param node: node to create tuntap interface for
|
||||
:param iface_data: interface data to create interface with
|
||||
:return: created tuntap interface
|
||||
"""
|
||||
Given a numerical NEM ID, return its interface. This returns the
|
||||
first interface that matches the given NEM ID.
|
||||
"""
|
||||
for netif in self.nemidmap:
|
||||
if self.nemidmap[netif] == nemid:
|
||||
return netif
|
||||
return None
|
||||
|
||||
def netifs(self, sort: bool = True) -> List[CoreInterface]:
|
||||
"""
|
||||
Retrieve list of linked interfaces sorted by node number.
|
||||
"""
|
||||
return sorted(self._netif.values(), key=lambda ifc: ifc.node.id)
|
||||
|
||||
def installnetifs(self) -> None:
|
||||
"""
|
||||
Install TAP devices into their namespaces. This is done after
|
||||
EMANE daemons have been started, because that is their only chance
|
||||
to bind to the TAPs.
|
||||
"""
|
||||
if (
|
||||
self.session.emane.genlocationevents()
|
||||
and self.session.emane.service is None
|
||||
):
|
||||
warntxt = "unable to publish EMANE events because the eventservice "
|
||||
warntxt += "Python bindings failed to load"
|
||||
logging.error(warntxt)
|
||||
|
||||
for netif in self.netifs():
|
||||
external = self.session.emane.get_config(
|
||||
"external", self.id, self.model.name
|
||||
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"
|
||||
)
|
||||
if external == "0":
|
||||
netif.setaddrs()
|
||||
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
|
||||
|
||||
if not self.session.emane.genlocationevents():
|
||||
netif.poshook = None
|
||||
continue
|
||||
|
||||
# at this point we register location handlers for generating
|
||||
# EMANE location events
|
||||
netif.poshook = self.setnemposition
|
||||
x, y, z = netif.node.position.get()
|
||||
self.setnemposition(netif, x, y, z)
|
||||
|
||||
def deinstallnetifs(self) -> None:
|
||||
"""
|
||||
Uninstall TAP devices. This invokes their shutdown method for
|
||||
any required cleanup; the device may be actually removed when
|
||||
emanetransportd terminates.
|
||||
"""
|
||||
for netif in self.netifs():
|
||||
if "virtual" in netif.transport_type.lower():
|
||||
netif.shutdown()
|
||||
netif.poshook = None
|
||||
|
||||
def setnemposition(
|
||||
self, netif: CoreInterface, x: float, y: float, z: float
|
||||
) -> None:
|
||||
"""
|
||||
Publish a NEM location change event using the EMANE event service.
|
||||
"""
|
||||
if self.session.emane.service is None:
|
||||
logging.info("position service not available")
|
||||
return
|
||||
nemid = self.getnemid(netif)
|
||||
ifname = netif.localname
|
||||
if nemid is None:
|
||||
logging.info("nemid for %s is unknown", ifname)
|
||||
return
|
||||
lat, lon, alt = self.session.location.getgeo(x, y, z)
|
||||
event = LocationEvent()
|
||||
|
||||
# altitude must be an integer or warning is printed
|
||||
# unused: yaw, pitch, roll, azimuth, elevation, velocity
|
||||
alt = int(round(alt))
|
||||
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
||||
self.session.emane.service.publish(0, event)
|
||||
|
||||
def setnempositions(self, moved_netifs: List[CoreInterface]) -> None:
|
||||
"""
|
||||
Several NEMs have moved, from e.g. a WaypointMobilityModel
|
||||
calculation. Generate an EMANE Location Event having several
|
||||
entries for each netif that has moved.
|
||||
"""
|
||||
if len(moved_netifs) == 0:
|
||||
return
|
||||
|
||||
if self.session.emane.service is None:
|
||||
logging.info("position service not available")
|
||||
return
|
||||
|
||||
event = LocationEvent()
|
||||
i = 0
|
||||
for netif in moved_netifs:
|
||||
nemid = self.getnemid(netif)
|
||||
ifname = netif.localname
|
||||
if nemid is None:
|
||||
logging.info("nemid for %s is unknown", ifname)
|
||||
continue
|
||||
x, y, z = netif.node.getposition()
|
||||
lat, lon, alt = self.session.location.getgeo(x, y, z)
|
||||
# altitude must be an integer or warning is printed
|
||||
alt = int(round(alt))
|
||||
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
|
||||
i += 1
|
||||
|
||||
self.session.emane.service.publish(0, event)
|
||||
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||
raise CoreError(
|
||||
f"emane network({self.name}) do not support adopting interfaces"
|
||||
)
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
"""
|
||||
rfpipe.py: EMANE RF-PIPE model for CORE
|
||||
"""
|
||||
import os
|
||||
|
||||
from core.emane import emanemodel
|
||||
|
||||
|
||||
class EmaneRfPipeModel(emanemodel.EmaneModel):
|
||||
# model name
|
||||
name = "emane_rfpipe"
|
||||
|
||||
# mac configuration
|
||||
mac_library = "rfpipemaclayer"
|
||||
mac_xml = "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"
|
||||
)
|
||||
super().load(emane_prefix)
|
|
@ -1,66 +0,0 @@
|
|||
"""
|
||||
tdma.py: EMANE TDMA model bindings for CORE
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
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 = "emane_tdma"
|
||||
|
||||
# mac configuration
|
||||
mac_library = "tdmaeventschedulerradiomodel"
|
||||
mac_xml = "tdmaeventschedulerradiomodel.xml"
|
||||
|
||||
# add custom schedule options and ignore it when writing emane xml
|
||||
schedule_name = "schedule"
|
||||
default_schedule = os.path.join(
|
||||
constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml"
|
||||
)
|
||||
config_ignore = {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,34 +1,17 @@
|
|||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import Mapping, Type
|
||||
from pathlib import Path
|
||||
|
||||
import core.services
|
||||
from core import configservices
|
||||
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:
|
||||
|
@ -36,7 +19,7 @@ class CoreEmu:
|
|||
Provides logic for creating and configuring CORE sessions and the nodes within them.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Mapping[str, str] = None) -> None:
|
||||
def __init__(self, config: dict[str, str] = None) -> None:
|
||||
"""
|
||||
Create a CoreEmu object.
|
||||
|
||||
|
@ -46,40 +29,83 @@ class CoreEmu:
|
|||
os.umask(0)
|
||||
|
||||
# configuration
|
||||
if config is None:
|
||||
config = {}
|
||||
self.config = config
|
||||
config = config if config else {}
|
||||
self.config: dict[str, str] = config
|
||||
|
||||
# session management
|
||||
self.sessions = {}
|
||||
self.sessions: dict[int, Session] = {}
|
||||
|
||||
# load services
|
||||
self.service_errors = []
|
||||
self.load_services()
|
||||
self.service_errors: list[str] = []
|
||||
self.service_manager: ConfigServiceManager = ConfigServiceManager()
|
||||
self._load_services()
|
||||
|
||||
# config services
|
||||
self.service_manager = ConfigServiceManager()
|
||||
config_services_path = os.path.abspath(os.path.dirname(configservices.__file__))
|
||||
self.service_manager.load(config_services_path)
|
||||
custom_dir = self.config.get("custom_config_services_dir")
|
||||
if custom_dir:
|
||||
self.service_manager.load(custom_dir)
|
||||
# check and load emane
|
||||
self.has_emane: bool = False
|
||||
self._load_emane()
|
||||
|
||||
# catch exit event
|
||||
atexit.register(self.shutdown)
|
||||
# check executables exist on path
|
||||
self._validate_env()
|
||||
|
||||
def load_services(self) -> None:
|
||||
def _validate_env(self) -> None:
|
||||
"""
|
||||
Validates executables CORE depends on exist on path.
|
||||
|
||||
:return: nothing
|
||||
:raises core.errors.CoreError: when an executable does not exist on path
|
||||
"""
|
||||
use_ovs = self.config.get("ovs") == "1"
|
||||
for requirement in get_requirements(use_ovs):
|
||||
utils.which(requirement, required=True)
|
||||
|
||||
def _load_services(self) -> None:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
|
@ -87,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.
|
||||
|
||||
|
@ -108,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
|
||||
|
||||
|
@ -119,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
|
||||
|
|
|
@ -1,122 +1,357 @@
|
|||
"""
|
||||
CORE data objects.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import collections
|
||||
import netaddr
|
||||
|
||||
ConfigData = collections.namedtuple(
|
||||
"ConfigData",
|
||||
[
|
||||
"message_type",
|
||||
"node",
|
||||
"object",
|
||||
"type",
|
||||
"data_types",
|
||||
"data_values",
|
||||
"captions",
|
||||
"bitmap",
|
||||
"possible_values",
|
||||
"groups",
|
||||
"session",
|
||||
"interface_number",
|
||||
"network_id",
|
||||
"opaque",
|
||||
],
|
||||
from core import utils
|
||||
from core.emulator.enumerations import (
|
||||
EventTypes,
|
||||
ExceptionLevels,
|
||||
LinkTypes,
|
||||
MessageFlags,
|
||||
)
|
||||
ConfigData.__new__.__defaults__ = (None,) * len(ConfigData._fields)
|
||||
|
||||
EventData = collections.namedtuple(
|
||||
"EventData", ["node", "event_type", "name", "data", "time", "session"]
|
||||
)
|
||||
EventData.__new__.__defaults__ = (None,) * len(EventData._fields)
|
||||
if TYPE_CHECKING:
|
||||
from core.nodes.base import CoreNode, NodeBase
|
||||
|
||||
ExceptionData = collections.namedtuple(
|
||||
"ExceptionData", ["node", "session", "level", "source", "date", "text", "opaque"]
|
||||
)
|
||||
ExceptionData.__new__.__defaults__ = (None,) * len(ExceptionData._fields)
|
||||
|
||||
FileData = collections.namedtuple(
|
||||
"FileData",
|
||||
[
|
||||
"message_type",
|
||||
"node",
|
||||
"name",
|
||||
"mode",
|
||||
"number",
|
||||
"type",
|
||||
"source",
|
||||
"session",
|
||||
"data",
|
||||
"compressed_data",
|
||||
],
|
||||
)
|
||||
FileData.__new__.__defaults__ = (None,) * len(FileData._fields)
|
||||
@dataclass
|
||||
class ConfigData:
|
||||
message_type: int = None
|
||||
node: int = None
|
||||
object: str = None
|
||||
type: int = None
|
||||
data_types: tuple[int] = None
|
||||
data_values: str = None
|
||||
captions: str = None
|
||||
bitmap: str = None
|
||||
possible_values: str = None
|
||||
groups: str = None
|
||||
session: int = None
|
||||
iface_id: int = None
|
||||
network_id: int = None
|
||||
opaque: str = None
|
||||
|
||||
NodeData = collections.namedtuple(
|
||||
"NodeData",
|
||||
[
|
||||
"message_type",
|
||||
"id",
|
||||
"node_type",
|
||||
"name",
|
||||
"ip_address",
|
||||
"mac_address",
|
||||
"ip6_address",
|
||||
"model",
|
||||
"emulation_id",
|
||||
"server",
|
||||
"session",
|
||||
"x_position",
|
||||
"y_position",
|
||||
"canvas",
|
||||
"network_id",
|
||||
"services",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"altitude",
|
||||
"icon",
|
||||
"opaque",
|
||||
"source",
|
||||
],
|
||||
)
|
||||
NodeData.__new__.__defaults__ = (None,) * len(NodeData._fields)
|
||||
|
||||
LinkData = collections.namedtuple(
|
||||
"LinkData",
|
||||
[
|
||||
"message_type",
|
||||
"node1_id",
|
||||
"node2_id",
|
||||
"delay",
|
||||
"bandwidth",
|
||||
"per",
|
||||
"dup",
|
||||
"jitter",
|
||||
"mer",
|
||||
"burst",
|
||||
"session",
|
||||
"mburst",
|
||||
"link_type",
|
||||
"gui_attributes",
|
||||
"unidirectional",
|
||||
"emulation_id",
|
||||
"network_id",
|
||||
"key",
|
||||
"interface1_id",
|
||||
"interface1_name",
|
||||
"interface1_ip4",
|
||||
"interface1_ip4_mask",
|
||||
"interface1_mac",
|
||||
"interface1_ip6",
|
||||
"interface1_ip6_mask",
|
||||
"interface2_id",
|
||||
"interface2_name",
|
||||
"interface2_ip4",
|
||||
"interface2_ip4_mask",
|
||||
"interface2_mac",
|
||||
"interface2_ip6",
|
||||
"interface2_ip6_mask",
|
||||
"opaque",
|
||||
],
|
||||
@dataclass
|
||||
class EventData:
|
||||
node: int = None
|
||||
event_type: EventTypes = None
|
||||
name: str = None
|
||||
data: str = None
|
||||
time: str = None
|
||||
session: int = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExceptionData:
|
||||
node: int = None
|
||||
session: int = None
|
||||
level: ExceptionLevels = None
|
||||
source: str = None
|
||||
date: str = None
|
||||
text: str = None
|
||||
opaque: str = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileData:
|
||||
message_type: MessageFlags = None
|
||||
node: int = None
|
||||
name: str = None
|
||||
mode: str = None
|
||||
number: int = None
|
||||
type: str = None
|
||||
source: str = None
|
||||
session: int = None
|
||||
data: str = None
|
||||
compressed_data: str = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class NodeOptions:
|
||||
"""
|
||||
Options for creating and updating nodes within core.
|
||||
"""
|
||||
|
||||
name: str = None
|
||||
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)
|
||||
x: float = None
|
||||
y: float = None
|
||||
lat: float = None
|
||||
lon: float = None
|
||||
alt: float = None
|
||||
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:
|
||||
"""
|
||||
Convenience method for setting position.
|
||||
|
||||
:param x: x position
|
||||
:param y: y position
|
||||
:return: nothing
|
||||
"""
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def set_location(self, lat: float, lon: float, alt: float) -> None:
|
||||
"""
|
||||
Convenience method for setting location.
|
||||
|
||||
:param lat: latitude
|
||||
:param lon: longitude
|
||||
:param alt: altitude
|
||||
:return: nothing
|
||||
"""
|
||||
self.lat = lat
|
||||
self.lon = lon
|
||||
self.alt = alt
|
||||
|
||||
|
||||
@dataclass
|
||||
class NodeData:
|
||||
"""
|
||||
Node to broadcast.
|
||||
"""
|
||||
|
||||
node: "NodeBase"
|
||||
message_type: MessageFlags = None
|
||||
source: str = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class InterfaceData:
|
||||
"""
|
||||
Convenience class for storing interface data.
|
||||
"""
|
||||
|
||||
id: int = None
|
||||
name: str = None
|
||||
mac: str = None
|
||||
ip4: str = None
|
||||
ip4_mask: int = None
|
||||
ip6: str = None
|
||||
ip6_mask: int = None
|
||||
mtu: int = None
|
||||
|
||||
def get_ips(self) -> list[str]:
|
||||
"""
|
||||
Returns a list of ip4 and ip6 addresses when present.
|
||||
|
||||
:return: list of ip addresses
|
||||
"""
|
||||
ips = []
|
||||
if self.ip4 and self.ip4_mask:
|
||||
ips.append(f"{self.ip4}/{self.ip4_mask}")
|
||||
if self.ip6 and self.ip6_mask:
|
||||
ips.append(f"{self.ip6}/{self.ip6_mask}")
|
||||
return ips
|
||||
|
||||
|
||||
@dataclass
|
||||
class LinkOptions:
|
||||
"""
|
||||
Options for creating and updating links within core.
|
||||
"""
|
||||
|
||||
delay: int = None
|
||||
bandwidth: int = None
|
||||
loss: float = None
|
||||
dup: int = None
|
||||
jitter: int = None
|
||||
mer: int = None
|
||||
burst: int = None
|
||||
mburst: int = None
|
||||
unidirectional: int = None
|
||||
key: int = None
|
||||
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
|
||||
)
|
||||
LinkData.__new__.__defaults__ = (None,) * len(LinkData._fields)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LinkData:
|
||||
"""
|
||||
Represents all data associated with a link.
|
||||
"""
|
||||
|
||||
message_type: MessageFlags = None
|
||||
type: LinkTypes = LinkTypes.WIRED
|
||||
label: str = None
|
||||
node1_id: int = None
|
||||
node2_id: int = None
|
||||
network_id: int = None
|
||||
iface1: InterfaceData = None
|
||||
iface2: InterfaceData = None
|
||||
options: LinkOptions = LinkOptions()
|
||||
color: str = None
|
||||
source: str = None
|
||||
|
||||
|
||||
class IpPrefixes:
|
||||
"""
|
||||
Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
|
||||
"""
|
||||
|
||||
def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
|
||||
"""
|
||||
Creates an IpPrefixes object.
|
||||
|
||||
:param ip4_prefix: ip4 prefix to use for generation
|
||||
:param ip6_prefix: ip6 prefix to use for generation
|
||||
:raises ValueError: when both ip4 and ip6 prefixes have not been provided
|
||||
"""
|
||||
if not ip4_prefix and not ip6_prefix:
|
||||
raise ValueError("ip4 or ip6 must be provided")
|
||||
|
||||
self.ip4 = None
|
||||
if ip4_prefix:
|
||||
self.ip4 = netaddr.IPNetwork(ip4_prefix)
|
||||
self.ip6 = None
|
||||
if ip6_prefix:
|
||||
self.ip6 = netaddr.IPNetwork(ip6_prefix)
|
||||
|
||||
def ip4_address(self, node_id: int) -> str:
|
||||
"""
|
||||
Convenience method to return the IP4 address for a node.
|
||||
|
||||
:param node_id: node id to get IP4 address for
|
||||
:return: IP4 address or None
|
||||
"""
|
||||
if not self.ip4:
|
||||
raise ValueError("ip4 prefixes have not been set")
|
||||
return str(self.ip4[node_id])
|
||||
|
||||
def ip6_address(self, node_id: int) -> str:
|
||||
"""
|
||||
Convenience method to return the IP6 address for a node.
|
||||
|
||||
:param node_id: node id to get IP6 address for
|
||||
:return: IP4 address or None
|
||||
"""
|
||||
if not self.ip6:
|
||||
raise ValueError("ip6 prefixes have not been set")
|
||||
return str(self.ip6[node_id])
|
||||
|
||||
def gen_iface(self, node_id: int, name: str = None, mac: str = None):
|
||||
"""
|
||||
Creates interface data for linking nodes, using the nodes unique id for
|
||||
generation, along with a random mac address, unless provided.
|
||||
|
||||
:param node_id: node id to create an interface for
|
||||
:param name: name to set for interface, default is eth{id}
|
||||
:param mac: mac address to use for this interface, default is random
|
||||
generation
|
||||
:return: new interface data for the provided node
|
||||
"""
|
||||
# generate ip4 data
|
||||
ip4 = None
|
||||
ip4_mask = None
|
||||
if self.ip4:
|
||||
ip4 = self.ip4_address(node_id)
|
||||
ip4_mask = self.ip4.prefixlen
|
||||
|
||||
# generate ip6 data
|
||||
ip6 = None
|
||||
ip6_mask = None
|
||||
if self.ip6:
|
||||
ip6 = self.ip6_address(node_id)
|
||||
ip6_mask = self.ip6.prefixlen
|
||||
|
||||
# random mac
|
||||
if not mac:
|
||||
mac = utils.random_mac()
|
||||
|
||||
return InterfaceData(
|
||||
name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac
|
||||
)
|
||||
|
||||
def create_iface(
|
||||
self, node: "CoreNode", name: str = None, mac: str = None
|
||||
) -> InterfaceData:
|
||||
"""
|
||||
Creates interface data for linking nodes, using the nodes unique id for
|
||||
generation, along with a random mac address, unless provided.
|
||||
|
||||
:param node: node to create interface for
|
||||
:param name: name to set for interface, default is eth{id}
|
||||
:param mac: mac address to use for this interface, default is random
|
||||
generation
|
||||
:return: new interface data for the provided node
|
||||
"""
|
||||
iface_data = self.gen_iface(node.id, name, mac)
|
||||
iface_data.id = node.next_iface_id()
|
||||
return iface_data
|
||||
|
|
|
@ -6,18 +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.errors import CoreCommandError
|
||||
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
|
||||
|
||||
|
@ -37,13 +42,13 @@ class DistributedServer:
|
|||
:param name: convenience name to associate with host
|
||||
:param host: host to connect to
|
||||
"""
|
||||
self.name = name
|
||||
self.host = host
|
||||
self.conn = Connection(host, user="root")
|
||||
self.lock = threading.Lock()
|
||||
self.name: str = name
|
||||
self.host: str = host
|
||||
self.conn: Connection = Connection(host, user="root")
|
||||
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.
|
||||
|
@ -60,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:
|
||||
|
@ -78,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)
|
||||
|
||||
|
||||
|
@ -117,12 +122,10 @@ class DistributedController:
|
|||
|
||||
:param session: session
|
||||
"""
|
||||
self.session = session
|
||||
self.servers = OrderedDict()
|
||||
self.tunnels = {}
|
||||
self.address = self.session.options.get_config(
|
||||
"distributed_address", default=None
|
||||
)
|
||||
self.session: "Session" = session
|
||||
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:
|
||||
"""
|
||||
|
@ -131,10 +134,19 @@ class DistributedController:
|
|||
:param name: distributed server name
|
||||
:param host: distributed server host address
|
||||
:return: nothing
|
||||
:raises CoreError: when there is an error validating server
|
||||
"""
|
||||
server = DistributedServer(name, host)
|
||||
for requirement in get_requirements(self.session.use_ovs()):
|
||||
try:
|
||||
server.remote_cmd(f"which {requirement}")
|
||||
except CoreCommandError:
|
||||
raise CoreError(
|
||||
f"server({server.name}) failed validation for "
|
||||
f"command({requirement})"
|
||||
)
|
||||
self.servers[name] = server
|
||||
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:
|
||||
|
@ -160,45 +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):
|
||||
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
|
||||
|
||||
if isinstance(node, CtrlNet) and 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
|
||||
|
@ -206,52 +228,39 @@ 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.create_interface(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.create_interface(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
|
||||
return tunnel
|
||||
|
||||
def tunnel_key(self, n1_id: int, n2_id: int) -> int:
|
||||
def tunnel_key(self, node1_id: int, node2_id: int) -> int:
|
||||
"""
|
||||
Compute a 32-bit key used to uniquely identify a GRE tunnel.
|
||||
The hash(n1num), hash(n2num) values are used, so node numbers may be
|
||||
None or string values (used for e.g. "ctrlnet").
|
||||
|
||||
:param n1_id: node one id
|
||||
:param n2_id: node two id
|
||||
:param node1_id: node one id
|
||||
:param node2_id: node two id
|
||||
:return: tunnel key for the node pair
|
||||
"""
|
||||
logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id)
|
||||
logger.debug("creating tunnel key for: %s, %s", node1_id, node2_id)
|
||||
key = (
|
||||
(self.session.id << 16) ^ utils.hashkey(n1_id) ^ (utils.hashkey(n2_id) << 8)
|
||||
(self.session.id << 16)
|
||||
^ utils.hashkey(node1_id)
|
||||
^ (utils.hashkey(node2_id) << 8)
|
||||
)
|
||||
return key & 0xFFFFFFFF
|
||||
|
||||
def get_tunnel(self, n1_id: int, n2_id: int) -> Tuple[GreTap, GreTap]:
|
||||
"""
|
||||
Return the GreTap between two nodes if it exists.
|
||||
|
||||
:param n1_id: node one id
|
||||
:param n2_id: node two id
|
||||
:return: gre tap between nodes or None
|
||||
"""
|
||||
key = self.tunnel_key(n1_id, n2_id)
|
||||
logging.debug("checking for tunnel key(%s) in: %s", key, self.tunnels)
|
||||
return self.tunnels.get(key)
|
||||
|
|
|
@ -1,335 +0,0 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.api.grpc.core_pb2 import LinkOptions
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.enumerations import LinkTypes
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.physical import PhysicalNode
|
||||
|
||||
|
||||
class IdGen:
|
||||
def __init__(self, _id: int = 0) -> None:
|
||||
self.id = _id
|
||||
|
||||
def next(self) -> int:
|
||||
self.id += 1
|
||||
return self.id
|
||||
|
||||
|
||||
def link_config(
|
||||
network: CoreNetworkBase,
|
||||
interface: CoreInterface,
|
||||
link_options: LinkOptions,
|
||||
devname: str = None,
|
||||
interface_two: CoreInterface = None,
|
||||
) -> None:
|
||||
"""
|
||||
Convenience method for configuring a link,
|
||||
|
||||
:param network: network to configure link for
|
||||
:param interface: interface to configure
|
||||
:param link_options: data to configure link with
|
||||
:param devname: device name, default is None
|
||||
:param interface_two: other interface associated, default is None
|
||||
:return: nothing
|
||||
"""
|
||||
config = {
|
||||
"netif": interface,
|
||||
"bw": link_options.bandwidth,
|
||||
"delay": link_options.delay,
|
||||
"loss": link_options.per,
|
||||
"duplicate": link_options.dup,
|
||||
"jitter": link_options.jitter,
|
||||
"netif2": interface_two,
|
||||
}
|
||||
|
||||
# hacky check here, because physical and emane nodes do not conform to the same
|
||||
# linkconfig interface
|
||||
if not isinstance(network, (EmaneNet, PhysicalNode)):
|
||||
config["devname"] = devname
|
||||
|
||||
network.linkconfig(**config)
|
||||
|
||||
|
||||
class NodeOptions:
|
||||
"""
|
||||
Options for creating and updating nodes within core.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = None, model: str = "PC", image: str = None) -> None:
|
||||
"""
|
||||
Create a NodeOptions object.
|
||||
|
||||
:param name: name of node, defaults to node class name postfix with its id
|
||||
:param model: defines services for default and physical nodes, defaults to
|
||||
"router"
|
||||
:param image: image to use for docker nodes
|
||||
"""
|
||||
self.name = name
|
||||
self.model = model
|
||||
self.canvas = None
|
||||
self.icon = None
|
||||
self.opaque = None
|
||||
self.services = []
|
||||
self.config_services = []
|
||||
self.x = None
|
||||
self.y = None
|
||||
self.lat = None
|
||||
self.lon = None
|
||||
self.alt = None
|
||||
self.emulation_id = None
|
||||
self.server = None
|
||||
self.image = image
|
||||
self.emane = None
|
||||
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Convenience method for setting position.
|
||||
|
||||
:param x: x position
|
||||
:param y: y position
|
||||
:return: nothing
|
||||
"""
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def set_location(self, lat: float, lon: float, alt: float) -> None:
|
||||
"""
|
||||
Convenience method for setting location.
|
||||
|
||||
:param lat: latitude
|
||||
:param lon: longitude
|
||||
:param alt: altitude
|
||||
:return: nothing
|
||||
"""
|
||||
self.lat = lat
|
||||
self.lon = lon
|
||||
self.alt = alt
|
||||
|
||||
|
||||
class LinkOptions:
|
||||
"""
|
||||
Options for creating and updating links within core.
|
||||
"""
|
||||
|
||||
def __init__(self, _type: LinkTypes = LinkTypes.WIRED) -> None:
|
||||
"""
|
||||
Create a LinkOptions object.
|
||||
|
||||
:param _type: type of link, defaults to
|
||||
wired
|
||||
"""
|
||||
self.type = _type
|
||||
self.session = None
|
||||
self.delay = None
|
||||
self.bandwidth = None
|
||||
self.per = None
|
||||
self.dup = None
|
||||
self.jitter = None
|
||||
self.mer = None
|
||||
self.burst = None
|
||||
self.mburst = None
|
||||
self.gui_attributes = None
|
||||
self.unidirectional = None
|
||||
self.emulation_id = None
|
||||
self.network_id = None
|
||||
self.key = None
|
||||
self.opaque = None
|
||||
|
||||
|
||||
class InterfaceData:
|
||||
"""
|
||||
Convenience class for storing interface data.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
_id: int,
|
||||
name: str,
|
||||
mac: str,
|
||||
ip4: str,
|
||||
ip4_mask: int,
|
||||
ip6: str,
|
||||
ip6_mask: int,
|
||||
) -> None:
|
||||
"""
|
||||
Creates an InterfaceData object.
|
||||
|
||||
:param _id: interface id
|
||||
:param name: name for interface
|
||||
:param mac: mac address
|
||||
:param ip4: ipv4 address
|
||||
:param ip4_mask: ipv4 bit mask
|
||||
:param ip6: ipv6 address
|
||||
:param ip6_mask: ipv6 bit mask
|
||||
"""
|
||||
self.id = _id
|
||||
self.name = name
|
||||
self.mac = mac
|
||||
self.ip4 = ip4
|
||||
self.ip4_mask = ip4_mask
|
||||
self.ip6 = ip6
|
||||
self.ip6_mask = ip6_mask
|
||||
|
||||
def has_ip4(self) -> bool:
|
||||
"""
|
||||
Determines if interface has an ip4 address.
|
||||
|
||||
:return: True if has ip4, False otherwise
|
||||
"""
|
||||
return all([self.ip4, self.ip4_mask])
|
||||
|
||||
def has_ip6(self) -> bool:
|
||||
"""
|
||||
Determines if interface has an ip6 address.
|
||||
|
||||
:return: True if has ip6, False otherwise
|
||||
"""
|
||||
return all([self.ip6, self.ip6_mask])
|
||||
|
||||
def ip4_address(self) -> Optional[str]:
|
||||
"""
|
||||
Retrieve a string representation of the ip4 address and netmask.
|
||||
|
||||
:return: ip4 string or None
|
||||
"""
|
||||
if self.has_ip4():
|
||||
return f"{self.ip4}/{self.ip4_mask}"
|
||||
else:
|
||||
return None
|
||||
|
||||
def ip6_address(self) -> Optional[str]:
|
||||
"""
|
||||
Retrieve a string representation of the ip6 address and netmask.
|
||||
|
||||
:return: ip4 string or None
|
||||
"""
|
||||
if self.has_ip6():
|
||||
return f"{self.ip6}/{self.ip6_mask}"
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_addresses(self) -> List[str]:
|
||||
"""
|
||||
Returns a list of ip4 and ip6 address when present.
|
||||
|
||||
:return: list of addresses
|
||||
"""
|
||||
ip4 = self.ip4_address()
|
||||
ip6 = self.ip6_address()
|
||||
return [i for i in [ip4, ip6] if i]
|
||||
|
||||
|
||||
class IpPrefixes:
|
||||
"""
|
||||
Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
|
||||
"""
|
||||
|
||||
def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
|
||||
"""
|
||||
Creates an IpPrefixes object.
|
||||
|
||||
:param ip4_prefix: ip4 prefix to use for generation
|
||||
:param ip6_prefix: ip6 prefix to use for generation
|
||||
:raises ValueError: when both ip4 and ip6 prefixes have not been provided
|
||||
"""
|
||||
if not ip4_prefix and not ip6_prefix:
|
||||
raise ValueError("ip4 or ip6 must be provided")
|
||||
|
||||
self.ip4 = None
|
||||
if ip4_prefix:
|
||||
self.ip4 = netaddr.IPNetwork(ip4_prefix)
|
||||
self.ip6 = None
|
||||
if ip6_prefix:
|
||||
self.ip6 = netaddr.IPNetwork(ip6_prefix)
|
||||
|
||||
def ip4_address(self, node: CoreNode) -> str:
|
||||
"""
|
||||
Convenience method to return the IP4 address for a node.
|
||||
|
||||
:param node: node to get IP4 address for
|
||||
:return: IP4 address or None
|
||||
"""
|
||||
if not self.ip4:
|
||||
raise ValueError("ip4 prefixes have not been set")
|
||||
return str(self.ip4[node.id])
|
||||
|
||||
def ip6_address(self, node: CoreNode) -> str:
|
||||
"""
|
||||
Convenience method to return the IP6 address for a node.
|
||||
|
||||
:param node: node to get IP6 address for
|
||||
:return: IP4 address or None
|
||||
"""
|
||||
if not self.ip6:
|
||||
raise ValueError("ip6 prefixes have not been set")
|
||||
return str(self.ip6[node.id])
|
||||
|
||||
def create_interface(
|
||||
self, node: CoreNode, name: str = None, mac: str = None
|
||||
) -> InterfaceData:
|
||||
"""
|
||||
Creates interface data for linking nodes, using the nodes unique id for
|
||||
generation, along with a random mac address, unless provided.
|
||||
|
||||
:param node: node to create interface for
|
||||
:param name: name to set for interface, default is eth{id}
|
||||
:param mac: mac address to use for this interface, default is random
|
||||
generation
|
||||
:return: new interface data for the provided node
|
||||
"""
|
||||
# interface id
|
||||
inteface_id = node.newifindex()
|
||||
|
||||
# generate ip4 data
|
||||
ip4 = None
|
||||
ip4_mask = None
|
||||
if self.ip4:
|
||||
ip4 = self.ip4_address(node)
|
||||
ip4_mask = self.ip4.prefixlen
|
||||
|
||||
# generate ip6 data
|
||||
ip6 = None
|
||||
ip6_mask = None
|
||||
if self.ip6:
|
||||
ip6 = self.ip6_address(node)
|
||||
ip6_mask = self.ip6.prefixlen
|
||||
|
||||
# random mac
|
||||
if not mac:
|
||||
mac = utils.random_mac()
|
||||
|
||||
return InterfaceData(
|
||||
_id=inteface_id,
|
||||
name=name,
|
||||
ip4=ip4,
|
||||
ip4_mask=ip4_mask,
|
||||
ip6=ip6,
|
||||
ip6_mask=ip6_mask,
|
||||
mac=mac,
|
||||
)
|
||||
|
||||
|
||||
def create_interface(
|
||||
node: CoreNode, network: CoreNetworkBase, interface_data: InterfaceData
|
||||
):
|
||||
"""
|
||||
Create an interface for a node on a network using provided interface data.
|
||||
|
||||
:param node: node to create interface for
|
||||
:param network: network to associate interface with
|
||||
:param interface_data: interface data
|
||||
:return: created interface
|
||||
"""
|
||||
node.newnetif(
|
||||
network,
|
||||
addrlist=interface_data.get_addresses(),
|
||||
hwaddr=interface_data.mac,
|
||||
ifindex=interface_data.id,
|
||||
ifname=interface_data.name,
|
||||
)
|
||||
return node.netif(interface_data.id)
|
|
@ -1,35 +1,16 @@
|
|||
"""
|
||||
Contains all legacy enumerations for interacting with legacy CORE code.
|
||||
Common enumerations used within CORE.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
CORE_API_VERSION = "1.23"
|
||||
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 MessageFlags(Enum):
|
||||
"""
|
||||
CORE message flags.
|
||||
"""
|
||||
|
||||
NONE = 0x00
|
||||
ADD = 0x01
|
||||
DELETE = 0x02
|
||||
CRI = 0x04
|
||||
|
@ -39,31 +20,15 @@ class MessageFlags(Enum):
|
|||
TTY = 0x40
|
||||
|
||||
|
||||
class NodeTlvs(Enum):
|
||||
class ConfigFlags(Enum):
|
||||
"""
|
||||
Node type, length, value enumerations.
|
||||
Configuration flags.
|
||||
"""
|
||||
|
||||
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
|
||||
NONE = 0x00
|
||||
REQUEST = 0x01
|
||||
UPDATE = 0x02
|
||||
RESET = 0x03
|
||||
|
||||
|
||||
class NodeTypes(Enum):
|
||||
|
@ -84,56 +49,8 @@ class NodeTypes(Enum):
|
|||
CONTROL_NET = 13
|
||||
DOCKER = 15
|
||||
LXC = 16
|
||||
|
||||
|
||||
class Rj45Models(Enum):
|
||||
"""
|
||||
RJ45 model types.
|
||||
"""
|
||||
|
||||
LINKED = 0
|
||||
WIRELESS = 1
|
||||
INSTALLED = 2
|
||||
|
||||
|
||||
# Link Message TLV Types
|
||||
class LinkTlvs(Enum):
|
||||
"""
|
||||
Link type, length, value enumerations.
|
||||
"""
|
||||
|
||||
N1_NUMBER = 0x01
|
||||
N2_NUMBER = 0x02
|
||||
DELAY = 0x03
|
||||
BANDWIDTH = 0x04
|
||||
PER = 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
|
||||
INTERFACE1_NUMBER = 0x30
|
||||
INTERFACE1_IP4 = 0x31
|
||||
INTERFACE1_IP4_MASK = 0x32
|
||||
INTERFACE1_MAC = 0x33
|
||||
INTERFACE1_IP6 = 0x34
|
||||
INTERFACE1_IP6_MASK = 0x35
|
||||
INTERFACE2_NUMBER = 0x36
|
||||
INTERFACE2_IP4 = 0x37
|
||||
INTERFACE2_IP4_MASK = 0x38
|
||||
INTERFACE2_MAC = 0x39
|
||||
INTERFACE2_IP6 = 0x40
|
||||
INTERFACE2_IP6_MASK = 0x41
|
||||
INTERFACE1_NAME = 0x42
|
||||
INTERFACE2_NAME = 0x43
|
||||
OPAQUE = 0x50
|
||||
WIRELESS = 17
|
||||
PODMAN = 18
|
||||
|
||||
|
||||
class LinkTypes(Enum):
|
||||
|
@ -145,20 +62,6 @@ class LinkTypes(Enum):
|
|||
WIRED = 1
|
||||
|
||||
|
||||
class ExecuteTlvs(Enum):
|
||||
"""
|
||||
Execute type, length, value enumerations.
|
||||
"""
|
||||
|
||||
NODE = 0x01
|
||||
NUMBER = 0x02
|
||||
TIME = 0x03
|
||||
COMMAND = 0x04
|
||||
RESULT = 0x05
|
||||
STATUS = 0x06
|
||||
SESSION = 0x0A
|
||||
|
||||
|
||||
class RegisterTlvs(Enum):
|
||||
"""
|
||||
Register type, length, value enumerations.
|
||||
|
@ -173,37 +76,6 @@ class RegisterTlvs(Enum):
|
|||
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
|
||||
INTERFACE_NUMBER = 0x0B
|
||||
NETWORK_ID = 0x24
|
||||
OPAQUE = 0x50
|
||||
|
||||
|
||||
class ConfigFlags(Enum):
|
||||
"""
|
||||
Configuration flags.
|
||||
"""
|
||||
|
||||
NONE = 0x00
|
||||
REQUEST = 0x01
|
||||
UPDATE = 0x02
|
||||
RESET = 0x03
|
||||
|
||||
|
||||
class ConfigDataTypes(Enum):
|
||||
"""
|
||||
Configuration data types.
|
||||
|
@ -222,55 +94,6 @@ class ConfigDataTypes(Enum):
|
|||
BOOL = 0x0B
|
||||
|
||||
|
||||
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 EventTypes(Enum):
|
||||
"""
|
||||
Event types.
|
||||
|
@ -293,34 +116,11 @@ class EventTypes(Enum):
|
|||
RECONFIGURE = 14
|
||||
INSTANTIATION_COMPLETE = 15
|
||||
|
||||
def should_start(self) -> bool:
|
||||
return self.value > self.DEFINITION_STATE.value
|
||||
|
||||
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
|
||||
def already_collected(self) -> bool:
|
||||
return self.value >= self.DATACOLLECT_STATE.value
|
||||
|
||||
|
||||
class ExceptionLevels(Enum):
|
||||
|
@ -333,3 +133,13 @@ class ExceptionLevels(Enum):
|
|||
ERROR = 2
|
||||
WARNING = 3
|
||||
NOTICE = 4
|
||||
|
||||
|
||||
class NetworkPolicy(Enum):
|
||||
ACCEPT = "ACCEPT"
|
||||
DROP = "DROP"
|
||||
|
||||
|
||||
class TransportType(Enum):
|
||||
RAW = "raw"
|
||||
VIRTUAL = "virtual"
|
||||
|
|
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,90 +1,87 @@
|
|||
from typing import Any
|
||||
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 = "session"
|
||||
options = [
|
||||
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",
|
||||
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.UTILITY.value
|
||||
|
||||
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.
|
||||
|
||||
|
@ -92,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
|
||||
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.
|
||||
|
||||
|
@ -105,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}"
|
||||
)
|
||||
|
||||
|
@ -22,3 +22,35 @@ class CoreError(Exception):
|
|||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreXmlError(Exception):
|
||||
"""
|
||||
Used when there was an error parsing a CORE xml file.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreServiceError(Exception):
|
||||
"""
|
||||
Used when there is an error related to accessing a service.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreServiceBootError(Exception):
|
||||
"""
|
||||
Used when there is an error booting a service.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreConfigError(Exception):
|
||||
"""
|
||||
Used when there is an error defining a configurable option.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
40
daemon/core/executables.py
Normal file
40
daemon/core/executables.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
BASH: str = "bash"
|
||||
ETHTOOL: str = "ethtool"
|
||||
IP: str = "ip"
|
||||
MOUNT: str = "mount"
|
||||
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] = [
|
||||
BASH,
|
||||
ETHTOOL,
|
||||
IP,
|
||||
MOUNT,
|
||||
NFTABLES,
|
||||
SYSCTL,
|
||||
TC,
|
||||
TEST,
|
||||
UMOUNT,
|
||||
VCMD,
|
||||
VNODED,
|
||||
]
|
||||
OVS_REQUIREMENTS: list[str] = [OVS_VSCTL]
|
||||
|
||||
|
||||
def get_requirements(use_ovs: bool) -> list[str]:
|
||||
"""
|
||||
Retrieve executable requirements needed to run CORE.
|
||||
|
||||
:param use_ovs: True if OVS is being used, False otherwise
|
||||
:return: list of executable requirements
|
||||
"""
|
||||
requirements = COMMON_REQUIREMENTS
|
||||
if use_ovs:
|
||||
requirements += OVS_REQUIREMENTS
|
||||
return requirements
|
|
@ -1,104 +1,220 @@
|
|||
import logging
|
||||
import math
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import PhotoImage, font, messagebox, ttk
|
||||
from tkinter.ttk import Progressbar
|
||||
from typing import Any, Optional
|
||||
|
||||
from core.gui import appconfig, themes
|
||||
import grpc
|
||||
|
||||
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.graph.graph import CanvasGraph
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.menuaction import MenuAction
|
||||
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.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
|
||||
from core.gui.validation import InputValidation
|
||||
|
||||
WIDTH = 1000
|
||||
HEIGHT = 800
|
||||
logger = logging.getLogger(__name__)
|
||||
WIDTH: int = 1000
|
||||
HEIGHT: int = 800
|
||||
|
||||
|
||||
class Application(tk.Frame):
|
||||
def __init__(self, proxy: bool):
|
||||
super().__init__(master=None)
|
||||
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 = None
|
||||
self.toolbar = None
|
||||
self.canvas = None
|
||||
self.statusbar = None
|
||||
self.validation = None
|
||||
self.menubar: Optional[Menubar] = None
|
||||
self.toolbar: Optional[Toolbar] = None
|
||||
self.right_frame: Optional[ttk.Frame] = None
|
||||
self.manager: Optional[CanvasManager] = None
|
||||
self.statusbar: Optional[StatusBar] = None
|
||||
self.progress: Optional[Progressbar] = None
|
||||
self.infobar: Optional[ttk.Frame] = None
|
||||
self.info_frame: Optional[InfoFrameBase] = None
|
||||
self.show_infobar: tk.BooleanVar = tk.BooleanVar(value=False)
|
||||
|
||||
# fonts
|
||||
self.fonts_size: dict[str, int] = {}
|
||||
self.icon_text_font: Optional[font.Font] = None
|
||||
self.edge_font: Optional[font.Font] = None
|
||||
|
||||
# setup
|
||||
self.guiconfig = appconfig.read()
|
||||
self.style = ttk.Style()
|
||||
self.guiconfig: GuiConfig = appconfig.read()
|
||||
self.app_scale: float = self.guiconfig.scale
|
||||
self.setup_scaling()
|
||||
self.style: ttk.Style = ttk.Style()
|
||||
self.setup_theme()
|
||||
self.core = CoreClient(self, proxy)
|
||||
self.core: CoreClient = CoreClient(self, proxy)
|
||||
self.setup_app()
|
||||
self.draw()
|
||||
self.core.set_up()
|
||||
self.core.setup(session_id)
|
||||
|
||||
def setup_theme(self):
|
||||
def setup_scaling(self) -> None:
|
||||
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
|
||||
text_scale = self.app_scale if self.app_scale < 1 else math.sqrt(self.app_scale)
|
||||
themes.scale_fonts(self.fonts_size, self.app_scale)
|
||||
self.icon_text_font = font.Font(family="TkIconFont", size=int(12 * text_scale))
|
||||
self.edge_font = font.Font(
|
||||
family="TkDefaultFont", size=int(8 * text_scale), weight=font.BOLD
|
||||
)
|
||||
|
||||
def setup_theme(self) -> None:
|
||||
themes.load(self.style)
|
||||
self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu)
|
||||
self.master.bind("<<ThemeChanged>>", themes.theme_change)
|
||||
self.style.theme_use(self.guiconfig["preferences"]["theme"])
|
||||
self.style.theme_use(self.guiconfig.preferences.theme)
|
||||
|
||||
def setup_app(self):
|
||||
def setup_app(self) -> None:
|
||||
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.pack(fill=tk.BOTH, expand=True)
|
||||
self.validation = InputValidation(self)
|
||||
self.master.option_add("*tearOff", tk.FALSE)
|
||||
self.setup_file_dialogs()
|
||||
|
||||
def center(self):
|
||||
def setup_file_dialogs(self) -> None:
|
||||
"""
|
||||
Hack code that needs to initialize a bad dialog so that we can apply,
|
||||
global settings for dialogs to not show hidden files by default and display
|
||||
the hidden file toggle.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
self.master.tk.call("tk_getOpenFile", "-foobar")
|
||||
except tk.TclError:
|
||||
pass
|
||||
self.master.tk.call("set", "::tk::dialog::file::showHiddenBtn", "1")
|
||||
self.master.tk.call("set", "::tk::dialog::file::showHiddenVar", "0")
|
||||
|
||||
def center(self) -> None:
|
||||
screen_width = self.master.winfo_screenwidth()
|
||||
screen_height = self.master.winfo_screenheight()
|
||||
x = int((screen_width / 2) - (WIDTH / 2))
|
||||
y = int((screen_height / 2) - (HEIGHT / 2))
|
||||
self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}")
|
||||
|
||||
def draw(self):
|
||||
self.master.option_add("*tearOff", tk.FALSE)
|
||||
self.menubar = Menubar(self.master, self)
|
||||
self.toolbar = Toolbar(self, self)
|
||||
self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2)
|
||||
self.draw_canvas()
|
||||
self.draw_status()
|
||||
|
||||
def draw_canvas(self):
|
||||
width = self.guiconfig["preferences"]["width"]
|
||||
height = self.guiconfig["preferences"]["height"]
|
||||
self.canvas = CanvasGraph(self, self.core, width, height)
|
||||
self.canvas.pack(fill=tk.BOTH, expand=True)
|
||||
scroll_x = ttk.Scrollbar(
|
||||
self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview
|
||||
x = int((screen_width / 2) - (WIDTH * self.app_scale / 2))
|
||||
y = int((screen_height / 2) - (HEIGHT * self.app_scale / 2))
|
||||
self.master.geometry(
|
||||
f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}"
|
||||
)
|
||||
scroll_x.pack(side=tk.BOTTOM, fill=tk.X)
|
||||
scroll_y = ttk.Scrollbar(self.canvas, command=self.canvas.yview)
|
||||
scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self.canvas.configure(xscrollcommand=scroll_x.set)
|
||||
self.canvas.configure(yscrollcommand=scroll_y.set)
|
||||
|
||||
def draw_status(self):
|
||||
self.statusbar = StatusBar(master=self, app=self)
|
||||
self.statusbar.pack(side=tk.BOTTOM, fill=tk.X)
|
||||
def draw(self) -> None:
|
||||
self.master.rowconfigure(0, weight=1)
|
||||
self.master.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
self.columnconfigure(1, weight=1)
|
||||
self.grid(sticky=tk.NSEW)
|
||||
self.toolbar = Toolbar(self)
|
||||
self.toolbar.grid(sticky=tk.NS)
|
||||
self.right_frame = ttk.Frame(self)
|
||||
self.right_frame.columnconfigure(0, weight=1)
|
||||
self.right_frame.rowconfigure(0, weight=1)
|
||||
self.right_frame.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
self.draw_canvas()
|
||||
self.draw_infobar()
|
||||
self.draw_status()
|
||||
self.progress = Progressbar(self.right_frame, mode="indeterminate")
|
||||
self.menubar = Menubar(self)
|
||||
self.master.config(menu=self.menubar)
|
||||
|
||||
def on_closing(self):
|
||||
menu_action = MenuAction(self, self.master)
|
||||
menu_action.on_quit()
|
||||
def draw_infobar(self) -> None:
|
||||
self.infobar = ttk.Frame(self.right_frame, padding=5, relief=tk.RAISED)
|
||||
self.infobar.columnconfigure(0, weight=1)
|
||||
self.infobar.rowconfigure(1, weight=1)
|
||||
label_font = font.Font(weight=font.BOLD, underline=tk.TRUE)
|
||||
label = ttk.Label(
|
||||
self.infobar, text="Details", anchor=tk.CENTER, font=label_font
|
||||
)
|
||||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
def save_config(self):
|
||||
def draw_canvas(self) -> None:
|
||||
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:
|
||||
if not self.show_infobar.get():
|
||||
return
|
||||
self.clear_info()
|
||||
self.info_frame = frame_class(self.infobar, **kwargs)
|
||||
self.info_frame.draw()
|
||||
self.info_frame.grid(sticky=tk.NSEW)
|
||||
|
||||
def clear_info(self) -> None:
|
||||
if self.info_frame:
|
||||
self.info_frame.destroy()
|
||||
self.info_frame = None
|
||||
|
||||
def default_info(self) -> None:
|
||||
self.clear_info()
|
||||
self.display_info(DefaultInfoFrame, app=self)
|
||||
|
||||
def show_info(self) -> None:
|
||||
self.default_info()
|
||||
self.infobar.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
|
||||
def hide_info(self) -> None:
|
||||
self.infobar.grid_forget()
|
||||
|
||||
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, 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_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:
|
||||
self.toolbar.picker.destroy()
|
||||
self.menubar.prompt_save_running_session(True)
|
||||
|
||||
def save_config(self) -> None:
|
||||
appconfig.save(self.guiconfig)
|
||||
|
||||
def joined_session_update(self):
|
||||
self.statusbar.progress_bar.stop()
|
||||
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 close(self):
|
||||
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_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,110 +1,224 @@
|
|||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
# gui home paths
|
||||
from core.gui import themes
|
||||
|
||||
HOME_PATH = Path.home().joinpath(".coretk")
|
||||
BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds")
|
||||
CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane")
|
||||
CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services")
|
||||
ICONS_PATH = HOME_PATH.joinpath("icons")
|
||||
MOBILITY_PATH = HOME_PATH.joinpath("mobility")
|
||||
XMLS_PATH = HOME_PATH.joinpath("xmls")
|
||||
CONFIG_PATH = HOME_PATH.joinpath("gui.yaml")
|
||||
LOG_PATH = HOME_PATH.joinpath("gui.log")
|
||||
HOME_PATH: Path = Path.home().joinpath(".coregui")
|
||||
BACKGROUNDS_PATH: Path = HOME_PATH.joinpath("backgrounds")
|
||||
CUSTOM_EMANE_PATH: Path = HOME_PATH.joinpath("custom_emane")
|
||||
CUSTOM_SERVICE_PATH: Path = HOME_PATH.joinpath("custom_services")
|
||||
ICONS_PATH: Path = HOME_PATH.joinpath("icons")
|
||||
MOBILITY_PATH: Path = HOME_PATH.joinpath("mobility")
|
||||
XMLS_PATH: Path = HOME_PATH.joinpath("xmls")
|
||||
CONFIG_PATH: Path = HOME_PATH.joinpath("config.yaml")
|
||||
LOG_PATH: Path = HOME_PATH.joinpath("gui.log")
|
||||
SCRIPT_PATH: Path = HOME_PATH.joinpath("scripts")
|
||||
|
||||
# local paths
|
||||
DATA_PATH = Path(__file__).parent.joinpath("data")
|
||||
LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute()
|
||||
LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute()
|
||||
LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute()
|
||||
LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute()
|
||||
DATA_PATH: Path = Path(__file__).parent.joinpath("data")
|
||||
LOCAL_ICONS_PATH: Path = DATA_PATH.joinpath("icons").absolute()
|
||||
LOCAL_BACKGROUND_PATH: Path = DATA_PATH.joinpath("backgrounds").absolute()
|
||||
LOCAL_XMLS_PATH: Path = DATA_PATH.joinpath("xmls").absolute()
|
||||
LOCAL_MOBILITY_PATH: Path = DATA_PATH.joinpath("mobility").absolute()
|
||||
|
||||
# configuration data
|
||||
TERMINALS = [
|
||||
"$TERM",
|
||||
"gnome-terminal --window --",
|
||||
"lxterminal -e",
|
||||
"konsole -e",
|
||||
"xterm -e",
|
||||
"aterm -e",
|
||||
"eterm -e",
|
||||
"rxvt -e",
|
||||
"xfce4-terminal -x",
|
||||
]
|
||||
EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||
TERMINALS: dict[str, str] = {
|
||||
"xterm": "xterm -e",
|
||||
"aterm": "aterm -e",
|
||||
"eterm": "eterm -e",
|
||||
"rxvt": "rxvt -e",
|
||||
"konsole": "konsole -e",
|
||||
"lxterminal": "lxterminal -e",
|
||||
"xfce4-terminal": "xfce4-terminal -x",
|
||||
"gnome-terminal": "gnome-terminal --window --",
|
||||
}
|
||||
EDITORS: list[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||
|
||||
|
||||
class IndentDumper(yaml.Dumper):
|
||||
def increase_indent(self, flow=False, indentless=False):
|
||||
return super().increase_indent(flow, False)
|
||||
def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
|
||||
super().increase_indent(flow, False)
|
||||
|
||||
|
||||
def copy_files(current_path, new_path):
|
||||
class CustomNode(yaml.YAMLObject):
|
||||
yaml_tag: str = "!CustomNode"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(self, name: str, image: str, services: list[str]) -> None:
|
||||
self.name: str = name
|
||||
self.image: str = image
|
||||
self.services: list[str] = services
|
||||
|
||||
|
||||
class CoreServer(yaml.YAMLObject):
|
||||
yaml_tag: str = "!CoreServer"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(self, name: str, address: str) -> None:
|
||||
self.name: str = name
|
||||
self.address: str = address
|
||||
|
||||
|
||||
class Observer(yaml.YAMLObject):
|
||||
yaml_tag: str = "!Observer"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(self, name: str, cmd: str) -> None:
|
||||
self.name: str = name
|
||||
self.cmd: str = cmd
|
||||
|
||||
|
||||
class PreferencesConfig(yaml.YAMLObject):
|
||||
yaml_tag: str = "!PreferencesConfig"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
editor: str = EDITORS[1],
|
||||
terminal: str = None,
|
||||
theme: str = themes.THEME_DARK,
|
||||
gui3d: str = "/usr/local/bin/std3d.sh",
|
||||
width: int = 1000,
|
||||
height: int = 750,
|
||||
) -> None:
|
||||
self.theme: str = theme
|
||||
self.editor: str = editor
|
||||
self.terminal: str = terminal
|
||||
self.gui3d: str = gui3d
|
||||
self.width: int = width
|
||||
self.height: int = height
|
||||
|
||||
|
||||
class LocationConfig(yaml.YAMLObject):
|
||||
yaml_tag: str = "!LocationConfig"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
x: float = 0.0,
|
||||
y: float = 0.0,
|
||||
z: float = 0.0,
|
||||
lat: float = 47.5791667,
|
||||
lon: float = -122.132322,
|
||||
alt: float = 2.0,
|
||||
scale: float = 150.0,
|
||||
) -> None:
|
||||
self.x: float = x
|
||||
self.y: float = y
|
||||
self.z: float = z
|
||||
self.lat: float = lat
|
||||
self.lon: float = lon
|
||||
self.alt: float = alt
|
||||
self.scale: float = scale
|
||||
|
||||
|
||||
class IpConfigs(yaml.YAMLObject):
|
||||
yaml_tag: str = "!IpConfigs"
|
||||
yaml_loader: type[yaml.SafeLoader] = yaml.SafeLoader
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
scale: float = 1.0,
|
||||
ips: IpConfigs = None,
|
||||
mac: str = "00:00:00:aa:00:00",
|
||||
) -> None:
|
||||
if preferences is None:
|
||||
preferences = PreferencesConfig()
|
||||
self.preferences: PreferencesConfig = preferences
|
||||
if location is None:
|
||||
location = LocationConfig()
|
||||
self.location: LocationConfig = location
|
||||
if servers is None:
|
||||
servers = []
|
||||
self.servers: list[CoreServer] = servers
|
||||
if nodes is None:
|
||||
nodes = []
|
||||
self.nodes: list[CustomNode] = nodes
|
||||
if recentfiles is None:
|
||||
recentfiles = []
|
||||
self.recentfiles: list[str] = recentfiles
|
||||
if observers is None:
|
||||
observers = []
|
||||
self.observers: list[Observer] = observers
|
||||
self.scale: float = scale
|
||||
if ips is None:
|
||||
ips = IpConfigs()
|
||||
self.ips: IpConfigs = ips
|
||||
self.mac: str = mac
|
||||
|
||||
|
||||
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)
|
||||
if not new_file.exists():
|
||||
shutil.copy(current_file, new_file)
|
||||
|
||||
|
||||
def check_directory():
|
||||
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()
|
||||
def find_terminal() -> Optional[str]:
|
||||
for term in sorted(TERMINALS):
|
||||
cmd = TERMINALS[term]
|
||||
if shutil.which(term):
|
||||
return cmd
|
||||
return None
|
||||
|
||||
|
||||
def check_directory() -> None:
|
||||
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)
|
||||
|
||||
if "TERM" in os.environ:
|
||||
terminal = TERMINALS[0]
|
||||
else:
|
||||
terminal = TERMINALS[1]
|
||||
if not CONFIG_PATH.exists():
|
||||
terminal = find_terminal()
|
||||
if "EDITOR" in os.environ:
|
||||
editor = EDITORS[0]
|
||||
else:
|
||||
editor = EDITORS[1]
|
||||
config = {
|
||||
"preferences": {
|
||||
"theme": themes.THEME_DARK,
|
||||
"editor": editor,
|
||||
"terminal": terminal,
|
||||
"gui3d": "/usr/local/bin/std3d.sh",
|
||||
"width": 1000,
|
||||
"height": 750,
|
||||
},
|
||||
"location": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"lat": 47.5791667,
|
||||
"lon": -122.132322,
|
||||
"alt": 2.0,
|
||||
"scale": 150.0,
|
||||
},
|
||||
"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}],
|
||||
"nodes": [],
|
||||
"recentfiles": [],
|
||||
"observers": [{"name": "hello", "cmd": "echo hello"}],
|
||||
}
|
||||
preferences = PreferencesConfig(editor, terminal)
|
||||
config = GuiConfig(preferences=preferences)
|
||||
save(config)
|
||||
|
||||
|
||||
def read():
|
||||
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):
|
||||
def save(config: GuiConfig) -> None:
|
||||
with CONFIG_PATH.open("w") as f:
|
||||
yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False)
|
||||
|
|
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/error.png
Normal file
BIN
daemon/core/gui/data/icons/error.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue