diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index 21dd95dc..00537c8e 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -35,7 +35,7 @@ jobs: - name: grpc run: | cd daemon/proto - pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/core.proto + pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto - name: test run: | cd daemon diff --git a/.gitignore b/.gitignore index 12e9577d..bcfbadeb 100644 --- a/.gitignore +++ b/.gitignore @@ -18,8 +18,8 @@ debian stamp-h1 # generated protobuf files -daemon/core/api/grpc/core_pb2.py -daemon/core/api/grpc/core_pb2_grpc.py +*_pb2.py +*_pb2_grpc.py # python build directory dist diff --git a/daemon/MANIFEST.in b/daemon/MANIFEST.in index 40dbefc8..c46dc828 100644 --- a/daemon/MANIFEST.in +++ b/daemon/MANIFEST.in @@ -1 +1,2 @@ graft core/gui/data +graft core/configservices/*/templates diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock index 71c11fd2..b3cacedc 100644 --- a/daemon/Pipfile.lock +++ b/daemon/Pipfile.lock @@ -162,11 +162,11 @@ }, "invoke": { "hashes": [ - "sha256:c52274d2e8a6d64ef0d61093e1983268ea1fc0cd13facb9448c4ef0c9a7ac7da", - "sha256:f4ec8a134c0122ea042c8912529f87652445d9f4de590b353d23f95bfa1f0efd", - "sha256:fc803a5c9052f15e63310aa81a43498d7c55542beb18564db88a9d75a176fa44" + "sha256:4668a4a594a47f2da2f0672ec2f7b1566f809cebf10bcd95ce2de9ecf39b95d1", + "sha256:ae7b4513638bde9afcda0825e9535599637a3f65bd819a27098356027bb17c8a", + "sha256:e04faba8ea7cdf6f5c912be42dcafd5c1074b7f2f306998992c4bfb40a9a690b" ], - "version": "==1.3.0" + "version": "==1.4.0" }, "lxml": { "hashes": [ @@ -199,6 +199,45 @@ ], "version": "==4.4.2" }, + "mako": { + "hashes": [ + "sha256:2984a6733e1d472796ceef37ad48c26f4a984bb18119bb2dbc37a44d8f6e75a4" + ], + "version": "==1.1.1" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, "netaddr": { "hashes": [ "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd", @@ -215,59 +254,53 @@ }, "pillow": { "hashes": [ - "sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031", - "sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71", - "sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c", - "sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340", - "sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa", - "sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b", - "sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573", - "sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e", - "sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab", - "sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9", - "sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e", - "sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291", - "sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12", - "sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871", - "sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281", - "sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08", - "sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41", - "sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2", - "sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5", - "sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb", - "sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547", - "sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75", - "sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9", - "sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1", - "sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a", - "sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96", - "sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132", - "sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a", - "sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5", - "sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0" + "sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be", + "sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946", + "sha256:54ebae163e8412aff0b9df1e88adab65788f5f5b58e625dc5c7f51eaf14a6837", + "sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f", + "sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00", + "sha256:5f7ae9126d16194f114435ebb79cc536b5682002a4fa57fa7bb2cbcde65f2f4d", + "sha256:62a889aeb0a79e50ecf5af272e9e3c164148f4bd9636cc6bcfa182a52c8b0533", + "sha256:7406f5a9b2fd966e79e6abdaf700585a4522e98d6559ce37fc52e5c955fade0a", + "sha256:8453f914f4e5a3d828281a6628cf517832abfa13ff50679a4848926dac7c0358", + "sha256:87269cc6ce1e3dee11f23fa515e4249ae678dbbe2704598a51cee76c52e19cda", + "sha256:875358310ed7abd5320f21dd97351d62de4929b0426cdb1eaa904b64ac36b435", + "sha256:8ac6ce7ff3892e5deaab7abaec763538ffd011f74dc1801d93d3c5fc541feee2", + "sha256:91b710e3353aea6fc758cdb7136d9bbdcb26b53cefe43e2cba953ac3ee1d3313", + "sha256:9d2ba4ed13af381233e2d810ff3bab84ef9f18430a9b336ab69eaf3cd24299ff", + "sha256:a62ec5e13e227399be73303ff301f2865bf68657d15ea50b038d25fc41097317", + "sha256:ab76e5580b0ed647a8d8d2d2daee170e8e9f8aad225ede314f684e297e3643c2", + "sha256:bf4003aa538af3f4205c5fac56eacaa67a6dd81e454ffd9e9f055fff9f1bc614", + "sha256:bf598d2e37cf8edb1a2f26ed3fb255191f5232badea4003c16301cb94ac5bdd0", + "sha256:c18f70dc27cc5d236f10e7834236aff60aadc71346a5bc1f4f83a4b3abee6386", + "sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9", + "sha256:dc058b7833184970d1248135b8b0ab702e6daa833be14035179f2acb78ff5636", + "sha256:ff3797f2f16bf9d17d53257612da84dd0758db33935777149b3334c01ff68865" ], - "version": "==6.2.1" + "version": "==7.0.0" }, "protobuf": { "hashes": [ - "sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd", - "sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed", - "sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057", - "sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce", - "sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03", - "sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46", - "sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33", - "sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c", - "sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9", - "sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef", - "sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b", - "sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d", - "sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8", - "sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6", - "sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941", - "sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13" + "sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37", + "sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36", + "sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4", + "sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d", + "sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574", + "sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0", + "sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5", + "sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f", + "sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946", + "sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104", + "sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e", + "sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7", + "sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a", + "sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507", + "sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1", + "sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593", + "sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c", + "sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1" ], - "version": "==3.11.1" + "version": "==3.11.2" }, "pycparser": { "hashes": [ @@ -303,26 +336,26 @@ }, "pyyaml": { "hashes": [ - "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", - "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", - "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", - "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", - "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", - "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", - "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", - "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", - "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", - "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", - "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" + "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", + "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", + "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", + "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", + "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", + "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", + "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", + "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", + "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", + "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", + "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" ], - "version": "==5.2" + "version": "==5.3" }, "six": { "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" ], - "version": "==1.13.0" + "version": "==1.14.0" } }, "develop": { @@ -483,18 +516,18 @@ }, "identify": { "hashes": [ - "sha256:7782115794ec28b011702815d9f5e532244560cd2bf0789c4f09381d43befd90", - "sha256:9e7521e9abeaede4d2d1092a106e418c65ddf6b3182b43930bcb3c8cfb974488" + "sha256:418f3b2313ac0b531139311a6b426854e9cbdfcfb6175447a5039aa6291d8b30", + "sha256:8ad99ed1f3a965612dcb881435bf58abcfbeb05e230bb8c352b51e8eac103360" ], - "version": "==1.4.8" + "version": "==1.4.10" }, "importlib-metadata": { "hashes": [ - "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", - "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f" + "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359", + "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8" ], "markers": "python_version < '3.8'", - "version": "==1.3.0" + "version": "==1.4.0" }, "importlib-resources": { "hashes": [ @@ -529,23 +562,23 @@ }, "more-itertools": { "hashes": [ - "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", - "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564" + "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39", + "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288" ], - "version": "==8.0.2" + "version": "==8.1.0" }, "nodeenv": { "hashes": [ - "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" + "sha256:561057acd4ae3809e665a9aaaf214afff110bbb6a6d5c8a96121aea6878408b3" ], - "version": "==1.3.3" + "version": "==1.3.4" }, "packaging": { "hashes": [ - "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", - "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" + "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", + "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334" ], - "version": "==19.2" + "version": "==20.1" }, "pluggy": { "hashes": [ @@ -556,39 +589,41 @@ }, "pre-commit": { "hashes": [ - "sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e", - "sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50" + "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850", + "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029" ], "index": "pypi", - "version": "==1.20.0" + "version": "==1.21.0" }, "protobuf": { "hashes": [ - "sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd", - "sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed", - "sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057", - "sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce", - "sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03", - "sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46", - "sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33", - "sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c", - "sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9", - "sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef", - "sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b", - "sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d", - "sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8", - "sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6", - "sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941", - "sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13" + "sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37", + "sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36", + "sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4", + "sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d", + "sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574", + "sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0", + "sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5", + "sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f", + "sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946", + "sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104", + "sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e", + "sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7", + "sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a", + "sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507", + "sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1", + "sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593", + "sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c", + "sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1" ], - "version": "==3.11.1" + "version": "==3.11.2" }, "py": { "hashes": [ - "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", - "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" ], - "version": "==1.8.0" + "version": "==1.8.1" }, "pycodestyle": { "hashes": [ @@ -606,41 +641,41 @@ }, "pyparsing": { "hashes": [ - "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f", - "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a" + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" ], - "version": "==2.4.5" + "version": "==2.4.6" }, "pytest": { "hashes": [ - "sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa", - "sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4" + "sha256:1d122e8be54d1a709e56f82e2d85dcba3018313d64647f38a91aec88c239b600", + "sha256:c13d1943c63e599b98cf118fcb9703e4d7bde7caa9a432567bcdcae4bf512d20" ], "index": "pypi", - "version": "==5.3.2" + "version": "==5.3.4" }, "pyyaml": { "hashes": [ - "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", - "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", - "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", - "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", - "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", - "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", - "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", - "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", - "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", - "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", - "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" + "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", + "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", + "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", + "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", + "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", + "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", + "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", + "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", + "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", + "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", + "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" ], - "version": "==5.2" + "version": "==5.3" }, "six": { "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" ], - "version": "==1.13.0" + "version": "==1.14.0" }, "toml": { "hashes": [ @@ -658,17 +693,17 @@ }, "wcwidth": { "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", + "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" ], - "version": "==0.1.7" + "version": "==0.1.8" }, "zipp": { "hashes": [ - "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", - "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + "sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af", + "sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67" ], - "version": "==0.6.0" + "version": "==2.0.1" } } } diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 07e9d8ef..0f939921 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -11,7 +11,21 @@ import grpc import netaddr from core import utils -from core.api.grpc import core_pb2, core_pb2_grpc +from core.api.grpc import configservices_pb2, core_pb2, core_pb2_grpc +from core.api.grpc.configservices_pb2 import ( + GetConfigServiceDefaultsRequest, + GetConfigServiceDefaultsResponse, + GetConfigServicesRequest, + GetConfigServicesResponse, + GetNodeConfigServiceConfigsRequest, + GetNodeConfigServiceConfigsResponse, + GetNodeConfigServiceRequest, + GetNodeConfigServiceResponse, + GetNodeConfigServicesRequest, + GetNodeConfigServicesResponse, + SetNodeConfigServiceRequest, + SetNodeConfigServiceResponse, +) class InterfaceHelper: @@ -163,6 +177,7 @@ class CoreGrpcClient: service_configs: List[core_pb2.ServiceConfig] = None, service_file_configs: List[core_pb2.ServiceFileConfig] = None, asymmetric_links: List[core_pb2.Link] = None, + config_service_configs: List[configservices_pb2.ConfigServiceConfig] = None, ) -> core_pb2.StartSessionResponse: """ Start a session. @@ -179,6 +194,7 @@ class CoreGrpcClient: :param service_configs: node service configurations :param service_file_configs: node service file configurations :param asymmetric_links: asymmetric links to edit + :param config_service_configs: config service configurations :return: start session response """ request = core_pb2.StartSessionRequest( @@ -194,6 +210,7 @@ class CoreGrpcClient: service_configs=service_configs, service_file_configs=service_file_configs, asymmetric_links=asymmetric_links, + config_service_configs=config_service_configs, ) return self.stub.StartSession(request) @@ -1078,6 +1095,44 @@ class CoreGrpcClient: request = core_pb2.GetInterfacesRequest() return self.stub.GetInterfaces(request) + def get_config_services(self) -> GetConfigServicesResponse: + request = GetConfigServicesRequest() + return self.stub.GetConfigServices(request) + + def get_config_service_defaults( + self, name: str + ) -> GetConfigServiceDefaultsResponse: + request = GetConfigServiceDefaultsRequest(name=name) + return self.stub.GetConfigServiceDefaults(request) + + def get_node_config_service_configs( + self, session_id: int + ) -> GetNodeConfigServiceConfigsResponse: + request = GetNodeConfigServiceConfigsRequest(session_id=session_id) + return self.stub.GetNodeConfigServiceConfigs(request) + + def get_node_config_service( + self, session_id: int, node_id: int, name: str + ) -> GetNodeConfigServiceResponse: + request = GetNodeConfigServiceRequest( + session_id=session_id, node_id=node_id, name=name + ) + return self.stub.GetNodeConfigService(request) + + def get_node_config_services( + self, session_id: int, node_id: int + ) -> GetNodeConfigServicesResponse: + request = GetNodeConfigServicesRequest(session_id=session_id, node_id=node_id) + return self.stub.GetNodeConfigServices(request) + + def set_node_config_service( + self, session_id: int, node_id: int, name: str, config: Dict[str, str] + ) -> SetNodeConfigServiceResponse: + request = SetNodeConfigServiceRequest( + session_id=session_id, node_id=node_id, name=name, config=config + ) + return self.stub.SetNodeConfigService(request) + def connect(self) -> None: """ Open connection to server, must be closed manually. diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index d54cba27..94a9b98c 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -3,7 +3,7 @@ import time from typing import Any, Dict, List, Tuple, Type from core import utils -from core.api.grpc import core_pb2 +from core.api.grpc import common_pb2, core_pb2 from core.config import ConfigurableOptions from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions @@ -33,6 +33,7 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption options.opaque = node_proto.opaque options.image = node_proto.image options.services = node_proto.services + options.config_services = node_proto.config_services if node_proto.emane: options.emane = node_proto.emane if node_proto.server: @@ -190,7 +191,7 @@ def convert_value(value: Any) -> str: def get_config_options( config: Dict[str, str], configurable_options: Type[ConfigurableOptions] -) -> Dict[str, core_pb2.ConfigOption]: +) -> Dict[str, common_pb2.ConfigOption]: """ Retrieve configuration options in a form that is used by the grpc server. @@ -201,7 +202,7 @@ def get_config_options( results = {} for configuration in configurable_options.configurations(): value = config[configuration.id] - config_option = core_pb2.ConfigOption( + config_option = common_pb2.ConfigOption( label=configuration.label, name=configuration.id, value=value, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 3d24b981..3a207ce4 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -5,11 +5,33 @@ import re import tempfile import time from concurrent import futures +from typing import Type import grpc from grpc import ServicerContext -from core.api.grpc import core_pb2, core_pb2_grpc, grpcutils +from core.api.grpc import ( + common_pb2, + configservices_pb2, + core_pb2, + core_pb2_grpc, + grpcutils, +) +from core.api.grpc.configservices_pb2 import ( + ConfigService, + GetConfigServiceDefaultsRequest, + GetConfigServiceDefaultsResponse, + GetConfigServicesRequest, + GetConfigServicesResponse, + GetNodeConfigServiceConfigsRequest, + GetNodeConfigServiceConfigsResponse, + GetNodeConfigServiceRequest, + GetNodeConfigServiceResponse, + GetNodeConfigServicesRequest, + GetNodeConfigServicesResponse, + SetNodeConfigServiceRequest, + SetNodeConfigServiceResponse, +) from core.api.grpc.events import EventStreamer from core.api.grpc.grpcutils import ( get_config_options, @@ -25,7 +47,7 @@ from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.emulator.session import Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility -from core.nodes.base import NodeBase +from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.docker import DockerNode from core.nodes.lxd import LxcNode from core.services.coreservices import ServiceManager @@ -79,6 +101,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: :return: session object that satisfies, if session not found then raise an exception + :raises Exception: raises grpc exception when session does not exist """ session = self.coreemu.sessions.get(session_id) if not session: @@ -95,12 +118,29 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param node_id: node id :param context: :return: node object that satisfies. If node not found then raise an exception. + :raises Exception: raises grpc exception when node does not exist """ try: return session.get_node(node_id) except CoreError: context.abort(grpc.StatusCode.NOT_FOUND, f"node {node_id} not found") + def validate_service( + self, name: str, context: ServicerContext + ) -> Type[ConfigService]: + """ + Validates a configuration service is a valid known service. + + :param name: name of service to validate + :param context: grpc context + :return: class for service to validate + :raises Exception: raises grpc exception when service does not exist + """ + service = self.coreemu.service_manager.services.get(name) + if not service: + context.abort(grpc.StatusCode.NOT_FOUND, f"unknown service {name}") + return service + def StartSession( self, request: core_pb2.StartSessionRequest, context: ServicerContext ) -> core_pb2.StartSessionResponse: @@ -108,7 +148,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): Start a session. :param request: start session request - :param context: grcp context + :param context: grpc context :return: start session response """ logging.debug("start session: %s", request) @@ -157,6 +197,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): for config in request.service_configs: grpcutils.service_configuration(session, config) + # config service configs + for config in request.config_service_configs: + node = self.get_node(session, config.node_id, context) + service = node.config_services[config.name] + if config.config: + service.set_config(config.config) + for name, template in config.templates.items(): + service.set_template(name, template) + # service file configs for config in request.service_file_configs: session.services.set_service_file( @@ -196,7 +245,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): Stop a running session. :param request: stop session request - :param context: grcp context + :param context: grpc context :return: stop session response """ logging.debug("stop session: %s", request) @@ -426,6 +475,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if services is None: services = [] services = [x.name for x in services] + config_services = getattr(node, "config_services", {}) + config_services = [x for x in config_services] emane_model = None if isinstance(node, EmaneNet): emane_model = node.model.name @@ -441,6 +492,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): services=services, icon=node.icon, image=image, + config_services=config_services, ) if isinstance(node, (DockerNode, LxcNode)): node_proto.image = node.image @@ -1429,3 +1481,152 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.EmaneLinkResponse(result=True) else: return core_pb2.EmaneLinkResponse(result=False) + + def GetConfigServices( + self, request: GetConfigServicesRequest, context: ServicerContext + ) -> GetConfigServicesResponse: + """ + Gets all currently known configuration services. + + :param request: get config services request + :param context: grpc context + :return: get config services response + """ + services = [] + for service in self.coreemu.service_manager.services.values(): + service_proto = ConfigService( + name=service.name, + group=service.group, + executables=service.executables, + dependencies=service.dependencies, + directories=service.directories, + files=service.files, + startup=service.startup, + validate=service.validate, + shutdown=service.shutdown, + validation_mode=service.validation_mode.value, + validation_timer=service.validation_timer, + validation_period=service.validation_period, + ) + services.append(service_proto) + return GetConfigServicesResponse(services=services) + + def GetNodeConfigService( + self, request: GetNodeConfigServiceRequest, context: ServicerContext + ) -> GetNodeConfigServiceResponse: + """ + Gets configuration, for a given configuration service, for a given node. + + :param request: get node config service request + :param context: grpc context + :return: get node config service response + """ + session = self.get_session(request.session_id, context) + node = self.get_node(session, request.node_id, context) + self.validate_service(request.name, context) + service = node.config_services.get(request.name) + if service: + config = service.render_config() + else: + service = self.coreemu.service_manager.get_service(request.name) + config = {x.id: x.default for x in service.default_configs} + return GetNodeConfigServiceResponse(config=config) + + def GetConfigServiceDefaults( + self, request: GetConfigServiceDefaultsRequest, context: ServicerContext + ) -> GetConfigServiceDefaultsResponse: + """ + Get default values for a given configuration service. + + :param request: get config service defaults request + :param context: grpc context + :return: get config service defaults response + """ + service_class = self.validate_service(request.name, context) + service = service_class(None) + templates = service.get_templates() + config = {} + for configuration in service.default_configs: + config_option = common_pb2.ConfigOption( + label=configuration.label, + name=configuration.id, + value=configuration.default, + type=configuration.type.value, + select=configuration.options, + group="Settings", + ) + config[configuration.id] = config_option + modes = [] + for name, mode_config in service.modes.items(): + mode = configservices_pb2.ConfigMode(name=name, config=mode_config) + modes.append(mode) + return GetConfigServiceDefaultsResponse( + templates=templates, config=config, modes=modes + ) + + def GetNodeConfigServiceConfigs( + self, request: GetNodeConfigServiceConfigsRequest, context: ServicerContext + ) -> GetNodeConfigServiceConfigsResponse: + """ + Get current custom templates and config for configuration services for a given + node. + + :param request: get node config service configs request + :param context: grpc context + :return: get node config service configs response + """ + session = self.get_session(request.session_id, context) + configs = [] + for node in session.nodes.values(): + if not isinstance(node, CoreNodeBase): + continue + + for name, service in node.config_services.items(): + if not service.custom_templates and not service.custom_config: + continue + config_proto = configservices_pb2.ConfigServiceConfig( + node_id=node.id, + name=name, + templates=service.custom_templates, + config=service.custom_config, + ) + configs.append(config_proto) + return GetNodeConfigServiceConfigsResponse(configs=configs) + + def GetNodeConfigServices( + self, request: GetNodeConfigServicesRequest, context: ServicerContext + ) -> GetNodeConfigServicesResponse: + """ + Get configuration services for a given node. + + :param request: get node config services request + :param context: grpc context + :return: get node config services response + """ + session = self.get_session(request.session_id, context) + node = self.get_node(session, request.node_id, context) + services = node.config_services.keys() + return GetNodeConfigServicesResponse(services=services) + + def SetNodeConfigService( + self, request: SetNodeConfigServiceRequest, context: ServicerContext + ) -> SetNodeConfigServiceResponse: + """ + Set custom config, for a given configuration service, for a given node. + + :param request: set node config service request + :param context: grpc context + :return: set node config service response + """ + session = self.get_session(request.session_id, context) + node = self.get_node(session, request.node_id, context) + self.validate_service(request.name, context) + service = node.config_services.get(request.name) + if service: + service.set_config(request.config) + return SetNodeConfigServiceResponse(result=True) + else: + context.abort( + grpc.StatusCode.NOT_FOUND, + f"node {node.name} missing service {request.name}", + ) diff --git a/Pipfile b/daemon/core/configservice/__init__.py similarity index 100% rename from Pipfile rename to daemon/core/configservice/__init__.py diff --git a/daemon/core/configservice/base.py b/daemon/core/configservice/base.py new file mode 100644 index 00000000..82598988 --- /dev/null +++ b/daemon/core/configservice/base.py @@ -0,0 +1,395 @@ +import abc +import enum +import inspect +import logging +import pathlib +import time +from typing import Any, Dict, List + +from mako import exceptions +from mako.lookup import TemplateLookup +from mako.template import Template + +from core.config import Configuration +from core.errors import CoreCommandError, CoreError +from core.nodes.base import CoreNode + +TEMPLATES_DIR = "templates" + + +class ConfigServiceMode(enum.Enum): + BLOCKING = 0 + NON_BLOCKING = 1 + TIMER = 2 + + +class ConfigServiceBootError(Exception): + pass + + +class ConfigService(abc.ABC): + """ + Base class for creating configurable services. + """ + + # validation period in seconds, how frequent validation is attempted + validation_period = 0.5 + + # time to wait in seconds for determining if service started successfully + validation_timer = 5 + + def __init__(self, node: CoreNode) -> None: + """ + Create ConfigService instance. + + :param node: node this service is assigned to + """ + self.node = node + class_file = inspect.getfile(self.__class__) + templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR) + self.templates = TemplateLookup(directories=templates_path) + self.config = {} + self.custom_templates = {} + self.custom_config = {} + configs = self.default_configs[:] + self._define_config(configs) + + @staticmethod + def clean_text(text: str) -> str: + """ + Returns space stripped text for string literals, while keeping space + indentations. + + :param text: text to clean + :return: cleaned text + """ + return inspect.cleandoc(text) + + @property + @abc.abstractmethod + def name(self) -> str: + raise NotImplementedError + + @property + @abc.abstractmethod + def group(self) -> str: + raise NotImplementedError + + @property + @abc.abstractmethod + def directories(self) -> List[str]: + raise NotImplementedError + + @property + @abc.abstractmethod + def files(self) -> List[str]: + raise NotImplementedError + + @property + @abc.abstractmethod + def default_configs(self) -> List[Configuration]: + raise NotImplementedError + + @property + @abc.abstractmethod + def modes(self) -> Dict[str, Dict[str, str]]: + raise NotImplementedError + + @property + @abc.abstractmethod + def executables(self) -> List[str]: + raise NotImplementedError + + @property + @abc.abstractmethod + def dependencies(self) -> List[str]: + raise NotImplementedError + + @property + @abc.abstractmethod + def startup(self) -> List[str]: + raise NotImplementedError + + @property + @abc.abstractmethod + def validate(self) -> List[str]: + raise NotImplementedError + + @property + @abc.abstractmethod + def shutdown(self) -> List[str]: + raise NotImplementedError + + @property + @abc.abstractmethod + def validation_mode(self) -> ConfigServiceMode: + raise NotImplementedError + + def start(self) -> None: + """ + Creates services files/directories, runs startup, and validates based on + validation mode. + + :return: nothing + :raises ConfigServiceBootError: when there is an error starting service + """ + logging.info("node(%s) service(%s) starting...", self.node.name, self.name) + self.create_dirs() + self.create_files() + wait = self.validation_mode == ConfigServiceMode.BLOCKING + self.run_startup(wait) + if not wait: + if self.validation_mode == ConfigServiceMode.TIMER: + self.wait_validation() + else: + self.run_validation() + + def stop(self) -> None: + """ + Stop service using shutdown commands. + + :return: nothing + """ + for cmd in self.shutdown: + try: + self.node.cmd(cmd) + except CoreCommandError: + logging.exception( + f"node({self.node.name}) service({self.name}) " + f"failed shutdown: {cmd}" + ) + + def restart(self) -> None: + """ + Restarts service by running stop and then start. + + :return: nothing + """ + self.stop() + self.start() + + def create_dirs(self) -> None: + """ + Creates directories for service. + + :return: nothing + :raises CoreError: when there is a failure creating a directory + """ + for directory in self.directories: + try: + self.node.privatedir(directory) + except (CoreCommandError, ValueError): + raise CoreError( + f"node({self.node.name}) service({self.name}) " + f"failure to create service directory: {directory}" + ) + + def data(self) -> Dict[str, Any]: + """ + Returns key/value data, used when rendering file templates. + + :return: key/value template data + """ + return {} + + def set_template(self, name: str, template: str) -> None: + """ + Store custom template to render for a given file. + + :param name: file to store custom template for + :param template: custom template to render + :return: nothing + """ + self.custom_templates[name] = template + + def get_text_template(self, name: str) -> str: + """ + Retrieves text based template for files that do not have a file based template. + + :param name: name of file to get template for + :return: template to render + """ + raise CoreError(f"service({self.name}) unknown template({name})") + + def get_templates(self) -> Dict[str, str]: + """ + Retrieves mapping of file names to templates for all cases, which + includes custom templates, file templates, and text templates. + + :return: mapping of files to templates + """ + templates = {} + for name in self.files: + basename = pathlib.Path(name).name + if name in self.custom_templates: + template = self.custom_templates[name] + template = self.clean_text(template) + elif self.templates.has_template(basename): + template = self.templates.get_template(basename).source + else: + template = self.get_text_template(name) + template = self.clean_text(template) + templates[name] = template + return templates + + def create_files(self) -> None: + """ + Creates service files inside associated node. + + :return: nothing + """ + data = self.data() + for name in self.files: + basename = pathlib.Path(name).name + if name in self.custom_templates: + text = self.custom_templates[name] + rendered = self.render_text(text, data) + elif self.templates.has_template(basename): + rendered = self.render_template(basename, data) + else: + text = self.get_text_template(name) + rendered = self.render_text(text, data) + logging.debug( + "node(%s) service(%s) template(%s): \n%s", + self.node.name, + self.name, + name, + rendered, + ) + self.node.nodefile(name, rendered) + + def run_startup(self, wait: bool) -> None: + """ + Run startup commands for service on node. + + :param wait: wait successful command exit status when True, ignore status + otherwise + :return: nothing + :raises ConfigServiceBootError: when a command that waits fails + """ + for cmd in self.startup: + try: + self.node.cmd(cmd, wait=wait) + except CoreCommandError as e: + raise ConfigServiceBootError( + f"node({self.node.name}) service({self.name}) failed startup: {e}" + ) + + def wait_validation(self) -> None: + """ + Waits for a period of time to consider service started successfully. + + :return: nothing + """ + time.sleep(self.validation_timer) + + def run_validation(self) -> None: + """ + Runs validation commands for service on node. + + :return: nothing + :raises ConfigServiceBootError: if there is a validation failure + """ + start = time.monotonic() + cmds = self.validate[:] + index = 0 + while cmds: + cmd = cmds[index] + try: + self.node.cmd(cmd) + del cmds[index] + index += 1 + except CoreCommandError: + logging.debug( + f"node({self.node.name}) service({self.name}) " + f"validate command failed: {cmd}" + ) + time.sleep(self.validation_period) + + if cmds and time.monotonic() - start > self.validation_timer: + raise ConfigServiceBootError( + f"node({self.node.name}) service({self.name}) failed to validate" + ) + + def _render(self, template: Template, data: Dict[str, Any] = None) -> str: + """ + Renders template providing all associated data to template. + + :param template: template to render + :param data: service specific defined data for template + :return: rendered template + """ + if data is None: + data = {} + return template.render_unicode( + node=self.node, config=self.render_config(), **data + ) + + def render_text(self, text: str, data: Dict[str, Any] = None) -> str: + """ + Renders text based template providing all associated data to template. + + :param text: text to render + :param data: service specific defined data for template + :return: rendered template + """ + text = self.clean_text(text) + try: + template = Template(text) + return self._render(template, data) + except Exception: + raise CoreError( + f"node({self.node.name}) service({self.name}) " + f"{exceptions.text_error_template().render_unicode()}" + ) + + def render_template(self, basename: str, data: Dict[str, Any] = None) -> str: + """ + Renders file based template providing all associated data to template. + + :param basename: base name for file to render + :param data: service specific defined data for template + :return: rendered template + """ + try: + template = self.templates.get_template(basename) + return self._render(template, data) + except Exception: + raise CoreError( + f"node({self.node.name}) service({self.name}) " + f"{exceptions.text_error_template().render_template()}" + ) + + def _define_config(self, configs: List[Configuration]) -> None: + """ + Initializes default configuration data. + + :param configs: configs to initialize + :return: nothing + """ + for config in configs: + self.config[config.id] = config + + def render_config(self) -> Dict[str, str]: + """ + Returns configuration data key/value pairs for rendering a template. + + :return: nothing + """ + if self.custom_config: + return self.custom_config + else: + return {k: v.default for k, v in self.config.items()} + + def set_config(self, data: Dict[str, str]) -> None: + """ + Set configuration data from key/value pairs. + + :param data: configuration key/values to set + :return: nothing + :raise CoreError: when an unknown configuration value is given + """ + for key, value in data.items(): + if key not in self.config: + raise CoreError(f"unknown config: {key}") + self.custom_config[key] = value diff --git a/daemon/core/configservice/dependencies.py b/daemon/core/configservice/dependencies.py new file mode 100644 index 00000000..92eede79 --- /dev/null +++ b/daemon/core/configservice/dependencies.py @@ -0,0 +1,123 @@ +import logging +from typing import TYPE_CHECKING, Dict, List + +if TYPE_CHECKING: + from core.configservice.base import ConfigService + + +class ConfigServiceDependencies: + """ + Generates sets of services to start in order of their dependencies. + """ + + def __init__(self, services: Dict[str, "ConfigService"]) -> None: + """ + Create a ConfigServiceDependencies instance. + + :param services: services for determining dependency sets + """ + # helpers to check validity + self.dependents = {} + self.started = set() + self.node_services = {} + for service in services.values(): + self.node_services[service.name] = service + for dependency in service.dependencies: + dependents = self.dependents.setdefault(dependency, set()) + dependents.add(service.name) + + # used to find paths + self.path = [] + self.visited = set() + self.visiting = set() + + def startup_paths(self) -> List[List["ConfigService"]]: + """ + Find startup path sets based on service dependencies. + + :return: lists of lists of services that can be started in parallel + """ + paths = [] + for name in self.node_services: + service = self.node_services[name] + if service.name in self.started: + logging.debug( + "skipping service that will already be started: %s", service.name + ) + continue + + path = self._start(service) + if path: + paths.append(path) + + if self.started != set(self.node_services): + raise ValueError( + "failure to start all services: %s != %s" + % (self.started, self.node_services.keys()) + ) + + return paths + + def _reset(self) -> None: + """ + Clear out metadata used for finding service dependency sets. + + :return: nothing + """ + self.path = [] + self.visited.clear() + self.visiting.clear() + + def _start(self, service: "ConfigService") -> List["ConfigService"]: + """ + Starts a oath for checking dependencies for a given service. + + :param service: service to check dependencies for + :return: list of config services to start in order + """ + logging.debug("starting service dependency check: %s", service.name) + self._reset() + return self._visit(service) + + def _visit(self, current_service: "ConfigService") -> List["ConfigService"]: + """ + Visits a service when discovering dependency chains for service. + + :param current_service: service being visited + :return: list of dependent services for a visited service + """ + logging.debug("visiting service(%s): %s", current_service.name, self.path) + self.visited.add(current_service.name) + self.visiting.add(current_service.name) + + # dive down + for service_name in current_service.dependencies: + if service_name not in self.node_services: + raise ValueError( + "required dependency was not included in node services: %s" + % service_name + ) + + if service_name in self.visiting: + raise ValueError( + "cyclic dependency at service(%s): %s" + % (current_service.name, service_name) + ) + + if service_name not in self.visited: + service = self.node_services[service_name] + self._visit(service) + + # add service when bottom is found + logging.debug("adding service to startup path: %s", current_service.name) + self.started.add(current_service.name) + self.path.append(current_service) + self.visiting.remove(current_service.name) + + # rise back up + for service_name in self.dependents.get(current_service.name, []): + if service_name not in self.visited: + service = self.node_services[service_name] + self._visit(service) + + return self.path diff --git a/daemon/core/configservice/manager.py b/daemon/core/configservice/manager.py new file mode 100644 index 00000000..1f806f7b --- /dev/null +++ b/daemon/core/configservice/manager.py @@ -0,0 +1,82 @@ +import logging +import pathlib +from typing import List, Type + +from core import utils +from core.configservice.base import ConfigService +from core.errors import CoreError + + +class ConfigServiceManager: + """ + Manager for configurable services. + """ + + def __init__(self): + """ + Create a ConfigServiceManager instance. + """ + self.services = {} + + def get_service(self, name: str) -> Type[ConfigService]: + """ + Retrieve a service by name. + + :param name: name of service + :return: service class + :raises CoreError: when service is not found + """ + service_class = self.services.get(name) + if service_class is None: + raise CoreError(f"service does not exit {name}") + return service_class + + def add(self, service: ConfigService) -> None: + """ + Add service to manager, checking service requirements have been met. + + :param service: service to add to manager + :return: nothing + :raises CoreError: when service is a duplicate or has unmet executables + """ + name = service.name + logging.debug("loading service: class(%s) name(%s)", service.__class__, name) + + # avoid duplicate services + if name in self.services: + raise CoreError(f"duplicate service being added: {name}") + + # validate dependent executables are present + for executable in service.executables: + try: + utils.which(executable, required=True) + except ValueError: + raise CoreError( + f"service({service.name}) missing executable {executable}" + ) + + # make service available + self.services[name] = service + + def load(self, path: str) -> List[str]: + """ + Search path provided for configurable services and add them for being managed. + + :param path: path to search configurable services + :return: list errors when loading and adding services + """ + path = pathlib.Path(path) + subdirs = [x for x in path.iterdir() if x.is_dir()] + subdirs.append(path) + service_errors = [] + for subdir in subdirs: + logging.debug("loading config services from: %s", subdir) + services = utils.load_classes(str(subdir), ConfigService) + for service in services: + logging.debug("found service: %s", service) + try: + self.add(service) + except CoreError as e: + service_errors.append(service.name) + logging.debug("not loading service(%s): %s", service.name, e) + return service_errors diff --git a/daemon/core/configservices/__init__.py b/daemon/core/configservices/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/core/configservices/frrservices/__init__.py b/daemon/core/configservices/frrservices/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py new file mode 100644 index 00000000..c4502f86 --- /dev/null +++ b/daemon/core/configservices/frrservices/services.py @@ -0,0 +1,391 @@ +import abc +from typing import Any, Dict + +import netaddr + +from core import constants +from core.configservice.base import ConfigService, ConfigServiceMode +from core.emane.nodes import EmaneNet +from core.nodes.base import CoreNodeBase +from core.nodes.interface import CoreInterface +from core.nodes.network import WlanNode + +GROUP = "FRR" + + +def has_mtu_mismatch(ifc: CoreInterface) -> bool: + """ + Helper to detect MTU mismatch and add the appropriate FRR + mtu-ignore command. This is needed when e.g. a node is linked via a + GreTap device. + """ + if ifc.mtu != 1500: + return True + if not ifc.net: + return False + for i in ifc.net.netifs(): + if i.mtu != ifc.mtu: + return True + return False + + +def get_min_mtu(ifc): + """ + Helper to discover the minimum MTU of interfaces linked with the + given interface. + """ + mtu = ifc.mtu + if not ifc.net: + return mtu + for i in ifc.net.netifs(): + if i.mtu < mtu: + mtu = i.mtu + return mtu + + +def get_router_id(node: CoreNodeBase) -> str: + """ + Helper to return the first IPv4 address of a node as its router ID. + """ + for ifc in node.netifs(): + if getattr(ifc, "control", False): + continue + for a in ifc.addrlist: + a = a.split("/")[0] + if netaddr.valid_ipv4(a): + return a + return "0.0.0.0" + + +class FRRZebra(ConfigService): + name = "FRRzebra" + group = GROUP + directories = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"] + files = [ + "/usr/local/etc/frr/frr.conf", + "frrboot.sh", + "/usr/local/etc/frr/vtysh.conf", + "/usr/local/etc/frr/daemons", + ] + executables = ["zebra"] + dependencies = [] + startup = ["sh frrboot.sh zebra"] + validate = ["pidof zebra"] + shutdown = ["killall zebra"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + frr_conf = self.files[0] + frr_bin_search = self.node.session.options.get_config( + "frr_bin_search", default="/usr/local/bin /usr/bin /usr/lib/frr" + ).strip('"') + frr_sbin_search = self.node.session.options.get_config( + "frr_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/frr" + ).strip('"') + + services = [] + want_ip4 = False + want_ip6 = False + for service in self.node.config_services.values(): + if self.name not in service.dependencies: + continue + if service.ipv4_routing: + want_ip4 = True + if service.ipv6_routing: + want_ip6 = True + services.append(service) + + interfaces = [] + for ifc in self.node.netifs(): + ip4s = [] + ip6s = [] + for x in ifc.addrlist: + addr = x.split("/")[0] + if netaddr.valid_ipv4(addr): + ip4s.append(x) + else: + ip6s.append(x) + is_control = getattr(ifc, "control", False) + interfaces.append((ifc, ip4s, ip6s, is_control)) + + return dict( + frr_conf=frr_conf, + frr_sbin_search=frr_sbin_search, + frr_bin_search=frr_bin_search, + frr_state_dir=constants.FRR_STATE_DIR, + interfaces=interfaces, + want_ip4=want_ip4, + want_ip6=want_ip6, + services=services, + ) + + +class FrrService(abc.ABC): + group = GROUP + directories = [] + files = [] + executables = [] + dependencies = ["FRRzebra"] + startup = [] + validate = [] + shutdown = [] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + ipv4_routing = False + ipv6_routing = False + + @abc.abstractmethod + def frr_interface_config(self, ifc: CoreInterface) -> str: + raise NotImplementedError + + @abc.abstractmethod + def frr_config(self) -> str: + raise NotImplementedError + + +class FRROspfv2(FrrService, ConfigService): + """ + The OSPFv2 service provides IPv4 routing for wired networks. It does + not build its own configuration file but has hooks for adding to the + unified frr.conf file. + """ + + name = "FRROSPFv2" + startup = () + shutdown = ["killall ospfd"] + validate = ["pidof ospfd"] + ipv4_routing = True + + def frr_config(self) -> str: + router_id = get_router_id(self.node) + addresses = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + for a in ifc.addrlist: + addr = a.split("/")[0] + if netaddr.valid_ipv4(addr): + addresses.append(a) + data = dict(router_id=router_id, addresses=addresses) + text = """ + router ospf + router-id ${router_id} + % for addr in addresses: + network ${addr} area 0 + % endfor + ! + """ + return self.render_text(text, data) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + if has_mtu_mismatch(ifc): + return "ip ospf mtu-ignore" + else: + return "" + + +class FRROspfv3(FrrService, ConfigService): + """ + The OSPFv3 service provides IPv6 routing for wired networks. It does + not build its own configuration file but has hooks for adding to the + unified frr.conf file. + """ + + name = "FRROSPFv3" + shutdown = ["killall ospf6d"] + validate = ["pidof ospf6d"] + ipv4_routing = True + ipv6_routing = True + + def frr_config(self) -> str: + router_id = get_router_id(self.node) + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + data = dict(router_id=router_id, ifnames=ifnames) + text = """ + router ospf6 + router-id ${router_id} + % for ifname in ifnames: + interface ${ifname} area 0.0.0.0 + % endfor + ! + """ + return self.render_text(text, data) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + mtu = get_min_mtu(ifc) + if mtu < ifc.mtu: + return f"ipv6 ospf6 ifmtu {mtu}" + else: + return "" + + +class FRRBgp(FrrService, ConfigService): + """ + The BGP service provides interdomain routing. + Peers must be manually configured, with a full mesh for those + having the same AS number. + """ + + name = "FRRBGP" + shutdown = ["killall bgpd"] + validate = ["pidof bgpd"] + custom_needed = True + ipv4_routing = True + ipv6_routing = True + + def frr_config(self) -> str: + router_id = get_router_id(self.node) + text = f""" + ! BGP configuration + ! You should configure the AS number below + ! along with this router's peers. + router bgp {self.node.id} + bgp router-id {router_id} + redistribute connected + !neighbor 1.2.3.4 remote-as 555 + ! + """ + return self.clean_text(text) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + return "" + + +class FRRRip(FrrService, ConfigService): + """ + The RIP service provides IPv4 routing for wired networks. + """ + + name = "FRRRIP" + shutdown = ["killall ripd"] + validate = ["pidof ripd"] + ipv4_routing = True + + def frr_config(self) -> str: + text = """ + router rip + redistribute static + redistribute connected + redistribute ospf + network 0.0.0.0/0 + ! + """ + return self.clean_text(text) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + return "" + + +class FRRRipng(FrrService, ConfigService): + """ + The RIP NG service provides IPv6 routing for wired networks. + """ + + name = "FRRRIPNG" + shutdown = ["killall ripngd"] + validate = ["pidof ripngd"] + ipv6_routing = True + + def frr_config(self) -> str: + text = """ + router ripng + redistribute static + redistribute connected + redistribute ospf6 + network ::/0 + ! + """ + return self.clean_text(text) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + return "" + + +class FRRBabel(FrrService, ConfigService): + """ + The Babel service provides a loop-avoiding distance-vector routing + protocol for IPv6 and IPv4 with fast convergence properties. + """ + + name = "FRRBabel" + shutdown = ["killall babeld"] + validate = ["pidof babeld"] + ipv6_routing = True + + def frr_config(self) -> str: + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + text = """ + router babel + % for ifname in ifnames: + network ${ifname} + % endfor + redistribute static + redistribute ipv4 connected + ! + """ + data = dict(ifnames=ifnames) + return self.render_text(text, data) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + if isinstance(ifc.net, (WlanNode, EmaneNet)): + text = """ + babel wireless + no babel split-horizon + """ + else: + text = """ + babel wired + babel split-horizon + """ + return self.clean_text(text) + + +class FRRpimd(FrrService, ConfigService): + """ + PIM multicast routing based on XORP. + """ + + name = "FRRpimd" + shutdown = ["killall pimd"] + validate = ["pidof pimd"] + ipv4_routing = True + + def frr_config(self) -> str: + ifname = "eth0" + for ifc in self.node.netifs(): + if ifc.name != "lo": + ifname = ifc.name + break + + text = f""" + router mfea + ! + router igmp + ! + router pim + !ip pim rp-address 10.0.0.1 + ip pim bsr-candidate {ifname} + ip pim rp-candidate {ifname} + !ip pim spt-threshold interval 10 bytes 80000 + ! + """ + return self.clean_text(text) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + text = """ + ip mfea + ip igmp + ip pim + """ + return self.clean_text(text) diff --git a/daemon/core/configservices/frrservices/templates/daemons b/daemon/core/configservices/frrservices/templates/daemons new file mode 100644 index 00000000..0f6bda53 --- /dev/null +++ b/daemon/core/configservices/frrservices/templates/daemons @@ -0,0 +1,59 @@ +# +# When activation a daemon at the first time, a config file, even if it is +# empty, has to be present *and* be owned by the user and group "frr", else +# the daemon will not be started by /etc/init.d/frr. The permissions should +# be u=rw,g=r,o=. +# When using "vtysh" such a config file is also needed. It should be owned by +# group "frrvty" and set to ug=rw,o= though. Check /etc/pam.d/frr, too. +# +# The watchfrr and zebra daemons are always started. +# +bgpd=yes +ospfd=yes +ospf6d=yes +ripd=yes +ripngd=yes +isisd=yes +pimd=yes +ldpd=yes +nhrpd=yes +eigrpd=yes +babeld=yes +sharpd=yes +pbrd=yes +bfdd=yes +fabricd=yes + +# +# If this option is set the /etc/init.d/frr script automatically loads +# the config via "vtysh -b" when the servers are started. +# Check /etc/pam.d/frr if you intend to use "vtysh"! +# +vtysh_enable=yes +zebra_options=" -A 127.0.0.1 -s 90000000" +bgpd_options=" -A 127.0.0.1" +ospfd_options=" -A 127.0.0.1" +ospf6d_options=" -A ::1" +ripd_options=" -A 127.0.0.1" +ripngd_options=" -A ::1" +isisd_options=" -A 127.0.0.1" +pimd_options=" -A 127.0.0.1" +ldpd_options=" -A 127.0.0.1" +nhrpd_options=" -A 127.0.0.1" +eigrpd_options=" -A 127.0.0.1" +babeld_options=" -A 127.0.0.1" +sharpd_options=" -A 127.0.0.1" +pbrd_options=" -A 127.0.0.1" +staticd_options="-A 127.0.0.1" +bfdd_options=" -A 127.0.0.1" +fabricd_options="-A 127.0.0.1" + +# The list of daemons to watch is automatically generated by the init script. +#watchfrr_options="" + +# for debugging purposes, you can specify a "wrap" command to start instead +# of starting the daemon directly, e.g. to use valgrind on ospfd: +# ospfd_wrap="/usr/bin/valgrind" +# or you can use "all_wrap" for all daemons, e.g. to use perf record: +# all_wrap="/usr/bin/perf record --call-graph -" +# the normal daemon command is added to this at the end. diff --git a/daemon/core/configservices/frrservices/templates/frr.conf b/daemon/core/configservices/frrservices/templates/frr.conf new file mode 100644 index 00000000..748c8692 --- /dev/null +++ b/daemon/core/configservices/frrservices/templates/frr.conf @@ -0,0 +1,25 @@ +% for ifc, ip4s, ip6s, is_control in interfaces: +interface ${ifc.name} + % if want_ip4: + % for addr in ip4s: + ip address ${addr} + % endfor + % endif + % if want_ip6: + % for addr in ip6s: + ipv6 address ${addr} + % endfor + % endif + % if not is_control: + % for service in services: + % for line in service.frr_interface_config(ifc).split("\n"): + ${line} + % endfor + % endfor + % endif +! +% endfor + +% for service in services: +${service.frr_config()} +% endfor diff --git a/daemon/core/configservices/frrservices/templates/frrboot.sh b/daemon/core/configservices/frrservices/templates/frrboot.sh new file mode 100644 index 00000000..5a6a0e3d --- /dev/null +++ b/daemon/core/configservices/frrservices/templates/frrboot.sh @@ -0,0 +1,95 @@ +#!/bin/sh +# auto-generated by zebra service (frr.py) +FRR_CONF="${frr_conf}" +FRR_SBIN_SEARCH="${frr_sbin_search}" +FRR_BIN_SEARCH="${frr_bin_search}" +FRR_STATE_DIR="${frr_state_dir}" + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $FRR_CONF` + # if /etc/frr exists, point /etc/frr/frr.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/frr.conf ]; then + ln -s $CONF_DIR/frr.conf /etc/frr/frr.conf + fi + # if /etc/frr exists, point /etc/frr/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/frr/vtysh.conf + fi +} + +bootdaemon() +{ + FRR_SBIN_DIR=$(searchforprog $1 $FRR_SBIN_SEARCH) + if [ "z$FRR_SBIN_DIR" = "z" ]; then + echo "ERROR: FRR's '$1' daemon not found in search path:" + echo " $FRR_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "pimd" ] && \\ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $FRR_CONF; then + flags="$flags -6" + fi + + #force FRR to use CORE generated conf file + flags="$flags -d -f $FRR_CONF" + $FRR_SBIN_DIR/$1 $flags + + if [ "$?" != "0" ]; then + echo "ERROR: FRR's '$1' daemon failed to start!:" + return 1 + fi +} + +bootfrr() +{ + FRR_BIN_DIR=$(searchforprog 'vtysh' $FRR_BIN_SEARCH) + if [ "z$FRR_BIN_DIR" = "z" ]; then + echo "ERROR: FRR's 'vtysh' program not found in search path:" + echo " $FRR_BIN_SEARCH" + return 1 + fi + + # fix /var/run/frr permissions + id -u frr 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown frr $FRR_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \\<$${}{r}\\>" $FRR_CONF; then + bootdaemon "$${}{r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $FRR_CONF; then + bootdaemon "pimd" + fi + + $FRR_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all FRR daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootfrr diff --git a/daemon/core/configservices/frrservices/templates/vtysh.conf b/daemon/core/configservices/frrservices/templates/vtysh.conf new file mode 100644 index 00000000..e0ab9cb6 --- /dev/null +++ b/daemon/core/configservices/frrservices/templates/vtysh.conf @@ -0,0 +1 @@ +service integrated-vtysh-config diff --git a/daemon/core/configservices/nrlservices/__init__.py b/daemon/core/configservices/nrlservices/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/core/configservices/nrlservices/services.py b/daemon/core/configservices/nrlservices/services.py new file mode 100644 index 00000000..3dddf1ba --- /dev/null +++ b/daemon/core/configservices/nrlservices/services.py @@ -0,0 +1,212 @@ +from typing import Any, Dict + +import netaddr + +from core import utils +from core.configservice.base import ConfigService, ConfigServiceMode + +GROUP = "ProtoSvc" + + +class MgenSinkService(ConfigService): + name = "MGEN_Sink" + group = GROUP + directories = [] + files = ["mgensink.sh", "sink.mgen"] + executables = ["mgen"] + dependencies = [] + startup = ["sh mgensink.sh"] + validate = ["pidof mgen"] + shutdown = ["killall mgen"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + ifnames = [] + for ifc in self.node.netifs(): + name = utils.sysctl_devname(ifc.name) + ifnames.append(name) + return dict(ifnames=ifnames) + + +class NrlNhdp(ConfigService): + name = "NHDP" + group = GROUP + directories = [] + files = ["nrlnhdp.sh"] + executables = ["nrlnhdp"] + dependencies = [] + startup = ["sh nrlnhdp.sh"] + validate = ["pidof nrlnhdp"] + shutdown = ["killall nrlnhdp"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + has_smf = "SMF" in self.node.config_services + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + return dict(has_smf=has_smf, ifnames=ifnames) + + +class NrlSmf(ConfigService): + name = "SMF" + group = GROUP + directories = [] + files = ["startsmf.sh"] + executables = ["nrlsmf", "killall"] + dependencies = [] + startup = ["sh startsmf.sh"] + validate = ["pidof nrlsmf"] + shutdown = ["killall nrlsmf"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + has_arouted = "arouted" in self.node.config_services + has_nhdp = "NHDP" in self.node.config_services + has_olsr = "OLSR" in self.node.config_services + ifnames = [] + ip4_prefix = None + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + if ip4_prefix: + continue + for a in ifc.addrlist: + a = a.split("/")[0] + if netaddr.valid_ipv4(a): + ip4_prefix = f"{a}/{24}" + break + return dict( + has_arouted=has_arouted, + has_nhdp=has_nhdp, + has_olsr=has_olsr, + ifnames=ifnames, + ip4_prefix=ip4_prefix, + ) + + +class NrlOlsr(ConfigService): + name = "OLSR" + group = GROUP + directories = [] + files = ["nrlolsrd.sh"] + executables = ["nrlolsrd"] + dependencies = [] + startup = ["sh nrlolsrd.sh"] + validate = ["pidof nrlolsrd"] + shutdown = ["killall nrlolsrd"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + has_smf = "SMF" in self.node.config_services + has_zebra = "zebra" in self.node.config_services + ifname = None + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifname = ifc.name + break + return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname) + + +class NrlOlsrv2(ConfigService): + name = "OLSRv2" + group = GROUP + directories = [] + files = ["nrlolsrv2.sh"] + executables = ["nrlolsrv2"] + dependencies = [] + startup = ["sh nrlolsrv2.sh"] + validate = ["pidof nrlolsrv2"] + shutdown = ["killall nrlolsrv2"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + has_smf = "SMF" in self.node.config_services + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + return dict(has_smf=has_smf, ifnames=ifnames) + + +class OlsrOrg(ConfigService): + name = "OLSRORG" + group = GROUP + directories = ["/etc/olsrd"] + files = ["olsrd.sh", "/etc/olsrd/olsrd.conf"] + executables = ["olsrd"] + dependencies = [] + startup = ["sh olsrd.sh"] + validate = ["pidof olsrd"] + shutdown = ["killall olsrd"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + has_smf = "SMF" in self.node.config_services + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + return dict(has_smf=has_smf, ifnames=ifnames) + + +class MgenActor(ConfigService): + name = "MgenActor" + group = GROUP + directories = [] + files = ["start_mgen_actor.sh"] + executables = ["mgen"] + dependencies = [] + startup = ["sh start_mgen_actor.sh"] + validate = ["pidof mgen"] + shutdown = ["killall mgen"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + +class Arouted(ConfigService): + name = "arouted" + group = GROUP + directories = [] + files = ["startarouted.sh"] + executables = ["arouted"] + dependencies = [] + startup = ["sh startarouted.sh"] + validate = ["pidof arouted"] + shutdown = ["pkill arouted"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + ip4_prefix = None + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + if ip4_prefix: + continue + for a in ifc.addrlist: + a = a.split("/")[0] + if netaddr.valid_ipv4(a): + ip4_prefix = f"{a}/{24}" + break + return dict(ip4_prefix=ip4_prefix) diff --git a/daemon/core/configservices/nrlservices/templates/mgensink.sh b/daemon/core/configservices/nrlservices/templates/mgensink.sh new file mode 100644 index 00000000..bdbd0a8d --- /dev/null +++ b/daemon/core/configservices/nrlservices/templates/mgensink.sh @@ -0,0 +1 @@ +mgen input sink.mgen output mgen_${node.name}.log diff --git a/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh b/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh new file mode 100644 index 00000000..00b7e11d --- /dev/null +++ b/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh @@ -0,0 +1,7 @@ +<% + interfaces = "-i " + " -i ".join(ifnames) + smf = "" + if has_smf: + smf = "-flooding ecds -smfClient %s_smf" % node.name +%> +nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${interfaces} diff --git a/daemon/core/configservices/nrlservices/templates/nrlolsrd.sh b/daemon/core/configservices/nrlservices/templates/nrlolsrd.sh new file mode 100644 index 00000000..4072d189 --- /dev/null +++ b/daemon/core/configservices/nrlservices/templates/nrlolsrd.sh @@ -0,0 +1,9 @@ +<% + smf = "" + if has_smf: + smf = "-flooding s-mpr -smfClient %s_smf" % node.name + zebra = "" + if has_zebra: + zebra = "-z" +%> +nrlolsrd -i ${ifname} -l /var/log/nrlolsrd.log -rpipe ${node.name}_olsr ${smf} ${zebra} diff --git a/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh b/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh new file mode 100644 index 00000000..d7a8d3b6 --- /dev/null +++ b/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh @@ -0,0 +1,7 @@ +<% + interfaces = "-i " + " -i ".join(ifnames) + smf = "" + if has_smf: + smf = "-flooding ecds -smfClient %s_smf" % node.name +%> +nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${interfaces} diff --git a/daemon/core/configservices/nrlservices/templates/olsrd.conf b/daemon/core/configservices/nrlservices/templates/olsrd.conf new file mode 100644 index 00000000..a5716a98 --- /dev/null +++ b/daemon/core/configservices/nrlservices/templates/olsrd.conf @@ -0,0 +1,312 @@ +# +# OLSR.org routing daemon config file +# This file contains the usual options for an ETX based +# stationary network without fisheye +# (for other options see olsrd.conf.default.full) +# +# Lines starting with a # are discarded +# + +#### ATTENTION for IPv6 users #### +# Because of limitations in the parser IPv6 addresses must NOT +# begin with a ":", so please add a "0" as a prefix. + +########################### +### Basic configuration ### +########################### +# keep this settings at the beginning of your first configuration file + +# Debug level (0-9) +# If set to 0 the daemon runs in the background, unless "NoFork" is set to true +# (Default is 1) + +# DebugLevel 1 + +# IP version to use (4 or 6) +# (Default is 4) + +# IpVersion 4 + +################################# +### OLSRd agent configuration ### +################################# +# this parameters control the settings of the routing agent which are not +# related to the OLSR protocol and it's extensions + +# FIBMetric controls the metric value of the host-routes OLSRd sets. +# - "flat" means that the metric value is always 2. This is the preferred value +# because it helps the linux kernel routing to clean up older routes +# - "correct" use the hopcount as the metric value. +# - "approx" use the hopcount as the metric value too, but does only update the +# hopcount if the nexthop changes too +# (Default is "flat") + +# FIBMetric "flat" + +####################################### +### Linux specific OLSRd extensions ### +####################################### +# these parameters are only working on linux at the moment + +# SrcIpRoutes tells OLSRd to set the Src flag of host routes to the originator-ip +# of the node. In addition to this an additional localhost device is created +# to make sure the returning traffic can be received. +# (Default is "no") + +# SrcIpRoutes no + +# Specify the proto tag to be used for routes olsr inserts into kernel +# currently only implemented for linux +# valid values under linux are 1 .. 254 +# 1 gets remapped by olsrd to 0 UNSPECIFIED (1 is reserved for ICMP redirects) +# 2 KERNEL routes (not very wise to use) +# 3 BOOT (should in fact not be used by routing daemons) +# 4 STATIC +# 8 .. 15 various routing daemons (gated, zebra, bird, & co) +# (defaults to 0 which gets replaced by an OS-specific default value +# under linux 3 (BOOT) (for backward compatibility) + +# RtProto 0 + +# Activates (in IPv6 mode) the automatic use of NIIT +# (see README-Olsr-Extensions) +# (default is "yes") + +# UseNiit yes + +# Activates the smartgateway ipip tunnel feature. +# See README-Olsr-Extensions for a description of smartgateways. +# (default is "no") + +# SmartGateway no + +# Signals that the server tunnel must always be removed on shutdown, +# irrespective of the interface up/down state during startup. +# (default is "no") + +# SmartGatewayAlwaysRemoveServerTunnel no + +# Determines the maximum number of gateways that can be in use at any given +# time. This setting is used to mitigate the effects of breaking connections +# (due to the selection of a new gateway) on a dynamic network. +# (default is 1) + +# SmartGatewayUseCount 1 + +# Determines the take-down percentage for a non-current smart gateway tunnel. +# If the cost of the current smart gateway tunnel is less than this percentage +# of the cost of the non-current smart gateway tunnel, then the non-current smart +# gateway tunnel is taken down because it is then presumed to be 'too expensive'. +# This setting is only relevant when SmartGatewayUseCount is larger than 1; +# a value of 0 will result in the tunnels not being taken down proactively. +# (default is 0) + +# SmartGatewayTakeDownPercentage 0 + +# Determines the policy routing script that is executed during startup and +# shutdown of olsrd. The script is only executed when SmartGatewayUseCount +# is set to a value larger than 1. The script must setup policy routing +# rules such that multi-gateway mode works. A sample script is included. +# (default is not set) + +# SmartGatewayPolicyRoutingScript "" + +# Determines the egress interfaces that are part of the multi-gateway setup and +# therefore only relevant when SmartGatewayUseCount is larger than 1 (in which +# case it must be explicitly set). +# (default is not set) + +# SmartGatewayEgressInterfaces "" + +# Determines the routing tables offset for multi-gateway policy routing tables +# See the policy routing script for an explanation. +# (default is 90) + +# SmartGatewayTablesOffset 90 + +# Determines the policy routing rules offset for multi-gateway policy routing +# rules. See the policy routing script for an explanation. +# (default is 0, which indicates that the rules and tables should be aligned and +# puts this value at SmartGatewayTablesOffset - # egress interfaces - +# # olsr interfaces) + +# SmartGatewayRulesOffset 87 + +# Allows the selection of a smartgateway with NAT (only for IPv4) +# (default is "yes") + +# SmartGatewayAllowNAT yes + +# Determines the period (in milliseconds) on which a new smart gateway +# selection is performed. +# (default is 10000 milliseconds) + +# SmartGatewayPeriod 10000 + +# Determines the number of times the link state database must be stable +# before a new smart gateway is selected. +# (default is 6) + +# SmartGatewayStableCount 6 + +# When another gateway than the current one has a cost of less than the cost +# of the current gateway multiplied by SmartGatewayThreshold then the smart +# gateway is switched to the other gateway. The unit is percentage. +# (defaults to 0) + +# SmartGatewayThreshold 0 + +# The weighing factor for the gateway uplink bandwidth (exit link, uplink). +# See README-Olsr-Extensions for a description of smart gateways. +# (default is 1) + +# SmartGatewayWeightExitLinkUp 1 + +# The weighing factor for the gateway downlink bandwidth (exit link, downlink). +# See README-Olsr-Extensions for a description of smart gateways. +# (default is 1) + +# SmartGatewayWeightExitLinkDown 1 + +# The weighing factor for the ETX costs. +# See README-Olsr-Extensions for a description of smart gateways. +# (default is 1) + +# SmartGatewayWeightEtx 1 + +# The divider for the ETX costs. +# See README-Olsr-Extensions for a description of smart gateways. +# (default is 0) + +# SmartGatewayDividerEtx 0 + +# Defines what kind of Uplink this node will publish as a +# smartgateway. The existence of the uplink is detected by +# a route to 0.0.0.0/0, ::ffff:0:0/96 and/or 2000::/3. +# possible values are "none", "ipv4", "ipv6", "both" +# (default is "both") + +# SmartGatewayUplink "both" + +# Specifies if the local ipv4 uplink use NAT +# (default is "yes") + +# SmartGatewayUplinkNAT yes + +# Specifies the speed of the uplink in kilobit/s. +# First parameter is upstream, second parameter is downstream +# (default is 128/1024) + +# SmartGatewaySpeed 128 1024 + +# Specifies the EXTERNAL ipv6 prefix of the uplink. A prefix +# length of more than 64 is not allowed. +# (default is 0::/0 + +# SmartGatewayPrefix 0::/0 + +############################## +### OLSR protocol settings ### +############################## + +# HNA (Host network association) allows the OLSR to announce +# additional IPs or IP subnets to the net that are reachable +# through this node. +# Syntax for HNA4 is "network-address network-mask" +# Syntax for HNA6 is "network-address prefix-length" +# (default is no HNA) +Hna4 +{ +# Internet gateway +# 0.0.0.0 0.0.0.0 +# specific small networks reachable through this node +# 15.15.0.0 255.255.255.0 +} +Hna6 +{ +# Internet gateway +# 0:: 0 +# specific small networks reachable through this node +# fec0:2200:106:0:0:0:0:0 48 +} + +################################ +### OLSR protocol extensions ### +################################ + +# Link quality algorithm (only for lq level 2) +# (see README-Olsr-Extensions) +# - "etx_float", a floating point ETX with exponential aging +# - "etx_fpm", same as ext_float, but with integer arithmetic +# - "etx_ff" (ETX freifunk), an etx variant which use all OLSR +# traffic (instead of only hellos) for ETX calculation +# - "etx_ffeth", an incompatible variant of etx_ff that allows +# ethernet links with ETX 0.1. +# (defaults to "etx_ff") + +# LinkQualityAlgorithm "etx_ff" + +# Fisheye mechanism for TCs (0 meansoff, 1 means on) +# (default is 1) + +LinkQualityFishEye 0 + +##################################### +### Example plugin configurations ### +##################################### +# Olsrd plugins to load +# This must be the absolute path to the file +# or the loader will use the following scheme: +# - Try the paths in the LD_LIBRARY_PATH +# environment variable. +# - The list of libraries cached in /etc/ld.so.cache +# - /lib, followed by /usr/lib +# +# the examples in this list are for linux, so check if the plugin is +# available if you use windows. +# each plugin should have a README file in it's lib subfolder + +# LoadPlugin "olsrd_txtinfo.dll" +#LoadPlugin "olsrd_txtinfo.so.0.1" +#{ + # the default port is 2006 but you can change it like this: + #PlParam "port" "8080" + + # You can set a "accept" single address to allow to connect to + # txtinfo. If no address is specified, then localhost (127.0.0.1) + # is allowed by default. txtinfo will only use the first "accept" + # parameter specified and will ignore the rest. + + # to allow a specific host: + #PlParam "accept" "172.29.44.23" + # if you set it to 0.0.0.0, it will accept all connections + #PlParam "accept" "0.0.0.0" +#} + +############################################# +### OLSRD default interface configuration ### +############################################# +# the default interface section can have the same values as the following +# interface configuration. It will allow you so set common options for all +# interfaces. + +InterfaceDefaults { + Ip4Broadcast 255.255.255.255 +} + +###################################### +### OLSRd Interfaces configuration ### +###################################### +# multiple interfaces can be specified for a single configuration block +# multiple configuration blocks can be specified + +# WARNING, don't forget to insert your interface names here ! +#Interface "" "" +#{ + # Interface Mode is used to prevent unnecessary + # packet forwarding on switched ethernet interfaces + # valid Modes are "mesh" and "ether" + # (default is "mesh") + + # Mode "mesh" +#} diff --git a/daemon/core/configservices/nrlservices/templates/olsrd.sh b/daemon/core/configservices/nrlservices/templates/olsrd.sh new file mode 100644 index 00000000..076f049b --- /dev/null +++ b/daemon/core/configservices/nrlservices/templates/olsrd.sh @@ -0,0 +1,4 @@ +<% + interfaces = "-i " + " -i ".join(ifnames) +%> +olsrd ${interfaces} diff --git a/daemon/core/configservices/nrlservices/templates/sink.mgen b/daemon/core/configservices/nrlservices/templates/sink.mgen new file mode 100644 index 00000000..21d4fde6 --- /dev/null +++ b/daemon/core/configservices/nrlservices/templates/sink.mgen @@ -0,0 +1,4 @@ +0.0 LISTEN UDP 5000 +% for ifname in ifnames: +0.0 Join 224.225.1.2 INTERFACE ${ifname} +% endfor diff --git a/daemon/core/configservices/nrlservices/templates/start_mgen_actor.sh b/daemon/core/configservices/nrlservices/templates/start_mgen_actor.sh new file mode 100644 index 00000000..12630442 --- /dev/null +++ b/daemon/core/configservices/nrlservices/templates/start_mgen_actor.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# auto-generated by MgenActor service +mgenBasicActor.py -n ${node.name} -a 0.0.0.0 < /dev/null > /dev/null 2>&1 & diff --git a/daemon/core/configservices/nrlservices/templates/startarouted.sh b/daemon/core/configservices/nrlservices/templates/startarouted.sh new file mode 100644 index 00000000..20bcc45e --- /dev/null +++ b/daemon/core/configservices/nrlservices/templates/startarouted.sh @@ -0,0 +1,15 @@ +#!/bin/sh +for f in "/tmp/${node.name}_smf"; do + count=1 + until [ -e "$f" ]; do + if [ $count -eq 10 ]; then + echo "ERROR: nrlmsf pipe not found: $f" >&2 + exit 1 + fi + sleep 0.1 + count=$(($count + 1)) + done +done + +ip route add ${ip4_prefix} dev lo +arouted instance ${node.name}_smf tap ${node.name}_tap stability 10 2>&1 > /var/log/arouted.log & diff --git a/daemon/core/configservices/nrlservices/templates/startsmf.sh b/daemon/core/configservices/nrlservices/templates/startsmf.sh new file mode 100644 index 00000000..67fc0fe6 --- /dev/null +++ b/daemon/core/configservices/nrlservices/templates/startsmf.sh @@ -0,0 +1,15 @@ +<% + interfaces = ",".join(ifnames) + arouted = "" + if has_arouted: + arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0]) + if has_nhdp: + flood = "ecds" + elif has_olsr: + flood = "smpr" + else: + flood = "cf" +%> +#!/bin/sh +# auto-generated by NrlSmf service +nrlsmf instance ${node.name}_smf ${interfaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 & diff --git a/daemon/core/configservices/quaggaservices/__init__.py b/daemon/core/configservices/quaggaservices/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py new file mode 100644 index 00000000..32ce99be --- /dev/null +++ b/daemon/core/configservices/quaggaservices/services.py @@ -0,0 +1,424 @@ +import abc +import logging +from typing import Any, Dict + +import netaddr + +from core import constants +from core.configservice.base import ConfigService, ConfigServiceMode +from core.emane.nodes import EmaneNet +from core.nodes.base import CoreNodeBase +from core.nodes.interface import CoreInterface +from core.nodes.network import WlanNode + +GROUP = "Quagga" + + +def has_mtu_mismatch(ifc: CoreInterface) -> bool: + """ + Helper to detect MTU mismatch and add the appropriate OSPF + mtu-ignore command. This is needed when e.g. a node is linked via a + GreTap device. + """ + if ifc.mtu != 1500: + return True + if not ifc.net: + return False + for i in ifc.net.netifs(): + if i.mtu != ifc.mtu: + return True + return False + + +def get_min_mtu(ifc): + """ + Helper to discover the minimum MTU of interfaces linked with the + given interface. + """ + mtu = ifc.mtu + if not ifc.net: + return mtu + for i in ifc.net.netifs(): + if i.mtu < mtu: + mtu = i.mtu + return mtu + + +def get_router_id(node: CoreNodeBase) -> str: + """ + Helper to return the first IPv4 address of a node as its router ID. + """ + for ifc in node.netifs(): + if getattr(ifc, "control", False): + continue + for a in ifc.addrlist: + a = a.split("/")[0] + if netaddr.valid_ipv4(a): + return a + return "0.0.0.0" + + +class Zebra(ConfigService): + name = "zebra" + group = GROUP + directories = ["/usr/local/etc/quagga", "/var/run/quagga"] + files = [ + "/usr/local/etc/quagga/Quagga.conf", + "quaggaboot.sh", + "/usr/local/etc/quagga/vtysh.conf", + ] + executables = ["zebra"] + dependencies = [] + startup = ["sh quaggaboot.sh zebra"] + validate = ["pidof zebra"] + shutdown = ["killall zebra"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + quagga_bin_search = self.node.session.options.get_config( + "quagga_bin_search", default="/usr/local/bin /usr/bin /usr/lib/quagga" + ).strip('"') + quagga_sbin_search = self.node.session.options.get_config( + "quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga" + ).strip('"') + quagga_state_dir = constants.QUAGGA_STATE_DIR + quagga_conf = self.files[0] + + services = [] + want_ip4 = False + want_ip6 = False + for service in self.node.config_services.values(): + if self.name not in service.dependencies: + continue + if service.ipv4_routing: + want_ip4 = True + if service.ipv6_routing: + want_ip6 = True + services.append(service) + + interfaces = [] + for ifc in self.node.netifs(): + ip4s = [] + ip6s = [] + for x in ifc.addrlist: + addr = x.split("/")[0] + if netaddr.valid_ipv4(addr): + ip4s.append(x) + else: + ip6s.append(x) + is_control = getattr(ifc, "control", False) + interfaces.append((ifc, ip4s, ip6s, is_control)) + + return dict( + quagga_bin_search=quagga_bin_search, + quagga_sbin_search=quagga_sbin_search, + quagga_state_dir=quagga_state_dir, + quagga_conf=quagga_conf, + interfaces=interfaces, + want_ip4=want_ip4, + want_ip6=want_ip6, + services=services, + ) + + +class QuaggaService(abc.ABC): + group = GROUP + directories = [] + files = [] + executables = [] + dependencies = ["zebra"] + startup = [] + validate = [] + shutdown = [] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + ipv4_routing = False + ipv6_routing = False + + @abc.abstractmethod + def quagga_interface_config(self, ifc: CoreInterface) -> str: + raise NotImplementedError + + @abc.abstractmethod + def quagga_config(self) -> str: + raise NotImplementedError + + +class Ospfv2(QuaggaService, ConfigService): + """ + The OSPFv2 service provides IPv4 routing for wired networks. It does + not build its own configuration file but has hooks for adding to the + unified Quagga.conf file. + """ + + name = "OSPFv2" + validate = ["pidof ospfd"] + shutdown = ["killall ospfd"] + ipv4_routing = True + + def quagga_interface_config(self, ifc: CoreInterface) -> str: + if has_mtu_mismatch(ifc): + return "ip ospf mtu-ignore" + else: + return "" + + def quagga_config(self) -> str: + router_id = get_router_id(self.node) + addresses = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + for a in ifc.addrlist: + addr = a.split("/")[0] + if netaddr.valid_ipv4(addr): + addresses.append(a) + data = dict(router_id=router_id, addresses=addresses) + text = """ + router ospf + router-id ${router_id} + % for addr in addresses: + network ${addr} area 0 + % endfor + ! + """ + return self.render_text(text, data) + + +class Ospfv3(QuaggaService, ConfigService): + """ + The OSPFv3 service provides IPv6 routing for wired networks. It does + not build its own configuration file but has hooks for adding to the + unified Quagga.conf file. + """ + + name = "OSPFv3" + shutdown = ("killall ospf6d",) + validate = ("pidof ospf6d",) + ipv4_routing = True + ipv6_routing = True + + def quagga_interface_config(self, ifc: CoreInterface) -> str: + mtu = get_min_mtu(ifc) + if mtu < ifc.mtu: + return f"ipv6 ospf6 ifmtu {mtu}" + else: + return "" + + def quagga_config(self) -> str: + router_id = get_router_id(self.node) + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + data = dict(router_id=router_id, ifnames=ifnames) + text = """ + router ospf6 + instance-id 65 + router-id ${router_id} + % for ifname in ifnames: + interface ${ifname} area 0.0.0.0 + % endfor + ! + """ + return self.render_text(text, data) + + +class Ospfv3mdr(Ospfv3): + """ + The OSPFv3 MANET Designated Router (MDR) service provides IPv6 + routing for wireless networks. It does not build its own + configuration file but has hooks for adding to the + unified Quagga.conf file. + """ + + name = "OSPFv3MDR" + + def data(self) -> Dict[str, Any]: + for ifc in self.node.netifs(): + is_wireless = isinstance(ifc.net, (WlanNode, EmaneNet)) + logging.info("MDR wireless: %s", is_wireless) + return dict() + + def quagga_interface_config(self, ifc: CoreInterface) -> str: + config = super().quagga_interface_config(ifc) + if isinstance(ifc.net, (WlanNode, EmaneNet)): + config = self.clean_text( + f""" + {config} + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 twohoprefresh 3 + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa + """ + ) + return config + + +class Bgp(QuaggaService, ConfigService): + """ + The BGP service provides interdomain routing. + Peers must be manually configured, with a full mesh for those + having the same AS number. + """ + + name = "BGP" + shutdown = ["killall bgpd"] + validate = ["pidof bgpd"] + ipv4_routing = True + ipv6_routing = True + + def quagga_config(self) -> str: + return "" + + def quagga_interface_config(self, ifc: CoreInterface) -> str: + router_id = get_router_id(self.node) + text = f""" + ! BGP configuration + ! You should configure the AS number below + ! along with this router's peers. + router bgp {self.node.id} + bgp router-id {router_id} + redistribute connected + !neighbor 1.2.3.4 remote-as 555 + ! + """ + return self.clean_text(text) + + +class Rip(QuaggaService, ConfigService): + """ + The RIP service provides IPv4 routing for wired networks. + """ + + name = "RIP" + shutdown = ["killall ripd"] + validate = ["pidof ripd"] + ipv4_routing = True + + def quagga_config(self) -> str: + text = """ + router rip + redistribute static + redistribute connected + redistribute ospf + network 0.0.0.0/0 + ! + """ + return self.clean_text(text) + + def quagga_interface_config(self, ifc: CoreInterface) -> str: + return "" + + +class Ripng(QuaggaService, ConfigService): + """ + The RIP NG service provides IPv6 routing for wired networks. + """ + + name = "RIPNG" + shutdown = ["killall ripngd"] + validate = ["pidof ripngd"] + ipv6_routing = True + + def quagga_config(self) -> str: + text = """ + router ripng + redistribute static + redistribute connected + redistribute ospf6 + network ::/0 + ! + """ + return self.clean_text(text) + + def quagga_interface_config(self, ifc: CoreInterface) -> str: + return "" + + +class Babel(QuaggaService, ConfigService): + """ + The Babel service provides a loop-avoiding distance-vector routing + protocol for IPv6 and IPv4 with fast convergence properties. + """ + + name = "Babel" + shutdown = ["killall babeld"] + validate = ["pidof babeld"] + ipv6_routing = True + + def quagga_config(self) -> str: + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + text = """ + router babel + % for ifname in ifnames: + network ${ifname} + % endfor + redistribute static + redistribute connected + ! + """ + data = dict(ifnames=ifnames) + return self.render_text(text, data) + + def quagga_interface_config(self, ifc: CoreInterface) -> str: + if isinstance(ifc.net, (WlanNode, EmaneNet)): + text = """ + babel wireless + no babel split-horizon + """ + else: + text = """ + babel wired + babel split-horizon + """ + return self.clean_text(text) + + +class Xpimd(QuaggaService, ConfigService): + """ + PIM multicast routing based on XORP. + """ + + name = "Xpimd" + shutdown = ["killall xpimd"] + validate = ["pidof xpimd"] + ipv4_routing = True + + def quagga_config(self) -> str: + ifname = "eth0" + for ifc in self.node.netifs(): + if ifc.name != "lo": + ifname = ifc.name + break + + text = f""" + router mfea + ! + router igmp + ! + router pim + !ip pim rp-address 10.0.0.1 + ip pim bsr-candidate {ifname} + ip pim rp-candidate {ifname} + !ip pim spt-threshold interval 10 bytes 80000 + ! + """ + return self.clean_text(text) + + def quagga_interface_config(self, ifc: CoreInterface) -> str: + text = """ + ip mfea + ip pim + """ + return self.clean_text(text) diff --git a/daemon/core/configservices/quaggaservices/templates/Quagga.conf b/daemon/core/configservices/quaggaservices/templates/Quagga.conf new file mode 100644 index 00000000..853b1707 --- /dev/null +++ b/daemon/core/configservices/quaggaservices/templates/Quagga.conf @@ -0,0 +1,25 @@ +% for ifc, ip4s, ip6s, is_control in interfaces: +interface ${ifc.name} + % if want_ip4: + % for addr in ip4s: + ip address ${addr} + % endfor + % endif + % if want_ip6: + % for addr in ip6s: + ipv6 address ${addr} + % endfor + % endif + % if not is_control: + % for service in services: + % for line in service.quagga_interface_config(ifc).split("\n"): + ${line} + % endfor + % endfor + % endif +! +% endfor + +% for service in services: +${service.quagga_config()} +% endfor diff --git a/daemon/core/configservices/quaggaservices/templates/quaggaboot.sh b/daemon/core/configservices/quaggaservices/templates/quaggaboot.sh new file mode 100644 index 00000000..c22fdd5f --- /dev/null +++ b/daemon/core/configservices/quaggaservices/templates/quaggaboot.sh @@ -0,0 +1,92 @@ +#!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF="${quagga_conf}" +QUAGGA_SBIN_SEARCH="${quagga_sbin_search}" +QUAGGA_BIN_SEARCH="${quagga_bin_search}" +QUAGGA_STATE_DIR="${quagga_state_dir}" + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \\ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \\<$${}{r}\\>" $QUAGGA_CONF; then + bootdaemon "$${}{r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga diff --git a/daemon/core/configservices/quaggaservices/templates/vtysh.conf b/daemon/core/configservices/quaggaservices/templates/vtysh.conf new file mode 100644 index 00000000..e0ab9cb6 --- /dev/null +++ b/daemon/core/configservices/quaggaservices/templates/vtysh.conf @@ -0,0 +1 @@ +service integrated-vtysh-config diff --git a/daemon/core/configservices/sercurityservices/__init__.py b/daemon/core/configservices/sercurityservices/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/core/configservices/sercurityservices/services.py b/daemon/core/configservices/sercurityservices/services.py new file mode 100644 index 00000000..fca86373 --- /dev/null +++ b/daemon/core/configservices/sercurityservices/services.py @@ -0,0 +1,88 @@ +from typing import Any, Dict + +from core.configservice.base import ConfigService, ConfigServiceMode + +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 = [] + 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 = [] + modes = {} + + +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) diff --git a/daemon/core/configservices/sercurityservices/templates/firewall.sh b/daemon/core/configservices/sercurityservices/templates/firewall.sh new file mode 100644 index 00000000..a445d133 --- /dev/null +++ b/daemon/core/configservices/sercurityservices/templates/firewall.sh @@ -0,0 +1,30 @@ +# -------- CUSTOMIZATION REQUIRED -------- +# +# Below are sample iptables firewall rules that you can uncomment and edit. +# You can also use ip6tables rules for IPv6. +# + +# start by flushing all firewall rules (so this script may be re-run) +#iptables -F + +# allow traffic related to established connections +#iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# allow TCP packets from any source destined for 192.168.1.1 +#iptables -A INPUT -s 0/0 -i eth0 -d 192.168.1.1 -p TCP -j ACCEPT + +# allow OpenVPN server traffic from eth0 +#iptables -A INPUT -p udp --dport 1194 -j ACCEPT +#iptables -A INPUT -i eth0 -j DROP +#iptables -A OUTPUT -p udp --sport 1194 -j ACCEPT +#iptables -A OUTPUT -o eth0 -j DROP + +# allow ICMP ping traffic +#iptables -A OUTPUT -p icmp --icmp-type echo-request -j ACCEPT +#iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT + +# allow SSH traffic +#iptables -A -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT + +# drop all other traffic coming in eth0 +#iptables -A INPUT -i eth0 -j DROP diff --git a/daemon/core/configservices/sercurityservices/templates/ipsec.sh b/daemon/core/configservices/sercurityservices/templates/ipsec.sh new file mode 100644 index 00000000..e8fde77e --- /dev/null +++ b/daemon/core/configservices/sercurityservices/templates/ipsec.sh @@ -0,0 +1,114 @@ +# -------- CUSTOMIZATION REQUIRED -------- +# +# The IPsec service builds ESP tunnels between the specified peers using the +# racoon IKEv2 keying daemon. You need to provide keys and the addresses of +# peers, along with subnets to tunnel. + +# directory containing the certificate and key described below +keydir=/etc/core/keys + +# the name used for the "$certname.pem" x509 certificate and +# "$certname.key" RSA private key, which can be generated using openssl +certname=ipsec1 + +# list the public-facing IP addresses, starting with the localhost and followed +# by each tunnel peer, separated with a single space +tunnelhosts="172.16.0.1AND172.16.0.2 172.16.0.1AND172.16.2.1" + +# Define T where i is the index for each tunnel peer host from +# the tunnel_hosts list above (0 is localhost). +# T is a list of IPsec tunnels with peer i, with a local subnet address +# followed by the remote subnet address: +# T="AND AND" +# For example, 172.16.0.0/24 is a local network (behind this node) to be +# tunneled and 172.16.2.0/24 is a remote network (behind peer 1) +T1="172.16.3.0/24AND172.16.5.0/24" +T2="172.16.4.0/24AND172.16.5.0/24 172.16.4.0/24AND172.16.6.0/24" + +# -------- END CUSTOMIZATION -------- + +echo "building config $PWD/ipsec.conf..." +echo "building config $PWD/ipsec.conf..." > $PWD/ipsec.log + +checkip=0 +if [ "$(dpkg -l | grep " sipcalc ")" = "" ]; then + echo "WARNING: ip validation disabled because package sipcalc not installed + " >> $PWD/ipsec.log + checkip=1 +fi + +echo "#!/usr/sbin/setkey -f + # Flush the SAD and SPD + flush; + spdflush; + + # Security policies " > $PWD/ipsec.conf +i=0 +for hostpair in $tunnelhosts; do + i=`expr $i + 1` + # parse tunnel host IP + thishost=$${}{hostpair%%AND*} + peerhost=$${}{hostpair##*AND} + if [ $checkip = "0" ] && + [ "$(sipcalc "$thishost" "$peerhost" | grep ERR)" != "" ]; then + echo "ERROR: invalid host address $thishost or $peerhost " >> $PWD/ipsec.log + fi + # parse each tunnel addresses + tunnel_list_var_name=T$i + eval tunnels="$"$tunnel_list_var_name"" + for ttunnel in $tunnels; do + lclnet=$${}{ttunnel%%AND*} + rmtnet=$${}{ttunnel##*AND} + if [ $checkip = "0" ] && + [ "$(sipcalc "$lclnet" "$rmtnet"| grep ERR)" != "" ]; then + echo "ERROR: invalid tunnel address $lclnet and $rmtnet " >> $PWD/ipsec.log + fi + # add tunnel policies + echo " + spdadd $lclnet $rmtnet any -P out ipsec + esp/tunnel/$thishost-$peerhost/require; + spdadd $rmtnet $lclnet any -P in ipsec + esp/tunnel/$peerhost-$thishost/require; " >> $PWD/ipsec.conf + done +done + +echo "building config $PWD/racoon.conf..." +if [ ! -e $keydir\/$certname.key ] || [ ! -e $keydir\/$certname.pem ]; then + echo "ERROR: missing certification files under $keydir $certname.key or $certname.pem " >> $PWD/ipsec.log +fi +echo " + path certificate \"$keydir\"; + listen { + adminsock disabled; + } + remote anonymous + { + exchange_mode main; + certificate_type x509 \"$certname.pem\" \"$certname.key\"; + ca_type x509 \"ca-cert.pem\"; + my_identifier asn1dn; + peers_identifier asn1dn; + + proposal { + encryption_algorithm 3des ; + hash_algorithm sha1; + authentication_method rsasig ; + dh_group modp768; + } + } + sainfo anonymous + { + pfs_group modp768; + lifetime time 1 hour ; + encryption_algorithm 3des, blowfish 448, rijndael ; + authentication_algorithm hmac_sha1, hmac_md5 ; + compression_algorithm deflate ; + } + " > $PWD/racoon.conf + +# the setkey program is required from the ipsec-tools package +echo "running setkey -f $PWD/ipsec.conf..." +setkey -f $PWD/ipsec.conf + +echo "running racoon -d -f $PWD/racoon.conf..." +racoon -d -f $PWD/racoon.conf -l racoon.log diff --git a/daemon/core/configservices/sercurityservices/templates/nat.sh b/daemon/core/configservices/sercurityservices/templates/nat.sh new file mode 100644 index 00000000..80b96a08 --- /dev/null +++ b/daemon/core/configservices/sercurityservices/templates/nat.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# generated by security.py +# NAT out the first interface by default +% for index, ifname in enumerate(ifnames): +% if index == 0: +iptables -t nat -A POSTROUTING -o ${ifname} -j MASQUERADE +iptables -A FORWARD -i ${ifname} -m state --state RELATED,ESTABLISHED -j ACCEPT +iptables -A FORWARD -i ${ifname} -j DROP +% else: +# iptables -t nat -A POSTROUTING -o ${ifname} -j MASQUERADE +# iptables -A FORWARD -i ${ifname} -m state --state RELATED,ESTABLISHED -j ACCEPT +# iptables -A FORWARD -i ${ifname} -j DROP +% endif +% endfor diff --git a/daemon/core/configservices/sercurityservices/templates/vpnclient.sh b/daemon/core/configservices/sercurityservices/templates/vpnclient.sh new file mode 100644 index 00000000..9e2a5d10 --- /dev/null +++ b/daemon/core/configservices/sercurityservices/templates/vpnclient.sh @@ -0,0 +1,61 @@ +# -------- CUSTOMIZATION REQUIRED -------- +# +# The VPNClient service builds a VPN tunnel to the specified VPN server using +# OpenVPN software and a virtual TUN/TAP device. + +# directory containing the certificate and key described below +keydir=/etc/core/keys + +# the name used for a "$keyname.crt" certificate and "$keyname.key" private key. +keyname=client1 + +# the public IP address of the VPN server this client should connect with +vpnserver="10.0.2.10" + +# optional next hop for adding a static route to reach the VPN server +nexthop="10.0.1.1" + +# --------- END CUSTOMIZATION -------- + +# validate addresses +if [ "$(dpkg -l | grep " sipcalc ")" = "" ]; then + echo "WARNING: ip validation disabled because package sipcalc not installed + " > $PWD/vpnclient.log +else + if [ "$(sipcalc "$vpnserver" "$nexthop" | grep ERR)" != "" ]; then + echo "ERROR: invalide address $vpnserver or $nexthop " > $PWD/vpnclient.log + fi +fi + +# validate key and certification files +if [ ! -e $keydir\/$keyname.key ] || [ ! -e $keydir\/$keyname.crt ] \ + || [ ! -e $keydir\/ca.crt ] || [ ! -e $keydir\/dh1024.pem ]; then + echo "ERROR: missing certification or key files under $keydir $keyname.key or $keyname.crt or ca.crt or dh1024.pem" >> $PWD/vpnclient.log +fi + +# if necessary, add a static route for reaching the VPN server IP via the IF +vpnservernet=$${}{vpnserver%.*}.0/24 +if [ "$nexthop" != "" ]; then + ip route add $vpnservernet via $nexthop +fi + +# create openvpn client.conf +( +cat << EOF +client +dev tun +proto udp +remote $vpnserver 1194 +nobind +ca $keydir/ca.crt +cert $keydir/$keyname.crt +key $keydir/$keyname.key +dh $keydir/dh1024.pem +cipher AES-256-CBC +log $PWD/openvpn-client.log +verb 4 +daemon +EOF +) > client.conf + +openvpn --config client.conf diff --git a/daemon/core/configservices/sercurityservices/templates/vpnserver.sh b/daemon/core/configservices/sercurityservices/templates/vpnserver.sh new file mode 100644 index 00000000..c46812e2 --- /dev/null +++ b/daemon/core/configservices/sercurityservices/templates/vpnserver.sh @@ -0,0 +1,147 @@ +# -------- CUSTOMIZATION REQUIRED -------- +# +# The VPNServer service sets up the OpenVPN server for building VPN tunnels +# that allow access via TUN/TAP device to private networks. +# +# note that the IPForward and DefaultRoute services should be enabled + +# directory containing the certificate and key described below, in addition to +# a CA certificate and DH key +keydir=/etc/core/keys + +# the name used for a "$keyname.crt" certificate and "$keyname.key" private key. +keyname=server2 + +# the VPN subnet address from which the client VPN IP (for the TUN/TAP) +# will be allocated +vpnsubnet=10.0.200.0 + +# public IP address of this vpn server (same as VPNClient vpnserver= setting) +vpnserver=10.0.2.10 + +# optional list of private subnets reachable behind this VPN server +# each subnet and next hop is separated by a space +# ", , ..." +privatenets="10.0.11.0,10.0.10.1 10.0.12.0,10.0.10.1" + +# optional list of VPN clients, for statically assigning IP addresses to +# clients; also, an optional client subnet can be specified for adding static +# routes via the client +# Note: VPN addresses x.x.x.0-3 are reserved +# ",, ,, ..." +vpnclients="client1KeyFilename,10.0.200.5,10.0.0.0 client2KeyFilename,," + +# NOTE: you may need to enable the StaticRoutes service on nodes within the +# private subnet, in order to have routes back to the client. +# /sbin/ip ro add /24 via +# /sbin/ip ro add /24 via + +# -------- END CUSTOMIZATION -------- + +echo > $PWD/vpnserver.log +rm -f -r $PWD/ccd + +# validate key and certification files +if [ ! -e $keydir\/$keyname.key ] || [ ! -e $keydir\/$keyname.crt ] \ + || [ ! -e $keydir\/ca.crt ] || [ ! -e $keydir\/dh1024.pem ]; then + echo "ERROR: missing certification or key files under $keydir \ +$keyname.key or $keyname.crt or ca.crt or dh1024.pem" >> $PWD/vpnserver.log +fi + +# validate configuration IP addresses +checkip=0 +if [ "$(dpkg -l | grep " sipcalc ")" = "" ]; then + echo "WARNING: ip validation disabled because package sipcalc not installed\ + " >> $PWD/vpnserver.log + checkip=1 +else + if [ "$(sipcalc "$vpnsubnet" "$vpnserver" | grep ERR)" != "" ]; then + echo "ERROR: invalid vpn subnet or server address \ +$vpnsubnet or $vpnserver " >> $PWD/vpnserver.log + fi +fi + +# create client vpn ip pool file +( +cat << EOF +EOF +)> $PWD/ippool.txt + +# create server.conf file +( +cat << EOF +# openvpn server config +local $vpnserver +server $vpnsubnet 255.255.255.0 +push redirect-gateway def1 +EOF +)> $PWD/server.conf + +# add routes to VPN server private subnets, and push these routes to clients +for privatenet in $privatenets; do + if [ $privatenet != "" ]; then + net=$${}{privatenet%%,*} + nexthop=$${}{privatenet##*,} + if [ $checkip = "0" ] && + [ "$(sipcalc "$net" "$nexthop" | grep ERR)" != "" ]; then + echo "ERROR: invalid vpn server private net address \ +$net or $nexthop " >> $PWD/vpnserver.log + fi + echo push route $net 255.255.255.0 >> $PWD/server.conf + ip ro add $net/24 via $nexthop + ip ro add $vpnsubnet/24 via $nexthop + fi +done + +# allow subnet through this VPN, one route for each client subnet +for client in $vpnclients; do + if [ $client != "" ]; then + cSubnetIP=$${}{client##*,} + cVpnIP=$${}{client#*,} + cVpnIP=$${}{cVpnIP%%,*} + cKeyFilename=$${}{client%%,*} + if [ "$cSubnetIP" != "" ]; then + if [ $checkip = "0" ] && + [ "$(sipcalc "$cSubnetIP" "$cVpnIP" | grep ERR)" != "" ]; then + echo "ERROR: invalid vpn client and subnet address \ +$cSubnetIP or $cVpnIP " >> $PWD/vpnserver.log + fi + echo route $cSubnetIP 255.255.255.0 >> $PWD/server.conf + if ! test -d $PWD/ccd; then + mkdir -p $PWD/ccd + echo client-config-dir $PWD/ccd >> $PWD/server.conf + fi + if test -e $PWD/ccd/$cKeyFilename; then + echo iroute $cSubnetIP 255.255.255.0 >> $PWD/ccd/$cKeyFilename + else + echo iroute $cSubnetIP 255.255.255.0 > $PWD/ccd/$cKeyFilename + fi + fi + if [ "$cVpnIP" != "" ]; then + echo $cKeyFilename,$cVpnIP >> $PWD/ippool.txt + fi + fi +done + +( +cat << EOF +keepalive 10 120 +ca $keydir/ca.crt +cert $keydir/$keyname.crt +key $keydir/$keyname.key +dh $keydir/dh1024.pem +cipher AES-256-CBC +status /var/log/openvpn-status.log +log /var/log/openvpn-server.log +ifconfig-pool-linear +ifconfig-pool-persist $PWD/ippool.txt +port 1194 +proto udp +dev tun +verb 4 +daemon +EOF +)>> $PWD/server.conf + +# start vpn server +openvpn --config server.conf diff --git a/daemon/core/configservices/simpleservice.py b/daemon/core/configservices/simpleservice.py new file mode 100644 index 00000000..e727fe82 --- /dev/null +++ b/daemon/core/configservices/simpleservice.py @@ -0,0 +1,47 @@ +from core.config import Configuration +from core.configservice.base import ConfigService, ConfigServiceMode +from core.emulator.enumerations import ConfigDataTypes + + +class SimpleService(ConfigService): + name = "Simple" + group = "SimpleGroup" + directories = ["/etc/quagga", "/usr/local/lib"] + files = ["test1.sh", "test2.sh"] + executables = [] + dependencies = [] + startup = [] + validate = [] + shutdown = [] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [ + Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"), + Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"), + Configuration( + _id="value3", + _type=ConfigDataTypes.STRING, + label="Multiple Choice", + options=["value1", "value2", "value3"], + ), + ] + modes = { + "mode1": {"value1": "value1", "value2": "0", "value3": "value2"}, + "mode2": {"value1": "value2", "value2": "1", "value3": "value3"}, + "mode3": {"value1": "value3", "value2": "0", "value3": "value1"}, + } + + def get_text_template(self, name: str) -> str: + if name == "test1.sh": + return """ + # sample script 1 + # node id(${node.id}) name(${node.name}) + # config: ${config} + echo hello + """ + elif name == "test2.sh": + return """ + # sample script 2 + # node id(${node.id}) name(${node.name}) + # config: ${config} + echo hello2 + """ diff --git a/daemon/core/configservices/utilservices/__init__.py b/daemon/core/configservices/utilservices/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py new file mode 100644 index 00000000..671f12f1 --- /dev/null +++ b/daemon/core/configservices/utilservices/services.py @@ -0,0 +1,302 @@ +import logging +from typing import Any, Dict + +import netaddr + +from core import utils +from core.configservice.base import ConfigService, ConfigServiceMode + +GROUP_NAME = "Utility" + + +class DefaultRouteService(ConfigService): + name = "DefaultRoute" + group = GROUP_NAME + directories = [] + files = ["defaultroute.sh"] + executables = ["ip"] + dependencies = [] + startup = ["sh defaultroute.sh"] + validate = [] + shutdown = [] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + addresses = [] + for netif in self.node.netifs(): + if getattr(netif, "control", False): + continue + for addr in netif.addrlist: + logging.info("default route address: %s", addr) + net = netaddr.IPNetwork(addr) + if net[1] != net[-2]: + addresses.append(net[1]) + return dict(addresses=addresses) + + +class DefaultMulticastRouteService(ConfigService): + name = "DefaultMulticastRoute" + group = GROUP_NAME + directories = [] + files = ["defaultmroute.sh"] + executables = [] + dependencies = [] + startup = ["sh defaultmroute.sh"] + validate = [] + shutdown = [] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + ifname = None + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifname = ifc.name + break + return dict(ifname=ifname) + + +class StaticRouteService(ConfigService): + name = "StaticRoute" + group = GROUP_NAME + directories = [] + files = ["staticroute.sh"] + executables = [] + dependencies = [] + startup = ["sh staticroute.sh"] + validate = [] + shutdown = [] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + routes = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + for x in ifc.addrlist: + addr = x.split("/")[0] + if netaddr.valid_ipv6(addr): + dst = "3ffe:4::/64" + else: + dst = "10.9.8.0/24" + net = netaddr.IPNetwork(x) + if net[-2] != net[1]: + routes.append((dst, net[1])) + return dict(routes=routes) + + +class IpForwardService(ConfigService): + name = "IPForward" + group = GROUP_NAME + directories = [] + files = ["ipforward.sh"] + executables = ["sysctl"] + dependencies = [] + startup = ["sh ipforward.sh"] + validate = [] + shutdown = [] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + devnames = [] + for ifc in self.node.netifs(): + devname = utils.sysctl_devname(ifc.name) + devnames.append(devname) + return dict(devnames=devnames) + + +class SshService(ConfigService): + name = "SSH" + group = GROUP_NAME + directories = ["/etc/ssh", "/var/run/sshd"] + files = ["startsshd.sh", "/etc/ssh/sshd_config"] + executables = ["sshd"] + dependencies = [] + startup = ["sh startsshd.sh"] + validate = [] + shutdown = ["killall sshd"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + return dict( + sshcfgdir=self.directories[0], + sshstatedir=self.directories[1], + sshlibdir="/usr/lib/openssh", + ) + + +class DhcpService(ConfigService): + name = "DHCP" + group = GROUP_NAME + directories = ["/etc/dhcp", "/var/lib/dhcp"] + files = ["/etc/dhcp/dhcpd.conf"] + executables = ["dhcpd"] + dependencies = [] + startup = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"] + validate = ["pidof dhcpd"] + shutdown = ["killall dhcpd"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + subnets = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + for x in ifc.addrlist: + addr = x.split("/")[0] + if netaddr.valid_ipv4(addr): + net = netaddr.IPNetwork(x) + # divide the address space in half + index = (net.size - 2) / 2 + rangelow = net[index] + rangehigh = net[-2] + subnets.append((net.ip, net.netmask, rangelow, rangehigh, addr)) + return dict(subnets=subnets) + + +class DhcpClientService(ConfigService): + name = "DHCPClient" + group = GROUP_NAME + directories = [] + files = ["startdhcpclient.sh"] + executables = ["dhclient"] + dependencies = [] + startup = ["sh startdhcpclient.sh"] + validate = ["pidof dhclient"] + shutdown = ["killall dhclient"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + return dict(ifnames=ifnames) + + +class FtpService(ConfigService): + name = "FTP" + group = GROUP_NAME + directories = ["/var/run/vsftpd/empty", "/var/ftp"] + files = ["vsftpd.conf"] + executables = ["vsftpd"] + dependencies = [] + startup = ["vsftpd ./vsftpd.conf"] + validate = ["pidof vsftpd"] + shutdown = ["killall vsftpd"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + +class PcapService(ConfigService): + name = "pcap" + group = GROUP_NAME + directories = [] + files = ["pcap.sh"] + executables = ["tcpdump"] + dependencies = [] + startup = ["sh pcap.sh start"] + validate = ["pidof tcpdump"] + shutdown = ["sh pcap.sh stop"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + return dict() + + +class RadvdService(ConfigService): + name = "radvd" + group = GROUP_NAME + directories = ["/etc/radvd"] + files = ["/etc/radvd/radvd.conf"] + executables = ["radvd"] + dependencies = [] + startup = ["radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"] + validate = ["pidof radvd"] + shutdown = ["pkill radvd"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + interfaces = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + prefixes = [] + for x in ifc.addrlist: + addr = x.split("/")[0] + if netaddr.valid_ipv6(addr): + prefixes.append(x) + if not prefixes: + continue + interfaces.append((ifc.name, prefixes)) + return dict(interfaces=interfaces) + + +class AtdService(ConfigService): + name = "atd" + group = GROUP_NAME + directories = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"] + files = ["startatd.sh"] + executables = ["atd"] + dependencies = [] + startup = ["sh startatd.sh"] + validate = ["pidof atd"] + shutdown = ["pkill atd"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + +class HttpService(ConfigService): + name = "HTTP" + group = GROUP_NAME + directories = [ + "/etc/apache2", + "/var/run/apache2", + "/var/log/apache2", + "/run/lock", + "/var/lock/apache2", + "/var/www", + ] + files = ["/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html"] + executables = ["apache2ctl"] + dependencies = [] + startup = ["chown www-data /var/lock/apache2", "apache2ctl start"] + validate = ["pidof apache2"] + shutdown = ["apache2ctl stop"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + interfaces = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + interfaces.append(ifc) + return dict(interfaces=interfaces) diff --git a/daemon/core/configservices/utilservices/templates/apache2.conf b/daemon/core/configservices/utilservices/templates/apache2.conf new file mode 100644 index 00000000..c53e48af --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/apache2.conf @@ -0,0 +1,102 @@ +# apache2.conf generated by utility.py:HttpService +Mutex file:$APACHE_LOCK_DIR default + +PidFile $APACHE_PID_FILE +Timeout 300 +KeepAlive On +MaxKeepAliveRequests 100 +KeepAliveTimeout 5 + +LoadModule mpm_worker_module /usr/lib/apache2/modules/mod_mpm_worker.so + + + StartServers 5 + MinSpareServers 5 + MaxSpareServers 10 + MaxClients 150 + MaxRequestsPerChild 0 + + + + StartServers 2 + MinSpareThreads 25 + MaxSpareThreads 75 + ThreadLimit 64 + ThreadsPerChild 25 + MaxClients 150 + MaxRequestsPerChild 0 + + + + StartServers 2 + MinSpareThreads 25 + MaxSpareThreads 75 + ThreadLimit 64 + ThreadsPerChild 25 + MaxClients 150 + MaxRequestsPerChild 0 + + +User $APACHE_RUN_USER +Group $APACHE_RUN_GROUP + +AccessFileName .htaccess + + + Require all denied + + +DefaultType None + +HostnameLookups Off + +ErrorLog $APACHE_LOG_DIR/error.log +LogLevel warn + +#Include mods-enabled/*.load +#Include mods-enabled/*.conf +LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so +LoadModule auth_basic_module /usr/lib/apache2/modules/mod_auth_basic.so +LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so +LoadModule authz_host_module /usr/lib/apache2/modules/mod_authz_host.so +LoadModule authz_user_module /usr/lib/apache2/modules/mod_authz_user.so +LoadModule autoindex_module /usr/lib/apache2/modules/mod_autoindex.so +LoadModule dir_module /usr/lib/apache2/modules/mod_dir.so +LoadModule env_module /usr/lib/apache2/modules/mod_env.so + +NameVirtualHost *:80 +Listen 80 + + + Listen 443 + + + Listen 443 + + +LogFormat "%v:%p %h %l %u %t \\"%r\\" %>s %O \\"%{Referer}i\\" \\"%{User-Agent}i\\"" vhost_combined +LogFormat "%h %l %u %t \\"%r\\" %>s %O \\"%{Referer}i\\" \\"%{User-Agent}i\\"" combined +LogFormat "%h %l %u %t \\"%r\\" %>s %O" common +LogFormat "%{Referer}i -> %U" referer +LogFormat "%{User-agent}i" agent + +ServerTokens OS +ServerSignature On +TraceEnable Off + + + ServerAdmin webmaster@localhost + DocumentRoot /var/www + + Options FollowSymLinks + AllowOverride None + + + Options Indexes FollowSymLinks MultiViews + AllowOverride None + Require all granted + + ErrorLog $APACHE_LOG_DIR/error.log + LogLevel warn + CustomLog $APACHE_LOG_DIR/access.log combined + diff --git a/daemon/core/configservices/utilservices/templates/defaultmroute.sh b/daemon/core/configservices/utilservices/templates/defaultmroute.sh new file mode 100644 index 00000000..4a8d9403 --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/defaultmroute.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# auto-generated by DefaultMulticastRoute service (utility.py) +# the first interface is chosen below; please change it as needed +ip route add 224.0.0.0/4 dev ${ifname} diff --git a/daemon/core/configservices/utilservices/templates/defaultroute.sh b/daemon/core/configservices/utilservices/templates/defaultroute.sh new file mode 100644 index 00000000..879a8861 --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/defaultroute.sh @@ -0,0 +1,5 @@ +#!/bin/sh +# auto-generated by DefaultRoute service +% for address in addresses: +ip route add default via ${address} +% endfor diff --git a/daemon/core/configservices/utilservices/templates/dhcpd.conf b/daemon/core/configservices/utilservices/templates/dhcpd.conf new file mode 100644 index 00000000..7be7f4e8 --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/dhcpd.conf @@ -0,0 +1,22 @@ +# auto-generated by DHCP service (utility.py) +# NOTE: move these option lines into the desired pool { } block(s) below +#option domain-name "test.com"; +#option domain-name-servers 10.0.0.1; +#option routers 10.0.0.1; + +log-facility local6; + +default-lease-time 600; +max-lease-time 7200; + +ddns-update-style none; + +% for subnet, netmask, rangelow, rangehigh, addr in subnets: +subnet ${subnet} netmask ${netmask} { + pool { + range ${rangelow} ${rangehigh}; + default-lease-time 600; + option routers ${addr}; + } +} +% endfor diff --git a/daemon/core/configservices/utilservices/templates/envvars b/daemon/core/configservices/utilservices/templates/envvars new file mode 100644 index 00000000..fcfc4d9e --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/envvars @@ -0,0 +1,10 @@ +# this file is used by apache2ctl - generated by utility.py:HttpService +# these settings come from a default Ubuntu apache2 installation +export APACHE_RUN_USER=www-data +export APACHE_RUN_GROUP=www-data +export APACHE_PID_FILE=/var/run/apache2.pid +export APACHE_RUN_DIR=/var/run/apache2 +export APACHE_LOCK_DIR=/var/lock/apache2 +export APACHE_LOG_DIR=/var/log/apache2 +export LANG=C +export LANG diff --git a/daemon/core/configservices/utilservices/templates/index.html b/daemon/core/configservices/utilservices/templates/index.html new file mode 100644 index 00000000..aaf9d9fa --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/index.html @@ -0,0 +1,13 @@ + + + +

${node.name} web server

+

This is the default web page for this server.

+

The web server software is running but no content has been added, yet.

+
    +% for ifc in interfaces: +
  • ${ifc.name} - ${ifc.addrlist}
  • +% endfor +
+ + diff --git a/daemon/core/configservices/utilservices/templates/ipforward.sh b/daemon/core/configservices/utilservices/templates/ipforward.sh new file mode 100644 index 00000000..a8d3abed --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/ipforward.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# auto-generated by IPForward service (utility.py) +sysctl -w net.ipv4.conf.all.forwarding=1 +sysctl -w net.ipv4.conf.default.forwarding=1 +sysctl -w net.ipv6.conf.all.forwarding=1 +sysctl -w net.ipv6.conf.default.forwarding=1 +sysctl -w net.ipv4.conf.all.send_redirects=0 +sysctl -w net.ipv4.conf.default.send_redirects=0 +sysctl -w net.ipv4.conf.all.rp_filter=0 +sysctl -w net.ipv4.conf.default.rp_filter=0 +# setup forwarding for node interfaces +% for devname in devnames: +sysctl -w net.ipv4.conf.${devname}.forwarding=1 +sysctl -w net.ipv4.conf.${devname}.send_redirects=0 +sysctl -w net.ipv4.conf.${devname}.rp_filter=0 +% endfor diff --git a/daemon/core/configservices/utilservices/templates/pcap.sh b/daemon/core/configservices/utilservices/templates/pcap.sh new file mode 100644 index 00000000..6a099f8c --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/pcap.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# set tcpdump options here (see 'man tcpdump' for help) +# (-s snap length, -C limit pcap file length, -n disable name resolution) +if [ "x$1" = "xstart" ]; then +% for ifname in ifnames: + tcpdump -s 12288 -C 10 -n -w ${node.name}.${ifname}.pcap -i ${ifname} < /dev/null & +% endfor +elif [ "x$1" = "xstop" ]; then + mkdir -p $SESSION_DIR/pcap + mv *.pcap $SESSION_DIR/pcap +fi; diff --git a/daemon/core/configservices/utilservices/templates/radvd.conf b/daemon/core/configservices/utilservices/templates/radvd.conf new file mode 100644 index 00000000..1436f068 --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/radvd.conf @@ -0,0 +1,19 @@ +# auto-generated by RADVD service (utility.py) +% for ifname, prefixes in values: +interface ${ifname} +{ + AdvSendAdvert on; + MinRtrAdvInterval 3; + MaxRtrAdvInterval 10; + AdvDefaultPreference low; + AdvHomeAgentFlag off; +% for prefix in prefixes: + prefix ${prefix} + { + AdvOnLink on; + AdvAutonomous on; + AdvRouterAddr on; + }; +% endfor +}; +% endfor diff --git a/daemon/core/configservices/utilservices/templates/sshd_config b/daemon/core/configservices/utilservices/templates/sshd_config new file mode 100644 index 00000000..826dd098 --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/sshd_config @@ -0,0 +1,37 @@ +# auto-generated by SSH service (utility.py) +Port 22 +Protocol 2 +HostKey ${sshcfgdir}/ssh_host_rsa_key +UsePrivilegeSeparation yes +PidFile ${sshstatedir}/sshd.pid + +KeyRegenerationInterval 3600 +ServerKeyBits 768 + +SyslogFacility AUTH +LogLevel INFO + +LoginGraceTime 120 +PermitRootLogin yes +StrictModes yes + +RSAAuthentication yes +PubkeyAuthentication yes + +IgnoreRhosts yes +RhostsRSAAuthentication no +HostbasedAuthentication no + +PermitEmptyPasswords no +ChallengeResponseAuthentication no + +X11Forwarding yes +X11DisplayOffset 10 +PrintMotd no +PrintLastLog yes +TCPKeepAlive yes + +AcceptEnv LANG LC_* +Subsystem sftp ${sshlibdir}/sftp-server +UsePAM yes +UseDNS no diff --git a/daemon/core/configservices/utilservices/templates/startatd.sh b/daemon/core/configservices/utilservices/templates/startatd.sh new file mode 100644 index 00000000..6d9d2949 --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/startatd.sh @@ -0,0 +1,5 @@ +#!/bin/sh +echo 00001 > /var/spool/cron/atjobs/.SEQ +chown -R daemon /var/spool/cron/* +chmod -R 700 /var/spool/cron/* +atd diff --git a/daemon/core/configservices/utilservices/templates/startdhcpclient.sh b/daemon/core/configservices/utilservices/templates/startdhcpclient.sh new file mode 100644 index 00000000..061e66d7 --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/startdhcpclient.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# auto-generated by DHCPClient service (utility.py) +# uncomment this mkdir line and symlink line to enable client-side DNS\n# resolution based on the DHCP server response. +#mkdir -p /var/run/resolvconf/interface +% for ifname in ifnames: +#ln -s /var/run/resolvconf/interface/${ifname}.dhclient /var/run/resolvconf/resolv.conf +dhclient -nw -pf /var/run/dhclient-${ifname}.pid -lf /var/run/dhclient-${ifname}.lease ${ifname} +% endfor diff --git a/daemon/core/configservices/utilservices/templates/startsshd.sh b/daemon/core/configservices/utilservices/templates/startsshd.sh new file mode 100644 index 00000000..b35fdb07 --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/startsshd.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# auto-generated by SSH service (utility.py) +ssh-keygen -q -t rsa -N "" -f ${sshcfgdir}/ssh_host_rsa_key +chmod 655 ${sshstatedir} +# wait until RSA host key has been generated to launch sshd +$(which sshd) -f ${sshcfgdir}/sshd_config diff --git a/daemon/core/configservices/utilservices/templates/staticroute.sh b/daemon/core/configservices/utilservices/templates/staticroute.sh new file mode 100644 index 00000000..c47c09fd --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/staticroute.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# auto-generated by StaticRoute service (utility.py) +# NOTE: this service must be customized to be of any use +# Below are samples that you can uncomment and edit. +% for dest, addr in routes: +#ip route add ${dest} via ${addr} +% endfor diff --git a/daemon/core/configservices/utilservices/templates/vsftpd.conf b/daemon/core/configservices/utilservices/templates/vsftpd.conf new file mode 100644 index 00000000..988b8727 --- /dev/null +++ b/daemon/core/configservices/utilservices/templates/vsftpd.conf @@ -0,0 +1,12 @@ +# vsftpd.conf auto-generated by FTP service (utility.py) +listen=YES +anonymous_enable=YES +local_enable=YES +dirmessage_enable=YES +use_localtime=YES +xferlog_enable=YES +connect_from_port_20=YES +xferlog_file=/var/log/vsftpd.log +ftpd_banner=Welcome to the CORE FTP service +secure_chroot_dir=/var/run/vsftpd/empty +anon_root=/var/ftp diff --git a/daemon/core/emane/bypass.py b/daemon/core/emane/bypass.py index 24f5d45a..8454d8ce 100644 --- a/daemon/core/emane/bypass.py +++ b/daemon/core/emane/bypass.py @@ -21,7 +21,6 @@ class EmaneBypassModel(emanemodel.EmaneModel): _id="none", _type=ConfigDataTypes.BOOL, default="0", - options=["True", "False"], label="There are no parameters for the bypass model.", ) ] diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 3fe4a40f..4041cf40 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -6,6 +6,8 @@ import sys from typing import Mapping, Type import core.services +from core import configservices +from core.configservice.manager import ConfigServiceManager from core.emulator.session import Session from core.services.coreservices import ServiceManager @@ -55,6 +57,11 @@ class CoreEmu: self.service_errors = [] self.load_services() + # config services + self.service_manager = ConfigServiceManager() + config_services_path = os.path.abspath(os.path.dirname(configservices.__file__)) + self.service_manager.load(config_services_path) + # catch exit event atexit.register(self.shutdown) @@ -97,6 +104,7 @@ class CoreEmu: while _id in self.sessions: _id += 1 session = _cls(_id, config=self.config) + session.service_manager = self.service_manager logging.info("created session: %s", _id) self.sessions[_id] = session return session diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 96eab522..6a0ec8a6 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -75,6 +75,7 @@ class NodeOptions: self.icon = None self.opaque = None self.services = [] + self.config_services = [] self.x = None self.y = None self.lat = None diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 99c29f1e..a686ce9e 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -161,6 +161,9 @@ class Session: "host": ("DefaultRoute", "SSH"), } + # config services + self.service_manager = None + @classmethod def get_node_class(cls, _type: NodeTypes) -> Type[NodeBase]: """ @@ -726,6 +729,12 @@ class Session: logging.debug("set node type: %s", node.type) self.services.add_services(node, node.type, options.services) + # add config services + logging.info("setting node config services: %s", options.config_services) + for name in options.config_services: + service_class = self.service_manager.get_service(name) + node.add_config_service(service_class) + # ensure default emane configuration if isinstance(node, EmaneNet) and options.emane: self.emane.set_model_config(_id, options.emane) @@ -1602,6 +1611,7 @@ class Session: logging.info("booting node(%s): %s", node.name, [x.name for x in node.services]) self.add_remove_control_interface(node=node, remove=False) self.services.boot_services(node) + node.start_config_services() def boot_nodes(self) -> List[Exception]: """ diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index 38322efd..5f2d5916 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -36,21 +36,18 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): _id="enablerj45", _type=ConfigDataTypes.BOOL, default="1", - options=["On", "Off"], label="Enable RJ45s", ), Configuration( _id="preservedir", _type=ConfigDataTypes.BOOL, default="0", - options=["On", "Off"], label="Preserve session dir", ), Configuration( _id="enablesdt", _type=ConfigDataTypes.BOOL, default="0", - options=["On", "Off"], label="Enable SDT3D output", ), Configuration( diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index e4fd5fac..a84c99b2 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Dict, List import grpc -from core.api.grpc import client, core_pb2 +from core.api.grpc import client, common_pb2, configservices_pb2, core_pb2 from core.gui import appconfig from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.sessions import SessionsDialog @@ -74,6 +74,8 @@ class CoreClient: self.app = app self.master = app.master self.services = {} + self.config_services_groups = {} + self.config_services = {} self.default_services = {} self.emane_models = [] self.observer = None @@ -99,6 +101,7 @@ class CoreClient: self.emane_model_configs = {} self.emane_config = None self.service_configs = {} + self.config_service_configs = {} self.file_configs = {} self.mobility_players = {} self.handling_throughputs = None @@ -307,6 +310,18 @@ class CoreClient: data = config.files[file_name] files[file_name] = data + # get config service configurations + response = self.client.get_node_config_service_configs(self.session_id) + for config in response.configs: + node_configs = self.config_service_configs.setdefault( + config.node_id, {} + ) + service_config = node_configs.setdefault(config.name, {}) + if config.templates: + service_config["templates"] = config.templates + if config.config: + service_config["config"] = config.config + # draw session self.app.canvas.reset_and_redraw(session) @@ -427,6 +442,15 @@ class CoreClient: group_services = self.services.setdefault(service.group, set()) group_services.add(service.name) + # get config service informations + response = self.client.get_config_services() + for service in response.services: + self.config_services[service.name] = service + group_services = self.config_services_groups.setdefault( + service.group, set() + ) + group_services.add(service.name) + # if there are no sessions, create a new session, else join a session response = self.client.get_sessions() logging.info("current sessions: %s", response) @@ -464,6 +488,7 @@ class CoreClient: asymmetric_links = [ x.asymmetric_link for x in self.links.values() if x.asymmetric_link ] + config_service_configs = self.get_config_service_configs_proto() if self.emane_config: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: @@ -484,6 +509,7 @@ class CoreClient: service_configs, file_configs, asymmetric_links, + config_service_configs, ) logging.debug( "start session(%s), result: %s", self.session_id, response.result @@ -878,18 +904,34 @@ class CoreClient: configs.append(config_proto) return configs + def get_config_service_configs_proto( + self + ) -> List[configservices_pb2.ConfigServiceConfig]: + config_service_protos = [] + for node_id, node_config in self.config_service_configs.items(): + for name, service_config in node_config.items(): + config = service_config.get("config", {}) + config_proto = configservices_pb2.ConfigServiceConfig( + node_id=node_id, + name=name, + templates=service_config["templates"], + config=config, + ) + config_service_protos.append(config_proto) + return config_service_protos + def run(self, node_id: int) -> str: logging.info("running node(%s) cmd: %s", node_id, self.observer) return self.client.node_command(self.session_id, node_id, self.observer).output - def get_wlan_config(self, node_id: int) -> Dict[str, core_pb2.ConfigOption]: + def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]: config = self.wlan_configs.get(node_id) if not config: response = self.client.get_wlan_config(self.session_id, node_id) config = response.config return config - def get_mobility_config(self, node_id: int) -> Dict[str, core_pb2.ConfigOption]: + def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]: config = self.mobility_configs.get(node_id) if not config: response = self.client.get_mobility_config(self.session_id, node_id) @@ -898,7 +940,7 @@ class CoreClient: def get_emane_model_config( self, node_id: int, model: str, interface: int = None - ) -> Dict[str, core_pb2.ConfigOption]: + ) -> Dict[str, common_pb2.ConfigOption]: logging.info("getting emane model config: %s %s %s", node_id, model, interface) config = self.emane_model_configs.get((node_id, model, interface)) if not config: @@ -914,7 +956,7 @@ class CoreClient: self, node_id: int, model: str, - config: Dict[str, core_pb2.ConfigOption], + config: Dict[str, common_pb2.ConfigOption], interface: int = None, ): logging.info("setting emane model config: %s %s %s", node_id, model, interface) diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py new file mode 100644 index 00000000..f92d23bb --- /dev/null +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -0,0 +1,381 @@ +""" +Service configuration dialog +""" +import logging +import tkinter as tk +from tkinter import ttk +from typing import TYPE_CHECKING, Any, List + +import grpc + +from core.api.grpc import core_pb2 +from core.gui.dialogs.dialog import Dialog +from core.gui.errors import show_grpc_error +from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll + +if TYPE_CHECKING: + from core.gui.app import Application + + +class ConfigServiceConfigDialog(Dialog): + def __init__( + self, master: Any, app: "Application", service_name: str, node_id: int + ): + title = f"{service_name} Config Service" + super().__init__(master, app, title, modal=True) + self.master = master + self.app = app + self.core = app.core + self.node_id = node_id + self.service_name = service_name + self.service_configs = app.core.config_service_configs + + self.radiovar = tk.IntVar() + self.radiovar.set(2) + self.directories = [] + self.templates = [] + self.dependencies = [] + self.executables = [] + self.startup_commands = [] + self.validation_commands = [] + self.shutdown_commands = [] + self.default_startup = [] + self.default_validate = [] + self.default_shutdown = [] + self.validation_mode = None + self.validation_time = None + self.validation_period = tk.StringVar() + self.modes = [] + self.mode_configs = {} + + self.notebook = None + self.templates_combobox = None + self.modes_combobox = None + self.startup_commands_listbox = None + self.shutdown_commands_listbox = None + self.validate_commands_listbox = None + self.validation_time_entry = None + self.validation_mode_entry = None + self.template_text = None + self.validation_period_entry = None + self.original_service_files = {} + self.temp_service_files = {} + self.modified_files = set() + self.config_frame = None + self.default_config = None + self.config = None + self.load() + self.draw() + + def load(self): + try: + self.core.create_nodes_and_links() + service = self.core.config_services[self.service_name] + self.dependencies = service.dependencies[:] + self.executables = service.executables[:] + self.directories = service.directories[:] + self.templates = service.files[:] + self.startup_commands = service.startup[:] + self.validation_commands = service.validate[:] + self.shutdown_commands = service.shutdown[:] + self.validation_mode = service.validation_mode + self.validation_time = service.validation_timer + self.validation_period.set(service.validation_period) + + response = self.core.client.get_config_service_defaults(self.service_name) + self.original_service_files = response.templates + self.temp_service_files = dict(self.original_service_files) + + self.modes = sorted(x.name for x in response.modes) + self.mode_configs = {x.name: x.config for x in response.modes} + + node_configs = self.service_configs.get(self.node_id, {}) + service_config = node_configs.get(self.service_name, {}) + + self.config = response.config + self.default_config = {x.name: x.value for x in self.config.values()} + custom_config = service_config.get("config") + if custom_config: + for key, value in custom_config.items(): + self.config[key].value = value + logging.info("default config: %s", self.default_config) + + custom_templates = service_config.get("templates", {}) + for file, data in custom_templates.items(): + self.modified_files.add(file) + self.temp_service_files[file] = data + except grpc.RpcError as e: + show_grpc_error(e) + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + # draw notebook + self.notebook = ttk.Notebook(self.top) + self.notebook.grid(sticky="nsew", pady=PADY) + self.draw_tab_files() + if self.config: + self.draw_tab_config() + self.draw_tab_startstop() + self.draw_tab_validation() + self.draw_buttons() + + def draw_tab_files(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Directories/Files") + + label = ttk.Label( + tab, text="Directories and templates that will be used for this service." + ) + label.grid(pady=PADY) + + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Directories") + label.grid(row=0, column=0, sticky="w", padx=PADX) + directories_combobox = ttk.Combobox( + frame, values=self.directories, state="readonly" + ) + directories_combobox.grid(row=0, column=1, sticky="ew", pady=PADY) + if self.directories: + directories_combobox.current(0) + + label = ttk.Label(frame, text="Templates") + label.grid(row=1, column=0, sticky="w", padx=PADX) + self.templates_combobox = ttk.Combobox( + frame, values=self.templates, state="readonly" + ) + self.templates_combobox.bind( + "<>", self.handle_template_changed + ) + self.templates_combobox.grid(row=1, column=1, sticky="ew", pady=PADY) + + self.template_text = CodeText(tab) + self.template_text.grid(sticky="nsew") + tab.rowconfigure(self.template_text.grid_info()["row"], weight=1) + if self.templates: + self.templates_combobox.current(0) + self.template_text.text.delete(1.0, "end") + self.template_text.text.insert( + "end", self.temp_service_files[self.templates[0]] + ) + self.template_text.text.bind("", self.update_template_file_data) + + def draw_tab_config(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Configuration") + + if self.modes: + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Modes") + label.grid(row=0, column=0, padx=PADX) + self.modes_combobox = ttk.Combobox( + frame, values=self.modes, state="readonly" + ) + self.modes_combobox.bind("<>", self.handle_mode_changed) + self.modes_combobox.grid(row=0, column=1, sticky="ew", pady=PADY) + + logging.info("config service config: %s", self.config) + self.config_frame = ConfigFrame(tab, self.app, self.config) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PADY) + tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1) + + def draw_tab_startstop(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + for i in range(3): + tab.rowconfigure(i, weight=1) + self.notebook.add(tab, text="Startup/Shutdown") + commands = [] + # tab 3 + for i in range(3): + label_frame = None + if i == 0: + label_frame = ttk.LabelFrame( + tab, text="Startup Commands", padding=FRAME_PAD + ) + commands = self.startup_commands + elif i == 1: + label_frame = ttk.LabelFrame( + tab, text="Shutdown Commands", padding=FRAME_PAD + ) + commands = self.shutdown_commands + elif i == 2: + label_frame = ttk.LabelFrame( + tab, text="Validation Commands", padding=FRAME_PAD + ) + commands = self.validation_commands + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY) + listbox_scroll = ListboxScroll(label_frame) + for command in commands: + listbox_scroll.listbox.insert("end", command) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(sticky="nsew") + if i == 0: + self.startup_commands_listbox = listbox_scroll.listbox + elif i == 1: + self.shutdown_commands_listbox = listbox_scroll.listbox + elif i == 2: + self.validate_commands_listbox = listbox_scroll.listbox + + def draw_tab_validation(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="ew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Validation", sticky="nsew") + + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + + label = ttk.Label(frame, text="Validation Time") + label.grid(row=0, column=0, sticky="w", padx=PADX) + self.validation_time_entry = ttk.Entry(frame) + self.validation_time_entry.insert("end", self.validation_time) + self.validation_time_entry.config(state=tk.DISABLED) + self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY) + + label = ttk.Label(frame, text="Validation Mode") + label.grid(row=1, column=0, sticky="w", padx=PADX) + if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: + mode = "BLOCKING" + elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING: + mode = "NON_BLOCKING" + else: + mode = "TIMER" + self.validation_mode_entry = ttk.Entry( + frame, textvariable=tk.StringVar(value=mode) + ) + self.validation_mode_entry.insert("end", mode) + self.validation_mode_entry.config(state=tk.DISABLED) + self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY) + + label = ttk.Label(frame, text="Validation Period") + label.grid(row=2, column=0, sticky="w", padx=PADX) + self.validation_period_entry = ttk.Entry( + frame, state=tk.DISABLED, textvariable=self.validation_period + ) + self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY) + + label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD) + label_frame.grid(sticky="nsew", pady=PADY) + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.grid(sticky="nsew") + tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) + for executable in self.executables: + listbox_scroll.listbox.insert("end", executable) + + label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD) + label_frame.grid(sticky="nsew", pady=PADY) + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.grid(sticky="nsew") + tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) + for dependency in self.dependencies: + listbox_scroll.listbox.insert("end", dependency) + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(4): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Defaults", command=self.click_defaults) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Copy...", command=self.click_copy) + button.grid(row=0, column=2, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=3, sticky="ew") + + def click_apply(self): + current_listbox = self.master.current.listbox + if not self.is_custom(): + if self.node_id in self.service_configs: + self.service_configs[self.node_id].pop(self.service_name, None) + current_listbox.itemconfig(current_listbox.curselection()[0], bg="") + self.destroy() + return + + try: + node_config = self.service_configs.setdefault(self.node_id, {}) + service_config = node_config.setdefault(self.service_name, {}) + if self.config_frame: + self.config_frame.parse_config() + service_config["config"] = { + x.name: x.value for x in self.config.values() + } + templates_config = service_config.setdefault("templates", {}) + for file in self.modified_files: + templates_config[file] = self.temp_service_files[file] + all_current = current_listbox.get(0, tk.END) + current_listbox.itemconfig(all_current.index(self.service_name), bg="green") + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + + def handle_template_changed(self, event: tk.Event): + template = self.templates_combobox.get() + self.template_text.text.delete(1.0, "end") + self.template_text.text.insert("end", self.temp_service_files[template]) + + def handle_mode_changed(self, event: tk.Event): + mode = self.modes_combobox.get() + config = self.mode_configs[mode] + logging.info("mode config: %s", config) + self.config_frame.set_values(config) + + def update_template_file_data(self, event: tk.Event): + scrolledtext = event.widget + template = self.templates_combobox.get() + self.temp_service_files[template] = scrolledtext.get(1.0, "end") + if self.temp_service_files[template] != self.original_service_files[template]: + self.modified_files.add(template) + else: + self.modified_files.discard(template) + + def is_custom(self): + has_custom_templates = len(self.modified_files) > 0 + has_custom_config = False + if self.config_frame: + current = self.config_frame.parse_config() + has_custom_config = self.default_config != current + return has_custom_templates or has_custom_config + + def click_defaults(self): + if self.node_id in self.service_configs: + node_config = self.service_configs.get(self.node_id, {}) + node_config.pop(self.service_name, None) + self.temp_service_files = dict(self.original_service_files) + filename = self.templates_combobox.get() + self.template_text.text.delete(1.0, "end") + self.template_text.text.insert("end", self.temp_service_files[filename]) + if self.config_frame: + logging.info("resetting defaults: %s", self.default_config) + self.config_frame.set_values(self.default_config) + + def click_copy(self): + pass + + def append_commands( + self, commands: List[str], listbox: tk.Listbox, to_add: List[str] + ): + for cmd in to_add: + commands.append(cmd) + listbox.insert(tk.END, cmd) diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py new file mode 100644 index 00000000..1230cede --- /dev/null +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -0,0 +1,161 @@ +""" +core node services +""" +import logging +import tkinter as tk +from tkinter import messagebox, ttk +from typing import TYPE_CHECKING, Any, Set + +from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.widgets import CheckboxList, ListboxScroll + +if TYPE_CHECKING: + from core.gui.app import Application + from core.gui.graph.node import CanvasNode + + +class NodeConfigServiceDialog(Dialog): + def __init__( + self, + master: Any, + app: "Application", + canvas_node: "CanvasNode", + services: Set[str] = None, + ): + title = f"{canvas_node.core_node.name} Config Services" + super().__init__(master, app, title, modal=True) + self.app = app + self.canvas_node = canvas_node + self.node_id = canvas_node.core_node.id + self.groups = None + self.services = None + self.current = None + if services is None: + services = set(canvas_node.core_node.config_services) + self.current_services = services + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + frame = ttk.Frame(self.top) + frame.grid(stick="nsew", pady=PADY) + frame.rowconfigure(0, weight=1) + for i in range(3): + frame.columnconfigure(i, weight=1) + label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) + label_frame.grid(row=0, column=0, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.groups = ListboxScroll(label_frame) + self.groups.grid(sticky="nsew") + for group in sorted(self.app.core.config_services_groups): + self.groups.listbox.insert(tk.END, group) + self.groups.listbox.bind("<>", self.handle_group_change) + self.groups.listbox.selection_set(0) + + label_frame = ttk.LabelFrame(frame, text="Services") + label_frame.grid(row=0, column=1, sticky="nsew") + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + self.services = CheckboxList( + label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD + ) + self.services.grid(sticky="nsew") + + label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) + label_frame.grid(row=0, column=2, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.current = ListboxScroll(label_frame) + self.current.grid(sticky="nsew") + for service in sorted(self.current_services): + self.current.listbox.insert(tk.END, service) + if self.is_custom_service(service): + self.current.listbox.itemconfig(tk.END, bg="green") + + frame = ttk.Frame(self.top) + frame.grid(stick="ew") + for i in range(4): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Configure", command=self.click_configure) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Save", command=self.click_save) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Remove", command=self.click_remove) + button.grid(row=0, column=2, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.click_cancel) + button.grid(row=0, column=3, sticky="ew") + + # trigger group change + self.groups.listbox.event_generate("<>") + + def handle_group_change(self, event: tk.Event = None): + selection = self.groups.listbox.curselection() + if selection: + index = selection[0] + group = self.groups.listbox.get(index) + self.services.clear() + for name in sorted(self.app.core.config_services_groups[group]): + checked = name in self.current_services + self.services.add(name, checked) + + def service_clicked(self, name: str, var: tk.IntVar): + if var.get() and name not in self.current_services: + self.current_services.add(name) + elif not var.get() and name in self.current_services: + self.current_services.remove(name) + self.current.listbox.delete(0, tk.END) + for name in sorted(self.current_services): + self.current.listbox.insert(tk.END, name) + if self.is_custom_service(name): + self.current.listbox.itemconfig(tk.END, bg="green") + self.canvas_node.core_node.config_services[:] = self.current_services + + def click_configure(self): + current_selection = self.current.listbox.curselection() + if len(current_selection): + dialog = ConfigServiceConfigDialog( + master=self, + app=self.app, + service_name=self.current.listbox.get(current_selection[0]), + node_id=self.node_id, + ) + dialog.show() + else: + messagebox.showinfo( + "Node service configuration", "Select a service to configure" + ) + + def click_save(self): + self.canvas_node.core_node.config_services[:] = self.current_services + logging.info( + "saved node config services: %s", self.canvas_node.core_node.config_services + ) + self.destroy() + + def click_cancel(self): + self.current_services = None + self.destroy() + + def click_remove(self): + cur = self.current.listbox.curselection() + if cur: + service = self.current.listbox.get(cur[0]) + self.current.listbox.delete(cur[0]) + self.current_services.remove(service) + for checkbutton in self.services.frame.winfo_children(): + if checkbutton["text"] == service: + checkbutton.invoke() + return + + def is_custom_service(self, service: str) -> bool: + node_configs = self.app.core.config_service_configs.get(self.node_id, {}) + service_config = node_configs.get(service) + if node_configs and service_config: + return True + else: + return False diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index c1d8e075..e5974625 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -10,6 +10,7 @@ from core.gui import themes from core.gui.dialogs.emaneconfig import EmaneConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog from core.gui.dialogs.nodeconfig import NodeConfigDialog +from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.errors import show_grpc_error @@ -180,6 +181,7 @@ class CanvasNode: context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): context.add_command(label="Services", state=tk.DISABLED) + context.add_command(label="Config Services", state=tk.DISABLED) if is_wlan: context.add_command(label="WLAN Config", command=self.show_wlan_config) if is_wlan and self.core_node.id in self.app.core.mobility_players: @@ -198,6 +200,9 @@ class CanvasNode: context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): context.add_command(label="Services", command=self.show_services) + context.add_command( + label="Config Services", command=self.show_config_services + ) if is_emane: context.add_command( label="EMANE Config", command=self.show_emane_config @@ -253,6 +258,11 @@ class CanvasNode: dialog = NodeServiceDialog(self.app.master, self.app, self) dialog.show() + def show_config_services(self): + self.canvas.context = None + dialog = NodeConfigServiceDialog(self.app.master, self.app, self) + dialog.show() + def has_emane_link(self, interface_id: int) -> core_pb2.Node: result = None for edge in self.edges: diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 8dc163ab..9c07e8c7 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -5,7 +5,7 @@ from pathlib import PosixPath from tkinter import filedialog, font, ttk from typing import TYPE_CHECKING, Dict -from core.api.grpc import core_pb2 +from core.api.grpc import common_pb2, core_pb2 from core.gui import themes from core.gui.themes import FRAME_PAD, PADX, PADY @@ -74,7 +74,7 @@ class ConfigFrame(ttk.Notebook): self, master: tk.Widget, app: "Application", - config: Dict[str, core_pb2.ConfigOption], + config: Dict[str, common_pb2.ConfigOption], **kw ): super().__init__(master, **kw) @@ -99,7 +99,7 @@ class ConfigFrame(ttk.Notebook): label.grid(row=index, pady=PADY, padx=PADX, sticky="w") value = tk.StringVar() if option.type == core_pb2.ConfigOptionType.BOOL: - select = tuple(option.select) + select = ("On", "Off") combobox = ttk.Combobox( tab.frame, textvariable=value, values=select, state="readonly" ) @@ -184,6 +184,17 @@ class ConfigFrame(ttk.Notebook): return {x: self.config[x].value for x in self.config} + def set_values(self, config: Dict[str, str]) -> None: + for name, data in config.items(): + option = self.config[name] + value = self.values[name] + if option.type == core_pb2.ConfigOptionType.BOOL: + if data == "1": + data = "On" + else: + data = "Off" + value.set(data) + class ListboxScroll(ttk.Frame): def __init__(self, master: tk.Widget = None, **kw): diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 703eaa9f..4689d217 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -906,11 +906,7 @@ class Ns2ScriptedMobility(WayPointMobility): label="refresh time (ms)", ), Configuration( - _id="loop", - _type=ConfigDataTypes.BOOL, - default="1", - options=["On", "Off"], - label="loop", + _id="loop", _type=ConfigDataTypes.BOOL, default="1", label="loop" ), Configuration( _id="autostart", diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index a475e672..19951efa 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -6,15 +6,16 @@ import logging import os import shutil import threading -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type import netaddr from core import utils +from core.configservice.dependencies import ConfigServiceDependencies from core.constants import MOUNT_BIN, VNODED_BIN from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import LinkTypes, NodeTypes -from core.errors import CoreCommandError +from core.errors import CoreCommandError, CoreError from core.nodes import client from core.nodes.interface import CoreInterface, TunTap, Veth from core.nodes.netclient import LinuxNetClient, get_net_client @@ -22,6 +23,9 @@ from core.nodes.netclient import LinuxNetClient, get_net_client if TYPE_CHECKING: from core.emulator.distributed import DistributedServer from core.emulator.session import Session + from core.configservice.base import ConfigService + + ConfigServiceType = Type[ConfigService] _DEFAULT_MTU = 1500 @@ -277,9 +281,47 @@ class CoreNodeBase(NodeBase): """ super().__init__(session, _id, name, start, server) self.services = [] + self.config_services = {} self.nodedir = None self.tmpnodedir = False + def add_config_service(self, service_class: "ConfigServiceType") -> None: + """ + Adds a configuration service to the node. + + :param service_class: configuration service class to assign to node + :return: nothing + """ + name = service_class.name + if name in self.config_services: + raise CoreError(f"node({self.name}) already has service({name})") + self.config_services[name] = service_class(self) + + def set_service_config(self, name: str, data: Dict[str, str]) -> None: + """ + Sets configuration service custom config data. + + :param name: name of configuration service + :param data: custom config data to set + :return: nothing + """ + service = self.config_services.get(name) + if service is None: + raise CoreError(f"node({self.name}) does not have service({name})") + service.set_config(data) + + def start_config_services(self) -> None: + """ + Determins startup paths and starts configuration services, based on their + dependency chains. + + :return: nothing + """ + startup_paths = ConfigServiceDependencies(self.config_services).startup_paths() + for startup_path in startup_paths: + for service in startup_path: + service.start() + def makenodedir(self) -> None: """ Create the node directory. diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 8cdc512c..a91f8a6c 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -9,7 +9,7 @@ from core.emane.nodes import EmaneNet from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import NodeTypes -from core.nodes.base import CoreNetworkBase, NodeBase +from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase from core.nodes.network import CtrlNet from core.services.coreservices import CoreService @@ -219,10 +219,15 @@ class DeviceElement(NodeElement): service_elements = etree.Element("services") for service in self.node.services: etree.SubElement(service_elements, "service", name=service.name) - if service_elements.getchildren(): self.element.append(service_elements) + config_service_elements = etree.Element("configservices") + for name, service in self.node.config_services.items(): + etree.SubElement(config_service_elements, "service", name=name) + if config_service_elements.getchildren(): + self.element.append(config_service_elements) + class NetworkElement(NodeElement): def __init__(self, session: "Session", node: NodeBase) -> None: @@ -261,6 +266,7 @@ class CoreXmlWriter: self.write_mobility_configs() self.write_emane_configs() self.write_service_configs() + self.write_configservice_configs() self.write_session_origin() self.write_session_hooks() self.write_session_options() @@ -399,6 +405,32 @@ class CoreXmlWriter: if service_configurations.getchildren(): self.scenario.append(service_configurations) + def write_configservice_configs(self) -> None: + service_configurations = etree.Element("configservice_configurations") + for node in self.session.nodes.values(): + if not isinstance(node, CoreNodeBase): + continue + for name, service in node.config_services.items(): + service_element = etree.SubElement( + service_configurations, "service", name=name + ) + add_attribute(service_element, "node", node.id) + if service.custom_config: + configs_element = etree.SubElement(service_element, "configs") + for key, value in service.custom_config.items(): + etree.SubElement( + configs_element, "config", key=key, value=value + ) + if service.custom_templates: + templates_element = etree.SubElement(service_element, "templates") + for template_name, template in service.custom_templates.items(): + template_element = etree.SubElement( + templates_element, "template", name=template_name + ) + template_element.text = etree.CDATA(template) + if service_configurations.getchildren(): + self.scenario.append(service_configurations) + def write_default_services(self) -> None: node_types = etree.Element("default_services") for node_type in self.session.services.default_services: @@ -568,6 +600,7 @@ class CoreXmlReader: self.read_mobility_configs() self.read_emane_configs() self.read_nodes() + self.read_configservice_configs() self.read_links() def read_default_services(self) -> None: @@ -770,6 +803,12 @@ class CoreXmlReader: if service_elements is not None: options.services = [x.get("name") for x in service_elements.iterchildren()] + config_service_elements = device_element.find("configservices") + if config_service_elements is not None: + options.config_services = [ + x.get("name") for x in config_service_elements.iterchildren() + ] + position_element = device_element.find("position") if position_element is not None: x = get_float(position_element, "x") @@ -812,6 +851,36 @@ class CoreXmlReader: ) self.session.add_node(_type=node_type, _id=node_id, options=options) + def read_configservice_configs(self) -> None: + configservice_configs = self.scenario.find("configservice_configurations") + if configservice_configs is None: + return + + for configservice_element in configservice_configs.iterchildren(): + name = configservice_element.get("name") + node_id = get_int(configservice_element, "node") + node = self.session.get_node(node_id) + service = node.config_services[name] + + configs_element = configservice_element.find("configs") + if configs_element is not None: + config = {} + for config_element in configs_element.iterchildren(): + key = config_element.get("key") + value = config_element.get("value") + config[key] = value + service.set_config(config) + + templates_element = configservice_element.find("templates") + if templates_element is not None: + for template_element in templates_element.iterchildren(): + name = template_element.get("name") + template = template_element.text + logging.info( + "loading xml template(%s): %s", type(template), template + ) + service.set_template(name, template) + def read_links(self) -> None: link_elements = self.scenario.find("links") if link_elements is None: diff --git a/daemon/examples/configservices/testing.py b/daemon/examples/configservices/testing.py new file mode 100644 index 00000000..5b193aee --- /dev/null +++ b/daemon/examples/configservices/testing.py @@ -0,0 +1,33 @@ +import logging + +from core.emulator.coreemu import CoreEmu +from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.enumerations import EventTypes, NodeTypes + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + + # setup basic network + prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") + options = NodeOptions(model="nothing") + coreemu = CoreEmu() + session = coreemu.create_session() + session.set_state(EventTypes.CONFIGURATION_STATE) + switch = session.add_node(_type=NodeTypes.SWITCH) + + # node one + options.config_services = ["DefaultRoute", "IPForward"] + node_one = session.add_node(options=options) + interface = prefixes.create_interface(node_one) + session.add_link(node_one.id, switch.id, interface_one=interface) + + # node two + node_two = session.add_node(options=options) + interface = prefixes.create_interface(node_two) + session.add_link(node_two.id, switch.id, interface_one=interface) + + # start session and run services + session.instantiate() + + input("press enter to exit") + session.shutdown() diff --git a/daemon/proto/Makefile.am b/daemon/proto/Makefile.am index 05f2d394..af535c1e 100644 --- a/daemon/proto/Makefile.am +++ b/daemon/proto/Makefile.am @@ -1,5 +1,6 @@ all: - $(PYTHON) -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/core.proto + $(PYTHON) -m grpc_tools.protoc -I . --python_out=.. core/api/grpc/*.proto + $(PYTHON) -m grpc_tools.protoc -I . --grpc_python_out=.. core/api/grpc/core.proto clean: - -rm -f ../core/api/grpc/core_pb2* + -rm -f ../core/api/grpc/*_pb2* diff --git a/daemon/proto/core/api/grpc/common.proto b/daemon/proto/core/api/grpc/common.proto new file mode 100644 index 00000000..590e2262 --- /dev/null +++ b/daemon/proto/core/api/grpc/common.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package common; + +message ConfigOption { + string label = 1; + string name = 2; + string value = 3; + int32 type = 4; + repeated string select = 5; + string group = 6; +} diff --git a/daemon/proto/core/api/grpc/configservices.proto b/daemon/proto/core/api/grpc/configservices.proto new file mode 100644 index 00000000..f1272df8 --- /dev/null +++ b/daemon/proto/core/api/grpc/configservices.proto @@ -0,0 +1,96 @@ +syntax = "proto3"; + +package configservices; + +import "core/api/grpc/common.proto"; + +message ConfigServiceConfig { + int32 node_id = 1; + string name = 2; + map templates = 3; + map config = 4; +} + +message ConfigServiceValidationMode { + enum Enum { + BLOCKING = 0; + NON_BLOCKING = 1; + TIMER = 2; + } +} + +message ConfigService { + string group = 1; + string name = 2; + repeated string executables = 3; + repeated string dependencies = 4; + repeated string directories = 5; + repeated string files = 6; + repeated string startup = 7; + repeated string validate = 8; + repeated string shutdown = 9; + ConfigServiceValidationMode.Enum validation_mode = 10; + int32 validation_timer = 11; + float validation_period = 12; +} + +message ConfigMode { + string name = 1; + map config = 2; +} + +message GetConfigServicesRequest { + int32 session_id = 1; +} + +message GetConfigServicesResponse { + repeated ConfigService services = 1; +} + +message GetConfigServiceDefaultsRequest { + string name = 1; +} + +message GetConfigServiceDefaultsResponse { + map templates = 1; + map config = 2; + repeated ConfigMode modes = 3; +} + +message GetNodeConfigServiceConfigsRequest { + int32 session_id = 1; +} + +message GetNodeConfigServiceConfigsResponse { + repeated ConfigServiceConfig configs = 1; +} + +message GetNodeConfigServiceRequest { + int32 session_id = 1; + int32 node_id = 2; + string name = 3; +} + +message GetNodeConfigServiceResponse { + map config = 1; +} + +message GetNodeConfigServicesRequest { + int32 session_id = 1; + int32 node_id = 2; +} + +message GetNodeConfigServicesResponse { + repeated string services = 1; +} + +message SetNodeConfigServiceRequest { + int32 session_id = 1; + int32 node_id = 2; + string name = 3; + map config = 4; +} + +message SetNodeConfigServiceResponse { + bool result = 1; +} diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 856ed7aa..aa9bde24 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -5,6 +5,9 @@ package core; option java_package = "com.core.client.grpc"; option java_outer_classname = "CoreProto"; +import "core/api/grpc/configservices.proto"; +import "core/api/grpc/common.proto"; + service CoreApi { // session rpc rpc StartSession (StartSessionRequest) returns (StartSessionResponse) { @@ -102,6 +105,20 @@ service CoreApi { rpc ServiceAction (ServiceActionRequest) returns (ServiceActionResponse) { } + // config services + rpc GetConfigServices (configservices.GetConfigServicesRequest) returns (configservices.GetConfigServicesResponse) { + } + rpc GetConfigServiceDefaults (configservices.GetConfigServiceDefaultsRequest) returns (configservices.GetConfigServiceDefaultsResponse) { + } + rpc GetNodeConfigServiceConfigs (configservices.GetNodeConfigServiceConfigsRequest) returns (configservices.GetNodeConfigServiceConfigsResponse) { + } + rpc GetNodeConfigService (configservices.GetNodeConfigServiceRequest) returns (configservices.GetNodeConfigServiceResponse) { + } + rpc GetNodeConfigServices (configservices.GetNodeConfigServicesRequest) returns (configservices.GetNodeConfigServicesResponse) { + } + rpc SetNodeConfigService (configservices.SetNodeConfigServiceRequest) returns (configservices.SetNodeConfigServiceResponse) { + } + // wlan rpc rpc GetWlanConfigs (GetWlanConfigsRequest) returns (GetWlanConfigsResponse) { } @@ -151,6 +168,7 @@ message StartSessionRequest { repeated ServiceConfig service_configs = 10; repeated ServiceFileConfig service_file_configs = 11; repeated Link asymmetric_links = 12; + repeated configservices.ConfigServiceConfig config_service_configs = 13; } message StartSessionResponse { @@ -203,7 +221,7 @@ message GetSessionOptionsRequest { } message GetSessionOptionsResponse { - map config = 2; + map config = 2; } message SetSessionOptionsRequest { @@ -494,7 +512,7 @@ message GetMobilityConfigRequest { } message GetMobilityConfigResponse { - map config = 1; + map config = 1; } message SetMobilityConfigRequest { @@ -619,7 +637,7 @@ message GetWlanConfigRequest { } message GetWlanConfigResponse { - map config = 1; + map config = 1; } message SetWlanConfigRequest { @@ -636,7 +654,7 @@ message GetEmaneConfigRequest { } message GetEmaneConfigResponse { - map config = 1; + map config = 1; } message SetEmaneConfigRequest { @@ -664,7 +682,7 @@ message GetEmaneModelConfigRequest { } message GetEmaneModelConfigResponse { - map config = 1; + map config = 1; } message SetEmaneModelConfigRequest { @@ -685,7 +703,7 @@ message GetEmaneModelConfigsResponse { int32 node_id = 1; string model = 2; int32 interface = 3; - map config = 4; + map config = 4; } repeated ModelConfig configs = 1; } @@ -903,16 +921,7 @@ message NodeServiceData { } message MappedConfig { - map config = 1; -} - -message ConfigOption { - string label = 1; - string name = 2; - string value = 3; - int32 type = 4; - repeated string select = 5; - string group = 6; + map config = 1; } message Session { @@ -941,6 +950,7 @@ message Node { string opaque = 9; string image = 10; string server = 11; + repeated string config_services = 12; } message Link { diff --git a/daemon/requirements.txt b/daemon/requirements.txt index eaddd657..d0defc6b 100644 --- a/daemon/requirements.txt +++ b/daemon/requirements.txt @@ -3,13 +3,15 @@ cffi==1.13.2 cryptography==2.8 fabric==2.5.0 grpcio==1.26.0 -invoke==1.3.0 +invoke==1.4.0 lxml==4.4.2 +Mako==1.1.1 +MarkupSafe==1.1.1 netaddr==0.7.19 paramiko==2.7.1 -pillow==6.2.1 -protobuf==3.11.1 +Pillow==7.0.0 +protobuf==3.11.2 pycparser==2.19 -pynacl==1.3.0 -pyyaml==5.2 -six==1.13.0 +PyNaCl==1.3.0 +PyYAML==5.3 +six==1.14.0 diff --git a/daemon/setup.py.in b/daemon/setup.py.in index e0faf01d..bdef71ab 100644 --- a/daemon/setup.py.in +++ b/daemon/setup.py.in @@ -39,6 +39,7 @@ setup( "netaddr", "invoke", "lxml", + "mako", "pillow", "protobuf", "pyyaml", diff --git a/daemon/tests/test_config_services.py b/daemon/tests/test_config_services.py new file mode 100644 index 00000000..eaba4d47 --- /dev/null +++ b/daemon/tests/test_config_services.py @@ -0,0 +1,300 @@ +from unittest import mock + +import pytest + +from core.config import Configuration +from core.configservice.base import ( + ConfigService, + ConfigServiceBootError, + ConfigServiceMode, +) +from core.emulator.enumerations import ConfigDataTypes +from core.errors import CoreCommandError, CoreError + +TEMPLATE_TEXT = "echo hello" + + +class MyService(ConfigService): + name = "MyService" + group = "MyGroup" + directories = ["/usr/local/lib"] + files = ["test.sh"] + executables = [] + dependencies = [] + startup = [f"sh {files[0]}"] + validate = [f"pidof {files[0]}"] + shutdown = [f"pkill {files[0]}"] + 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: + return TEMPLATE_TEXT + + +class TestConfigServices: + def test_set_template(self): + # given + node = mock.MagicMock() + text = "echo custom" + service = MyService(node) + + # when + service.set_template(MyService.files[0], text) + + # then + assert MyService.files[0] in service.custom_templates + assert service.custom_templates[MyService.files[0]] == text + + def test_create_directories(self): + # given + node = mock.MagicMock() + service = MyService(node) + + # when + service.create_dirs() + + # then + node.privatedir.assert_called_with(MyService.directories[0]) + + def test_create_files_custom(self): + # given + node = mock.MagicMock() + service = MyService(node) + text = "echo custom" + service.set_template(MyService.files[0], text) + + # when + service.create_files() + + # then + node.nodefile.assert_called_with(MyService.files[0], text) + + def test_create_files_text(self): + # given + node = mock.MagicMock() + service = MyService(node) + + # when + service.create_files() + + # then + node.nodefile.assert_called_with(MyService.files[0], TEMPLATE_TEXT) + + def test_run_startup(self): + # given + node = mock.MagicMock() + wait = True + service = MyService(node) + + # when + service.run_startup(wait=wait) + + # then + node.cmd.assert_called_with(MyService.startup[0], wait=wait) + + def test_run_startup_exception(self): + # given + node = mock.MagicMock() + node.cmd.side_effect = CoreCommandError(1, "error") + service = MyService(node) + + # when + with pytest.raises(ConfigServiceBootError): + service.run_startup(wait=True) + + def test_shutdown(self): + # given + node = mock.MagicMock() + service = MyService(node) + + # when + service.stop() + + # then + node.cmd.assert_called_with(MyService.shutdown[0]) + + def test_run_validation(self): + # given + node = mock.MagicMock() + service = MyService(node) + + # when + service.run_validation() + + # then + node.cmd.assert_called_with(MyService.validate[0]) + + def test_run_validation_timer(self): + # given + node = mock.MagicMock() + service = MyService(node) + service.validation_mode = ConfigServiceMode.TIMER + service.validation_timer = 0 + + # when + service.run_validation() + + # then + node.cmd.assert_called_with(MyService.validate[0]) + + def test_run_validation_timer_exception(self): + # given + node = mock.MagicMock() + node.cmd.side_effect = CoreCommandError(1, "error") + service = MyService(node) + service.validation_mode = ConfigServiceMode.TIMER + service.validation_period = 0 + service.validation_timer = 0 + + # when + with pytest.raises(ConfigServiceBootError): + service.run_validation() + + def test_run_validation_non_blocking(self): + # given + node = mock.MagicMock() + service = MyService(node) + service.validation_mode = ConfigServiceMode.NON_BLOCKING + service.validation_period = 0 + service.validation_timer = 0 + + # when + service.run_validation() + + # then + node.cmd.assert_called_with(MyService.validate[0]) + + def test_run_validation_non_blocking_exception(self): + # given + node = mock.MagicMock() + node.cmd.side_effect = CoreCommandError(1, "error") + service = MyService(node) + service.validation_mode = ConfigServiceMode.NON_BLOCKING + service.validation_period = 0 + service.validation_timer = 0 + + # when + with pytest.raises(ConfigServiceBootError): + service.run_validation() + + def test_render_config(self): + # given + node = mock.MagicMock() + service = MyService(node) + + # when + config = service.render_config() + + # then + assert config == {"value1": "", "value2": "", "value3": ""} + + def test_render_config_custom(self): + # given + node = mock.MagicMock() + service = MyService(node) + custom_config = {"value1": "1", "value2": "2", "value3": "3"} + service.set_config(custom_config) + + # when + config = service.render_config() + + # then + assert config == custom_config + + def test_set_config(self): + # given + node = mock.MagicMock() + service = MyService(node) + custom_config = {"value1": "1", "value2": "2", "value3": "3"} + + # when + service.set_config(custom_config) + + # then + assert service.custom_config == custom_config + + def test_set_config_exception(self): + # given + node = mock.MagicMock() + service = MyService(node) + custom_config = {"value4": "1"} + + # when + with pytest.raises(CoreError): + service.set_config(custom_config) + + def test_start_blocking(self): + # given + node = mock.MagicMock() + service = MyService(node) + service.create_dirs = mock.MagicMock() + service.create_files = mock.MagicMock() + service.run_startup = mock.MagicMock() + service.run_validation = mock.MagicMock() + service.wait_validation = mock.MagicMock() + + # when + service.start() + + # then + service.create_files.assert_called_once() + service.create_dirs.assert_called_once() + service.run_startup.assert_called_once() + service.run_validation.assert_not_called() + service.wait_validation.assert_not_called() + + def test_start_timer(self): + # given + node = mock.MagicMock() + service = MyService(node) + service.validation_mode = ConfigServiceMode.TIMER + service.create_dirs = mock.MagicMock() + service.create_files = mock.MagicMock() + service.run_startup = mock.MagicMock() + service.run_validation = mock.MagicMock() + service.wait_validation = mock.MagicMock() + + # when + service.start() + + # then + service.create_files.assert_called_once() + service.create_dirs.assert_called_once() + service.run_startup.assert_called_once() + service.run_validation.assert_not_called() + service.wait_validation.assert_called_once() + + def test_start_non_blocking(self): + # given + node = mock.MagicMock() + service = MyService(node) + service.validation_mode = ConfigServiceMode.NON_BLOCKING + service.create_dirs = mock.MagicMock() + service.create_files = mock.MagicMock() + service.run_startup = mock.MagicMock() + service.run_validation = mock.MagicMock() + service.wait_validation = mock.MagicMock() + + # when + service.start() + + # then + service.create_files.assert_called_once() + service.create_dirs.assert_called_once() + service.run_startup.assert_called_once() + service.run_validation.assert_called_once() + service.wait_validation.assert_not_called()