diff --git a/.github/workflows/coretk-checks.yml b/.github/workflows/coretk-checks.yml new file mode 100644 index 00000000..e99ed214 --- /dev/null +++ b/.github/workflows/coretk-checks.yml @@ -0,0 +1,35 @@ +name: CORE Tk Checks + +on: [push] + +jobs: + build: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install pipenv + run: | + python -m pip install --upgrade pip + pip install pipenv + cd daemon + cp setup.py.in setup.py + cp core/constants.py.in core/constants.py + sed -i 's/True/False/g' core/constants.py + cd ../coretk + pipenv install --dev + - name: isort + run: | + cd coretk + pipenv run isort -c + - name: black + run: | + cd coretk + pipenv run black --check . + - name: flake8 + run: | + cd coretk + pipenv run flake8 diff --git a/.gitignore b/.gitignore index 038146aa..700cbf17 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ ns3/setup.py # ignore corefx build corefx/target + +# python +__pycache__ diff --git a/coretk/Pipfile b/coretk/Pipfile new file mode 100644 index 00000000..b30b3d54 --- /dev/null +++ b/coretk/Pipfile @@ -0,0 +1,19 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[scripts] +coretk = "python coretk/app.py" + +[dev-packages] +flake8 = "*" +isort = "*" +black = "==19.3b0" +pre-commit = "*" + +[packages] +coretk = {path = ".",editable = true} +core = {path = "./../daemon",editable = true} +pyyaml = "*" +netaddr = "*" diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock new file mode 100644 index 00000000..7ff33122 --- /dev/null +++ b/coretk/Pipfile.lock @@ -0,0 +1,521 @@ +{ + "_meta": { + "hash": { + "sha256": "9024ff4821ee3ccffee21a83f5436953371ad7d64a81a22b6c3723002c92b2cd" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "bcrypt": { + "hashes": [ + "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", + "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", + "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", + "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", + "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752", + "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", + "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", + "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", + "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", + "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", + "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", + "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", + "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", + "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", + "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", + "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1", + "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", + "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" + ], + "version": "==3.1.7" + }, + "cffi": { + "hashes": [ + "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", + "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", + "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", + "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", + "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", + "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", + "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", + "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", + "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", + "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", + "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", + "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", + "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", + "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", + "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", + "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", + "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", + "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", + "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", + "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", + "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", + "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", + "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", + "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", + "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", + "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", + "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", + "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", + "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", + "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", + "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", + "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", + "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d" + ], + "version": "==1.13.2" + }, + "core": { + "editable": true, + "path": "./../daemon" + }, + "coretk": { + "editable": true, + "path": "." + }, + "cryptography": { + "hashes": [ + "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", + "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", + "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", + "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", + "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", + "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", + "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", + "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", + "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", + "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", + "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", + "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", + "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", + "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", + "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", + "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", + "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", + "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", + "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", + "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", + "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8" + ], + "version": "==2.8" + }, + "fabric": { + "hashes": [ + "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389", + "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6" + ], + "version": "==2.5.0" + }, + "grpcio": { + "hashes": [ + "sha256:0419ae5a45f49c7c40d9ae77ae4de9442431b7822851dfbbe56ee0eacb5e5654", + "sha256:1e8631eeee0fb0b4230aeb135e4890035f6ef9159c2a3555fa184468e325691a", + "sha256:24db2fa5438f3815a4edb7a189035051760ca6aa2b0b70a6a948b28bfc63c76b", + "sha256:2adb1cdb7d33e91069517b41249622710a94a1faece1fed31cd36904e4201cde", + "sha256:2cd51f35692b551aeb1fdeb7a256c7c558f6d78fcddff00640942d42f7aeba5f", + "sha256:3247834d24964589f8c2b121b40cd61319b3c2e8d744a6a82008643ef8a378b1", + "sha256:3433cb848b4209717722b62392e575a77a52a34d67c6730138102abc0a441685", + "sha256:39671b7ff77a962bd745746d9d2292c8ed227c5748f16598d16d8631d17dd7e5", + "sha256:40a0b8b2e6f6dd630f8b267eede2f40a848963d0f3c40b1b1f453a4a870f679e", + "sha256:40f9a74c7aa210b3e76eb1c9d56aa8d08722b73426a77626967019df9bbac287", + "sha256:423f76aa504c84cb94594fb88b8a24027c887f1c488cf58f2173f22f4fbd046c", + "sha256:43bd04cec72281a96eb361e1b0232f0f542b46da50bcfe72ef7e5a1b41d00cb3", + "sha256:43e38762635c09e24885d15e3a8e374b72d105d4178ee2cc9491855a8da9c380", + "sha256:4413b11c2385180d7de03add6c8845dd66692b148d36e27ec8c9ef537b2553a1", + "sha256:4450352a87094fd58daf468b04c65a9fa19ad11a0ac8ac7b7ff17d46f873cbc1", + "sha256:49ffda04a6e44de028b3b786278ac9a70043e7905c3eea29eed88b6524d53a29", + "sha256:4a38c4dde4c9120deef43aaabaa44f19186c98659ce554c29788c4071ab2f0a4", + "sha256:50b1febdfd21e2144b56a9aa226829e93a79c354ef22a4e5b013d9965e1ec0ed", + "sha256:559b1a3a8be7395ded2943ea6c2135d096f8cc7039d6d12127110b6496f251fe", + "sha256:5de86c182667ec68cf84019aa0d8ceccf01d352cdca19bf9e373725204bdbf50", + "sha256:5fc069bb481fe3fad0ba24d3baaf69e22dfa6cc1b63290e6dfeaf4ac1e996fb7", + "sha256:6a19d654da49516296515d6f65de4bbcbd734bc57913b21a610cfc45e6df3ff1", + "sha256:7535b3e52f498270e7877dde1c8944d6b7720e93e2e66b89c82a11447b5818f5", + "sha256:7c4e495bcabc308198b8962e60ca12f53b27eb8f03a21ac1d2d711d6dd9ecfca", + "sha256:8a8fc4a0220367cb8370cedac02272d574079ccc32bffbb34d53aaf9e38b5060", + "sha256:8b008515e067232838daca020d1af628bf6520c8cc338bf383284efe6d8bd083", + "sha256:8d1684258e1385e459418f3429e107eec5fb3d75e1f5a8c52e5946b3f329d6ea", + "sha256:8eb5d54b87fb561dc2e00a5c5226c33ffe8dbc13f2e4033a412bafb7b37b194d", + "sha256:94cdef0c61bd014bb7af495e21a1c3a369dd0399c3cd1965b1502043f5c88d94", + "sha256:9d9f3be69c7a5e84c3549a8c4403fa9ac7672da456863d21e390b2bbf45ccad1", + "sha256:9fb6fb5975a448169756da2d124a1beb38c0924ff6c0306d883b6848a9980f38", + "sha256:a5eaae8700b87144d7dfb475aa4675e500ff707292caba3deff41609ddc5b845", + "sha256:aaeac2d552772b76d24eaff67a5d2325bc5205c74c0d4f9fbe71685d4a971db2", + "sha256:bb611e447559b3b5665e12a7da5160c0de6876097f62bf1d23ba66911564868e", + "sha256:bc0d41f4eb07da8b8d3ea85e50b62f6491ab313834db86ae2345be07536a4e5a", + "sha256:bf51051c129b847d1bb63a9b0826346b5f52fb821b15fe5e0d5ef86f268510f5", + "sha256:c948c034d8997526011960db54f512756fb0b4be1b81140a15b4ef094c6594a4", + "sha256:d435a01334157c3b126b4ee5141401d44bdc8440993b18b05e2f267a6647f92d", + "sha256:d46c1f95672b73288e08cdca181e14e84c6229b5879561b7b8cfd48374e09287", + "sha256:d5d58309b42064228b16b0311ff715d6c6e20230e81b35e8d0c8cfa1bbdecad8", + "sha256:dc6e2e91365a1dd6314d615d80291159c7981928b88a4c65654e3fefac83a836", + "sha256:e0dfb5f7a39029a6cbec23affa923b22a2c02207960fd66f109e01d6f632c1eb", + "sha256:eb4bf58d381b1373bd21d50837a53953d625d1693f1b58fed12743c75d3dd321", + "sha256:ebb211a85248dbc396b29320273c1ffde484b898852432613e8df0164c091006", + "sha256:ec759ece4786ae993a5b7dc3b3dead6e9375d89a6c65dfd6860076d2eb2abe7b", + "sha256:f55108397a8fa164268238c3e69cc134e945d1f693572a2f05a028b8d0d2b837", + "sha256:f6c706866d424ff285b85a02de7bbe5ed0ace227766b2c42cbe12f3d9ea5a8aa", + "sha256:f8370ad332b36fbad117440faf0dd4b910e80b9c49db5648afd337abdde9a1b6" + ], + "version": "==1.25.0" + }, + "invoke": { + "hashes": [ + "sha256:c52274d2e8a6d64ef0d61093e1983268ea1fc0cd13facb9448c4ef0c9a7ac7da", + "sha256:f4ec8a134c0122ea042c8912529f87652445d9f4de590b353d23f95bfa1f0efd", + "sha256:fc803a5c9052f15e63310aa81a43498d7c55542beb18564db88a9d75a176fa44" + ], + "version": "==1.3.0" + }, + "lxml": { + "hashes": [ + "sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4", + "sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc", + "sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1", + "sha256:1409b14bf83a7d729f92e2a7fbfe7ec929d4883ca071b06e95c539ceedb6497c", + "sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046", + "sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36", + "sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5", + "sha256:2e8f77db25b0a96af679e64ff9bf9dddb27d379c9900c3272f3041c4d1327c9d", + "sha256:4dffd405390a45ecb95ab5ab1c1b847553c18b0ef8ed01e10c1c8b1a76452916", + "sha256:6b899931a5648862c7b88c795eddff7588fb585e81cecce20f8d9da16eff96e0", + "sha256:726c17f3e0d7a7200718c9a890ccfeab391c9133e363a577a44717c85c71db27", + "sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc", + "sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7", + "sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38", + "sha256:9277562f175d2334744ad297568677056861070399cec56ff06abbe2564d1232", + "sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5", + "sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832", + "sha256:ae88588d687bd476be588010cbbe551e9c2872b816f2da8f01f6f1fda74e1ef0", + "sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a", + "sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f", + "sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9", + "sha256:c7fccd08b14aa437fe096c71c645c0f9be0655a9b1a4b7cffc77bcb23b3d61d2", + "sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692", + "sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84", + "sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79", + "sha256:fe489d486cd00b739be826e8c1be188ddb74c7a1ca784d93d06fda882a6a1681" + ], + "version": "==4.4.1" + }, + "netaddr": { + "hashes": [ + "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd", + "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca" + ], + "index": "pypi", + "version": "==0.7.19" + }, + "paramiko": { + "hashes": [ + "sha256:99f0179bdc176281d21961a003ffdb2ec369daac1a1007241f53374e376576cf", + "sha256:f4b2edfa0d226b70bd4ca31ea7e389325990283da23465d572ed1f70a7583041" + ], + "version": "==2.6.0" + }, + "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" + ], + "version": "==6.2.1" + }, + "protobuf": { + "hashes": [ + "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", + "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", + "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", + "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", + "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", + "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", + "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", + "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", + "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", + "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", + "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", + "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", + "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", + "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", + "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", + "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de" + ], + "version": "==3.10.0" + }, + "pycparser": { + "hashes": [ + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + ], + "version": "==2.19" + }, + "pynacl": { + "hashes": [ + "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", + "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", + "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", + "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", + "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", + "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", + "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", + "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", + "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", + "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", + "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", + "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", + "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", + "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", + "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", + "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", + "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", + "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", + "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", + "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", + "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" + ], + "version": "==1.3.0" + }, + "pyyaml": { + "hashes": [ + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "index": "pypi", + "version": "==5.1.2" + }, + "six": { + "hashes": [ + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + ], + "version": "==1.13.0" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "aspy.yaml": { + "hashes": [ + "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", + "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "black": { + "hashes": [ + "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", + "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + ], + "index": "pypi", + "version": "==19.3b0" + }, + "cfgv": { + "hashes": [ + "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", + "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289" + ], + "version": "==2.0.1" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "flake8": { + "hashes": [ + "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", + "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" + ], + "index": "pypi", + "version": "==3.7.9" + }, + "identify": { + "hashes": [ + "sha256:4f1fe9a59df4e80fcb0213086fcf502bc1765a01ea4fe8be48da3b65afd2a017", + "sha256:d8919589bd2a5f99c66302fec0ef9027b12ae150b0b0213999ad3f695fc7296e" + ], + "version": "==1.4.7" + }, + "importlib-metadata": { + "hashes": [ + "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", + "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + ], + "markers": "python_version < '3.8'", + "version": "==0.23" + }, + "importlib-resources": { + "hashes": [ + "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b", + "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078" + ], + "markers": "python_version < '3.7'", + "version": "==1.0.2" + }, + "isort": { + "hashes": [ + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + ], + "index": "pypi", + "version": "==4.3.21" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + ], + "version": "==7.2.0" + }, + "nodeenv": { + "hashes": [ + "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" + ], + "version": "==1.3.3" + }, + "pre-commit": { + "hashes": [ + "sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e", + "sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50" + ], + "index": "pypi", + "version": "==1.20.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "pyyaml": { + "hashes": [ + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "index": "pypi", + "version": "==5.1.2" + }, + "six": { + "hashes": [ + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + ], + "version": "==1.13.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "virtualenv": { + "hashes": [ + "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589", + "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136" + ], + "version": "==16.7.7" + }, + "zipp": { + "hashes": [ + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + ], + "version": "==0.6.0" + } + } +} diff --git a/coretk/coretk/__init__.py b/coretk/coretk/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py new file mode 100644 index 00000000..c8d4e597 --- /dev/null +++ b/coretk/coretk/app.py @@ -0,0 +1,107 @@ +import logging +import tkinter as tk +from tkinter import ttk + +from coretk import appconfig, themes +from coretk.coreclient import CoreClient +from coretk.graph.graph import CanvasGraph +from coretk.images import ImageEnum, Images +from coretk.menuaction import MenuAction +from coretk.menubar import Menubar +from coretk.nodeutils import NodeUtils +from coretk.statusbar import StatusBar +from coretk.toolbar import Toolbar +from coretk.validation import InputValidation + +WIDTH = 1000 +HEIGHT = 800 + + +class Application(tk.Frame): + def __init__(self, master=None): + super().__init__(master) + # load node icons + NodeUtils.setup() + + # widgets + self.menubar = None + self.toolbar = None + self.canvas = None + self.statusbar = None + self.validation = None + + # setup + self.guiconfig = appconfig.read() + self.style = ttk.Style() + self.setup_theme() + self.core = CoreClient(self) + self.setup_app() + self.draw() + self.core.set_up() + + def setup_theme(self): + themes.load(self.style) + self.master.bind_class("Menu", "<>", themes.theme_change_menu) + self.master.bind("<>", themes.theme_change) + self.style.theme_use(self.guiconfig["preferences"]["theme"]) + + def setup_app(self): + self.master.title("CORE") + self.center() + self.master.protocol("WM_DELETE_WINDOW", self.on_closing) + image = Images.get(ImageEnum.CORE, 16) + self.master.tk.call("wm", "iconphoto", self.master._w, image) + self.pack(fill=tk.BOTH, expand=True) + self.validation = InputValidation(self) + + def center(self): + screen_width = self.master.winfo_screenwidth() + screen_height = self.master.winfo_screenheight() + x = int((screen_width / 2) - (WIDTH / 2)) + y = int((screen_height / 2) - (HEIGHT / 2)) + self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}") + + def draw(self): + self.master.option_add("*tearOff", tk.FALSE) + self.menubar = Menubar(self.master, self) + self.toolbar = Toolbar(self, self) + self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) + self.draw_canvas() + self.draw_status() + + def draw_canvas(self): + width = self.guiconfig["preferences"]["width"] + height = self.guiconfig["preferences"]["height"] + self.canvas = CanvasGraph(self, self.core, width, height) + self.canvas.pack(fill=tk.BOTH, expand=True) + scroll_x = ttk.Scrollbar( + self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview + ) + scroll_x.pack(side=tk.BOTTOM, fill=tk.X) + scroll_y = ttk.Scrollbar(self.canvas, command=self.canvas.yview) + scroll_y.pack(side=tk.RIGHT, fill=tk.Y) + self.canvas.configure(xscrollcommand=scroll_x.set) + self.canvas.configure(yscrollcommand=scroll_y.set) + + def draw_status(self): + self.statusbar = StatusBar(master=self, app=self) + self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) + + def on_closing(self): + menu_action = MenuAction(self, self.master) + menu_action.on_quit() + + def save_config(self): + appconfig.save(self.guiconfig) + + def close(self): + self.master.destroy() + + +if __name__ == "__main__": + log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" + logging.basicConfig(level=logging.DEBUG, format=log_format) + Images.load_all() + appconfig.check_directory() + app = Application() + app.mainloop() diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py new file mode 100644 index 00000000..33b08793 --- /dev/null +++ b/coretk/coretk/appconfig.py @@ -0,0 +1,111 @@ +import logging +import os +import shutil +from pathlib import Path + +import yaml + +# gui home paths +from coretk import themes + +HOME_PATH = Path.home().joinpath(".coretk") +BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") +CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") +CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services") +ICONS_PATH = HOME_PATH.joinpath("icons") +MOBILITY_PATH = HOME_PATH.joinpath("mobility") +XMLS_PATH = HOME_PATH.joinpath("xmls") +CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") + +# local paths +DATA_PATH = Path(__file__).parent.joinpath("data") +LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute() +LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute() +LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute() +LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute() + +# configuration data +TERMINALS = [ + "$TERM", + "gnome-terminal --window --", + "lxterminal -e", + "konsole -e", + "xterm -e", + "aterm -e", + "eterm -e", + "rxvt -e", + "xfce4-terminal -x", +] +EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] + + +class IndentDumper(yaml.Dumper): + def increase_indent(self, flow=False, indentless=False): + return super().increase_indent(flow, False) + + +def copy_files(current_path, new_path): + for current_file in current_path.glob("*"): + new_file = new_path.joinpath(current_file.name) + shutil.copy(current_file, new_file) + + +def check_directory(): + if HOME_PATH.exists(): + logging.info("~/.coretk exists") + return + logging.info("creating ~/.coretk") + HOME_PATH.mkdir() + BACKGROUNDS_PATH.mkdir() + CUSTOM_EMANE_PATH.mkdir() + CUSTOM_SERVICE_PATH.mkdir() + ICONS_PATH.mkdir() + MOBILITY_PATH.mkdir() + XMLS_PATH.mkdir() + + copy_files(LOCAL_ICONS_PATH, ICONS_PATH) + copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH) + copy_files(LOCAL_XMLS_PATH, XMLS_PATH) + copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH) + + if "TERM" in os.environ: + terminal = TERMINALS[0] + else: + terminal = TERMINALS[1] + if "EDITOR" in os.environ: + editor = EDITORS[0] + else: + editor = EDITORS[1] + config = { + "preferences": { + "theme": themes.THEME_DARK, + "editor": editor, + "terminal": terminal, + "gui3d": "/usr/local/bin/std3d.sh", + "width": 1000, + "height": 750, + }, + "location": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "lat": 47.5791667, + "lon": -122.132322, + "alt": 2.0, + "scale": 150.0, + }, + "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], + "nodes": [], + "observers": [{"name": "hello", "cmd": "echo hello"}], + } + save(config) + + +def read(): + with CONFIG_PATH.open("r") as f: + return yaml.load(f, Loader=yaml.SafeLoader) + + +def save(config): + with CONFIG_PATH.open("w") as f: + yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py new file mode 100644 index 00000000..8f2d479d --- /dev/null +++ b/coretk/coretk/coreclient.py @@ -0,0 +1,909 @@ +""" +Incorporate grpc into python tkinter GUI +""" +import json +import logging +import os +import time +from pathlib import Path + +import grpc + +from core.api.grpc import client, core_pb2 +from coretk import appconfig +from coretk.dialogs.mobilityplayer import MobilityPlayer +from coretk.dialogs.sessions import SessionsDialog +from coretk.errors import show_grpc_error +from coretk.graph import tags +from coretk.graph.shape import AnnotationData, Shape +from coretk.graph.shapeutils import ShapeType +from coretk.interface import InterfaceManager +from coretk.nodeutils import NodeDraw, NodeUtils + +GUI_SOURCE = "gui" +OBSERVERS = { + "processes": "ps", + "ifconfig": "ifconfig", + "IPV4 Routes": "ip -4 ro", + "IPV6 Routes": "ip -6 ro", + "Listening sockets": "netstat -tuwnl", + "IPv4 MFC entries": "ip -4 mroute show", + "IPv6 MFC entries": "ip -6 mroute show", + "firewall rules": "iptables -L", + "IPSec policies": "setkey -DP", +} + + +class CoreServer: + def __init__(self, name, address, port): + self.name = name + self.address = address + self.port = port + + +class Observer: + def __init__(self, name, cmd): + self.name = name + self.cmd = cmd + + +class CoreClient: + def __init__(self, app): + """ + Create a CoreGrpc instance + """ + self.client = client.CoreGrpcClient() + self.session_id = None + self.node_ids = [] + self.app = app + self.master = app.master + self.services = {} + self.default_services = {} + self.emane_models = [] + self.observer = None + + # loaded configuration data + self.servers = {} + self.custom_nodes = {} + self.custom_observers = {} + self.read_config() + + # helpers + self.interface_to_edge = {} + self.interfaces_manager = InterfaceManager(self.app) + + # session data + self.state = None + self.canvas_nodes = {} + self.location = None + self.links = {} + self.hooks = {} + self.wlan_configs = {} + self.mobility_configs = {} + self.emane_model_configs = {} + self.emane_config = None + self.service_configs = {} + self.file_configs = {} + self.mobility_players = {} + self.handling_throughputs = None + self.handling_events = None + + def reset(self): + # helpers + self.interfaces_manager.reset() + self.interface_to_edge.clear() + # session data + self.canvas_nodes.clear() + self.links.clear() + self.hooks.clear() + self.wlan_configs.clear() + self.mobility_configs.clear() + self.emane_model_configs.clear() + self.emane_config = None + self.service_configs.clear() + self.file_configs.clear() + self.mobility_players.clear() + # clear streams + if self.handling_throughputs: + self.handling_throughputs.cancel() + self.handling_throughputs = None + if self.handling_events: + self.handling_events.cancel() + self.handling_events = None + + def set_observer(self, value): + self.observer = value + + def read_config(self): + # read distributed server + for config in self.app.guiconfig.get("servers", []): + server = CoreServer(config["name"], config["address"], config["port"]) + self.servers[server.name] = server + + # read custom nodes + for config in self.app.guiconfig.get("nodes", []): + name = config["name"] + image_file = config["image"] + services = set(config["services"]) + node_draw = NodeDraw.from_custom(name, image_file, services) + self.custom_nodes[name] = node_draw + + # read observers + for config in self.app.guiconfig.get("observers", []): + observer = Observer(config["name"], config["cmd"]) + self.custom_observers[observer.name] = observer + + def handle_events(self, event): + if event.session_id != self.session_id: + logging.warn( + "ignoring event session(%s) current(%s)", + event.session_id, + self.session_id, + ) + return + + if event.HasField("link_event"): + logging.info("link event: %s", event) + self.handle_link_event(event.link_event) + elif event.HasField("session_event"): + logging.info("session event: %s", event) + session_event = event.session_event + if session_event.event <= core_pb2.SessionState.SHUTDOWN: + self.state = event.session_event.event + elif session_event.event in {7, 8, 9}: + node_id = session_event.node_id + dialog = self.mobility_players.get(node_id) + if dialog: + if session_event.event == 7: + dialog.set_play() + elif session_event.event == 8: + dialog.set_stop() + else: + dialog.set_pause() + else: + logging.warning("unknown session event: %s", session_event) + elif event.HasField("node_event"): + self.handle_node_event(event.node_event) + elif event.HasField("config_event"): + logging.info("config event: %s", event) + elif event.HasField("exception_event"): + self.handle_exception_event(event.exception_event) + else: + logging.info("unhandled event: %s", event) + + def handle_link_event(self, event): + node_one_id = event.link.node_one_id + node_two_id = event.link.node_two_id + canvas_node_one = self.canvas_nodes[node_one_id] + canvas_node_two = self.canvas_nodes[node_two_id] + + if event.message_type == core_pb2.MessageType.ADD: + self.app.canvas.add_wireless_edge(canvas_node_one, canvas_node_two) + elif event.message_type == core_pb2.MessageType.DELETE: + self.app.canvas.delete_wireless_edge(canvas_node_one, canvas_node_two) + else: + logging.warning("unknown link event: %s", event.message_type) + + def handle_node_event(self, event): + if event.source == GUI_SOURCE: + return + node_id = event.node.id + x = event.node.position.x + y = event.node.position.y + canvas_node = self.canvas_nodes[node_id] + canvas_node.move(x, y) + + def enable_throughputs(self): + self.handling_throughputs = self.client.throughputs( + self.session_id, self.handle_throughputs + ) + + def cancel_throughputs(self): + self.handling_throughputs.cancel() + self.handling_throughputs = None + + def handle_throughputs(self, event): + if event.session_id != self.session_id: + logging.warn( + "ignoring throughput event session(%s) current(%s)", + event.session_id, + self.session_id, + ) + return + logging.info("handling throughputs event: %s", event) + self.app.canvas.throughput_draw.process_grpc_throughput_event( + event.interface_throughputs + ) + + def handle_exception_event(self, event): + logging.info("exception event: %s", event) + self.app.statusbar.core_alarms.append(event) + + def join_session(self, session_id, query_location=True): + # update session and title + self.session_id = session_id + self.master.title(f"CORE Session({self.session_id})") + + # clear session data + self.reset() + + # get session data + try: + response = self.client.get_session(self.session_id) + session = response.session + self.state = session.state + self.handling_events = self.client.events( + self.session_id, self.handle_events + ) + + # get location + if query_location: + response = self.client.get_session_location(self.session_id) + self.location = response.location + + # get emane models + response = self.client.get_emane_models(self.session_id) + self.emane_models = response.models + + # get hooks + response = self.client.get_hooks(self.session_id) + for hook in response.hooks: + self.hooks[hook.file] = hook + + # get mobility configs + response = self.client.get_mobility_configs(self.session_id) + for node_id in response.configs: + node_config = response.configs[node_id].config + self.mobility_configs[node_id] = node_config + + # get emane config + response = self.client.get_emane_config(self.session_id) + self.emane_config = response.config + + # get emane model config + response = self.client.get_emane_model_configs(self.session_id) + for config in response.configs: + interface = None + if config.interface != -1: + interface = config.interface + self.set_emane_model_config( + config.node_id, config.model, config.config, interface + ) + + # get wlan configurations + response = self.client.get_wlan_configs(self.session_id) + for _id in response.configs: + mapped_config = response.configs[_id] + self.wlan_configs[_id] = mapped_config.config + + # get service configurations + response = self.client.get_node_service_configs(self.session_id) + for config in response.configs: + service_configs = self.service_configs.setdefault(config.node_id, {}) + service_configs[config.service] = config.data + logging.info("service file configs: %s", config.files) + for file_name in config.files: + file_configs = self.file_configs.setdefault(config.node_id, {}) + files = file_configs.setdefault(config.service, {}) + data = config.files[file_name] + files[file_name] = data + + # draw session + self.app.canvas.reset_and_redraw(session) + + # get metadata + response = self.client.get_session_metadata(self.session_id) + self.parse_metadata(response.config) + except grpc.RpcError as e: + show_grpc_error(e) + + # update ui to represent current state + if self.is_runtime(): + self.app.toolbar.runtime_frame.tkraise() + self.app.toolbar.click_runtime_selection() + else: + self.app.toolbar.design_frame.tkraise() + self.app.toolbar.click_selection() + self.app.statusbar.progress_bar.stop() + + def is_runtime(self): + return self.state == core_pb2.SessionState.RUNTIME + + def parse_metadata(self, config): + # canvas setting + canvas_config = config.get("canvas") + logging.info("canvas metadata: %s", canvas_config) + if canvas_config: + canvas_config = json.loads(canvas_config) + + gridlines = canvas_config.get("gridlines", True) + self.app.canvas.show_grid.set(gridlines) + + fit_image = canvas_config.get("fit_image", False) + self.app.canvas.adjust_to_dim.set(fit_image) + + wallpaper_style = canvas_config.get("wallpaper-style", 1) + self.app.canvas.scale_option.set(wallpaper_style) + + width = self.app.guiconfig["preferences"]["width"] + height = self.app.guiconfig["preferences"]["height"] + dimensions = canvas_config.get("dimensions", [width, height]) + self.app.canvas.redraw_canvas(dimensions) + + wallpaper = canvas_config.get("wallpaper") + if wallpaper: + wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + self.app.canvas.set_wallpaper(wallpaper) + else: + self.app.canvas.redraw_canvas() + self.app.canvas.set_wallpaper(None) + + # load saved shapes + shapes_config = config.get("shapes") + if shapes_config: + shapes_config = json.loads(shapes_config) + for shape_config in shapes_config: + logging.info("loading shape: %s", shape_config) + shape_type = shape_config["type"] + try: + shape_type = ShapeType(shape_type) + coords = shape_config["iconcoords"] + data = AnnotationData( + shape_config["label"], + shape_config["fontfamily"], + shape_config["fontsize"], + shape_config["labelcolor"], + shape_config["color"], + shape_config["border"], + shape_config["width"], + shape_config["bold"], + shape_config["italic"], + shape_config["underline"], + ) + shape = Shape( + self.app, self.app.canvas, shape_type, *coords, data=data + ) + self.app.canvas.shapes[shape.id] = shape + except ValueError: + logging.exception("unknown shape: %s", shape_type) + + for tag in tags.ABOVE_WALLPAPER_TAGS: + self.app.canvas.tag_raise(tag) + + def create_new_session(self): + """ + Create a new session + + :return: nothing + """ + try: + response = self.client.create_session() + logging.info("created session: %s", response) + location_config = self.app.guiconfig["location"] + self.location = core_pb2.SessionLocation( + x=location_config["x"], + y=location_config["y"], + z=location_config["z"], + lat=location_config["lat"], + lon=location_config["lon"], + alt=location_config["alt"], + scale=location_config["scale"], + ) + self.join_session(response.session_id, query_location=False) + except grpc.RpcError as e: + show_grpc_error(e) + + def delete_session(self, session_id=None): + if session_id is None: + session_id = self.session_id + try: + response = self.client.delete_session(session_id) + logging.info("deleted session result: %s", response) + except grpc.RpcError as e: + show_grpc_error(e) + + def set_up(self): + """ + Query sessions, if there exist any, prompt whether to join one + + :return: existing sessions + """ + try: + self.client.connect() + + # get service information + response = self.client.get_services() + for service in response.services: + group_services = self.services.setdefault(service.group, set()) + group_services.add(service.name) + + # if there are no sessions, create a new session, else join a session + response = self.client.get_sessions() + logging.info("current sessions: %s", response) + sessions = response.sessions + if len(sessions) == 0: + self.create_new_session() + else: + dialog = SessionsDialog(self.app, self.app) + dialog.show() + + response = self.client.get_service_defaults(self.session_id) + self.default_services = { + x.node_type: set(x.services) for x in response.defaults + } + except grpc.RpcError as e: + show_grpc_error(e) + self.app.close() + + def edit_node(self, core_node): + try: + self.client.edit_node( + self.session_id, core_node.id, core_node.position, source=GUI_SOURCE + ) + except grpc.RpcError as e: + show_grpc_error(e) + + def start_session(self): + nodes = [x.core_node for x in self.canvas_nodes.values()] + links = [x.link for x in self.links.values()] + wlan_configs = self.get_wlan_configs_proto() + mobility_configs = self.get_mobility_configs_proto() + emane_model_configs = self.get_emane_model_configs_proto() + hooks = list(self.hooks.values()) + service_configs = self.get_service_configs_proto() + file_configs = self.get_service_file_configs_proto() + asymmetric_links = [ + x.asymmetric_link for x in self.links.values() if x.asymmetric_link + ] + if self.emane_config: + emane_config = {x: self.emane_config[x].value for x in self.emane_config} + else: + emane_config = None + + start = time.perf_counter() + try: + response = self.client.start_session( + self.session_id, + nodes, + links, + self.location, + hooks, + emane_config, + emane_model_configs, + wlan_configs, + mobility_configs, + service_configs, + file_configs, + asymmetric_links, + ) + self.set_metadata() + process_time = time.perf_counter() - start + logging.debug( + "start session(%s), result: %s", self.session_id, response.result + ) + self.app.statusbar.start_session_callback(process_time) + + # display mobility players + for node_id, config in self.mobility_configs.items(): + canvas_node = self.canvas_nodes[node_id] + mobility_player = MobilityPlayer( + self.app, self.app, canvas_node, config + ) + mobility_player.show() + self.mobility_players[node_id] = mobility_player + except grpc.RpcError as e: + show_grpc_error(e) + + def stop_session(self, session_id=None): + if not session_id: + session_id = self.session_id + start = time.perf_counter() + try: + response = self.client.stop_session(session_id) + logging.debug( + "stopped session(%s), result: %s", session_id, response.result + ) + process_time = time.perf_counter() - start + self.app.statusbar.stop_session_callback(process_time) + except grpc.RpcError as e: + show_grpc_error(e) + + def set_metadata(self): + # create canvas data + wallpaper = None + if self.app.canvas.wallpaper_file: + wallpaper = Path(self.app.canvas.wallpaper_file).name + canvas_config = { + "wallpaper": wallpaper, + "wallpaper-style": self.app.canvas.scale_option.get(), + "gridlines": self.app.canvas.show_grid.get(), + "fit_image": self.app.canvas.adjust_to_dim.get(), + "dimensions": self.app.canvas.current_dimensions, + } + canvas_config = json.dumps(canvas_config) + + # create shapes data + shapes = [] + for shape in self.app.canvas.shapes.values(): + shapes.append(shape.metadata()) + shapes = json.dumps(shapes) + + metadata = {"canvas": canvas_config, "shapes": shapes} + response = self.client.set_session_metadata(self.session_id, metadata) + logging.info("set session metadata: %s", response) + + def launch_terminal(self, node_id): + try: + response = self.client.get_node_terminal(self.session_id, node_id) + logging.info("get terminal %s", response.terminal) + os.system(f"xterm -e {response.terminal} &") + except grpc.RpcError as e: + show_grpc_error(e) + + def save_xml(self, file_path): + """ + Save core session as to an xml file + + :param str file_path: file path that user pick + :return: nothing + """ + try: + if self.state != core_pb2.SessionState.RUNTIME: + logging.debug( + "session state not runtime, send session data to the daemon..." + ) + self.send_data() + response = self.client.save_xml(self.session_id, file_path) + logging.info("saved xml(%s): %s", file_path, response) + except grpc.RpcError as e: + show_grpc_error(e) + + def open_xml(self, file_path): + """ + Open core xml + + :param str file_path: file to open + :return: session id + """ + try: + response = self.client.open_xml(file_path) + logging.debug("open xml: %s", response) + self.join_session(response.session_id) + except grpc.RpcError as e: + show_grpc_error(e) + + def get_node_service(self, node_id, service_name): + response = self.client.get_node_service(self.session_id, node_id, service_name) + logging.debug("get node service %s", response) + return response.service + + def set_node_service(self, node_id, service_name, startups, validations, shutdowns): + response = self.client.set_node_service( + self.session_id, node_id, service_name, startups, validations, shutdowns + ) + logging.debug("set node service %s", response) + response = self.client.get_node_service(self.session_id, node_id, service_name) + logging.debug("get node service : %s", response) + return response.service + + def get_node_service_file(self, node_id, service_name, file_name): + response = self.client.get_node_service_file( + self.session_id, node_id, service_name, file_name + ) + logging.debug("get service file %s", response) + return response.data + + def set_node_service_file(self, node_id, service_name, file_name, data): + response = self.client.set_node_service_file( + self.session_id, node_id, service_name, file_name, data + ) + logging.debug("set node service file %s", response) + + def create_nodes_and_links(self): + """ + create nodes and links that have not been created yet + + :return: nothing + """ + node_protos = [x.core_node for x in self.canvas_nodes.values()] + link_protos = [x.link for x in self.links.values()] + if self.state != core_pb2.SessionState.DEFINITION: + self.client.set_session_state( + self.session_id, core_pb2.SessionState.DEFINITION + ) + + self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) + for node_proto in node_protos: + response = self.client.add_node(self.session_id, node_proto) + logging.debug("create node: %s", response) + for link_proto in link_protos: + response = self.client.add_link( + self.session_id, + link_proto.node_one_id, + link_proto.node_two_id, + link_proto.interface_one, + link_proto.interface_two, + link_proto.options, + ) + logging.debug("create link: %s", response) + + def send_data(self): + """ + send to daemon all session info, but don't start the session + + :return: nothing + """ + self.create_nodes_and_links() + for config_proto in self.get_wlan_configs_proto(): + self.client.set_wlan_config( + self.session_id, config_proto.node_id, config_proto.config + ) + for config_proto in self.get_mobility_configs_proto(): + self.client.set_mobility_config( + self.session_id, config_proto.node_id, config_proto.config + ) + for config_proto in self.get_service_configs_proto(): + self.client.set_node_service( + self.session_id, + config_proto.node_id, + config_proto.service, + config_proto.startup, + config_proto.validate, + config_proto.shutdown, + ) + for config_proto in self.get_service_file_configs_proto(): + self.client.set_node_service_file( + self.session_id, + config_proto.node_id, + config_proto.service, + config_proto.file, + config_proto.data, + ) + for hook in self.hooks.values(): + self.client.add_hook(self.session_id, hook.state, hook.file, hook.data) + for config_proto in self.get_emane_model_configs_proto(): + self.client.set_emane_model_config( + self.session_id, + config_proto.node_id, + config_proto.model, + config_proto.config, + config_proto.interface_id, + ) + if self.emane_config: + config = {x: self.emane_config[x].value for x in self.emane_config} + self.client.set_emane_config(self.session_id, config) + + def close(self): + """ + Clean ups when done using grpc + + :return: nothing + """ + logging.debug("close grpc") + self.client.close() + + def next_node_id(self): + """ + Get the next usable node id. + + :return: the next id to be used + :rtype: int + """ + i = 1 + while True: + if i not in self.canvas_nodes: + break + i += 1 + return i + + def create_node(self, x, y, node_type, model): + """ + Add node, with information filled in, to grpc manager + + :param int x: x coord + :param int y: y coord + :param core_pb2.NodeType node_type: node type + :param str model: node model + :return: nothing + """ + node_id = self.next_node_id() + position = core_pb2.Position(x=x, y=y) + image = None + if NodeUtils.is_image_node(node_type): + image = "ubuntu:latest" + emane = None + if node_type == core_pb2.NodeType.EMANE: + emane = self.emane_models[0] + node = core_pb2.Node( + id=node_id, + type=node_type, + name=f"n{node_id}", + model=model, + position=position, + image=image, + emane=emane, + ) + logging.debug( + "adding node to core session: %s, coords: (%s, %s), name: %s", + self.session_id, + x, + y, + node.name, + ) + return node + + def delete_graph_nodes(self, canvas_nodes): + """ + remove the nodes selected by the user and anything related to that node + such as link, configurations, interfaces + + :param list canvas_nodes: list of nodes to delete + :return: nothing + """ + edges = set() + for canvas_node in canvas_nodes: + node_id = canvas_node.core_node.id + if node_id not in self.canvas_nodes: + logging.error("unknown node: %s", node_id) + continue + del self.canvas_nodes[node_id] + if node_id in self.mobility_configs: + del self.mobility_configs[node_id] + if node_id in self.wlan_configs: + del self.wlan_configs[node_id] + for key in list(self.emane_model_configs): + node_id, _, _ = key + if node_id == node_id: + del self.emane_model_configs[key] + + for edge in canvas_node.edges: + if edge in edges: + continue + edges.add(edge) + if edge.token not in self.links: + logging.error("unknown edge: %s", edge.token) + del self.links[edge.token] + + def create_interface(self, canvas_node): + node = canvas_node.core_node + ip4, ip6, prefix = self.interfaces_manager.get_ips(node.id) + interface_id = len(canvas_node.interfaces) + name = f"eth{interface_id}" + interface = core_pb2.Interface( + id=interface_id, name=name, ip4=ip4, ip4mask=prefix, ip6=ip6, ip6mask=prefix + ) + canvas_node.interfaces.append(interface) + logging.debug( + "create node(%s) interface IPv4: %s, name: %s", + node.name, + interface.ip4, + interface.name, + ) + return interface + + def create_link(self, edge, canvas_src_node, canvas_dst_node): + """ + Create core link for a pair of canvas nodes, with token referencing + the canvas edge. + + :param edge: edge for link + :param canvas_src_node: canvas node one + :param canvas_dst_node: canvas node two + + :return: nothing + """ + src_node = canvas_src_node.core_node + dst_node = canvas_dst_node.core_node + + # determine subnet + self.interfaces_manager.determine_subnet(canvas_src_node, canvas_dst_node) + + src_interface = None + if NodeUtils.is_container_node(src_node.type): + src_interface = self.create_interface(canvas_src_node) + edge.src_interface = src_interface + self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token + + dst_interface = None + if NodeUtils.is_container_node(dst_node.type): + dst_interface = self.create_interface(canvas_dst_node) + edge.dst_interface = dst_interface + self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token + + link = core_pb2.Link( + type=core_pb2.LinkType.WIRED, + node_one_id=src_node.id, + node_two_id=dst_node.id, + interface_one=src_interface, + interface_two=dst_interface, + ) + edge.set_link(link) + self.links[edge.token] = edge + + def get_wlan_configs_proto(self): + configs = [] + for node_id, config in self.wlan_configs.items(): + config = {x: config[x].value for x in config} + wlan_config = core_pb2.WlanConfig(node_id=node_id, config=config) + configs.append(wlan_config) + return configs + + def get_mobility_configs_proto(self): + configs = [] + for node_id, config in self.mobility_configs.items(): + config = {x: config[x].value for x in config} + mobility_config = core_pb2.MobilityConfig(node_id=node_id, config=config) + configs.append(mobility_config) + return configs + + def get_emane_model_configs_proto(self): + configs = [] + for key, config in self.emane_model_configs.items(): + node_id, model, interface = key + config = {x: config[x].value for x in config} + if interface is None: + interface = -1 + config_proto = core_pb2.EmaneModelConfig( + node_id=node_id, interface_id=interface, model=model, config=config + ) + configs.append(config_proto) + return configs + + def get_service_configs_proto(self): + configs = [] + for node_id, services in self.service_configs.items(): + for name, config in services.items(): + config_proto = core_pb2.ServiceConfig( + node_id=node_id, + service=name, + startup=config.startup, + validate=config.validate, + shutdown=config.shutdown, + ) + configs.append(config_proto) + return configs + + def get_service_file_configs_proto(self): + configs = [] + for (node_id, file_configs) in self.file_configs.items(): + for service, file_config in file_configs.items(): + for file, data in file_config.items(): + config_proto = core_pb2.ServiceFileConfig( + node_id=node_id, service=service, file=file, data=data + ) + configs.append(config_proto) + return configs + + def run(self, node_id): + 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): + 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): + config = self.mobility_configs.get(node_id) + if not config: + response = self.client.get_mobility_config(self.session_id, node_id) + config = response.config + return config + + def get_emane_model_config(self, node_id, model, interface=None): + logging.info("getting emane model config: %s %s %s", node_id, model, interface) + config = self.emane_model_configs.get((node_id, model, interface)) + if not config: + if interface is None: + interface = -1 + response = self.client.get_emane_model_config( + self.session_id, node_id, model, interface + ) + config = response.config + return config + + def set_emane_model_config(self, node_id, model, config, interface=None): + logging.info("setting emane model config: %s %s %s", node_id, model, interface) + self.emane_model_configs[(node_id, model, interface)] = config diff --git a/coretk/coretk/data/backgrounds/sample1-bg.gif b/coretk/coretk/data/backgrounds/sample1-bg.gif new file mode 100644 index 00000000..98344744 Binary files /dev/null and b/coretk/coretk/data/backgrounds/sample1-bg.gif differ diff --git a/coretk/coretk/data/backgrounds/sample4-bg.jpg b/coretk/coretk/data/backgrounds/sample4-bg.jpg new file mode 100644 index 00000000..2b3d29dc Binary files /dev/null and b/coretk/coretk/data/backgrounds/sample4-bg.jpg differ diff --git a/coretk/coretk/data/icons/OVS.gif b/coretk/coretk/data/icons/OVS.gif new file mode 100755 index 00000000..38fcbb2e Binary files /dev/null and b/coretk/coretk/data/icons/OVS.gif differ diff --git a/coretk/coretk/data/icons/alert.png b/coretk/coretk/data/icons/alert.png new file mode 100644 index 00000000..718fa9f1 Binary files /dev/null and b/coretk/coretk/data/icons/alert.png differ diff --git a/coretk/coretk/data/icons/antenna.gif b/coretk/coretk/data/icons/antenna.gif new file mode 100644 index 00000000..55814324 Binary files /dev/null and b/coretk/coretk/data/icons/antenna.gif differ diff --git a/coretk/coretk/data/icons/core-icon.png b/coretk/coretk/data/icons/core-icon.png new file mode 100644 index 00000000..0b0ff5aa Binary files /dev/null and b/coretk/coretk/data/icons/core-icon.png differ diff --git a/coretk/coretk/data/icons/docker.png b/coretk/coretk/data/icons/docker.png new file mode 100644 index 00000000..6727a58b Binary files /dev/null and b/coretk/coretk/data/icons/docker.png differ diff --git a/coretk/coretk/data/icons/document-new.gif b/coretk/coretk/data/icons/document-new.gif new file mode 100644 index 00000000..570b45e6 Binary files /dev/null and b/coretk/coretk/data/icons/document-new.gif differ diff --git a/coretk/coretk/data/icons/document-properties.gif b/coretk/coretk/data/icons/document-properties.gif new file mode 100644 index 00000000..732d8436 Binary files /dev/null and b/coretk/coretk/data/icons/document-properties.gif differ diff --git a/coretk/coretk/data/icons/document-save.gif b/coretk/coretk/data/icons/document-save.gif new file mode 100644 index 00000000..165bcb90 Binary files /dev/null and b/coretk/coretk/data/icons/document-save.gif differ diff --git a/coretk/coretk/data/icons/edit-delete.gif b/coretk/coretk/data/icons/edit-delete.gif new file mode 100644 index 00000000..d23f758c Binary files /dev/null and b/coretk/coretk/data/icons/edit-delete.gif differ diff --git a/coretk/coretk/data/icons/edit-node.png b/coretk/coretk/data/icons/edit-node.png new file mode 100644 index 00000000..28490eff Binary files /dev/null and b/coretk/coretk/data/icons/edit-node.png differ diff --git a/coretk/coretk/data/icons/emane.png b/coretk/coretk/data/icons/emane.png new file mode 100644 index 00000000..1baa933d Binary files /dev/null and b/coretk/coretk/data/icons/emane.png differ diff --git a/coretk/coretk/data/icons/fileopen.gif b/coretk/coretk/data/icons/fileopen.gif new file mode 100644 index 00000000..fb744207 Binary files /dev/null and b/coretk/coretk/data/icons/fileopen.gif differ diff --git a/coretk/coretk/data/icons/host.png b/coretk/coretk/data/icons/host.png new file mode 100644 index 00000000..e6efda08 Binary files /dev/null and b/coretk/coretk/data/icons/host.png differ diff --git a/coretk/coretk/data/icons/hub.png b/coretk/coretk/data/icons/hub.png new file mode 100644 index 00000000..c9a2523b Binary files /dev/null and b/coretk/coretk/data/icons/hub.png differ diff --git a/coretk/coretk/data/icons/lanswitch.png b/coretk/coretk/data/icons/lanswitch.png new file mode 100644 index 00000000..eb9ba593 Binary files /dev/null and b/coretk/coretk/data/icons/lanswitch.png differ diff --git a/coretk/coretk/data/icons/link.png b/coretk/coretk/data/icons/link.png new file mode 100644 index 00000000..d6b6745b Binary files /dev/null and b/coretk/coretk/data/icons/link.png differ diff --git a/coretk/coretk/data/icons/lxc.png b/coretk/coretk/data/icons/lxc.png new file mode 100644 index 00000000..7b515142 Binary files /dev/null and b/coretk/coretk/data/icons/lxc.png differ diff --git a/coretk/coretk/data/icons/marker.png b/coretk/coretk/data/icons/marker.png new file mode 100644 index 00000000..8c60bacb Binary files /dev/null and b/coretk/coretk/data/icons/marker.png differ diff --git a/coretk/coretk/data/icons/markerclear.png b/coretk/coretk/data/icons/markerclear.png new file mode 100644 index 00000000..6f58c005 Binary files /dev/null and b/coretk/coretk/data/icons/markerclear.png differ diff --git a/coretk/coretk/data/icons/mdr.png b/coretk/coretk/data/icons/mdr.png new file mode 100644 index 00000000..b0678ee7 Binary files /dev/null and b/coretk/coretk/data/icons/mdr.png differ diff --git a/coretk/coretk/data/icons/observe.gif b/coretk/coretk/data/icons/observe.gif new file mode 100644 index 00000000..6b66e730 Binary files /dev/null and b/coretk/coretk/data/icons/observe.gif differ diff --git a/coretk/coretk/data/icons/oval.png b/coretk/coretk/data/icons/oval.png new file mode 100644 index 00000000..1babf1b7 Binary files /dev/null and b/coretk/coretk/data/icons/oval.png differ diff --git a/coretk/coretk/data/icons/pause.png b/coretk/coretk/data/icons/pause.png new file mode 100644 index 00000000..9ac4e6ea Binary files /dev/null and b/coretk/coretk/data/icons/pause.png differ diff --git a/coretk/coretk/data/icons/pc.png b/coretk/coretk/data/icons/pc.png new file mode 100644 index 00000000..3f587e70 Binary files /dev/null and b/coretk/coretk/data/icons/pc.png differ diff --git a/coretk/coretk/data/icons/plot.gif b/coretk/coretk/data/icons/plot.gif new file mode 100644 index 00000000..3924adbf Binary files /dev/null and b/coretk/coretk/data/icons/plot.gif differ diff --git a/coretk/coretk/data/icons/prouter.png b/coretk/coretk/data/icons/prouter.png new file mode 100644 index 00000000..b0ccf664 Binary files /dev/null and b/coretk/coretk/data/icons/prouter.png differ diff --git a/coretk/coretk/data/icons/rectangle.png b/coretk/coretk/data/icons/rectangle.png new file mode 100644 index 00000000..ca6c8c06 Binary files /dev/null and b/coretk/coretk/data/icons/rectangle.png differ diff --git a/coretk/coretk/data/icons/rj45.png b/coretk/coretk/data/icons/rj45.png new file mode 100644 index 00000000..c9d87cfd Binary files /dev/null and b/coretk/coretk/data/icons/rj45.png differ diff --git a/coretk/coretk/data/icons/router.png b/coretk/coretk/data/icons/router.png new file mode 100644 index 00000000..1de5014a Binary files /dev/null and b/coretk/coretk/data/icons/router.png differ diff --git a/coretk/coretk/data/icons/run.png b/coretk/coretk/data/icons/run.png new file mode 100644 index 00000000..a39a997f Binary files /dev/null and b/coretk/coretk/data/icons/run.png differ diff --git a/coretk/coretk/data/icons/select.png b/coretk/coretk/data/icons/select.png new file mode 100644 index 00000000..04e18891 Binary files /dev/null and b/coretk/coretk/data/icons/select.png differ diff --git a/coretk/coretk/data/icons/start.png b/coretk/coretk/data/icons/start.png new file mode 100644 index 00000000..719f4cd9 Binary files /dev/null and b/coretk/coretk/data/icons/start.png differ diff --git a/coretk/coretk/data/icons/stop.png b/coretk/coretk/data/icons/stop.png new file mode 100644 index 00000000..1e87c929 Binary files /dev/null and b/coretk/coretk/data/icons/stop.png differ diff --git a/coretk/coretk/data/icons/text.png b/coretk/coretk/data/icons/text.png new file mode 100644 index 00000000..14a85dc0 Binary files /dev/null and b/coretk/coretk/data/icons/text.png differ diff --git a/coretk/coretk/data/icons/tunnel.png b/coretk/coretk/data/icons/tunnel.png new file mode 100644 index 00000000..2871b74f Binary files /dev/null and b/coretk/coretk/data/icons/tunnel.png differ diff --git a/coretk/coretk/data/icons/twonode.png b/coretk/coretk/data/icons/twonode.png new file mode 100644 index 00000000..6828db8e Binary files /dev/null and b/coretk/coretk/data/icons/twonode.png differ diff --git a/coretk/coretk/data/icons/wlan.png b/coretk/coretk/data/icons/wlan.png new file mode 100644 index 00000000..db979a09 Binary files /dev/null and b/coretk/coretk/data/icons/wlan.png differ diff --git a/coretk/coretk/data/mobility/sample1.scen b/coretk/coretk/data/mobility/sample1.scen new file mode 100644 index 00000000..c2fc5a44 --- /dev/null +++ b/coretk/coretk/data/mobility/sample1.scen @@ -0,0 +1,28 @@ +# +# nodes: 4, max time: 27.000000, max x: 600.00, max y: 600.00 +# nominal range: 300.00 link bw: 54000000.00 +# pause: 30.00, min speed 1.50 max speed: 4.50 + +$node_(6) set X_ 780.0 +$node_(6) set Y_ 228.0 +$node_(6) set Z_ 0.00 +$node_(7) set X_ 816.0 +$node_(7) set Y_ 348.0 +$node_(7) set Z_ 0.00 +$node_(8) set X_ 672.0 +$node_(8) set Y_ 420.0 +$node_(8) set Z_ 0.00 +$node_(9) set X_ 672.0 +$node_(9) set Y_ 96.0 +$node_(9) set Z_ 0.00 +$ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0" +$ns_ at 2.00 "$node_(7) setdest 400.0 288.0 15.0" +$ns_ at 1.00 "$node_(8) setdest 590.0 520.0 17.0" +$ns_ at 3.00 "$node_(9) setdest 720.0 300.0 20.0" +$ns_ at 8.00 "$node_(7) setdest 600.0 350.0 10.0" +$ns_ at 9.00 "$node_(8) setdest 730.0 300.0 15.0" +$ns_ at 10.00 "$node_(6) setdest 600.0 108.0 10.0" +$ns_ at 16.00 "$node_(9) setdest 672.0 96.0 20.0" +$ns_ at 17.00 "$node_(7) setdest 816.0 348.0 20.0" +$ns_ at 18.00 "$node_(6) setdest 780.0 228.0 25.0" +$ns_ at 22.00 "$node_(8) setdest 672.0 420.0 20.0" diff --git a/coretk/coretk/data/oldicons/docker.gif b/coretk/coretk/data/oldicons/docker.gif new file mode 100644 index 00000000..dde25750 Binary files /dev/null and b/coretk/coretk/data/oldicons/docker.gif differ diff --git a/coretk/coretk/data/oldicons/emane.gif b/coretk/coretk/data/oldicons/emane.gif new file mode 100644 index 00000000..0531a932 Binary files /dev/null and b/coretk/coretk/data/oldicons/emane.gif differ diff --git a/coretk/coretk/data/oldicons/host.gif b/coretk/coretk/data/oldicons/host.gif new file mode 100644 index 00000000..5bd60ae3 Binary files /dev/null and b/coretk/coretk/data/oldicons/host.gif differ diff --git a/coretk/coretk/data/oldicons/hub.gif b/coretk/coretk/data/oldicons/hub.gif new file mode 100644 index 00000000..17f7c4d3 Binary files /dev/null and b/coretk/coretk/data/oldicons/hub.gif differ diff --git a/coretk/coretk/data/oldicons/lanswitch.gif b/coretk/coretk/data/oldicons/lanswitch.gif new file mode 100644 index 00000000..38fcbb2e Binary files /dev/null and b/coretk/coretk/data/oldicons/lanswitch.gif differ diff --git a/coretk/coretk/data/oldicons/link.gif b/coretk/coretk/data/oldicons/link.gif new file mode 100644 index 00000000..55532ecf Binary files /dev/null and b/coretk/coretk/data/oldicons/link.gif differ diff --git a/coretk/coretk/data/oldicons/lxc.gif b/coretk/coretk/data/oldicons/lxc.gif new file mode 100644 index 00000000..e9f57aa3 Binary files /dev/null and b/coretk/coretk/data/oldicons/lxc.gif differ diff --git a/coretk/coretk/data/oldicons/marker.gif b/coretk/coretk/data/oldicons/marker.gif new file mode 100644 index 00000000..d7ed153c Binary files /dev/null and b/coretk/coretk/data/oldicons/marker.gif differ diff --git a/coretk/coretk/data/oldicons/mdr.gif b/coretk/coretk/data/oldicons/mdr.gif new file mode 100644 index 00000000..d6762f65 Binary files /dev/null and b/coretk/coretk/data/oldicons/mdr.gif differ diff --git a/coretk/coretk/data/oldicons/oval.gif b/coretk/coretk/data/oldicons/oval.gif new file mode 100644 index 00000000..4b3124d4 Binary files /dev/null and b/coretk/coretk/data/oldicons/oval.gif differ diff --git a/coretk/coretk/data/oldicons/pc.gif b/coretk/coretk/data/oldicons/pc.gif new file mode 100644 index 00000000..743fb500 Binary files /dev/null and b/coretk/coretk/data/oldicons/pc.gif differ diff --git a/coretk/coretk/data/oldicons/rectangle.gif b/coretk/coretk/data/oldicons/rectangle.gif new file mode 100644 index 00000000..ed271f57 Binary files /dev/null and b/coretk/coretk/data/oldicons/rectangle.gif differ diff --git a/coretk/coretk/data/oldicons/rj45.gif b/coretk/coretk/data/oldicons/rj45.gif new file mode 100644 index 00000000..9ab7ac56 Binary files /dev/null and b/coretk/coretk/data/oldicons/rj45.gif differ diff --git a/coretk/coretk/data/oldicons/router.gif b/coretk/coretk/data/oldicons/router.gif new file mode 100644 index 00000000..eaf145eb Binary files /dev/null and b/coretk/coretk/data/oldicons/router.gif differ diff --git a/coretk/coretk/data/oldicons/router_green.gif b/coretk/coretk/data/oldicons/router_green.gif new file mode 100644 index 00000000..76e3ecd5 Binary files /dev/null and b/coretk/coretk/data/oldicons/router_green.gif differ diff --git a/coretk/coretk/data/oldicons/run.gif b/coretk/coretk/data/oldicons/run.gif new file mode 100644 index 00000000..71dcc67e Binary files /dev/null and b/coretk/coretk/data/oldicons/run.gif differ diff --git a/coretk/coretk/data/oldicons/select.gif b/coretk/coretk/data/oldicons/select.gif new file mode 100644 index 00000000..bb7e128c Binary files /dev/null and b/coretk/coretk/data/oldicons/select.gif differ diff --git a/coretk/coretk/data/oldicons/start.gif b/coretk/coretk/data/oldicons/start.gif new file mode 100644 index 00000000..a73c36fb Binary files /dev/null and b/coretk/coretk/data/oldicons/start.gif differ diff --git a/coretk/coretk/data/oldicons/stop.gif b/coretk/coretk/data/oldicons/stop.gif new file mode 100644 index 00000000..02c28668 Binary files /dev/null and b/coretk/coretk/data/oldicons/stop.gif differ diff --git a/coretk/coretk/data/oldicons/text.gif b/coretk/coretk/data/oldicons/text.gif new file mode 100644 index 00000000..65508214 Binary files /dev/null and b/coretk/coretk/data/oldicons/text.gif differ diff --git a/coretk/coretk/data/oldicons/tunnel.gif b/coretk/coretk/data/oldicons/tunnel.gif new file mode 100644 index 00000000..d574147f Binary files /dev/null and b/coretk/coretk/data/oldicons/tunnel.gif differ diff --git a/coretk/coretk/data/oldicons/twonode.gif b/coretk/coretk/data/oldicons/twonode.gif new file mode 100644 index 00000000..28e75fac Binary files /dev/null and b/coretk/coretk/data/oldicons/twonode.gif differ diff --git a/coretk/coretk/data/oldicons/wlan.gif b/coretk/coretk/data/oldicons/wlan.gif new file mode 100644 index 00000000..56618576 Binary files /dev/null and b/coretk/coretk/data/oldicons/wlan.gif differ diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml new file mode 100644 index 00000000..afec8874 --- /dev/null +++ b/coretk/coretk/data/xmls/sample1.xml @@ -0,0 +1,1869 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.3.2/24 + ipv6 address a:3::2/64 +! +interface eth1 + ip address 10.0.5.1/24 + ipv6 address a:5::1/64 +! +router ospf + router-id 10.0.3.2 + network 10.0.3.0/24 area 0 + network 10.0.5.0/24 area 0 +! +router ospf6 + router-id 10.0.3.2 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +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 + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.1.1/24 + ipv6 address a:1::1/64 +! +interface eth1 + ip address 10.0.2.1/24 + ipv6 address a:2::1/64 +! +router ospf + router-id 10.0.1.1 + network 10.0.1.0/24 area 0 + network 10.0.2.0/24 area 0 +! +router ospf6 + router-id 10.0.1.1 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +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 + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.2.2/24 + ipv6 address a:2::2/64 +! +interface eth1 + ip address 10.0.3.1/24 + ipv6 address a:3::1/64 +! +interface eth2 + ip address 10.0.4.1/24 + ipv6 address a:4::1/64 +! +router ospf + router-id 10.0.2.2 + network 10.0.2.0/24 area 0 + network 10.0.3.0/24 area 0 + network 10.0.4.0/24 area 0 +! +router ospf6 + router-id 10.0.2.2 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 + interface eth2 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +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 + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth2.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth2.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth2.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.9/32 + ipv6 address a::9/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.9 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +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 + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.6/32 + ipv6 address a::6/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.6 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +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 + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.7/32 + ipv6 address a::7/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.7 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +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 + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.5/32 + ipv6 address a::3/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +interface eth1 + ip address 10.0.6.2/24 + !ip ospf hello-interval 2 + !ip ospf dead-interval 6 + !ip ospf retransmit-interval 5 + !ip ospf network point-to-point + ipv6 address a:6::2/64 +! +router ospf + router-id 10.0.0.5 + network 10.0.0.5/32 area 0 + network 10.0.6.0/24 area 0 + redistribute connected metric-type 1 + redistribute ospf6 metric-type 1 +! +router ospf6 + router-id 10.0.0.5 + interface eth0 area 0.0.0.0 + redistribute connected + redistribute ospf +! + + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +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 + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.8/32 + ipv6 address a::8/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.8 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +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 + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + /etc/ssh + /var/run/sshd + + + sh startsshd.sh + + + killall sshd + + + #!/bin/sh +# auto-generated by SSH service (utility.py) +ssh-keygen -q -t rsa -N "" -f /etc/ssh/ssh_host_rsa_key +chmod 655 /var/run/sshd +# wait until RSA host key has been generated to launch sshd +/usr/sbin/sshd -f /etc/ssh/sshd_config + + # auto-generated by SSH service (utility.py) +Port 22 +Protocol 2 +HostKey /etc/ssh/ssh_host_rsa_key +UsePrivilegeSeparation yes +PidFile /var/run/sshd/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 /usr/lib/openssh/sftp-server +UsePAM yes +UseDNS no + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.4.2/24 + ipv6 address a:4::2/64 +! +interface eth1 + ip address 10.0.5.2/24 + ipv6 address a:5::2/64 +! +interface eth2 + ip address 10.0.6.1/24 + ipv6 address a:6::1/64 +! +router ospf + router-id 10.0.4.2 + network 10.0.4.0/24 area 0 + network 10.0.5.0/24 area 0 + network 10.0.6.0/24 area 0 +! +router ospf6 + router-id 10.0.4.2 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 + interface eth2 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +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 + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth2.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth2.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth2.rp_filter=0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/coretk/coretk/dialogs/__init__.py b/coretk/coretk/dialogs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/coretk/coretk/dialogs/about.py b/coretk/coretk/dialogs/about.py new file mode 100644 index 00000000..9e3ff7a9 --- /dev/null +++ b/coretk/coretk/dialogs/about.py @@ -0,0 +1,44 @@ +import tkinter as tk + +from coretk.dialogs.dialog import Dialog +from coretk.widgets import CodeText + +LICENSE = """\ +Copyright (c) 2005-2020, the Boeing Company. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE.\ +""" + + +class AboutDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "About CORE", modal=True) + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + codetext = CodeText(self.top) + codetext.text.insert("1.0", LICENSE) + codetext.text.config(state=tk.DISABLED) + codetext.grid(sticky="nsew") diff --git a/coretk/coretk/dialogs/alerts.py b/coretk/coretk/dialogs/alerts.py new file mode 100644 index 00000000..e782547f --- /dev/null +++ b/coretk/coretk/dialogs/alerts.py @@ -0,0 +1,171 @@ +""" +check engine light +""" +import tkinter as tk +from tkinter import ttk + +from grpc import RpcError + +from core.api.grpc import core_pb2 +from coretk.dialogs.dialog import Dialog +from coretk.themes import PADX, PADY +from coretk.widgets import CodeText + + +class AlertsDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Alerts", modal=True) + self.app = app + self.tree = None + self.codetext = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + frame.grid(sticky="nsew", pady=PADY) + self.tree = ttk.Treeview( + frame, + columns=("time", "level", "session_id", "node", "source"), + show="headings", + ) + self.tree.grid(row=0, column=0, sticky="nsew") + self.tree.column("time", stretch=tk.YES) + self.tree.heading("time", text="Time") + self.tree.column("level", stretch=tk.YES) + self.tree.heading("level", text="Level") + self.tree.column("session_id", stretch=tk.YES) + self.tree.heading("session_id", text="Session ID") + self.tree.column("node", stretch=tk.YES) + self.tree.heading("node", text="Node") + self.tree.column("source", stretch=tk.YES) + self.tree.heading("source", text="Source") + self.tree.bind("<>", self.click_select) + + for alarm in self.app.statusbar.core_alarms: + level = self.get_level(alarm.level) + self.tree.insert( + "", + tk.END, + text=str(alarm.date), + values=( + alarm.date, + level + " (%s)" % alarm.level, + alarm.session_id, + alarm.node_id, + alarm.source, + ), + tags=(level,), + ) + + self.tree.tag_configure("ERROR", background="#ff6666") + self.tree.tag_configure("FATAL", background="#d9d9d9") + self.tree.tag_configure("WARNING", background="#ffff99") + self.tree.tag_configure("NOTICE", background="#85e085") + + yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") + self.tree.configure(yscrollcommand=yscrollbar.set) + + xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=1, sticky="ew") + self.tree.configure(xscrollcommand=xscrollbar.set) + + self.codetext = CodeText(self.top) + self.codetext.text.config(state=tk.DISABLED) + self.codetext.grid(sticky="nsew", pady=PADY) + + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.columnconfigure(3, weight=1) + button = ttk.Button(frame, text="Reset", command=self.reset_alerts) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Daemon Log", command=self.daemon_log) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Node Log") + button.grid(row=0, column=2, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Close", command=self.destroy) + button.grid(row=0, column=3, sticky="ew") + + def reset_alerts(self): + self.codetext.text.delete("1.0", tk.END) + for item in self.tree.get_children(): + self.tree.delete(item) + self.app.statusbar.core_alarms.clear() + + def daemon_log(self): + dialog = DaemonLog(self, self.app) + dialog.show() + + def get_level(self, level): + if level == core_pb2.ExceptionLevel.ERROR: + return "ERROR" + if level == core_pb2.ExceptionLevel.FATAL: + return "FATAL" + if level == core_pb2.ExceptionLevel.WARNING: + return "WARNING" + if level == core_pb2.ExceptionLevel.NOTICE: + return "NOTICE" + + def click_select(self, event): + current = self.tree.selection() + values = self.tree.item(current)["values"] + time = values[0] + level = values[1] + session_id = values[2] + node_id = values[3] + source = values[4] + text = "DATE: %s\nLEVEL: %s\nNODE: %s (%s)\nSESSION: %s\nSOURCE: %s\n\n" % ( + time, + level, + node_id, + self.app.core.canvas_nodes[node_id].core_node.name, + session_id, + source, + ) + try: + sid = self.app.core.session_id + self.app.core.client.get_node(sid, node_id) + text = text + "node created" + except RpcError: + text = text + "node not created" + self.codetext.text.delete("1.0", "end") + self.codetext.text.insert("1.0", text) + + +class DaemonLog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "core-daemon log", modal=True) + self.columnconfigure(0, weight=1) + self.path = tk.StringVar(value="/var/log/core-daemon.log") + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) + frame = ttk.Frame(self.top) + frame.grid(row=0, column=0, sticky="ew", pady=PADY) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=9) + label = ttk.Label(frame, text="File", anchor="w") + label.grid(row=0, column=0, sticky="ew") + entry = ttk.Entry(frame, textvariable=self.path, state="disabled") + entry.grid(row=0, column=1, sticky="ew") + try: + file = open("/var/log/core-daemon.log", "r") + log = file.readlines() + except FileNotFoundError: + log = "Log file not found" + codetext = CodeText(self.top) + codetext.text.insert("1.0", log) + codetext.text.see("end") + codetext.text.config(state=tk.DISABLED) + codetext.grid(row=1, column=0, sticky="nsew") diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py new file mode 100644 index 00000000..0a113936 --- /dev/null +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -0,0 +1,254 @@ +""" +size and scale +""" +import tkinter as tk +from tkinter import font, ttk + +from coretk.dialogs.dialog import Dialog +from coretk.themes import FRAME_PAD, PADX, PADY + +PIXEL_SCALE = 100 + + +class SizeAndScaleDialog(Dialog): + def __init__(self, master, app): + """ + create an instance for size and scale object + + :param app: main application + """ + super().__init__(master, app, "Canvas Size and Scale", modal=True) + self.canvas = self.app.canvas + self.validation = app.validation + self.section_font = font.Font(weight="bold") + width, height = self.canvas.current_dimensions + self.pixel_width = tk.IntVar(value=width) + self.pixel_height = tk.IntVar(value=height) + location = self.app.core.location + self.x = tk.DoubleVar(value=location.x) + self.y = tk.DoubleVar(value=location.y) + self.lat = tk.DoubleVar(value=location.lat) + self.lon = tk.DoubleVar(value=location.lon) + self.alt = tk.DoubleVar(value=location.alt) + self.scale = tk.DoubleVar(value=location.scale) + self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale) + self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale) + self.save_default = tk.BooleanVar(value=False) + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.draw_size() + self.draw_scale() + self.draw_reference_point() + self.draw_save_as_default() + self.draw_spacer() + self.draw_buttons() + + def draw_size(self): + label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) + + # draw size row 1 + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + label = ttk.Label(frame, text="Width") + label.grid(row=0, column=0, sticky="w", padx=PADX) + entry = ttk.Entry( + frame, + textvariable=self.pixel_width, + validate="key", + validatecommand=(self.validation.positive_int, "%P"), + ) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) + label = ttk.Label(frame, text="x Height") + label.grid(row=0, column=2, sticky="w", padx=PADX) + entry = ttk.Entry( + frame, + textvariable=self.pixel_height, + validate="key", + validatecommand=(self.validation.positive_int, "%P"), + ) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) + label = ttk.Label(frame, text="Pixels") + label.grid(row=0, column=4, sticky="w") + + # draw size row 2 + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + label = ttk.Label(frame, text="Width") + label.grid(row=0, column=0, sticky="w", padx=PADX) + entry = ttk.Entry( + frame, + textvariable=self.meters_width, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) + label = ttk.Label(frame, text="x Height") + label.grid(row=0, column=2, sticky="w", padx=PADX) + entry = ttk.Entry( + frame, + textvariable=self.meters_height, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) + label = ttk.Label(frame, text="Meters") + label.grid(row=0, column=4, sticky="w") + + def draw_scale(self): + label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) + + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =") + label.grid(row=0, column=0, sticky="w", padx=PADX) + entry = ttk.Entry( + frame, + textvariable=self.scale, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) + label = ttk.Label(frame, text="Meters") + label.grid(row=0, column=2, sticky="w") + + def draw_reference_point(self): + label_frame = ttk.Labelframe( + self.top, text="Reference Point", padding=FRAME_PAD + ) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) + + label = ttk.Label( + label_frame, text="Default is (0, 0), the upper left corner of the canvas" + ) + label.grid() + + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + + label = ttk.Label(frame, text="X") + label.grid(row=0, column=0, sticky="w", padx=PADX) + entry = ttk.Entry( + frame, + textvariable=self.x, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) + + label = ttk.Label(frame, text="Y") + label.grid(row=0, column=2, sticky="w", padx=PADX) + entry = ttk.Entry( + frame, + textvariable=self.y, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) + + label = ttk.Label(label_frame, text="Translates To") + label.grid() + + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + frame.columnconfigure(5, weight=1) + + label = ttk.Label(frame, text="Lat") + label.grid(row=0, column=0, sticky="w", padx=PADX) + entry = ttk.Entry( + frame, + textvariable=self.lat, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) + + label = ttk.Label(frame, text="Lon") + label.grid(row=0, column=2, sticky="w", padx=PADX) + entry = ttk.Entry( + frame, + textvariable=self.lon, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) + + label = ttk.Label(frame, text="Alt") + label.grid(row=0, column=4, sticky="w", padx=PADX) + entry = ttk.Entry( + frame, + textvariable=self.alt, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry.grid(row=0, column=5, sticky="ew") + + def draw_save_as_default(self): + button = ttk.Checkbutton( + self.top, text="Save as default?", variable=self.save_default + ) + button.grid(sticky="w", pady=PADY) + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(sticky="ew") + + 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="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_apply(self): + width, height = self.pixel_width.get(), self.pixel_height.get() + self.canvas.redraw_canvas((width, height)) + if self.canvas.wallpaper: + self.canvas.redraw_wallpaper() + location = self.app.core.location + location.x = self.x.get() + location.y = self.y.get() + location.lat = self.lat.get() + location.lon = self.lon.get() + location.alt = self.alt.get() + location.scale = self.scale.get() + if self.save_default.get(): + location_config = self.app.guiconfig["location"] + location_config["x"] = location.x + location_config["y"] = location.y + location_config["z"] = location.z + location_config["lat"] = location.lat + location_config["lon"] = location.lon + location_config["alt"] = location.alt + location_config["scale"] = location.scale + preferences = self.app.guiconfig["preferences"] + preferences["width"] = width + preferences["height"] = height + self.app.save_config() + self.destroy() diff --git a/coretk/coretk/dialogs/canvaswallpaper.py b/coretk/coretk/dialogs/canvaswallpaper.py new file mode 100644 index 00000000..570bfa08 --- /dev/null +++ b/coretk/coretk/dialogs/canvaswallpaper.py @@ -0,0 +1,179 @@ +""" +set wallpaper +""" +import logging +import tkinter as tk +from tkinter import ttk + +from coretk.appconfig import BACKGROUNDS_PATH +from coretk.dialogs.dialog import Dialog +from coretk.images import Images +from coretk.themes import PADX, PADY +from coretk.widgets import image_chooser + + +class CanvasBackgroundDialog(Dialog): + def __init__(self, master, app): + """ + create an instance of CanvasWallpaper object + + :param coretk.app.Application app: root application + """ + super().__init__(master, app, "Canvas Background", modal=True) + self.canvas = self.app.canvas + self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) + self.show_grid = tk.BooleanVar(value=self.canvas.show_grid.get()) + self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get()) + self.filename = tk.StringVar(value=self.canvas.wallpaper_file) + self.image_label = None + self.options = [] + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.draw_image() + self.draw_image_label() + self.draw_image_selection() + self.draw_options() + self.draw_additional_options() + self.draw_spacer() + self.draw_buttons() + + def draw_image(self): + self.image_label = ttk.Label( + self.top, text="(image preview)", width=32, anchor=tk.CENTER + ) + self.image_label.grid(pady=PADY) + + def draw_image_label(self): + label = ttk.Label(self.top, text="Image filename: ") + label.grid(sticky="ew") + if self.filename.get(): + self.draw_preview() + + def draw_image_selection(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=2) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.grid(sticky="ew") + + entry = ttk.Entry(frame, textvariable=self.filename) + entry.focus() + entry.grid(row=0, column=0, sticky="ew", padx=PADX) + + button = ttk.Button(frame, text="...", command=self.click_open_image) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + + button = ttk.Button(frame, text="Clear", command=self.click_clear) + button.grid(row=0, column=2, sticky="ew") + + def draw_options(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.columnconfigure(3, weight=1) + frame.grid(sticky="ew") + + button = ttk.Radiobutton( + frame, text="upper-left", value=1, variable=self.scale_option + ) + button.grid(row=0, column=0, sticky="ew") + self.options.append(button) + + button = ttk.Radiobutton( + frame, text="centered", value=2, variable=self.scale_option + ) + button.grid(row=0, column=1, sticky="ew") + self.options.append(button) + + button = ttk.Radiobutton( + frame, text="scaled", value=3, variable=self.scale_option + ) + button.grid(row=0, column=2, sticky="ew") + self.options.append(button) + + button = ttk.Radiobutton( + frame, text="titled", value=4, variable=self.scale_option + ) + button.grid(row=0, column=3, sticky="ew") + self.options.append(button) + + def draw_additional_options(self): + checkbutton = ttk.Checkbutton( + self.top, text="Show grid", variable=self.show_grid + ) + checkbutton.grid(sticky="ew", padx=PADX) + + checkbutton = ttk.Checkbutton( + self.top, + text="Adjust canvas size to image dimensions", + variable=self.adjust_to_dim, + command=self.click_adjust_canvas, + ) + checkbutton.grid(sticky="ew", padx=PADX) + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(pady=PADY, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, 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="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_open_image(self): + filename = image_chooser(self, BACKGROUNDS_PATH) + if filename: + self.filename.set(filename) + self.draw_preview() + + def draw_preview(self): + image = Images.create(self.filename.get(), 250, 135) + self.image_label.config(image=image) + self.image_label.image = image + + def click_clear(self): + """ + delete like shown in image link entry if there is any + + :return: nothing + """ + # delete entry + self.filename.set("") + # delete display image + self.image_label.config(image="", width=32) + self.image_label.image = None + + def click_adjust_canvas(self): + # deselect all radio buttons and grey them out + if self.adjust_to_dim.get(): + self.scale_option.set(0) + for option in self.options: + option.config(state=tk.DISABLED) + # turn back the radio button to active state so that user can choose again + else: + self.scale_option.set(1) + for option in self.options: + option.config(state=tk.NORMAL) + + def click_apply(self): + self.canvas.scale_option.set(self.scale_option.get()) + self.canvas.show_grid.set(self.show_grid.get()) + self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) + self.canvas.update_grid() + + filename = self.filename.get() + if not filename: + filename = None + + try: + self.canvas.set_wallpaper(filename) + except FileNotFoundError: + logging.error("invalid background: %s", filename) + + self.destroy() diff --git a/coretk/coretk/dialogs/colorpicker.py b/coretk/coretk/dialogs/colorpicker.py new file mode 100644 index 00000000..9734468d --- /dev/null +++ b/coretk/coretk/dialogs/colorpicker.py @@ -0,0 +1,251 @@ +""" +custom color picker +""" +import logging +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + + +class ColorPicker(Dialog): + def __init__(self, master, app, initcolor="#000000"): + super().__init__(master, app, "color picker", modal=True) + self.red_entry = None + self.blue_entry = None + self.green_entry = None + self.hex_entry = None + self.red_label = None + self.green_label = None + self.blue_label = None + self.display = None + self.color = initcolor + red, green, blue = self.get_rgb(initcolor) + self.red = tk.IntVar(value=red) + self.blue = tk.IntVar(value=blue) + self.green = tk.IntVar(value=green) + self.hex = tk.StringVar(value=initcolor) + self.red_scale = tk.IntVar(value=red) + self.green_scale = tk.IntVar(value=green) + self.blue_scale = tk.IntVar(value=blue) + self.draw() + self.set_bindings() + + def askcolor(self): + self.show() + return self.color + + def draw(self): + self.top.columnconfigure(0, weight=1) + # rgb frames + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=6) + frame.columnconfigure(3, weight=2) + label = ttk.Label(frame, text="R: ") + label.grid(row=0, column=0) + self.red_entry = ttk.Entry( + frame, + width=4, + textvariable=self.red, + validate="key", + validatecommand=(self.app.validation.rgb, "%P"), + ) + self.red_entry.grid(row=0, column=1, sticky="nsew") + scale = ttk.Scale( + frame, + from_=0, + to=255, + value=0, + # length=200, + orient=tk.HORIZONTAL, + variable=self.red_scale, + command=lambda x: self.scale_callback(self.red_scale, self.red), + ) + scale.grid(row=0, column=2, sticky="nsew") + self.red_label = ttk.Label( + frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5 + ) + self.red_label.grid(row=0, column=3, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=6) + frame.columnconfigure(3, weight=2) + label = ttk.Label(frame, text="G: ") + label.grid(row=0, column=0) + self.green_entry = ttk.Entry( + frame, + width=4, + textvariable=self.green, + validate="key", + validatecommand=(self.app.validation.rgb, "%P"), + ) + self.green_entry.grid(row=0, column=1, sticky="nsew") + scale = ttk.Scale( + frame, + from_=0, + to=255, + value=0, + # length=200, + orient=tk.HORIZONTAL, + variable=self.green_scale, + command=lambda x: self.scale_callback(self.green_scale, self.green), + ) + scale.grid(row=0, column=2, sticky="nsew") + self.green_label = ttk.Label( + frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5 + ) + self.green_label.grid(row=0, column=3, sticky="nsew") + frame.grid(row=1, column=0, sticky="nsew") + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=6) + frame.columnconfigure(3, weight=2) + label = ttk.Label(frame, text="B: ") + label.grid(row=0, column=0) + self.blue_entry = ttk.Entry( + frame, + width=4, + textvariable=self.blue, + validate="key", + validatecommand=(self.app.validation.rgb, "%P"), + ) + self.blue_entry.grid(row=0, column=1, sticky="nsew") + scale = ttk.Scale( + frame, + from_=0, + to=255, + value=0, + # length=200, + orient=tk.HORIZONTAL, + variable=self.blue_scale, + command=lambda x: self.scale_callback(self.blue_scale, self.blue), + ) + scale.grid(row=0, column=2, sticky="nsew") + self.blue_label = ttk.Label( + frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5 + ) + self.blue_label.grid(row=0, column=3, sticky="nsew") + frame.grid(row=2, column=0, sticky="nsew") + + # hex code and color display + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + label = ttk.Label(frame, text="Selection: ") + label.grid(row=0, column=0, sticky="nsew") + self.hex_entry = ttk.Entry( + frame, + textvariable=self.hex, + validate="key", + validatecommand=(self.app.validation.hex, "%P"), + ) + self.hex_entry.grid(row=1, column=0, sticky="nsew") + self.display = tk.Frame(frame, background=self.color, width=100, height=100) + self.display.grid(row=2, column=0) + frame.grid(row=3, column=0, sticky="nsew") + + # button frame + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = ttk.Button(frame, text="OK", command=self.button_ok) + button.grid(row=0, column=0, sticky="nsew") + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="nsew") + frame.grid(row=4, column=0, sticky="nsew") + + def set_bindings(self): + self.red_entry.bind("", lambda x: self.current_focus("rgb")) + self.green_entry.bind("", lambda x: self.current_focus("rgb")) + self.blue_entry.bind("", lambda x: self.current_focus("rgb")) + self.hex_entry.bind("", lambda x: self.current_focus("hex")) + self.red.trace_add("write", self.update_color) + self.green.trace_add("write", self.update_color) + self.blue.trace_add("write", self.update_color) + self.hex.trace_add("write", self.update_color) + + def button_ok(self): + logging.debug("not implemented") + self.color = self.hex.get() + self.destroy() + + def get_hex(self): + """ + convert current RGB values into hex color + + :rtype: str + :return: hex color + """ + red = self.red_entry.get() + blue = self.blue_entry.get() + green = self.green_entry.get() + return "#%02x%02x%02x" % (int(red), int(green), int(blue)) + + def current_focus(self, focus): + self.focus = focus + + def update_color(self, arg1=None, arg2=None, arg3=None): + if self.focus == "rgb": + red = self.red_entry.get() + blue = self.blue_entry.get() + green = self.green_entry.get() + self.set_scale(red, green, blue) + if red and blue and green: + hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue)) + self.hex.set(hex_code) + self.display.config(background=hex_code) + self.set_label(red, green, blue) + elif self.focus == "hex": + hex_code = self.hex.get() + if len(hex_code) == 4 or len(hex_code) == 7: + red, green, blue = self.get_rgb(hex_code) + else: + return + self.set_entry(red, green, blue) + self.set_scale(red, green, blue) + self.display.config(background=hex_code) + self.set_label(red, green, blue) + + def scale_callback(self, var, color_var): + color_var.set(var.get()) + self.focus = "rgb" + self.update_color() + + def set_scale(self, red, green, blue): + self.red_scale.set(red) + self.green_scale.set(green) + self.blue_scale.set(blue) + + def set_entry(self, red, green, blue): + self.red.set(red) + self.green.set(green) + self.blue.set(blue) + + def set_label(self, red, green, blue): + self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0)) + self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0)) + self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue))) + + def get_rgb(self, hex_code): + """ + convert a valid hex code to RGB values + + :param string hex_code: color in hex + :rtype: tuple(int, int, int) + :return: the RGB values + """ + if len(hex_code) == 4: + red = hex_code[1] + green = hex_code[2] + blue = hex_code[3] + else: + red = hex_code[1:3] + green = hex_code[3:5] + blue = hex_code[5:] + return int(red, 16), int(green, 16), int(blue, 16) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py new file mode 100644 index 00000000..94f2a32f --- /dev/null +++ b/coretk/coretk/dialogs/customnodes.py @@ -0,0 +1,261 @@ +import logging +import tkinter as tk +from pathlib import Path +from tkinter import ttk + +from coretk import nodeutils +from coretk.appconfig import ICONS_PATH +from coretk.dialogs.dialog import Dialog +from coretk.images import Images +from coretk.nodeutils import NodeDraw +from coretk.themes import FRAME_PAD, PADX, PADY +from coretk.widgets import CheckboxList, ListboxScroll, image_chooser + + +class ServicesSelectDialog(Dialog): + def __init__(self, master, app, current_services): + super().__init__(master, app, "Node Services", modal=True) + self.groups = None + self.services = None + self.current = None + self.current_services = set(current_services) + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + frame = ttk.LabelFrame(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.services): + 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) + + frame = ttk.Frame(self.top) + frame.grid(stick="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Save", command=self.destroy) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.click_cancel) + button.grid(row=0, column=1, sticky="ew") + + # trigger group change + self.groups.listbox.event_generate("<>") + + def handle_group_change(self, event): + selection = self.groups.listbox.curselection() + if selection: + index = selection[0] + group = self.groups.listbox.get(index) + self.services.clear() + for name in sorted(self.app.core.services[group]): + checked = name in self.current_services + self.services.add(name, checked) + + def service_clicked(self, name, var): + 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) + + def click_cancel(self): + self.current_services = None + self.destroy() + + +class CustomNodesDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Custom Nodes", modal=True) + self.edit_button = None + self.delete_button = None + self.nodes_list = None + self.name = tk.StringVar() + self.image_button = None + self.image = None + self.image_file = None + self.services = set() + self.selected = None + self.selected_index = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.draw_node_config() + self.draw_node_buttons() + self.draw_buttons() + + def draw_node_config(self): + frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD) + frame.grid(sticky="nsew", pady=PADY) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + self.nodes_list = ListboxScroll(frame) + self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX) + self.nodes_list.listbox.bind("<>", self.handle_node_select) + for name in sorted(self.app.core.custom_nodes): + self.nodes_list.listbox.insert(tk.END, name) + + frame = ttk.Frame(frame) + frame.grid(row=0, column=2, sticky="nsew") + frame.columnconfigure(0, weight=1) + entry = ttk.Entry(frame, textvariable=self.name) + entry.grid(sticky="ew") + self.image_button = ttk.Button( + frame, text="Icon", compound=tk.LEFT, command=self.click_icon + ) + self.image_button.grid(sticky="ew") + button = ttk.Button(frame, text="Services", command=self.click_services) + button.grid(sticky="ew") + + def draw_node_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PADY) + for i in range(3): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + + self.edit_button = ttk.Button( + frame, text="Edit", state=tk.DISABLED, command=self.click_edit + ) + self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX) + + self.delete_button = ttk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) + self.delete_button.grid(row=0, column=2, sticky="ew") + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Save", command=self.click_save) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def reset_values(self): + self.name.set("") + self.image = None + self.image_file = None + self.services = set() + self.image_button.config(image="") + + def click_icon(self): + file_path = image_chooser(self, ICONS_PATH) + if file_path: + image = Images.create(file_path, nodeutils.ICON_SIZE) + self.image = image + self.image_file = file_path + self.image_button.config(image=self.image) + + def click_services(self): + dialog = ServicesSelectDialog(self, self.app, self.services) + dialog.show() + if dialog.current_services is not None: + self.services.clear() + self.services.update(dialog.current_services) + + def click_save(self): + self.app.guiconfig["nodes"].clear() + for name in sorted(self.app.core.custom_nodes): + node_draw = self.app.core.custom_nodes[name] + self.app.guiconfig["nodes"].append( + { + "name": name, + "image": node_draw.image_file, + "services": list(node_draw.services), + } + ) + logging.info("saving custom nodes: %s", self.app.guiconfig["nodes"]) + self.app.save_config() + self.destroy() + + def click_create(self): + name = self.name.get() + if name not in self.app.core.custom_nodes: + image_file = Path(self.image_file).stem + node_draw = NodeDraw.from_custom(name, image_file, set(self.services)) + self.app.core.custom_nodes[name] = node_draw + self.nodes_list.listbox.insert(tk.END, name) + self.reset_values() + + def click_edit(self): + name = self.name.get() + if self.selected: + previous_name = self.selected + self.selected = name + node_draw = self.app.core.custom_nodes.pop(previous_name) + node_draw.model = name + node_draw.image_file = Path(self.image_file).stem + node_draw.image = self.image + node_draw.services = self.services + self.app.core.custom_nodes[name] = node_draw + self.nodes_list.listbox.delete(self.selected_index) + self.nodes_list.listbox.insert(self.selected_index, name) + self.nodes_list.listbox.selection_set(self.selected_index) + + def click_delete(self): + if self.selected and self.selected in self.app.core.custom_nodes: + self.nodes_list.listbox.delete(self.selected_index) + del self.app.core.custom_nodes[self.selected] + self.reset_values() + self.nodes_list.listbox.selection_clear(0, tk.END) + self.nodes_list.listbox.event_generate("<>") + + def handle_node_select(self, event): + selection = self.nodes_list.listbox.curselection() + if selection: + self.selected_index = selection[0] + self.selected = self.nodes_list.listbox.get(self.selected_index) + node_draw = self.app.core.custom_nodes[self.selected] + self.name.set(node_draw.model) + self.services = node_draw.services + self.image = node_draw.image + self.image_file = node_draw.image_file + self.image_button.config(image=self.image) + self.edit_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected = None + self.selected_index = None + self.edit_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py new file mode 100644 index 00000000..92d9a7db --- /dev/null +++ b/coretk/coretk/dialogs/dialog.py @@ -0,0 +1,37 @@ +import tkinter as tk +from tkinter import ttk + +from coretk.images import ImageEnum, Images +from coretk.themes import DIALOG_PAD + + +class Dialog(tk.Toplevel): + def __init__(self, master, app, title, modal=False): + super().__init__(master) + self.withdraw() + self.app = app + self.modal = modal + self.title(title) + self.protocol("WM_DELETE_WINDOW", self.destroy) + image = Images.get(ImageEnum.CORE, 16) + self.tk.call("wm", "iconphoto", self._w, image) + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.top = ttk.Frame(self, padding=DIALOG_PAD) + self.top.grid(sticky="nsew") + + def show(self): + self.transient(self.master) + self.focus_force() + self.update() + self.deiconify() + if self.modal: + self.wait_visibility() + self.grab_set() + self.wait_window() + + def draw_spacer(self, row=None): + frame = ttk.Frame(self.top) + frame.grid(row=row, sticky="nsew") + frame.rowconfigure(0, weight=1) + self.top.rowconfigure(frame.grid_info()["row"], weight=1) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py new file mode 100644 index 00000000..a1c30a6f --- /dev/null +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -0,0 +1,235 @@ +""" +emane configuration +""" +import logging +import tkinter as tk +import webbrowser +from tkinter import ttk + +import grpc + +from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error +from coretk.images import ImageEnum, Images +from coretk.themes import PADX, PADY +from coretk.widgets import ConfigFrame + + +class GlobalEmaneDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "EMANE Configuration", modal=True) + self.config_frame = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PADY) + self.draw_spacer() + self.draw_buttons() + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + 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="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_apply(self): + self.config_frame.parse_config() + self.destroy() + + +class EmaneModelDialog(Dialog): + def __init__(self, master, app, node, model, interface=None): + super().__init__(master, app, f"{node.name} {model} Configuration", modal=True) + self.node = node + self.model = f"emane_{model}" + self.interface = interface + self.config_frame = None + try: + self.config = self.app.core.get_emane_model_config( + self.node.id, self.model, self.interface + ) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PADY) + self.draw_spacer() + self.draw_buttons() + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + 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="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_apply(self): + self.config_frame.parse_config() + self.app.core.set_emane_model_config( + self.node.id, self.model, self.config, self.interface + ) + self.destroy() + + +class EmaneConfigDialog(Dialog): + def __init__(self, master, app, canvas_node): + super().__init__( + master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True + ) + self.app = app + self.canvas_node = canvas_node + self.node = canvas_node.core_node + self.radiovar = tk.IntVar() + self.radiovar.set(1) + self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models] + self.emane_model = tk.StringVar(value=self.node.emane.split("_")[1]) + self.emane_model_button = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.draw_emane_configuration() + self.draw_emane_models() + self.draw_emane_buttons() + self.draw_spacer() + self.draw_apply_and_cancel() + + def draw_emane_configuration(self): + """ + draw the main frame for emane configuration + + :return: nothing + """ + label = ttk.Label( + self.top, + text="The EMANE emulation system provides more complex wireless radio emulation " + "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", + justify=tk.CENTER, + ) + label.grid(pady=PADY) + + image = Images.get(ImageEnum.EDITNODE, 16) + button = ttk.Button( + self.top, + image=image, + text="EMANE Wiki", + compound=tk.RIGHT, + command=lambda: webbrowser.open_new( + "https://github.com/adjacentlink/emane/wiki" + ), + ) + button.image = image + button.grid(sticky="ew", pady=PADY) + + def draw_emane_models(self): + """ + create a combobox that has all the known emane models + + :return: nothing + """ + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + + label = ttk.Label(frame, text="Model") + label.grid(row=0, column=0, sticky="w") + + # create combo box and its binding + combobox = ttk.Combobox( + frame, + textvariable=self.emane_model, + values=self.emane_models, + state="readonly", + ) + combobox.grid(row=0, column=1, sticky="ew") + combobox.bind("<>", self.emane_model_change) + + def draw_emane_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PADY) + for i in range(2): + frame.columnconfigure(i, weight=1) + + image = Images.get(ImageEnum.EDITNODE, 16) + self.emane_model_button = ttk.Button( + frame, + text=f"{self.emane_model.get()} options", + image=image, + compound=tk.RIGHT, + command=self.click_model_config, + ) + self.emane_model_button.image = image + self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky="ew") + + image = Images.get(ImageEnum.EDITNODE, 16) + button = ttk.Button( + frame, + text="EMANE options", + image=image, + compound=tk.RIGHT, + command=self.click_emane_config, + ) + button.image = image + button.grid(row=0, column=1, sticky="ew") + + def draw_apply_and_cancel(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, padx=PADX, sticky="ew") + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_emane_config(self): + dialog = GlobalEmaneDialog(self, self.app) + dialog.show() + + def click_model_config(self): + """ + draw emane model configuration + + :return: nothing + """ + model_name = self.emane_model.get() + logging.info("configuring emane model: %s", model_name) + dialog = EmaneModelDialog( + self, self.app, self.canvas_node.core_node, model_name + ) + dialog.show() + + def emane_model_change(self, event): + """ + update emane model options button + + :param event: + :return: nothing + """ + model_name = self.emane_model.get() + self.emane_model_button.config(text=f"{model_name} options") + + def click_apply(self): + self.node.emane = f"emane_{self.emane_model.get()}" + self.destroy() diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py new file mode 100644 index 00000000..37503d66 --- /dev/null +++ b/coretk/coretk/dialogs/hooks.py @@ -0,0 +1,152 @@ +import tkinter as tk +from tkinter import ttk + +from core.api.grpc import core_pb2 +from coretk.dialogs.dialog import Dialog +from coretk.themes import PADX, PADY +from coretk.widgets import CodeText, ListboxScroll + + +class HookDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Hook", modal=True) + self.name = tk.StringVar() + self.codetext = None + self.hook = core_pb2.Hook() + self.state = tk.StringVar() + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) + + # name and states + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(0, weight=2) + frame.columnconfigure(1, weight=7) + frame.columnconfigure(2, weight=1) + label = ttk.Label(frame, text="Name") + label.grid(row=0, column=0, sticky="ew", padx=PADX) + entry = ttk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) + values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE") + initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) + self.state.set(initial_state) + self.name.set(f"{initial_state.lower()}_hook.sh") + combobox = ttk.Combobox( + frame, textvariable=self.state, values=values, state="readonly" + ) + combobox.grid(row=0, column=2, sticky="ew") + combobox.bind("<>", self.state_change) + + # data + self.codetext = CodeText(self.top) + self.codetext.text.insert( + 1.0, + ( + "#!/bin/sh\n" + "# session hook script; write commands here to execute on the host at the\n" + "# specified state\n" + ), + ) + self.codetext.grid(sticky="nsew", pady=PADY) + + # button row + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Save", command=lambda: self.save()) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) + button.grid(row=0, column=1, sticky="ew") + + def state_change(self, event): + state_name = self.state.get() + self.name.set(f"{state_name.lower()}_hook.sh") + + def set(self, hook): + self.hook = hook + self.name.set(hook.file) + self.codetext.text.delete(1.0, tk.END) + self.codetext.text.insert(tk.END, hook.data) + state_name = core_pb2.SessionState.Enum.Name(hook.state) + self.state.set(state_name) + + def save(self): + data = self.codetext.text.get("1.0", tk.END).strip() + state_value = core_pb2.SessionState.Enum.Value(self.state.get()) + self.hook.file = self.name.get() + self.hook.data = data + self.hook.state = state_value + self.destroy() + + +class HooksDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Hooks", modal=True) + self.listbox = None + self.edit_button = None + self.delete_button = None + self.selected = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + listbox_scroll = ListboxScroll(self.top) + listbox_scroll.grid(sticky="nsew", pady=PADY) + self.listbox = listbox_scroll.listbox + self.listbox.bind("<>", self.select) + for hook_file in self.app.core.hooks: + self.listbox.insert(tk.END, hook_file) + + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(4): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + self.edit_button = ttk.Button( + frame, text="Edit", state=tk.DISABLED, command=self.click_edit + ) + self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX) + self.delete_button = ttk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) + self.delete_button.grid(row=0, column=2, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) + button.grid(row=0, column=3, sticky="ew") + + def click_create(self): + dialog = HookDialog(self, self.app) + dialog.show() + hook = dialog.hook + if hook: + self.app.core.hooks[hook.file] = hook + self.listbox.insert(tk.END, hook.file) + + def click_edit(self): + hook = self.app.core.hooks[self.selected] + dialog = HookDialog(self, self.app) + dialog.set(hook) + dialog.show() + + def click_delete(self): + del self.app.core.hooks[self.selected] + self.listbox.delete(tk.ANCHOR) + self.edit_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) + + def select(self, event): + if self.listbox.curselection(): + index = self.listbox.curselection()[0] + self.selected = self.listbox.get(index) + self.edit_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected = None + self.edit_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py new file mode 100644 index 00000000..cf0daafc --- /dev/null +++ b/coretk/coretk/dialogs/linkconfig.py @@ -0,0 +1,362 @@ +""" +link configuration +""" +import logging +import tkinter as tk +from tkinter import ttk + +from core.api.grpc import core_pb2 +from coretk.dialogs.colorpicker import ColorPicker +from coretk.dialogs.dialog import Dialog +from coretk.themes import PADX, PADY + + +def get_int(var): + value = var.get() + if value != "": + return int(value) + else: + return None + + +def get_float(var): + value = var.get() + if value != "": + return float(value) + else: + return None + + +class LinkConfiguration(Dialog): + def __init__(self, master, app, edge): + super().__init__(master, app, "Link Configuration", modal=True) + self.app = app + self.edge = edge + self.is_symmetric = edge.link.options.unidirectional is False + if self.is_symmetric: + self.symmetry_var = tk.StringVar(value=">>") + else: + self.symmetry_var = tk.StringVar(value="<<") + + self.bandwidth = tk.StringVar() + self.delay = tk.StringVar() + self.jitter = tk.StringVar() + self.loss = tk.StringVar() + self.duplicate = tk.StringVar() + + self.down_bandwidth = tk.StringVar() + self.down_delay = tk.StringVar() + self.down_jitter = tk.StringVar() + self.down_loss = tk.StringVar() + self.down_duplicate = tk.StringVar() + + self.color = tk.StringVar(value="#000000") + self.color_button = None + self.width = tk.DoubleVar() + + self.load_link_config() + self.symmetric_frame = None + self.asymmetric_frame = None + + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + source_name = self.app.canvas.nodes[self.edge.src].core_node.name + dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name + label = ttk.Label( + self.top, text=f"Link from {source_name} to {dest_name}", anchor=tk.CENTER + ) + label.grid(row=0, column=0, sticky="ew", pady=PADY) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=1, column=0, sticky="ew", pady=PADY) + button = ttk.Button(frame, text="Unlimited") + button.grid(row=0, column=0, sticky="ew", padx=PADX) + if self.is_symmetric: + button = ttk.Button( + frame, textvariable=self.symmetry_var, command=self.change_symmetry + ) + else: + button = ttk.Button( + frame, textvariable=self.symmetry_var, command=self.change_symmetry + ) + button.grid(row=0, column=1, sticky="ew") + + if self.is_symmetric: + self.symmetric_frame = self.get_frame() + self.symmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY) + else: + self.asymmetric_frame = self.get_frame() + self.asymmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY) + + self.draw_spacer(row=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=4, column=0, sticky="ew") + 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="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def get_frame(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(1, weight=1) + if self.is_symmetric: + label_name = "Symmetric Link Effects" + else: + label_name = "Asymmetric Effects: Downstream / Upstream " + row = 0 + label = ttk.Label(frame, text=label_name, anchor=tk.CENTER) + label.grid(row=row, column=0, columnspan=2, sticky="ew", pady=PADY) + row = row + 1 + + label = ttk.Label(frame, text="Bandwidth (bps)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.bandwidth, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) + if not self.is_symmetric: + entry = ttk.Entry( + frame, + textvariable=self.down_bandwidth, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew", pady=PADY) + row = row + 1 + + label = ttk.Label(frame, text="Delay (us)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.delay, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) + if not self.is_symmetric: + entry = ttk.Entry( + frame, + textvariable=self.down_delay, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew", pady=PADY) + row = row + 1 + + label = ttk.Label(frame, text="Jitter (us)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.jitter, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) + if not self.is_symmetric: + entry = ttk.Entry( + frame, + textvariable=self.down_jitter, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew", pady=PADY) + row = row + 1 + + label = ttk.Label(frame, text="Loss (%)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.loss, + validate="key", + validatecommand=(self.app.validation.positive_float, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) + if not self.is_symmetric: + entry = ttk.Entry( + frame, + textvariable=self.down_loss, + validate="key", + validatecommand=(self.app.validation.positive_float, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew", pady=PADY) + row = row + 1 + + label = ttk.Label(frame, text="Duplicate (%)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.duplicate, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) + if not self.is_symmetric: + entry = ttk.Entry( + frame, + textvariable=self.down_duplicate, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew", pady=PADY) + row = row + 1 + + label = ttk.Label(frame, text="Color") + label.grid(row=row, column=0, sticky="ew") + self.color_button = tk.Button( + frame, + textvariable=self.color, + background=self.color.get(), + bd=0, + relief=tk.FLAT, + highlightthickness=0, + command=self.click_color, + ) + self.color_button.grid(row=row, column=1, sticky="ew", pady=PADY) + row = row + 1 + + label = ttk.Label(frame, text="Width") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.width, + validate="key", + validatecommand=(self.app.validation.positive_float, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) + + return frame + + def click_color(self): + dialog = ColorPicker(self, self.app, self.color.get()) + color = dialog.askcolor() + self.color.set(color) + self.color_button.config(background=color) + + def click_apply(self): + logging.debug("click apply") + self.app.canvas.itemconfigure(self.edge.id, width=self.width.get()) + self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get()) + link = self.edge.link + bandwidth = get_int(self.bandwidth) + jitter = get_int(self.jitter) + delay = get_int(self.delay) + duplicate = get_int(self.duplicate) + loss = get_float(self.loss) + options = core_pb2.LinkOptions( + bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, per=loss + ) + link.options.CopyFrom(options) + + interface_one = None + if link.HasField("interface_one"): + interface_one = link.interface_one.id + interface_two = None + if link.HasField("interface_two"): + interface_two = link.interface_two.id + + if not self.is_symmetric: + link.options.unidirectional = True + asym_interface_one = None + if interface_one: + asym_interface_one = core_pb2.Interface(id=interface_one) + asym_interface_two = None + if interface_two: + asym_interface_two = core_pb2.Interface(id=interface_two) + down_bandwidth = get_int(self.down_bandwidth) + down_jitter = get_int(self.down_jitter) + down_delay = get_int(self.down_delay) + down_duplicate = get_int(self.down_duplicate) + down_loss = get_float(self.down_loss) + options = core_pb2.LinkOptions( + bandwidth=down_bandwidth, + jitter=down_jitter, + delay=down_delay, + dup=down_duplicate, + per=down_loss, + unidirectional=True, + ) + self.edge.asymmetric_link = core_pb2.Link( + node_one_id=link.node_two_id, + node_two_id=link.node_one_id, + interface_one=asym_interface_one, + interface_two=asym_interface_two, + options=options, + ) + else: + link.options.unidirectional = False + self.edge.asymmetric_link = None + + if self.app.core.is_runtime() and link.HasField("options"): + session_id = self.app.core.session_id + self.app.core.client.edit_link( + session_id, + link.node_one_id, + link.node_two_id, + link.options, + interface_one, + interface_two, + ) + if self.edge.asymmetric_link: + self.app.core.client.edit_link( + session_id, + link.node_two_id, + link.node_one_id, + self.edge.asymmetric_link.options, + interface_one, + interface_two, + ) + + self.destroy() + + def change_symmetry(self): + logging.debug("change symmetry") + + if self.is_symmetric: + self.is_symmetric = False + self.symmetry_var.set("<<") + if not self.asymmetric_frame: + self.asymmetric_frame = self.get_frame() + self.symmetric_frame.grid_forget() + self.asymmetric_frame.grid(row=2, column=0) + else: + self.is_symmetric = True + self.symmetry_var.set(">>") + if not self.symmetric_frame: + self.symmetric_frame = self.get_frame() + self.asymmetric_frame.grid_forget() + self.symmetric_frame.grid(row=2, column=0) + + def load_link_config(self): + """ + populate link config to the table + + :return: nothing + """ + width = self.app.canvas.itemcget(self.edge.id, "width") + self.width.set(width) + color = self.app.canvas.itemcget(self.edge.id, "fill") + self.color.set(color) + link = self.edge.link + if link.HasField("options"): + self.bandwidth.set(str(link.options.bandwidth)) + self.jitter.set(str(link.options.jitter)) + self.duplicate.set(str(link.options.dup)) + self.loss.set(str(link.options.per)) + self.delay.set(str(link.options.delay)) + if not self.is_symmetric: + asym_link = self.edge.asymmetric_link + self.down_bandwidth.set(str(asym_link.options.bandwidth)) + self.down_jitter.set(str(asym_link.options.jitter)) + self.down_duplicate.set(str(asym_link.options.dup)) + self.down_loss.set(str(asym_link.options.per)) + self.down_delay.set(str(asym_link.options.delay)) diff --git a/coretk/coretk/dialogs/marker.py b/coretk/coretk/dialogs/marker.py new file mode 100644 index 00000000..5648a89b --- /dev/null +++ b/coretk/coretk/dialogs/marker.py @@ -0,0 +1,73 @@ +""" +marker dialog +""" + +import logging +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.colorpicker import ColorPicker +from coretk.dialogs.dialog import Dialog + +MARKER_THICKNESS = [3, 5, 8, 10] + + +class Marker(Dialog): + def __init__(self, master, app, initcolor="#000000"): + super().__init__(master, app, "marker tool", modal=False) + self.app = app + self.color = initcolor + self.radius = MARKER_THICKNESS[0] + self.marker_thickness = tk.IntVar(value=MARKER_THICKNESS[0]) + self.draw() + self.top.bind("", self.close_marker) + + def draw(self): + button = ttk.Button(self.top, text="clear", command=self.clear_marker) + button.grid(row=0, column=0, sticky="nsew") + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=1, column=0, sticky="nsew") + label = ttk.Label(frame, text="Thickness: ") + label.grid(row=0, column=0, sticky="nsew") + combobox = ttk.Combobox( + frame, + textvariable=self.marker_thickness, + values=MARKER_THICKNESS, + state="readonly", + ) + combobox.grid(row=0, column=1, sticky="nsew") + combobox.bind("<>", self.change_thickness) + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=2, column=0, sticky="nsew") + label = ttk.Label(frame, text="Color: ") + label.grid(row=0, column=0, sticky="nsew") + label = ttk.Label(frame, background=self.color) + label.grid(row=0, column=1, sticky="nsew") + label.bind("", self.change_color) + + def clear_marker(self): + canvas = self.app.canvas + for i in canvas.find_withtag("marker"): + canvas.delete(i) + + def change_color(self, event): + color_picker = ColorPicker(self, self.app, self.color) + color = color_picker.askcolor() + event.widget.configure(background=color) + self.color = color + + def change_thickness(self, event): + self.radius = self.marker_thickness.get() + + def close_marker(self, event): + logging.debug("destroy marker dialog") + self.app.toolbar.marker_tool = None + + def position(self): + print(self.winfo_width(), self.winfo_height()) + self.geometry("+{}+{}".format(self.app.master.winfo_x, self.app.master.winfo_y)) diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py new file mode 100644 index 00000000..19dc46f4 --- /dev/null +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -0,0 +1,55 @@ +""" +mobility configuration +""" +from tkinter import ttk + +import grpc + +from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error +from coretk.themes import PADX, PADY +from coretk.widgets import ConfigFrame + + +class MobilityConfigDialog(Dialog): + def __init__(self, master, app, canvas_node): + super().__init__( + master, + app, + f"{canvas_node.core_node.name} Mobility Configuration", + modal=True, + ) + self.canvas_node = canvas_node + self.node = canvas_node.core_node + self.config_frame = None + try: + self.config = self.app.core.get_mobility_config(self.node.id) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PADY) + self.draw_apply_buttons() + + def draw_apply_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, padx=PADX, sticky="ew") + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_apply(self): + self.config_frame.parse_config() + self.app.core.mobility_configs[self.node.id] = self.config + self.destroy() diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py new file mode 100644 index 00000000..f0b46499 --- /dev/null +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -0,0 +1,163 @@ +import tkinter as tk +from tkinter import ttk + +import grpc + +from core.api.grpc.core_pb2 import MobilityAction +from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error +from coretk.images import ImageEnum, Images +from coretk.themes import PADX, PADY + +ICON_SIZE = 16 + + +class MobilityPlayer: + def __init__(self, master, app, canvas_node, config): + self.master = master + self.app = app + self.canvas_node = canvas_node + self.config = config + self.dialog = None + self.state = None + + def show(self): + if self.dialog: + self.dialog.destroy() + self.dialog = MobilityPlayerDialog( + self.master, self.app, self.canvas_node, self.config + ) + self.dialog.protocol("WM_DELETE_WINDOW", self.handle_close) + if self.state == MobilityAction.START: + self.set_play() + elif self.state == MobilityAction.PAUSE: + self.set_pause() + else: + self.set_stop() + self.dialog.show() + + def handle_close(self): + self.dialog.destroy() + self.dialog = None + + def set_play(self): + self.state = MobilityAction.START + if self.dialog: + self.dialog.set_play() + + def set_pause(self): + self.state = MobilityAction.PAUSE + if self.dialog: + self.dialog.set_pause() + + def set_stop(self): + self.state = MobilityAction.STOP + if self.dialog: + self.dialog.set_stop() + + +class MobilityPlayerDialog(Dialog): + def __init__(self, master, app, canvas_node, config): + super().__init__( + master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False + ) + self.resizable(False, False) + self.geometry("") + self.canvas_node = canvas_node + self.node = canvas_node.core_node + self.config = config + self.play_button = None + self.pause_button = None + self.stop_button = None + self.progressbar = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + + file_name = self.config["file"].value + label = ttk.Label(self.top, text=file_name) + label.grid(sticky="ew", pady=PADY) + + self.progressbar = ttk.Progressbar(self.top, mode="indeterminate") + self.progressbar.grid(sticky="ew", pady=PADY) + + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PADY) + for i in range(3): + frame.columnconfigure(i, weight=1) + + image = Images.get(ImageEnum.START, width=ICON_SIZE) + self.play_button = ttk.Button(frame, image=image, command=self.click_play) + self.play_button.image = image + self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX) + + image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE) + self.pause_button = ttk.Button(frame, image=image, command=self.click_pause) + self.pause_button.image = image + self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX) + + image = Images.get(ImageEnum.STOP, width=ICON_SIZE) + self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) + self.stop_button.image = image + self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX) + + loop = tk.IntVar(value=int(self.config["loop"].value == "1")) + checkbutton = ttk.Checkbutton( + frame, text="Loop?", variable=loop, state=tk.DISABLED + ) + checkbutton.grid(row=0, column=3, padx=PADX) + + rate = self.config["refresh_ms"].value + label = ttk.Label(frame, text=f"rate {rate} ms") + label.grid(row=0, column=4) + + def clear_buttons(self): + self.play_button.state(["!pressed"]) + self.pause_button.state(["!pressed"]) + self.stop_button.state(["!pressed"]) + + def set_play(self): + self.clear_buttons() + self.play_button.state(["pressed"]) + self.progressbar.start() + + def set_pause(self): + self.clear_buttons() + self.pause_button.state(["pressed"]) + self.progressbar.stop() + + def set_stop(self): + self.clear_buttons() + self.stop_button.state(["pressed"]) + self.progressbar.stop() + + def click_play(self): + self.set_play() + session_id = self.app.core.session_id + try: + self.app.core.client.mobility_action( + session_id, self.node.id, MobilityAction.START + ) + except grpc.RpcError as e: + show_grpc_error(e) + + def click_pause(self): + self.set_pause() + session_id = self.app.core.session_id + try: + self.app.core.client.mobility_action( + session_id, self.node.id, MobilityAction.PAUSE + ) + except grpc.RpcError as e: + show_grpc_error(e) + + def click_stop(self): + self.set_stop() + session_id = self.app.core.session_id + try: + self.app.core.client.mobility_action( + session_id, self.node.id, MobilityAction.STOP + ) + except grpc.RpcError as e: + show_grpc_error(e) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py new file mode 100644 index 00000000..4e5bc864 --- /dev/null +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -0,0 +1,237 @@ +import logging +import tkinter as tk +from functools import partial +from tkinter import ttk + +from coretk import nodeutils +from coretk.appconfig import ICONS_PATH +from coretk.dialogs.dialog import Dialog +from coretk.dialogs.emaneconfig import EmaneModelDialog +from coretk.images import Images +from coretk.nodeutils import NodeUtils +from coretk.themes import FRAME_PAD, PADX, PADY +from coretk.widgets import image_chooser + + +def mac_auto(is_auto, entry): + logging.info("mac auto clicked") + if is_auto.get(): + logging.info("disabling mac") + entry.var.set("") + entry.config(state=tk.DISABLED) + else: + entry.var.set("00:00:00:00:00:00") + entry.config(state=tk.NORMAL) + + +class InterfaceData: + def __init__(self, is_auto, mac, ip4, ip6): + self.is_auto = is_auto + self.mac = mac + self.ip4 = ip4 + self.ip6 = ip6 + + +class NodeConfigDialog(Dialog): + def __init__(self, master, app, canvas_node): + """ + create an instance of node configuration + + :param master: dialog master + :param coretk.app.Application: main app + :param coretk.graph.CanvasNode canvas_node: canvas node object + """ + super().__init__( + master, app, f"{canvas_node.core_node.name} Configuration", modal=True + ) + self.canvas_node = canvas_node + self.node = canvas_node.core_node + self.image = canvas_node.image + self.image_file = None + self.image_button = None + self.name = tk.StringVar(value=self.node.name) + self.type = tk.StringVar(value=self.node.model) + self.container_image = tk.StringVar(value=self.node.image) + server = "localhost" + if self.node.server: + server = self.node.server + self.server = tk.StringVar(value=server) + self.interfaces = {} + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + row = 0 + + # field frame + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + + # icon field + label = ttk.Label(frame, text="Icon") + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) + self.image_button = ttk.Button( + frame, + text="Icon", + image=self.image, + compound=tk.NONE, + command=self.click_icon, + ) + self.image_button.grid(row=row, column=1, sticky="ew") + row += 1 + + # name field + label = ttk.Label(frame, text="Name") + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) + entry = ttk.Entry( + frame, + textvariable=self.name, + validate="key", + validatecommand=(self.app.validation.name, "%P"), + ) + entry.bind( + "", lambda event: self.app.validation.focus_out(event, "noname") + ) + entry.grid(row=row, column=1, sticky="ew") + row += 1 + + # node type field + if NodeUtils.is_model_node(self.node.type): + label = ttk.Label(frame, text="Type") + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) + combobox = ttk.Combobox( + frame, + textvariable=self.type, + values=list(NodeUtils.NODE_MODELS), + state="readonly", + ) + combobox.grid(row=row, column=1, sticky="ew") + row += 1 + + # container image field + if NodeUtils.is_image_node(self.node.type): + label = ttk.Label(frame, text="Image") + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) + entry = ttk.Entry(frame, textvariable=self.container_image) + entry.grid(row=row, column=1, sticky="ew") + row += 1 + + if NodeUtils.is_container_node(self.node.type): + # server + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Server") + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) + servers = ["localhost"] + servers.extend(list(sorted(self.app.core.servers.keys()))) + combobox = ttk.Combobox( + frame, textvariable=self.server, values=servers, state="readonly" + ) + combobox.grid(row=row, column=1, sticky="ew") + row += 1 + + # interfaces + if self.canvas_node.interfaces: + self.draw_interfaces() + + self.draw_spacer() + self.draw_buttons() + + def draw_interfaces(self): + notebook = ttk.Notebook(self.top) + notebook.grid(sticky="nsew", pady=PADY) + self.top.rowconfigure(notebook.grid_info()["row"], weight=1) + + for interface in self.canvas_node.interfaces: + logging.info("interface: %s", interface) + tab = ttk.Frame(notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew", pady=PADY) + tab.columnconfigure(1, weight=1) + tab.columnconfigure(2, weight=1) + notebook.add(tab, text=interface.name) + + row = 0 + emane_node = self.canvas_node.has_emane_link(interface.id) + if emane_node: + emane_model = emane_node.emane.split("_")[1] + button = ttk.Button( + tab, + text=f"Configure EMANE {emane_model}", + command=lambda: self.click_emane_config(emane_model, interface.id), + ) + button.grid(row=row, sticky="ew", columnspan=3, pady=PADY) + row += 1 + + label = ttk.Label(tab, text="MAC") + label.grid(row=row, column=0, padx=PADX, pady=PADY) + is_auto = tk.BooleanVar(value=True) + checkbutton = ttk.Checkbutton(tab, text="Auto?", variable=is_auto) + checkbutton.var = is_auto + checkbutton.grid(row=row, column=1, padx=PADX) + mac = tk.StringVar(value=interface.mac) + entry = ttk.Entry(tab, textvariable=mac, state=tk.DISABLED) + entry.grid(row=row, column=2, sticky="ew") + func = partial(mac_auto, is_auto, entry) + checkbutton.config(command=func) + row += 1 + + label = ttk.Label(tab, text="IPv4") + label.grid(row=row, column=0, padx=PADX, pady=PADY) + ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}") + entry = ttk.Entry(tab, textvariable=ip4) + entry.bind("", self.app.validation.ip_focus_out) + entry.grid(row=row, column=1, columnspan=2, sticky="ew") + row += 1 + + label = ttk.Label(tab, text="IPv6") + label.grid(row=row, column=0, padx=PADX, pady=PADY) + ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}") + entry = ttk.Entry(tab, textvariable=ip6) + entry.bind("", self.app.validation.ip_focus_out) + entry.grid(row=row, column=1, columnspan=2, sticky="ew") + + self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + + button = ttk.Button(frame, text="Apply", command=self.config_apply) + button.grid(row=0, column=0, padx=PADX, sticky="ew") + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_emane_config(self, emane_model, interface_id): + dialog = EmaneModelDialog(self, self.app, self.node, emane_model, interface_id) + dialog.show() + + def click_icon(self): + file_path = image_chooser(self, ICONS_PATH) + if file_path: + self.image = Images.create(file_path, nodeutils.ICON_SIZE) + self.image_button.config(image=self.image) + self.image_file = file_path + + def config_apply(self): + # update core node + self.node.name = self.name.get() + if NodeUtils.is_image_node(self.node.type): + self.node.image = self.container_image.get() + server = self.server.get() + if NodeUtils.is_container_node(self.node.type) and server != "localhost": + self.node.server = server + + # set custom icon + if self.image_file: + self.node.icon = self.image_file + + # update canvas node + self.canvas_node.image = self.image + + # redraw + self.canvas_node.redraw() + self.destroy() diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py new file mode 100644 index 00000000..8ad87649 --- /dev/null +++ b/coretk/coretk/dialogs/nodeservice.py @@ -0,0 +1,134 @@ +""" +core node services +""" +import tkinter as tk +from tkinter import messagebox, ttk + +from coretk.dialogs.dialog import Dialog +from coretk.dialogs.serviceconfiguration import ServiceConfiguration +from coretk.themes import FRAME_PAD, PADX, PADY +from coretk.widgets import CheckboxList, ListboxScroll + + +class NodeService(Dialog): + def __init__(self, master, app, canvas_node, services=None): + title = f"{canvas_node.core_node.name} 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 = canvas_node.core_node.services + model = canvas_node.core_node.model + if len(services) == 0: + services = set(self.app.core.default_services[model]) + else: + services = set(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.services): + 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) + + frame = ttk.Frame(self.top) + frame.grid(stick="ew") + for i in range(3): + 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="Cancel", command=self.click_cancel) + button.grid(row=0, column=2, sticky="ew") + + # trigger group change + self.groups.listbox.event_generate("<>") + + def handle_group_change(self, event): + selection = self.groups.listbox.curselection() + if selection: + index = selection[0] + group = self.groups.listbox.get(index) + self.services.clear() + for name in sorted(self.app.core.services[group]): + checked = name in self.current_services + self.services.add(name, checked) + + def service_clicked(self, name, var): + 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) + self.canvas_node.core_node.services[:] = self.current_services + + def click_configure(self): + current_selection = self.current.listbox.curselection() + if len(current_selection): + dialog = ServiceConfiguration( + 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): + if ( + self.current_services + != self.app.core.default_services[self.canvas_node.core_node.model] + ): + self.canvas_node.core_node.services[:] = self.current_services + else: + if len(self.canvas_node.core_node.services) > 0: + self.canvas_node.core_node.services[:] = [] + self.destroy() + + def click_cancel(self): + self.current_services = None + self.destroy() diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py new file mode 100644 index 00000000..de857b76 --- /dev/null +++ b/coretk/coretk/dialogs/observers.py @@ -0,0 +1,143 @@ +import tkinter as tk +from tkinter import ttk + +from coretk.coreclient import Observer +from coretk.dialogs.dialog import Dialog +from coretk.themes import PADX, PADY +from coretk.widgets import ListboxScroll + + +class ObserverDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Observer Widgets", modal=True) + self.observers = None + self.save_button = None + self.delete_button = None + self.selected = None + self.selected_index = None + self.name = tk.StringVar() + self.cmd = tk.StringVar() + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.draw_listbox() + self.draw_form_fields() + self.draw_config_buttons() + self.draw_apply_buttons() + + def draw_listbox(self): + listbox_scroll = ListboxScroll(self.top) + listbox_scroll.grid(sticky="nsew", pady=PADY) + listbox_scroll.columnconfigure(0, weight=1) + listbox_scroll.rowconfigure(0, weight=1) + self.observers = listbox_scroll.listbox + self.observers.grid(row=0, column=0, sticky="nsew") + self.observers.bind("<>", self.handle_observer_change) + for name in sorted(self.app.core.custom_observers): + self.observers.insert(tk.END, name) + + def draw_form_fields(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + + label = ttk.Label(frame, text="Name") + label.grid(row=0, column=0, sticky="w", padx=PADX) + entry = ttk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=1, sticky="ew") + + label = ttk.Label(frame, text="Command") + label.grid(row=1, column=0, sticky="w", padx=PADX) + entry = ttk.Entry(frame, textvariable=self.cmd) + entry.grid(row=1, column=1, sticky="ew") + + def draw_config_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PADY) + for i in range(3): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + + self.save_button = ttk.Button( + frame, text="Save", state=tk.DISABLED, command=self.click_save + ) + self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX) + + self.delete_button = ttk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) + self.delete_button.grid(row=0, column=2, sticky="ew") + + def draw_apply_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Save", command=self.click_save_config) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_save_config(self): + observers = [] + for name in sorted(self.app.core.custom_observers): + observer = self.app.core.custom_observers[name] + observers.append({"name": observer.name, "cmd": observer.cmd}) + self.app.guiconfig["observers"] = observers + self.app.save_config() + self.destroy() + + def click_create(self): + name = self.name.get() + if name not in self.app.core.custom_observers: + cmd = self.cmd.get() + observer = Observer(name, cmd) + self.app.core.custom_observers[name] = observer + self.observers.insert(tk.END, name) + + def click_save(self): + name = self.name.get() + if self.selected: + previous_name = self.selected + self.selected = name + observer = self.app.core.custom_observers.pop(previous_name) + observer.name = name + observer.cmd = self.cmd.get() + self.app.core.custom_observers[name] = observer + self.observers.delete(self.selected_index) + self.observers.insert(self.selected_index, name) + self.observers.selection_set(self.selected_index) + + def click_delete(self): + if self.selected: + self.observers.delete(self.selected_index) + del self.app.core.custom_observers[self.selected] + self.selected = None + self.selected_index = None + self.name.set("") + self.cmd.set("") + self.observers.selection_clear(0, tk.END) + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) + + def handle_observer_change(self, event): + selection = self.observers.curselection() + if selection: + self.selected_index = selection[0] + self.selected = self.observers.get(self.selected_index) + observer = self.app.core.custom_observers[self.selected] + self.name.set(observer.name) + self.cmd.set(observer.cmd) + self.save_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected_index = None + self.selected = None + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py new file mode 100644 index 00000000..8c369027 --- /dev/null +++ b/coretk/coretk/dialogs/preferences.py @@ -0,0 +1,87 @@ +import logging +import tkinter as tk +from tkinter import ttk + +from coretk import appconfig +from coretk.dialogs.dialog import Dialog +from coretk.themes import FRAME_PAD, PADX, PADY + + +class PreferencesDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Preferences", modal=True) + preferences = self.app.guiconfig["preferences"] + self.editor = tk.StringVar(value=preferences["editor"]) + self.theme = tk.StringVar(value=preferences["theme"]) + self.terminal = tk.StringVar(value=preferences["terminal"]) + self.gui3d = tk.StringVar(value=preferences["gui3d"]) + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.draw_preferences() + self.draw_buttons() + + def draw_preferences(self): + frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD) + frame.grid(sticky="nsew", pady=PADY) + frame.columnconfigure(1, weight=1) + + label = ttk.Label(frame, text="Theme") + label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky="w") + themes = self.app.style.theme_names() + combobox = ttk.Combobox( + frame, textvariable=self.theme, values=themes, state="readonly" + ) + combobox.set(self.theme.get()) + combobox.grid(row=0, column=1, sticky="ew") + combobox.bind("<>", self.theme_change) + + label = ttk.Label(frame, text="Editor") + label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky="w") + combobox = ttk.Combobox( + frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" + ) + combobox.grid(row=1, column=1, sticky="ew") + + label = ttk.Label(frame, text="Terminal") + label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w") + combobox = ttk.Combobox( + frame, + textvariable=self.terminal, + values=appconfig.TERMINALS, + state="readonly", + ) + combobox.grid(row=2, column=1, sticky="ew") + + label = ttk.Label(frame, text="3D GUI") + label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky="w") + entry = ttk.Entry(frame, textvariable=self.gui3d) + entry.grid(row=3, column=1, sticky="ew") + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Save", command=self.click_save) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def theme_change(self, event): + theme = self.theme.get() + logging.info("changing theme: %s", theme) + self.app.style.theme_use(theme) + + def click_save(self): + preferences = self.app.guiconfig["preferences"] + preferences["terminal"] = self.terminal.get() + preferences["editor"] = self.editor.get() + preferences["gui3d"] = self.gui3d.get() + preferences["theme"] = self.theme.get() + self.app.save_config() + self.destroy() diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py new file mode 100644 index 00000000..c380c63d --- /dev/null +++ b/coretk/coretk/dialogs/servers.py @@ -0,0 +1,173 @@ +import tkinter as tk +from tkinter import ttk + +from coretk.coreclient import CoreServer +from coretk.dialogs.dialog import Dialog +from coretk.themes import FRAME_PAD, PADX, PADY +from coretk.widgets import ListboxScroll + +DEFAULT_NAME = "example" +DEFAULT_ADDRESS = "127.0.0.1" +DEFAULT_PORT = 50051 + + +class ServersDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "CORE Servers", modal=True) + self.name = tk.StringVar(value=DEFAULT_NAME) + self.address = tk.StringVar(value=DEFAULT_ADDRESS) + self.port = tk.IntVar(value=DEFAULT_PORT) + self.servers = None + self.selected_index = None + self.selected = None + self.save_button = None + self.delete_button = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.draw_servers() + self.draw_servers_buttons() + self.draw_server_configuration() + self.draw_apply_buttons() + + def draw_servers(self): + listbox_scroll = ListboxScroll(self.top) + listbox_scroll.grid(pady=PADY, sticky="nsew") + listbox_scroll.columnconfigure(0, weight=1) + listbox_scroll.rowconfigure(0, weight=1) + + self.servers = listbox_scroll.listbox + self.servers.grid(row=0, column=0, sticky="nsew") + self.servers.bind("<>", self.handle_server_change) + + for server in self.app.core.servers: + self.servers.insert(tk.END, server) + + def draw_server_configuration(self): + frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD) + frame.grid(pady=PADY, sticky="ew") + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + frame.columnconfigure(5, weight=1) + + label = ttk.Label(frame, text="Name") + label.grid(row=0, column=0, sticky="w", padx=PADX, pady=PADY) + entry = ttk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=1, sticky="ew") + + label = ttk.Label(frame, text="Address") + label.grid(row=0, column=2, sticky="w", padx=PADX, pady=PADY) + entry = ttk.Entry(frame, textvariable=self.address) + entry.grid(row=0, column=3, sticky="ew") + + label = ttk.Label(frame, text="Port") + label.grid(row=0, column=4, sticky="w", padx=PADX, pady=PADY) + entry = ttk.Entry( + frame, + textvariable=self.port, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.bind( + "", lambda event: self.app.validation.focus_out(event, "50051") + ) + entry.grid(row=0, column=5, sticky="ew") + + def draw_servers_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(pady=PADY, sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + + self.save_button = ttk.Button( + frame, text="Save", state=tk.DISABLED, command=self.click_save + ) + self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX) + + self.delete_button = ttk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) + self.delete_button.grid(row=0, column=2, sticky="ew") + + def draw_apply_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = ttk.Button( + frame, text="Save Configuration", command=self.click_save_configuration + ) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_save_configuration(self): + servers = [] + for name in sorted(self.app.core.servers): + server = self.app.core.servers[name] + servers.append( + {"name": server.name, "address": server.address, "port": server.port} + ) + self.app.guiconfig["servers"] = servers + self.app.save_config() + self.destroy() + + def click_create(self): + name = self.name.get() + if name not in self.app.core.servers: + address = self.address.get() + port = self.port.get() + server = CoreServer(name, address, port) + self.app.core.servers[name] = server + self.servers.insert(tk.END, name) + + def click_save(self): + name = self.name.get() + if self.selected: + previous_name = self.selected + self.selected = name + server = self.app.core.servers.pop(previous_name) + server.name = name + server.address = self.address.get() + server.port = self.port.get() + self.app.core.servers[name] = server + self.servers.delete(self.selected_index) + self.servers.insert(self.selected_index, name) + self.servers.selection_set(self.selected_index) + + def click_delete(self): + if self.selected: + self.servers.delete(self.selected_index) + del self.app.core.servers[self.selected] + self.selected = None + self.selected_index = None + self.name.set(DEFAULT_NAME) + self.address.set(DEFAULT_ADDRESS) + self.port.set(DEFAULT_PORT) + self.servers.selection_clear(0, tk.END) + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) + + def handle_server_change(self, event): + selection = self.servers.curselection() + if selection: + self.selected_index = selection[0] + self.selected = self.servers.get(self.selected_index) + server = self.app.core.servers[self.selected] + self.name.set(server.name) + self.address.set(server.address) + self.port.set(server.port) + self.save_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected_index = None + self.selected = None + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py new file mode 100644 index 00000000..53aca1b3 --- /dev/null +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -0,0 +1,453 @@ +"Service configuration dialog" +import logging +import tkinter as tk +from tkinter import ttk + +import grpc + +from core.api.grpc import core_pb2 +from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error +from coretk.images import ImageEnum, Images +from coretk.themes import FRAME_PAD, PADX, PADY +from coretk.widgets import CodeText, ListboxScroll + + +class ServiceConfiguration(Dialog): + def __init__(self, master, app, service_name, node_id): + title = f"{service_name} Service" + super().__init__(master, app, title, modal=True) + self.app = app + self.core = app.core + self.node_id = node_id + self.service_name = service_name + self.radiovar = tk.IntVar() + self.radiovar.set(2) + self.metadata = "" + self.filenames = [] + self.dependencies = [] + self.executables = [] + self.startup_commands = [] + self.validation_commands = [] + self.shutdown_commands = [] + self.validation_mode = None + self.validation_time = None + self.validation_period = None + self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16) + self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16) + + self.notebook = None + self.metadata_entry = None + self.filename_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.service_file_data = None + self.validation_period_entry = None + self.original_service_files = {} + self.temp_service_files = {} + self.modified_files = set() + self.load() + self.draw() + + def load(self): + try: + self.app.core.create_nodes_and_links() + service_configs = self.app.core.service_configs + if ( + self.node_id in service_configs + and self.service_name in service_configs[self.node_id] + ): + service_config = self.app.core.service_configs[self.node_id][ + self.service_name + ] + else: + service_config = self.app.core.get_node_service( + self.node_id, self.service_name + ) + self.dependencies = [x for x in service_config.dependencies] + self.executables = [x for x in service_config.executables] + self.metadata = service_config.meta + self.filenames = [x for x in service_config.configs] + self.startup_commands = [x for x in service_config.startup] + self.validation_commands = [x for x in service_config.validate] + self.shutdown_commands = [x for x in service_config.shutdown] + self.validation_mode = service_config.validation_mode + self.validation_time = service_config.validation_timer + self.original_service_files = { + x: self.app.core.get_node_service_file( + self.node_id, self.service_name, x + ) + for x in self.filenames + } + self.temp_service_files = { + x: self.original_service_files[x] for x in self.original_service_files + } + file_configs = self.app.core.file_configs + if ( + self.node_id in file_configs + and self.service_name in file_configs[self.node_id] + ): + for file, data in file_configs[self.node_id][self.service_name].items(): + 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(1, weight=1) + + # draw metadata + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Meta-data") + label.grid(row=0, column=0, sticky="w", padx=PADX) + self.metadata_entry = ttk.Entry(frame, textvariable=self.metadata) + self.metadata_entry.grid(row=0, column=1, sticky="ew") + + # draw notebook + self.notebook = ttk.Notebook(self.top) + self.notebook.grid(sticky="nsew", pady=PADY) + self.draw_tab_files() + self.draw_tab_directories() + self.draw_tab_startstop() + self.draw_tab_configuration() + + button = ttk.Button(self.top, text="Only Save Changes") + button.grid(sticky="ew", pady=PADY) + 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="Files") + + label = ttk.Label( + tab, text="Config files and scripts that are generated for this service." + ) + label.grid() + + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="File Name") + label.grid(row=0, column=0, padx=PADX, sticky="w") + self.filename_combobox = ttk.Combobox( + frame, values=self.filenames, state="readonly" + ) + self.filename_combobox.bind( + "<>", self.display_service_file_data + ) + self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, image=self.documentnew_img, state="disabled") + button.bind("", self.add_filename) + button.grid(row=0, column=2, padx=PADX) + button = ttk.Button(frame, image=self.editdelete_img, state="disabled") + button.bind("", self.delete_filename) + button.grid(row=0, column=3) + + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + button = ttk.Radiobutton( + frame, + variable=self.radiovar, + text="Copy Source File", + value=1, + state=tk.DISABLED, + ) + button.grid(row=0, column=0, sticky="w", padx=PADX) + entry = ttk.Entry(frame, state=tk.DISABLED) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) + image = Images.get(ImageEnum.FILEOPEN, 16) + button = ttk.Button(frame, image=image) + button.image = image + button.grid(row=0, column=2) + + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(0, weight=1) + button = ttk.Radiobutton( + frame, + variable=self.radiovar, + text="Use text below for file contents", + value=2, + ) + button.grid(row=0, column=0, sticky="ew") + image = Images.get(ImageEnum.FILEOPEN, 16) + button = ttk.Button(frame, image=image) + button.image = image + button.grid(row=0, column=1) + image = Images.get(ImageEnum.DOCUMENTSAVE, 16) + button = ttk.Button(frame, image=image) + button.image = image + button.grid(row=0, column=2) + + self.service_file_data = CodeText(tab) + self.service_file_data.grid(sticky="nsew") + tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1) + if len(self.filenames) > 0: + self.filename_combobox.current(0) + self.service_file_data.text.delete(1.0, "end") + self.service_file_data.text.insert( + "end", self.temp_service_files[self.filenames[0]] + ) + self.service_file_data.text.bind( + "", self.update_temp_service_file_data + ) + + def draw_tab_directories(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Directories") + + label = ttk.Label( + tab, + text="Directories required by this service that are unique for each node.", + ) + label.grid() + + 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") + + # 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(1, weight=1) + label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY) + + frame = ttk.Frame(label_frame) + frame.grid(row=0, column=0, sticky="nsew", pady=PADY) + frame.columnconfigure(0, weight=1) + entry = ttk.Entry(frame, textvariable=tk.StringVar()) + entry.grid(row=0, column=0, stick="ew", padx=PADX) + button = ttk.Button(frame, image=self.documentnew_img) + button.bind("", self.add_command) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=2, sticky="ew") + button.bind("", self.delete_command) + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.bind("<>", self.update_entry) + for command in commands: + listbox_scroll.listbox.insert("end", command) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(row=1, column=0, 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_configuration(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Configuration", 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=tk.StringVar() + ) + 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, state="disabled" + ) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button( + frame, text="Copy...", command=self.click_copy, state="disabled" + ) + 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 add_filename(self, event): + # not worry about it for now + return + frame_contains_button = event.widget.master + combobox = frame_contains_button.grid_slaves(row=0, column=1)[0] + filename = combobox.get() + if filename not in combobox["values"]: + combobox["values"] += (filename,) + + def delete_filename(self, event): + # not worry about it for now + return + frame_comntains_button = event.widget.master + combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0] + filename = combobox.get() + if filename in combobox["values"]: + combobox["values"] = tuple([x for x in combobox["values"] if x != filename]) + combobox.set("") + + def add_command(self, event): + frame_contains_button = event.widget.master + listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox + command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get() + if command_to_add == "": + return + for cmd in listbox.get(0, tk.END): + if cmd == command_to_add: + return + listbox.insert(tk.END, command_to_add) + + def update_entry(self, event): + listbox = event.widget + current_selection = listbox.curselection() + if len(current_selection) > 0: + cmd = listbox.get(current_selection[0]) + entry = listbox.master.master.grid_slaves(row=0, column=0)[0].grid_slaves( + row=0, column=0 + )[0] + entry.delete(0, "end") + entry.insert(0, cmd) + + def delete_command(self, event): + button = event.widget + frame_contains_button = button.master + listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox + current_selection = listbox.curselection() + if len(current_selection) > 0: + listbox.delete(current_selection[0]) + entry = frame_contains_button.grid_slaves(row=0, column=0)[0] + entry.delete(0, tk.END) + + def click_apply(self): + service_configs = self.app.core.service_configs + startup_commands = self.startup_commands_listbox.get(0, "end") + shutdown_commands = self.shutdown_commands_listbox.get(0, "end") + validate_commands = self.validate_commands_listbox.get(0, "end") + try: + config = self.core.set_node_service( + self.node_id, + self.service_name, + startup_commands, + validate_commands, + shutdown_commands, + ) + if self.node_id not in service_configs: + service_configs[self.node_id] = {} + if self.service_name not in service_configs[self.node_id]: + self.app.core.service_configs[self.node_id][self.service_name] = config + for file in self.modified_files: + file_configs = self.app.core.file_configs + if self.node_id not in file_configs: + file_configs[self.node_id] = {} + if self.service_name not in file_configs[self.node_id]: + file_configs[self.node_id][self.service_name] = {} + file_configs[self.node_id][self.service_name][ + file + ] = self.temp_service_files[file] + + self.app.core.set_node_service_file( + self.node_id, self.service_name, file, self.temp_service_files[file] + ) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + + def display_service_file_data(self, event): + combobox = event.widget + filename = combobox.get() + self.service_file_data.text.delete(1.0, "end") + self.service_file_data.text.insert("end", self.temp_service_files[filename]) + + def update_temp_service_file_data(self, event): + scrolledtext = event.widget + filename = self.filename_combobox.get() + self.temp_service_files[filename] = scrolledtext.get(1.0, "end") + if self.temp_service_files[filename] != self.original_service_files[filename]: + self.modified_files.add(filename) + else: + self.modified_files.discard(filename) + + def click_defaults(self): + logging.info("not implemented") + + def click_copy(self): + logging.info("not implemented") + + def click_cancel(self): + logging.info("not implemented") diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py new file mode 100644 index 00000000..24ff7381 --- /dev/null +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -0,0 +1,53 @@ +import logging +from tkinter import ttk + +import grpc + +from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error +from coretk.themes import PADX, PADY +from coretk.widgets import ConfigFrame + + +class SessionOptionsDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Session Options", modal=True) + self.config_frame = None + self.config = self.get_config() + self.draw() + + def get_config(self): + try: + session_id = self.app.core.session_id + response = self.app.core.client.get_session_options(session_id) + return response.config + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + self.config_frame = ConfigFrame(self.top, self.app, config=self.config) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PADY) + + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Save", command=self.save) + button.grid(row=0, column=0, padx=PADX, sticky="ew") + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, padx=PADX, sticky="ew") + + def save(self): + config = self.config_frame.parse_config() + try: + session_id = self.app.core.session_id + response = self.app.core.client.set_session_options(session_id, config) + logging.info("saved session config: %s", response) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py new file mode 100644 index 00000000..b1fb970f --- /dev/null +++ b/coretk/coretk/dialogs/sessions.py @@ -0,0 +1,181 @@ +import logging +import threading +import tkinter as tk +from tkinter import ttk + +import grpc + +from core.api.grpc import core_pb2 +from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error +from coretk.images import ImageEnum, Images +from coretk.themes import PADX, PADY + + +class SessionsDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Sessions", modal=True) + self.selected = False + self.selected_id = None + self.tree = None + self.sessions = self.get_sessions() + self.draw() + + def get_sessions(self): + try: + response = self.app.core.client.get_sessions() + logging.info("sessions: %s", response) + return response.sessions + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) + self.draw_description() + self.draw_tree() + self.draw_buttons() + + def draw_description(self): + """ + write a short description + :return: nothing + """ + label = ttk.Label( + self.top, + text="Below is a list of active CORE sessions. Double-click to \n" + "connect to an existing session. Usually, only sessions in \n" + "the RUNTIME state persist in the daemon, except for the \n" + "one you might be concurrently editting.", + justify=tk.CENTER, + ) + label.grid(pady=PADY) + + def draw_tree(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + frame.grid(sticky="nsew", pady=PADY) + self.tree = ttk.Treeview( + frame, columns=("id", "state", "nodes"), show="headings" + ) + self.tree.grid(sticky="nsew") + self.tree.column("id", stretch=tk.YES) + self.tree.heading("id", text="ID") + self.tree.column("state", stretch=tk.YES) + self.tree.heading("state", text="State") + self.tree.column("nodes", stretch=tk.YES) + self.tree.heading("nodes", text="Node Count") + + for index, session in enumerate(self.sessions): + state_name = core_pb2.SessionState.Enum.Name(session.state) + self.tree.insert( + "", + tk.END, + text=str(session.id), + values=(session.id, state_name, session.nodes), + ) + self.tree.bind("", self.on_selected) + self.tree.bind("<>", self.click_select) + + yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") + self.tree.configure(yscrollcommand=yscrollbar.set) + + xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=1, sticky="ew") + self.tree.configure(xscrollcommand=xscrollbar.set) + + def draw_buttons(self): + frame = ttk.Frame(self.top) + for i in range(4): + frame.columnconfigure(i, weight=1) + frame.grid(sticky="ew") + + image = Images.get(ImageEnum.DOCUMENTNEW, 16) + b = ttk.Button( + frame, image=image, text="New", compound=tk.LEFT, command=self.click_new + ) + b.image = image + b.grid(row=0, padx=PADX, sticky="ew") + + image = Images.get(ImageEnum.FILEOPEN, 16) + b = ttk.Button( + frame, + image=image, + text="Connect", + compound=tk.LEFT, + command=self.click_connect, + ) + b.image = image + b.grid(row=0, column=1, padx=PADX, sticky="ew") + + image = Images.get(ImageEnum.EDITDELETE, 16) + b = ttk.Button( + frame, + image=image, + text="Shutdown", + compound=tk.LEFT, + command=self.click_shutdown, + ) + b.image = image + b.grid(row=0, column=2, padx=PADX, sticky="ew") + + b = ttk.Button(frame, text="Cancel", command=self.click_new) + b.grid(row=0, column=3, sticky="ew") + + def click_new(self): + self.app.core.create_new_session() + self.destroy() + + def click_select(self, event): + item = self.tree.selection() + session_id = int(self.tree.item(item, "text")) + self.selected = True + self.selected_id = session_id + + def click_connect(self): + """ + if no session is selected yet, create a new one else join that session + + :return: nothing + """ + if self.selected and self.selected_id is not None: + self.join_session(self.selected_id) + elif not self.selected and self.selected_id is None: + self.click_new() + else: + logging.error("sessions invalid state") + + def click_shutdown(self): + """ + if no session is currently selected create a new session else shut the selected + session down. + + :return: nothing + """ + if self.selected and self.selected_id is not None: + self.shutdown_session(self.selected_id) + elif not self.selected and self.selected_id is None: + self.click_new() + else: + logging.error("querysessiondrawing.py invalid state") + + def join_session(self, session_id): + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread( + target=self.app.core.join_session, args=([session_id]) + ) + thread.start() + self.destroy() + + def on_selected(self, event): + item = self.tree.selection() + sid = int(self.tree.item(item, "text")) + self.join_session(sid) + + def shutdown_session(self, sid): + self.app.core.stop_session(sid) + self.click_new() + self.destroy() diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py new file mode 100644 index 00000000..62fed9f9 --- /dev/null +++ b/coretk/coretk/dialogs/shapemod.py @@ -0,0 +1,252 @@ +""" +shape input dialog +""" +import tkinter as tk +from tkinter import font, ttk + +from coretk.dialogs.colorpicker import ColorPicker +from coretk.dialogs.dialog import Dialog +from coretk.graph import tags +from coretk.graph.shapeutils import is_draw_shape, is_shape_text +from coretk.themes import FRAME_PAD, PADX, PADY + +FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] +BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + +class ShapeDialog(Dialog): + def __init__(self, master, app, shape): + if is_draw_shape(shape.shape_type): + title = "Add Shape" + else: + title = "Add Text" + super().__init__(master, app, title, modal=True) + self.canvas = app.canvas + self.fill = None + self.border = None + self.shape = shape + data = shape.shape_data + self.shape_text = tk.StringVar(value=data.text) + self.font = tk.StringVar(value=data.font) + self.font_size = tk.IntVar(value=data.font_size) + self.text_color = data.text_color + fill_color = data.fill_color + if not fill_color: + fill_color = "#CFCFFF" + self.fill_color = fill_color + self.border_color = data.border_color + self.border_width = tk.IntVar(value=0) + self.bold = tk.BooleanVar(value=data.bold) + self.italic = tk.BooleanVar(value=data.italic) + self.underline = tk.BooleanVar(value=data.underline) + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.draw_label_options() + if is_draw_shape(self.shape.shape_type): + self.draw_shape_options() + self.draw_spacer() + self.draw_buttons() + + def draw_label_options(self): + label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) + + entry = ttk.Entry(label_frame, textvariable=self.shape_text) + entry.grid(sticky="ew", pady=PADY) + + # font options + frame = ttk.Frame(label_frame) + frame.grid(sticky="nsew", pady=PADY) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + combobox = ttk.Combobox( + frame, + textvariable=self.font, + values=sorted(font.families()), + state="readonly", + ) + combobox.grid(row=0, column=0, sticky="nsew") + combobox = ttk.Combobox( + frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly" + ) + combobox.grid(row=0, column=1, padx=PADX, sticky="nsew") + button = ttk.Button(frame, text="Color", command=self.choose_text_color) + button.grid(row=0, column=2, sticky="nsew") + + # style options + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) + button = ttk.Checkbutton(frame, variable=self.bold, text="Bold") + button.grid(row=0, column=0, sticky="ew") + button = ttk.Checkbutton(frame, variable=self.italic, text="Italic") + button.grid(row=0, column=1, padx=PADX, sticky="ew") + button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") + button.grid(row=0, column=2, sticky="ew") + + def draw_shape_options(self): + label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD) + label_frame.grid(sticky="ew", pady=PADY) + label_frame.columnconfigure(0, weight=1) + + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew") + for i in range(1, 3): + frame.columnconfigure(i, weight=1) + label = ttk.Label(frame, text="Fill Color") + label.grid(row=0, column=0, padx=PADX, sticky="w") + self.fill = ttk.Label(frame, text=self.fill_color, background=self.fill_color) + self.fill.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Color", command=self.choose_fill_color) + button.grid(row=0, column=2, sticky="ew") + + label = ttk.Label(frame, text="Border Color") + label.grid(row=1, column=0, sticky="w", padx=PADX) + self.border = ttk.Label( + frame, text=self.border_color, background=self.border_color + ) + self.border.grid(row=1, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Color", command=self.choose_border_color) + button.grid(row=1, column=2, sticky="ew") + + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Border Width") + label.grid(row=0, column=0, sticky="w", padx=PADX) + combobox = ttk.Combobox( + frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly" + ) + combobox.grid(row=0, column=1, sticky="nsew") + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = ttk.Button(frame, text="Add shape", command=self.click_add) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.cancel) + button.grid(row=0, column=1, sticky="ew") + + def choose_text_color(self): + color_picker = ColorPicker(self, self.app, "#000000") + color = color_picker.askcolor() + self.text_color = color + + def choose_fill_color(self): + color_picker = ColorPicker(self, self.app, self.fill_color) + color = color_picker.askcolor() + self.fill_color = color + self.fill.config(background=color, text=color) + + def choose_border_color(self): + color_picker = ColorPicker(self, self.app, self.border_color) + color = color_picker.askcolor() + self.border_color = color + self.border.config(background=color, text=color) + + def cancel(self): + self.shape.delete() + self.canvas.shapes.pop(self.shape.id) + self.destroy() + + def click_add(self): + if is_draw_shape(self.shape.shape_type): + self.add_shape() + elif is_shape_text(self.shape.shape_type): + self.add_text() + self.destroy() + + def make_font(self): + """ + create font for text or shape label + :return: list(font specifications) + """ + size = int(self.font_size.get()) + text_font = [self.font.get(), size] + if self.bold.get(): + text_font.append("bold") + if self.italic.get(): + text_font.append("italic") + if self.underline.get(): + text_font.append("underline") + return text_font + + def save_text(self): + """ + save info related to text or shape label + + :return: nothing + """ + data = self.shape.shape_data + data.text = self.shape_text.get() + data.font = self.font.get() + data.font_size = int(self.font_size.get()) + data.text_color = self.text_color + data.bold = self.bold.get() + data.italic = self.italic.get() + data.underline = self.underline.get() + + def save_shape(self): + """ + save info related to shape + + :return: nothing + """ + data = self.shape.shape_data + data.fill_color = self.fill_color + data.border_color = self.border_color + data.border_width = int(self.border_width.get()) + + def add_text(self): + """ + add text to canvas + + :return: nothing + """ + text = self.shape_text.get() + text_font = self.make_font() + self.canvas.itemconfig( + self.shape.id, text=text, fill=self.text_color, font=text_font + ) + self.save_text() + + def add_shape(self): + self.canvas.itemconfig( + self.shape.id, + fill=self.fill_color, + dash="", + outline=self.border_color, + width=int(self.border_width.get()), + ) + shape_text = self.shape_text.get() + size = int(self.font_size.get()) + x0, y0, x1, y1 = self.canvas.bbox(self.shape.id) + _y = y0 + 1.5 * size + _x = (x0 + x1) / 2 + text_font = self.make_font() + if self.shape.text_id is None: + self.shape.text_id = self.canvas.create_text( + _x, + _y, + text=shape_text, + fill=self.text_color, + font=text_font, + tags=tags.SHAPE_TEXT, + ) + self.shape.created = True + else: + self.canvas.itemconfig( + self.shape.text_id, + text=shape_text, + fill=self.text_color, + font=text_font, + ) + self.save_text() + self.save_shape() diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py new file mode 100644 index 00000000..20966d2b --- /dev/null +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -0,0 +1,66 @@ +""" +wlan configuration +""" + +from tkinter import ttk + +import grpc + +from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error +from coretk.themes import PADX, PADY +from coretk.widgets import ConfigFrame + + +class WlanConfigDialog(Dialog): + def __init__(self, master, app, canvas_node): + super().__init__( + master, app, f"{canvas_node.core_node.name} Wlan Configuration", modal=True + ) + self.canvas_node = canvas_node + self.node = canvas_node.core_node + self.config_frame = None + try: + self.config = self.app.core.get_wlan_config(self.node.id) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PADY) + self.draw_apply_buttons() + + def draw_apply_buttons(self): + """ + create node configuration options + + :return: nothing + """ + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, padx=PADX, sticky="ew") + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_apply(self): + """ + retrieve user's wlan configuration and store the new configuration values + + :return: nothing + """ + config = self.config_frame.parse_config() + self.app.core.wlan_configs[self.node.id] = self.config + if self.app.core.is_runtime(): + session_id = self.app.core.session_id + self.app.core.client.set_wlan_config(session_id, self.node.id, config) + self.destroy() diff --git a/coretk/coretk/errors.py b/coretk/coretk/errors.py new file mode 100644 index 00000000..936968ad --- /dev/null +++ b/coretk/coretk/errors.py @@ -0,0 +1,8 @@ +from tkinter import messagebox + + +def show_grpc_error(e): + title = [x.capitalize() for x in e.code().name.lower().split("_")] + title = " ".join(title) + title = f"GRPC {title}" + messagebox.showerror(title, e.details()) diff --git a/coretk/coretk/graph/__init__.py b/coretk/coretk/graph/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py new file mode 100644 index 00000000..e25a5305 --- /dev/null +++ b/coretk/coretk/graph/edges.py @@ -0,0 +1,181 @@ +import logging +import tkinter as tk +from tkinter.font import Font + +from coretk import themes +from coretk.dialogs.linkconfig import LinkConfiguration +from coretk.graph import tags +from coretk.nodeutils import NodeUtils + +TEXT_DISTANCE = 0.30 + + +class CanvasWirelessEdge: + def __init__(self, token, position, src, dst, canvas): + self.token = token + self.src = src + self.dst = dst + self.canvas = canvas + self.id = self.canvas.create_line( + *position, tags=tags.WIRELESS_EDGE, width=1.5, fill="#009933" + ) + + def delete(self): + self.canvas.delete(self.id) + + +class CanvasEdge: + """ + Canvas edge class + """ + + width = 3 + + def __init__(self, x1, y1, x2, y2, src, canvas): + """ + Create an instance of canvas edge object + :param int x1: source x-coord + :param int y1: source y-coord + :param int x2: destination x-coord + :param int y2: destination y-coord + :param int src: source id + :param coretk.graph.graph.GraphCanvas canvas: canvas object + """ + self.src = src + self.dst = None + self.src_interface = None + self.dst_interface = None + self.canvas = canvas + self.id = self.canvas.create_line( + x1, y1, x2, y2, tags=tags.EDGE, width=self.width, fill="#ff0000" + ) + self.text_src = None + self.text_dst = None + self.token = None + self.font = Font(size=8) + self.link = None + self.asymmetric_link = None + self.throughput = None + self.set_binding() + + def set_binding(self): + self.canvas.tag_bind(self.id, "", self.create_context) + + def set_link(self, link): + self.link = link + self.draw_labels() + + def get_coordinates(self): + x1, y1, x2, y2 = self.canvas.coords(self.id) + v1 = x2 - x1 + v2 = y2 - y1 + ux = TEXT_DISTANCE * v1 + uy = TEXT_DISTANCE * v2 + x1 = x1 + ux + y1 = y1 + uy + x2 = x2 - ux + y2 = y2 - uy + return x1, y1, x2, y2 + + def draw_labels(self): + x1, y1, x2, y2 = self.get_coordinates() + label_one = None + if self.link.HasField("interface_one"): + label_one = ( + f"{self.link.interface_one.ip4}/{self.link.interface_one.ip4mask}\n" + f"{self.link.interface_one.ip6}/{self.link.interface_one.ip6mask}\n" + ) + label_two = None + if self.link.HasField("interface_two"): + label_two = ( + f"{self.link.interface_two.ip4}/{self.link.interface_two.ip4mask}\n" + f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n" + ) + self.text_src = self.canvas.create_text( + x1, + y1, + text=label_one, + justify=tk.CENTER, + font=self.font, + tags=tags.LINK_INFO, + ) + self.text_dst = self.canvas.create_text( + x2, + y2, + text=label_two, + justify=tk.CENTER, + font=self.font, + tags=tags.LINK_INFO, + ) + + def update_labels(self): + """ + Move edge labels based on current position. + + :return: nothing + """ + x1, y1, x2, y2 = self.get_coordinates() + self.canvas.coords(self.text_src, x1, y1) + self.canvas.coords(self.text_dst, x2, y2) + + def complete(self, dst): + self.dst = dst + self.token = tuple(sorted((self.src, self.dst))) + x, y = self.canvas.coords(self.dst) + x1, y1, _, _ = self.canvas.coords(self.id) + self.canvas.coords(self.id, x1, y1, x, y) + self.check_wireless() + self.canvas.tag_raise(self.src) + self.canvas.tag_raise(self.dst) + + def check_wireless(self): + src_node = self.canvas.nodes[self.src] + dst_node = self.canvas.nodes[self.dst] + src_node_type = src_node.core_node.type + dst_node_type = dst_node.core_node.type + is_src_wireless = NodeUtils.is_wireless_node(src_node_type) + is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) + if is_src_wireless or is_dst_wireless: + self.canvas.itemconfig(self.id, state=tk.HIDDEN) + self._check_antenna() + + def _check_antenna(self): + src_node = self.canvas.nodes[self.src] + dst_node = self.canvas.nodes[self.dst] + src_node_type = src_node.core_node.type + dst_node_type = dst_node.core_node.type + is_src_wireless = NodeUtils.is_wireless_node(src_node_type) + is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) + if is_src_wireless or is_dst_wireless: + if is_src_wireless and not is_dst_wireless: + dst_node.add_antenna() + elif not is_src_wireless and is_dst_wireless: + src_node.add_antenna() + # TODO: remove this? dont allow linking wireless nodes? + else: + src_node.add_antenna() + + def delete(self): + self.canvas.delete(self.id) + if self.link: + self.canvas.delete(self.text_src) + self.canvas.delete(self.text_dst) + + def create_context(self, event): + logging.debug("create link context") + context = tk.Menu(self.canvas) + themes.style_menu(context) + context.add_command(label="Configure", command=self.configure) + context.add_command(label="Delete") + context.add_command(label="Split") + context.add_command(label="Merge") + if self.canvas.app.core.is_runtime(): + context.entryconfigure(1, state="disabled") + context.entryconfigure(2, state="disabled") + context.entryconfigure(3, state="disabled") + context.post(event.x_root, event.y_root) + + def configure(self): + logging.debug("link configuration") + dialog = LinkConfiguration(self.canvas, self.canvas.app, self) + dialog.show() diff --git a/coretk/coretk/graph/enums.py b/coretk/coretk/graph/enums.py new file mode 100644 index 00000000..b292938f --- /dev/null +++ b/coretk/coretk/graph/enums.py @@ -0,0 +1,18 @@ +import enum + + +class GraphMode(enum.Enum): + SELECT = 0 + EDGE = 1 + PICKNODE = 2 + NODE = 3 + ANNOTATION = 4 + OTHER = 5 + + +class ScaleOption(enum.Enum): + NONE = 0 + UPPER_LEFT = 1 + CENTERED = 2 + SCALED = 3 + TILED = 4 diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py new file mode 100644 index 00000000..5d34f566 --- /dev/null +++ b/coretk/coretk/graph/graph.py @@ -0,0 +1,839 @@ +import logging +import tkinter as tk + +from PIL import Image, ImageTk + +from core.api.grpc import core_pb2 +from coretk import nodeutils +from coretk.dialogs.shapemod import ShapeDialog +from coretk.graph import tags +from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge +from coretk.graph.enums import GraphMode, ScaleOption +from coretk.graph.linkinfo import Throughput +from coretk.graph.node import CanvasNode +from coretk.graph.shape import Shape +from coretk.graph.shapeutils import ShapeType, is_draw_shape, is_marker +from coretk.images import Images +from coretk.nodeutils import NodeUtils + +ZOOM_IN = 1.1 +ZOOM_OUT = 0.9 + + +class CanvasGraph(tk.Canvas): + def __init__(self, master, core, width, height): + super().__init__(master, highlightthickness=0, background="#cccccc") + self.app = master + self.core = core + self.mode = GraphMode.SELECT + self.annotation_type = None + self.selection = {} + self.select_box = None + self.selected = None + self.node_draw = None + self.context = None + self.nodes = {} + self.edges = {} + self.shapes = {} + self.wireless_edges = {} + self.drawing_edge = None + self.grid = None + self.throughput_draw = Throughput(self, core) + self.shape_drawing = False + self.default_dimensions = (width, height) + self.current_dimensions = self.default_dimensions + self.ratio = 1.0 + self.offset = (0, 0) + self.cursor = (0, 0) + self.marker_tool = None + + # background related + self.wallpaper_id = None + self.wallpaper = None + self.wallpaper_drawn = None + self.wallpaper_file = "" + self.scale_option = tk.IntVar(value=1) + self.show_grid = tk.BooleanVar(value=True) + self.adjust_to_dim = tk.BooleanVar(value=False) + + # bindings + self.setup_bindings() + + # draw base canvas + self.draw_canvas() + self.draw_grid() + + def draw_canvas(self, dimensions=None): + if self.grid is not None: + self.delete(self.grid) + if not dimensions: + dimensions = self.default_dimensions + self.current_dimensions = dimensions + self.grid = self.create_rectangle( + 0, + 0, + *dimensions, + outline="#000000", + fill="#ffffff", + width=1, + tags="rectangle", + ) + self.configure(scrollregion=self.bbox(tk.ALL)) + + def reset_and_redraw(self, session): + """ + Reset the private variables CanvasGraph object, redraw nodes given the new grpc + client. + + :param core.api.grpc.core_pb2.Session session: session to draw + :return: nothing + """ + # hide context + self.hide_context() + + # delete any existing drawn items + for tag in tags.COMPONENT_TAGS: + self.delete(tag) + + # set the private variables to default value + self.mode = GraphMode.SELECT + self.annotation_type = None + self.node_draw = None + self.selected = None + self.nodes.clear() + self.edges.clear() + self.shapes.clear() + self.wireless_edges.clear() + self.drawing_edge = None + self.draw_session(session) + + def setup_bindings(self): + """ + Bind any mouse events or hot keys to the matching action + + :return: nothing + """ + self.bind("", self.click_press) + self.bind("", self.click_release) + self.bind("", self.click_motion) + self.bind("", self.click_context) + self.bind("", self.press_delete) + self.bind("", self.ctrl_click) + self.bind("", self.double_click) + self.bind("", self.zoom) + self.bind("", lambda e: self.zoom(e, ZOOM_IN)) + self.bind("", lambda e: self.zoom(e, ZOOM_OUT)) + self.bind("", lambda e: self.scan_mark(e.x, e.y)) + self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) + + def hide_context(self): + if self.context: + self.context.unpost() + self.context = None + + def get_actual_coords(self, x, y): + actual_x = (x - self.offset[0]) / self.ratio + actual_y = (y - self.offset[1]) / self.ratio + return actual_x, actual_y + + def get_scaled_coords(self, x, y): + scaled_x = (x * self.ratio) + self.offset[0] + scaled_y = (y * self.ratio) + self.offset[1] + return scaled_x, scaled_y + + def inside_canvas(self, x, y): + x1, y1, x2, y2 = self.bbox(self.grid) + valid_x = x1 <= x <= x2 + valid_y = y1 <= y <= y2 + return valid_x and valid_y + + def valid_position(self, x1, y1, x2, y2): + valid_topleft = self.inside_canvas(x1, y1) + valid_bottomright = self.inside_canvas(x2, y2) + return valid_topleft and valid_bottomright + + def draw_grid(self): + """ + Create grid. + + :return: nothing + """ + width, height = self.width_and_height() + width = int(width) + height = int(height) + for i in range(0, width, 27): + self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE) + for i in range(0, height, 27): + self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE) + self.tag_lower(tags.GRIDLINE) + self.tag_lower(self.grid) + + def add_wireless_edge(self, src, dst): + """ + add a wireless edge between 2 canvas nodes + + :param CanvasNode src: source node + :param CanvasNode dst: destination node + :return: nothing + """ + token = tuple(sorted((src.id, dst.id))) + x1, y1 = self.coords(src.id) + x2, y2 = self.coords(dst.id) + position = (x1, y1, x2, y2) + edge = CanvasWirelessEdge(token, position, src.id, dst.id, self) + self.wireless_edges[token] = edge + src.wireless_edges.add(edge) + dst.wireless_edges.add(edge) + self.tag_raise(src.id) + self.tag_raise(dst.id) + + def delete_wireless_edge(self, src, dst): + token = tuple(sorted((src.id, dst.id))) + edge = self.wireless_edges.pop(token) + edge.delete() + src.wireless_edges.remove(edge) + dst.wireless_edges.remove(edge) + + def draw_session(self, session): + """ + Draw existing session. + + :return: nothing + """ + # draw existing nodes + for core_node in session.nodes: + # peer to peer node is not drawn on the GUI + if NodeUtils.is_ignore_node(core_node.type): + continue + + # draw nodes on the canvas + logging.info("drawing core node: %s", core_node) + image = NodeUtils.node_icon(core_node.type, core_node.model) + if core_node.icon: + try: + image = Images.create(core_node.icon, nodeutils.ICON_SIZE) + except OSError: + logging.error("invalid icon: %s", core_node.icon) + + x = core_node.position.x + y = core_node.position.y + node = CanvasNode(self.master, x, y, core_node, image) + self.nodes[node.id] = node + self.core.canvas_nodes[core_node.id] = node + + # draw existing links + for link in session.links: + logging.info("drawing link: %s", link) + canvas_node_one = self.core.canvas_nodes[link.node_one_id] + node_one = canvas_node_one.core_node + canvas_node_two = self.core.canvas_nodes[link.node_two_id] + node_two = canvas_node_two.core_node + token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) + + if link.type == core_pb2.LinkType.WIRELESS: + self.add_wireless_edge(canvas_node_one, canvas_node_two) + else: + if token not in self.edges: + edge = CanvasEdge( + node_one.position.x, + node_one.position.y, + node_two.position.x, + node_two.position.y, + canvas_node_one.id, + self, + ) + edge.token = token + edge.dst = canvas_node_two.id + edge.set_link(link) + edge.check_wireless() + canvas_node_one.edges.add(edge) + canvas_node_two.edges.add(edge) + self.edges[edge.token] = edge + self.core.links[edge.token] = edge + if link.HasField("interface_one"): + canvas_node_one.interfaces.append(link.interface_one) + if link.HasField("interface_two"): + canvas_node_two.interfaces.append(link.interface_two) + elif link.options.unidirectional: + edge = self.edges[token] + edge.asymmetric_link = link + else: + logging.error("duplicate link received: %s", link) + + # raise the nodes so they on top of the links + self.tag_raise(tags.NODE) + + def canvas_xy(self, event): + """ + Convert window coordinate to canvas coordinate + + :param event: + :rtype: (int, int) + :return: x, y canvas coordinate + """ + x = self.canvasx(event.x) + y = self.canvasy(event.y) + return x, y + + def get_selected(self, event): + """ + Retrieve the item id that is on the mouse position + + :param event: mouse event + :rtype: int + :return: the item that the mouse point to + """ + x, y = self.canvas_xy(event) + overlapping = self.find_overlapping(x, y, x, y) + selected = None + for _id in overlapping: + if self.drawing_edge and self.drawing_edge.id == _id: + continue + + if _id in self.nodes: + selected = _id + break + + if _id in self.shapes: + selected = _id + + return selected + + def click_release(self, event): + """ + Draw a node or finish drawing an edge according to the current graph mode + + :param event: mouse event + :return: nothing + """ + logging.debug("click release") + x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + + if self.context: + self.hide_context() + else: + if self.mode == GraphMode.ANNOTATION: + self.focus_set() + if self.shape_drawing: + shape = self.shapes[self.selected] + shape.shape_complete(x, y) + self.shape_drawing = False + elif self.mode == GraphMode.SELECT: + self.focus_set() + if self.select_box: + x0, y0, x1, y1 = self.coords(self.select_box.id) + inside = [ + x + for x in self.find_enclosed(x0, y0, x1, y1) + if "node" in self.gettags(x) or "shape" in self.gettags(x) + ] + for i in inside: + self.select_object(i, True) + self.select_box.disappear() + self.select_box = None + else: + self.focus_set() + self.selected = self.get_selected(event) + logging.debug( + f"click release selected({self.selected}) mode({self.mode})" + ) + if self.mode == GraphMode.EDGE: + self.handle_edge_release(event) + elif self.mode == GraphMode.NODE: + self.add_node(x, y) + elif self.mode == GraphMode.PICKNODE: + self.mode = GraphMode.NODE + self.selected = None + + def handle_edge_release(self, event): + edge = self.drawing_edge + self.drawing_edge = None + + # not drawing edge return + if edge is None: + return + + # edge dst must be a node + logging.debug(f"current selected: {self.selected}") + dst_node = self.nodes.get(self.selected) + if not dst_node: + edge.delete() + return + + # edge dst is same as src, delete edge + if edge.src == self.selected: + edge.delete() + return + + # ignore repeated edges + token = tuple(sorted((edge.src, self.selected))) + if token in self.edges: + edge.delete() + return + + # set dst node and snap edge to center + edge.complete(self.selected) + logging.debug("drawing edge token: %s", edge.token) + + self.edges[edge.token] = edge + node_src = self.nodes[edge.src] + node_src.edges.add(edge) + node_dst = self.nodes[edge.dst] + node_dst.edges.add(edge) + self.core.create_link(edge, node_src, node_dst) + + def select_object(self, object_id, choose_multiple=False): + """ + create a bounding box when a node is selected + """ + if not choose_multiple: + self.clear_selection() + + # draw a bounding box if node hasn't been selected yet + if object_id not in self.selection: + x0, y0, x1, y1 = self.bbox(object_id) + selection_id = self.create_rectangle( + (x0 - 6, y0 - 6, x1 + 6, y1 + 6), + activedash=True, + dash="-", + tags=tags.SELECTION, + ) + self.selection[object_id] = selection_id + else: + selection_id = self.selection.pop(object_id) + self.delete(selection_id) + + def clear_selection(self): + """ + Clear current selection boxes. + + :return: nothing + """ + for _id in self.selection.values(): + self.delete(_id) + self.selection.clear() + + def move_selection(self, object_id, x_offset, y_offset): + select_id = self.selection.get(object_id) + if select_id is not None: + self.move(select_id, x_offset, y_offset) + + def delete_selection_objects(self): + edges = set() + nodes = [] + for object_id in self.selection: + # delete selection box + selection_id = self.selection[object_id] + self.delete(selection_id) + + # delete node and related edges + if object_id in self.nodes: + canvas_node = self.nodes.pop(object_id) + canvas_node.delete() + nodes.append(canvas_node) + is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) + + # delete related edges + for edge in canvas_node.edges: + if edge in edges: + continue + edges.add(edge) + self.throughput_draw.delete(edge) + del self.edges[edge.token] + edge.delete() + + # update node connected to edge being deleted + other_id = edge.src + other_interface = edge.src_interface + if edge.src == object_id: + other_id = edge.dst + other_interface = edge.dst_interface + other_node = self.nodes[other_id] + other_node.edges.remove(edge) + try: + other_node.interfaces.remove(other_interface) + except ValueError: + pass + if is_wireless: + other_node.delete_antenna() + + # delete shape + if object_id in self.shapes: + shape = self.shapes.pop(object_id) + shape.delete() + + self.selection.clear() + return nodes + + def zoom(self, event, factor=None): + if not factor: + factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT + event.x, event.y = self.canvasx(event.x), self.canvasy(event.y) + self.scale(tk.ALL, event.x, event.y, factor, factor) + self.configure(scrollregion=self.bbox(tk.ALL)) + self.ratio *= float(factor) + self.offset = ( + self.offset[0] * factor + event.x * (1 - factor), + self.offset[1] * factor + event.y * (1 - factor), + ) + logging.info("ratio: %s", self.ratio) + logging.info("offset: %s", self.offset) + self.app.statusbar.zoom.config(text="%s" % (int(self.ratio * 100)) + "%") + + if self.wallpaper: + self.redraw_wallpaper() + + def click_press(self, event): + """ + Start drawing an edge if mouse click is on a node + + :param event: mouse event + :return: nothing + """ + x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + + self.cursor = x, y + selected = self.get_selected(event) + logging.debug("click press(%s): %s", self.cursor, selected) + x_check = self.cursor[0] - self.offset[0] + y_check = self.cursor[1] - self.offset[1] + logging.debug("clock press ofset(%s, %s)", x_check, y_check) + is_node = selected in self.nodes + if self.mode == GraphMode.EDGE and is_node: + x, y = self.coords(selected) + self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) + + if self.mode == GraphMode.ANNOTATION: + if is_marker(self.annotation_type): + r = self.app.toolbar.marker_tool.radius + self.create_oval( + x - r, + y - r, + x + r, + y + r, + fill=self.app.toolbar.marker_tool.color, + outline="", + tags="marker", + ) + return + if selected is None: + shape = Shape(self.app, self, self.annotation_type, x, y) + self.selected = shape.id + self.shape_drawing = True + self.shapes[shape.id] = shape + + if selected is not None: + if selected not in self.selection: + if selected in self.shapes: + shape = self.shapes[selected] + self.select_object(shape.id) + self.selected = selected + elif selected in self.nodes: + node = self.nodes[selected] + self.select_object(node.id) + self.selected = selected + logging.info( + "selected coords: (%s, %s)", + node.core_node.position.x, + node.core_node.position.y, + ) + else: + logging.debug("create selection box") + if self.mode == GraphMode.SELECT: + shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y) + self.select_box = shape + self.clear_selection() + + def ctrl_click(self, event): + # update cursor location + x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + + self.cursor = x, y + + # handle multiple selections + logging.debug("control left click: %s", event) + selected = self.get_selected(event) + if ( + selected not in self.selection + and selected in self.shapes + or selected in self.nodes + ): + self.select_object(selected, choose_multiple=True) + + def click_motion(self, event): + """ + Redraw drawing edge according to the current position of the mouse + + :param event: mouse event + :return: nothing + """ + x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + if self.select_box: + self.select_box.delete() + self.select_box = None + if is_draw_shape(self.annotation_type) and self.shape_drawing: + shape = self.shapes.pop(self.selected) + shape.delete() + self.shape_drawing = False + return + + x_offset = x - self.cursor[0] + y_offset = y - self.cursor[1] + self.cursor = x, y + + if self.mode == GraphMode.EDGE and self.drawing_edge is not None: + x1, y1, _, _ = self.coords(self.drawing_edge.id) + self.coords(self.drawing_edge.id, x1, y1, x, y) + if self.mode == GraphMode.ANNOTATION: + if is_draw_shape(self.annotation_type) and self.shape_drawing: + shape = self.shapes[self.selected] + shape.shape_motion(x, y) + elif is_marker(self.annotation_type): + r = self.app.toolbar.marker_tool.radius + self.create_oval( + x - r, + y - r, + x + r, + y + r, + fill=self.app.toolbar.marker_tool.color, + outline="", + tags="marker", + ) + return + + if self.mode == GraphMode.EDGE: + return + + # move selected objects + if self.selection: + for selected_id in self.selection: + if selected_id in self.shapes: + shape = self.shapes[selected_id] + shape.motion(x_offset, y_offset) + + if selected_id in self.nodes: + node = self.nodes[selected_id] + node.motion(x_offset, y_offset, update=self.core.is_runtime()) + else: + if self.select_box and self.mode == GraphMode.SELECT: + self.select_box.shape_motion(x, y) + + def click_context(self, event): + logging.info("context event: %s", self.context) + if not self.context: + selected = self.get_selected(event) + canvas_node = self.nodes.get(selected) + if canvas_node: + logging.debug(f"node context: {selected}") + self.context = canvas_node.create_context() + self.context.post(event.x_root, event.y_root) + else: + self.hide_context() + + def press_delete(self, event): + """ + delete selected nodes and any data that relates to it + :param event: + :return: + """ + logging.debug("press delete key") + nodes = self.delete_selection_objects() + self.core.delete_graph_nodes(nodes) + + def double_click(self, event): + selected = self.get_selected(event) + if selected is not None and selected in self.shapes: + shape = self.shapes[selected] + dialog = ShapeDialog(self.app, self.app, shape) + dialog.show() + + def add_node(self, x, y): + if self.selected is None or self.selected in self.shapes: + actual_x, actual_y = self.get_actual_coords(x, y) + core_node = self.core.create_node( + actual_x, actual_y, self.node_draw.node_type, self.node_draw.model + ) + node = CanvasNode(self.master, x, y, core_node, self.node_draw.image) + self.core.canvas_nodes[core_node.id] = node + self.nodes[node.id] = node + return node + + def width_and_height(self): + """ + retrieve canvas width and height in pixels + + :return: nothing + """ + x0, y0, x1, y1 = self.coords(self.grid) + canvas_w = abs(x0 - x1) + canvas_h = abs(y0 - y1) + return canvas_w, canvas_h + + def get_wallpaper_image(self): + width = int(self.wallpaper.width * self.ratio) + height = int(self.wallpaper.height * self.ratio) + image = self.wallpaper.resize((width, height), Image.ANTIALIAS) + return image + + def draw_wallpaper(self, image, x=None, y=None): + if x is None and y is None: + x1, y1, x2, y2 = self.bbox(self.grid) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + + self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER) + self.wallpaper_drawn = image + + def wallpaper_upper_left(self): + self.delete(self.wallpaper_id) + + # create new scaled image, cropped if needed + width, height = self.width_and_height() + image = self.get_wallpaper_image() + cropx = image.width + cropy = image.height + if image.width > width: + cropx = image.width + if image.height > height: + cropy = image.height + cropped = image.crop((0, 0, cropx, cropy)) + image = ImageTk.PhotoImage(cropped) + + # draw on canvas + x1, y1, _, _ = self.bbox(self.grid) + x = (cropx / 2) + x1 + y = (cropy / 2) + y1 + self.draw_wallpaper(image, x, y) + + def wallpaper_center(self): + """ + place the image at the center of canvas + + :return: nothing + """ + self.delete(self.wallpaper_id) + + # dimension of the cropped image + width, height = self.width_and_height() + image = self.get_wallpaper_image() + cropx = 0 + if image.width > width: + cropx = (image.width - width) / 2 + cropy = 0 + if image.height > height: + cropy = (image.height - height) / 2 + x1 = 0 + cropx + y1 = 0 + cropy + x2 = image.width - cropx + y2 = image.height - cropy + cropped = image.crop((x1, y1, x2, y2)) + image = ImageTk.PhotoImage(cropped) + self.draw_wallpaper(image) + + def wallpaper_scaled(self): + """ + scale image based on canvas dimension + + :return: nothing + """ + self.delete(self.wallpaper_id) + canvas_w, canvas_h = self.width_and_height() + image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) + image = ImageTk.PhotoImage(image) + self.draw_wallpaper(image) + + def resize_to_wallpaper(self): + self.delete(self.wallpaper_id) + image = ImageTk.PhotoImage(self.wallpaper) + self.redraw_canvas((image.width(), image.height())) + self.draw_wallpaper(image) + + def redraw_canvas(self, dimensions=None): + logging.info("redrawing canvas to dimensions: %s", dimensions) + + # reset scale and move back to original position + logging.info("resetting scaling: %s %s", self.ratio, self.offset) + factor = 1 / self.ratio + self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor) + self.move(tk.ALL, -self.offset[0], -self.offset[1]) + + # reset ratio and offset + self.ratio = 1.0 + self.offset = (0, 0) + + # redraw canvas rectangle + self.draw_canvas(dimensions) + + # redraw gridlines to new canvas size + self.delete(tags.GRIDLINE) + self.draw_grid() + self.update_grid() + + def redraw_wallpaper(self): + if self.adjust_to_dim.get(): + logging.info("drawing wallpaper to canvas dimensions") + self.resize_to_wallpaper() + else: + option = ScaleOption(self.scale_option.get()) + logging.info("drawing canvas using scaling option: %s", option) + if option == ScaleOption.UPPER_LEFT: + self.wallpaper_upper_left() + elif option == ScaleOption.CENTERED: + self.wallpaper_center() + elif option == ScaleOption.SCALED: + self.wallpaper_scaled() + elif option == ScaleOption.TILED: + logging.warning("tiled background not implemented yet") + + # raise items above wallpaper + for component in tags.ABOVE_WALLPAPER_TAGS: + self.tag_raise(component) + + def update_grid(self): + logging.info("updating grid show: %s", self.show_grid.get()) + if self.show_grid.get(): + self.itemconfig(tags.GRIDLINE, state=tk.NORMAL) + else: + self.itemconfig(tags.GRIDLINE, state=tk.HIDDEN) + + def set_wallpaper(self, filename): + logging.info("setting wallpaper: %s", filename) + if filename: + img = Image.open(filename) + self.wallpaper = img + self.wallpaper_file = filename + self.redraw_wallpaper() + else: + if self.wallpaper_id is not None: + self.delete(self.wallpaper_id) + self.wallpaper = None + self.wallpaper_file = None + + def is_selection_mode(self): + return self.mode == GraphMode.SELECT + + def create_edge(self, source, dest): + """ + create an edge between source node and destination node + + :param CanvasNode source: source node + :param CanvasNode dest: destination node + :return: nothing + """ + if (source.id, dest.id) not in self.edges: + pos0 = source.core_node.position + x0 = pos0.x + y0 = pos0.y + edge = CanvasEdge(x0, y0, x0, y0, source.id, self) + edge.complete(dest.id) + self.edges[edge.token] = edge + self.nodes[source.id].edges.add(edge) + self.nodes[dest.id].edges.add(edge) + self.core.create_link(edge, source, dest) diff --git a/coretk/coretk/graph/linkinfo.py b/coretk/coretk/graph/linkinfo.py new file mode 100644 index 00000000..de4b2c6e --- /dev/null +++ b/coretk/coretk/graph/linkinfo.py @@ -0,0 +1,152 @@ +""" +Link information, such as IPv4, IPv6 and throughput drawn in the canvas +""" + +from core.api.grpc import core_pb2 + + +class Throughput: + def __init__(self, canvas, core): + self.canvas = canvas + self.core = core + # edge canvas id mapped to throughput value + self.tracker = {} + # map an edge canvas id to a throughput canvas id + self.map = {} + # map edge canvas id to token + self.edge_id_to_token = {} + + def load_throughput_info(self, interface_throughputs): + """ + load all interface throughouts from an event + + :param repeated core_bp2.InterfaceThroughputinterface_throughputs: interface + throughputs + :return: nothing + """ + for throughput in interface_throughputs: + nid = throughput.node_id + iid = throughput.interface_id + tp = throughput.throughput + token = self.core.interface_to_edge.get((nid, iid)) + if token: + edge = self.canvas.edges.get(token) + if edge: + edge_id = edge.id + self.edge_id_to_token[edge_id] = token + if edge_id not in self.tracker: + self.tracker[edge_id] = tp + else: + temp = self.tracker[edge_id] + self.tracker[edge_id] = (temp + tp) / 2 + else: + self.core.interface_to_edge.pop((nid, iid), None) + + def edge_is_wired(self, token): + """ + determine whether link is a WIRED link + + :param token: + :return: + """ + canvas_edge = self.canvas.edges[token] + canvas_src_id = canvas_edge.src + canvas_dst_id = canvas_edge.dst + src = self.canvas.nodes[canvas_src_id].core_node + dst = self.canvas.nodes[canvas_dst_id].core_node + return not ( + src.type == core_pb2.NodeType.WIRELESS_LAN + and dst.model == "mdr" + or src.model == "mdr" + and dst.type == core_pb2.NodeType.WIRELESS_LAN + ) + + def draw_wired_throughput(self, edge_id): + + x0, y0, x1, y1 = self.canvas.coords(edge_id) + x = (x0 + x1) / 2 + y = (y0 + y1) / 2 + if edge_id not in self.map: + tpid = self.canvas.create_text( + x, + y, + tags="throughput", + font=("Arial", 8), + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + self.map[edge_id] = tpid + else: + tpid = self.map[edge_id] + self.canvas.coords(tpid, x, y) + self.canvas.itemconfig( + tpid, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) + ) + + def draw_wireless_throughput(self, edge_id): + token = self.edge_id_to_token[edge_id] + canvas_edge = self.canvas.edges[token] + canvas_src_id = canvas_edge.src + canvas_dst_id = canvas_edge.dst + src_node = self.canvas.nodes[canvas_src_id] + dst_node = self.canvas.nodes[canvas_dst_id] + + not_wlan = ( + dst_node + if src_node.core_node.type == core_pb2.NodeType.WIRELESS_LAN + else src_node + ) + + x, y = self.canvas.coords(not_wlan.id) + if edge_id not in self.map: + tp_id = self.canvas.create_text( + x + 50, + y + 25, + font=("Arial", 8), + tags="throughput", + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + self.map[edge_id] = tp_id + + # redraw throughput + else: + self.canvas.itemconfig( + self.map[edge_id], + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + + def draw_throughputs(self): + for edge_id in self.tracker: + if self.edge_is_wired(self.edge_id_to_token[edge_id]): + self.draw_wired_throughput(edge_id) + else: + self.draw_wireless_throughput(edge_id) + + def process_grpc_throughput_event(self, interface_throughputs): + self.load_throughput_info(interface_throughputs) + self.draw_throughputs() + + def move(self, edge): + tpid = self.map.get(edge.id) + if tpid: + if self.edge_is_wired(edge.token): + x0, y0, x1, y1 = self.canvas.coords(edge.id) + self.canvas.coords(tpid, (x0 + x1) / 2, (y0 + y1) / 2) + else: + if ( + self.canvas.nodes[edge.src].core_node.type + == core_pb2.NodeType.WIRELESS_LAN + ): + x, y = self.canvas.coords(edge.dst) + self.canvas.coords(tpid, x + 50, y + 20) + else: + x, y = self.canvas.coords(edge.src) + self.canvas.coords(tpid, x + 50, y + 25) + + def delete(self, edge): + tpid = self.map.get(edge.id) + if tpid: + eid = edge.id + self.canvas.delete(tpid) + self.tracker.pop(eid) + self.map.pop(eid) + self.edge_id_to_token.pop(eid) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py new file mode 100644 index 00000000..a5893ff3 --- /dev/null +++ b/coretk/coretk/graph/node.py @@ -0,0 +1,274 @@ +import tkinter as tk +from tkinter import font + +import grpc + +from core.api.grpc import core_pb2 +from core.api.grpc.core_pb2 import NodeType +from coretk import themes +from coretk.dialogs.emaneconfig import EmaneConfigDialog +from coretk.dialogs.mobilityconfig import MobilityConfigDialog +from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.nodeservice import NodeService +from coretk.dialogs.wlanconfig import WlanConfigDialog +from coretk.errors import show_grpc_error +from coretk.graph import tags +from coretk.graph.tooltip import CanvasTooltip +from coretk.nodeutils import NodeUtils + +NODE_TEXT_OFFSET = 5 + + +class CanvasNode: + def __init__(self, app, x, y, core_node, image): + self.app = app + self.canvas = app.canvas + self.image = image + self.core_node = core_node + self.id = self.canvas.create_image( + x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE + ) + text_font = font.Font(family="TkIconFont", size=12) + label_y = self._get_label_y() + self.text_id = self.canvas.create_text( + x, + label_y, + text=self.core_node.name, + tags=tags.NODE_NAME, + font=text_font, + fill="#0000CD", + ) + self.tooltip = CanvasTooltip(self.canvas) + self.edges = set() + self.interfaces = [] + self.wireless_edges = set() + self.antennae = [] + self.setup_bindings() + + def setup_bindings(self): + self.canvas.tag_bind(self.id, "", self.double_click) + self.canvas.tag_bind(self.id, "", self.on_enter) + self.canvas.tag_bind(self.id, "", self.on_leave) + + def delete(self): + self.canvas.delete(self.id) + self.canvas.delete(self.text_id) + self.delete_antennae() + + def add_antenna(self): + x, y = self.canvas.coords(self.id) + offset = len(self.antennae) * 8 + antenna_id = self.canvas.create_image( + x - 16 + offset, + y - 23, + anchor=tk.CENTER, + image=NodeUtils.ANTENNA_ICON, + tags=tags.ANTENNA, + ) + self.antennae.append(antenna_id) + + def delete_antenna(self): + """ + delete one antenna + + :return: nothing + """ + if self.antennae: + antenna_id = self.antennae.pop() + self.canvas.delete(antenna_id) + + def delete_antennae(self): + """ + delete all antennas + + :return: nothing + """ + for antenna_id in self.antennae: + self.canvas.delete(antenna_id) + self.antennae.clear() + + def redraw(self): + self.canvas.itemconfig(self.id, image=self.image) + self.canvas.itemconfig(self.text_id, text=self.core_node.name) + + def _get_label_y(self): + image_box = self.canvas.bbox(self.id) + return image_box[3] + NODE_TEXT_OFFSET + + def move(self, x, y): + x, y = self.canvas.get_scaled_coords(x, y) + current_x, current_y = self.canvas.coords(self.id) + x_offset = x - current_x + y_offset = y - current_y + self.motion(x_offset, y_offset, update=False) + + def motion(self, x_offset, y_offset, update=True): + original_position = self.canvas.coords(self.id) + self.canvas.move(self.id, x_offset, y_offset) + x, y = self.canvas.coords(self.id) + + # check new position + bbox = self.canvas.bbox(self.id) + if not self.canvas.valid_position(*bbox): + self.canvas.coords(self.id, original_position) + return + + # move test and selection box + self.canvas.move(self.text_id, x_offset, y_offset) + self.canvas.move_selection(self.id, x_offset, y_offset) + + # move antennae + for antenna_id in self.antennae: + self.canvas.move(antenna_id, x_offset, y_offset) + + # move edges + for edge in self.edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if edge.src == self.id: + self.canvas.coords(edge.id, x, y, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, x, y) + self.canvas.throughput_draw.move(edge) + edge.update_labels() + + for edge in self.wireless_edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if edge.src == self.id: + self.canvas.coords(edge.id, x, y, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, x, y) + + # set actual coords for node and update core is running + real_x, real_y = self.canvas.get_actual_coords(x, y) + self.core_node.position.x = real_x + self.core_node.position.y = real_y + if self.app.core.is_runtime() and update: + self.app.core.edit_node(self.core_node) + + def on_enter(self, event): + if self.app.core.is_runtime() and self.app.core.observer: + self.tooltip.text.set("waiting...") + self.tooltip.on_enter(event) + try: + output = self.app.core.run(self.core_node.id) + self.tooltip.text.set(output) + except grpc.RpcError as e: + show_grpc_error(e) + + def on_leave(self, event): + self.tooltip.on_leave(event) + + def double_click(self, event): + if self.app.core.is_runtime(): + self.canvas.core.launch_terminal(self.core_node.id) + else: + self.show_config() + + def create_context(self): + is_wlan = self.core_node.type == NodeType.WIRELESS_LAN + is_emane = self.core_node.type == NodeType.EMANE + context = tk.Menu(self.canvas) + themes.style_menu(context) + if self.app.core.is_runtime(): + 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) + 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: + context.add_command( + label="Mobility Player", command=self.show_mobility_player + ) + context.add_command(label="Select Adjacent", state=tk.DISABLED) + context.add_command(label="Hide", state=tk.DISABLED) + if NodeUtils.is_container_node(self.core_node.type): + context.add_command(label="Shell Window", state=tk.DISABLED) + context.add_command(label="Tcpdump", state=tk.DISABLED) + context.add_command(label="Tshark", state=tk.DISABLED) + context.add_command(label="Wireshark", state=tk.DISABLED) + context.add_command(label="View Log", state=tk.DISABLED) + else: + 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) + if is_emane: + context.add_command( + label="EMANE Config", command=self.show_emane_config + ) + if is_wlan: + context.add_command(label="WLAN Config", command=self.show_wlan_config) + context.add_command( + label="Mobility Config", command=self.show_mobility_config + ) + if NodeUtils.is_wireless_node(self.core_node.type): + context.add_command( + label="Link To Selected", command=self.wireless_link_selected + ) + context.add_command(label="Select Members", state=tk.DISABLED) + context.add_command(label="Select Adjacent", state=tk.DISABLED) + context.add_command(label="Create Link To", state=tk.DISABLED) + context.add_command(label="Assign To", state=tk.DISABLED) + context.add_command(label="Move To", state=tk.DISABLED) + context.add_command(label="Cut", state=tk.DISABLED) + context.add_command(label="Copy", state=tk.DISABLED) + context.add_command(label="Paste", state=tk.DISABLED) + context.add_command(label="Delete", state=tk.DISABLED) + context.add_command(label="Hide", state=tk.DISABLED) + return context + + def show_config(self): + self.canvas.context = None + dialog = NodeConfigDialog(self.app, self.app, self) + dialog.show() + + def show_wlan_config(self): + self.canvas.context = None + dialog = WlanConfigDialog(self.app, self.app, self) + dialog.show() + + def show_mobility_config(self): + self.canvas.context = None + dialog = MobilityConfigDialog(self.app, self.app, self) + dialog.show() + + def show_mobility_player(self): + self.canvas.context = None + mobility_player = self.app.core.mobility_players[self.core_node.id] + mobility_player.show() + + def show_emane_config(self): + self.canvas.context = None + dialog = EmaneConfigDialog(self.app, self.app, self) + dialog.show() + + def show_services(self): + self.canvas.context = None + dialog = NodeService(self.app.master, self.app, self) + dialog.show() + + def has_emane_link(self, interface_id): + result = None + for edge in self.edges: + if self.id == edge.src: + other_id = edge.dst + edge_interface_id = edge.src_interface.id + else: + other_id = edge.src + edge_interface_id = edge.dst_interface.id + if edge_interface_id != interface_id: + continue + other_node = self.canvas.nodes[other_id] + if other_node.core_node.type == NodeType.EMANE: + result = other_node.core_node + break + return result + + def wireless_link_selected(self): + self.canvas.context = None + for canvas_nid in [ + x for x in self.canvas.selection if "node" in self.canvas.gettags(x) + ]: + core_node = self.canvas.nodes[canvas_nid].core_node + if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr": + self.canvas.create_edge(self, self.canvas.nodes[canvas_nid]) + self.canvas.clear_selection() diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py new file mode 100644 index 00000000..e7277b49 --- /dev/null +++ b/coretk/coretk/graph/shape.py @@ -0,0 +1,179 @@ +import logging + +from coretk.dialogs.shapemod import ShapeDialog +from coretk.graph import tags +from coretk.graph.shapeutils import ShapeType + + +class AnnotationData: + def __init__( + self, + text="", + font="Arial", + font_size=12, + text_color="#000000", + fill_color="", + border_color="#000000", + border_width=1, + bold=False, + italic=False, + underline=False, + ): + self.text = text + self.font = font + self.font_size = font_size + self.text_color = text_color + self.fill_color = fill_color + self.border_color = border_color + self.border_width = border_width + self.bold = bold + self.italic = italic + self.underline = underline + + +class Shape: + def __init__(self, app, canvas, shape_type, x1, y1, x2=None, y2=None, data=None): + self.app = app + self.canvas = canvas + self.shape_type = shape_type + self.id = None + self.text_id = None + self.x1 = x1 + self.y1 = y1 + if x2 is None: + x2 = x1 + self.x2 = x2 + if y2 is None: + y2 = y1 + self.y2 = y2 + if data is None: + self.created = False + self.shape_data = AnnotationData() + else: + self.created = True + self.shape_data = data + self.draw() + + def draw(self): + if self.created: + dash = None + else: + dash = "-" + if self.shape_type == ShapeType.OVAL: + self.id = self.canvas.create_oval( + self.x1, + self.y1, + self.x2, + self.y2, + tags=tags.SHAPE, + dash=dash, + fill=self.shape_data.fill_color, + outline=self.shape_data.border_color, + width=self.shape_data.border_width, + ) + self.draw_shape_text() + elif self.shape_type == ShapeType.RECTANGLE: + self.id = self.canvas.create_rectangle( + self.x1, + self.y1, + self.x2, + self.y2, + tags=tags.SHAPE, + dash=dash, + fill=self.shape_data.fill_color, + outline=self.shape_data.border_color, + width=self.shape_data.border_width, + ) + self.draw_shape_text() + elif self.shape_type == ShapeType.TEXT: + font = self.get_font() + self.id = self.canvas.create_text( + self.x1, + self.y1, + tags=tags.SHAPE_TEXT, + text=self.shape_data.text, + fill=self.shape_data.text_color, + font=font, + ) + else: + logging.error("unknown shape type: %s", self.shape_type) + self.created = True + + def get_font(self): + font = [self.shape_data.font, self.shape_data.font_size] + if self.shape_data.bold: + font.append("bold") + if self.shape_data.italic: + font.append("italic") + if self.shape_data.underline: + font.append("underline") + return font + + def draw_shape_text(self): + if self.shape_data.text: + x = (self.x1 + self.x2) / 2 + y = self.y1 + 1.5 * self.shape_data.font_size + font = self.get_font() + self.text_id = self.canvas.create_text( + x, + y, + tags=tags.SHAPE_TEXT, + text=self.shape_data.text, + fill=self.shape_data.text_color, + font=font, + ) + + def shape_motion(self, x1, y1): + self.canvas.coords(self.id, self.x1, self.y1, x1, y1) + + def shape_complete(self, x, y): + for component in tags.ABOVE_SHAPE: + self.canvas.tag_raise(component) + s = ShapeDialog(self.app, self.app, self) + s.show() + + def disappear(self): + self.canvas.delete(self.id) + + def motion(self, x_offset, y_offset): + original_position = self.canvas.coords(self.id) + self.canvas.move(self.id, x_offset, y_offset) + coords = self.canvas.coords(self.id) + if not self.canvas.valid_position(*coords): + self.canvas.coords(self.id, original_position) + return + + self.canvas.move_selection(self.id, x_offset, y_offset) + if self.text_id is not None: + self.canvas.move(self.text_id, x_offset, y_offset) + + def delete(self): + self.canvas.delete(self.id) + self.canvas.delete(self.text_id) + + def metadata(self): + coords = self.canvas.coords(self.id) + # update coords to actual positions + if len(coords) == 4: + x1, y1, x2, y2 = coords + x1, y1 = self.canvas.get_actual_coords(x1, y1) + x2, y2 = self.canvas.get_actual_coords(x2, y2) + coords = (x1, y1, x2, y2) + else: + x1, y1 = coords + x1, y1 = self.canvas.get_actual_coords(x1, y1) + coords = (x1, y1) + return { + "type": self.shape_type.value, + "iconcoords": coords, + "label": self.shape_data.text, + "fontfamily": self.shape_data.font, + "fontsize": self.shape_data.font_size, + "labelcolor": self.shape_data.text_color, + "color": self.shape_data.fill_color, + "border": self.shape_data.border_color, + "width": self.shape_data.border_width, + "bold": self.shape_data.bold, + "italic": self.shape_data.italic, + "underline": self.shape_data.underline, + } diff --git a/coretk/coretk/graph/shapeutils.py b/coretk/coretk/graph/shapeutils.py new file mode 100644 index 00000000..0e2cc29c --- /dev/null +++ b/coretk/coretk/graph/shapeutils.py @@ -0,0 +1,23 @@ +import enum + + +class ShapeType(enum.Enum): + MARKER = "marker" + OVAL = "oval" + RECTANGLE = "rectangle" + TEXT = "text" + + +SHAPES = {ShapeType.OVAL, ShapeType.RECTANGLE} + + +def is_draw_shape(shape_type): + return shape_type in SHAPES + + +def is_shape_text(shape_type): + return shape_type == ShapeType.TEXT + + +def is_marker(shape_type): + return shape_type == ShapeType.MARKER diff --git a/coretk/coretk/graph/tags.py b/coretk/coretk/graph/tags.py new file mode 100644 index 00000000..42f4ff5f --- /dev/null +++ b/coretk/coretk/graph/tags.py @@ -0,0 +1,35 @@ +GRIDLINE = "gridline" +SHAPE = "shape" +SHAPE_TEXT = "shapetext" +EDGE = "edge" +LINK_INFO = "linkinfo" +WIRELESS_EDGE = "wireless" +ANTENNA = "antenna" +NODE_NAME = "nodename" +NODE = "node" +WALLPAPER = "wallpaper" +SELECTION = "selectednodes" +ABOVE_WALLPAPER_TAGS = [ + GRIDLINE, + SHAPE, + SHAPE_TEXT, + EDGE, + LINK_INFO, + WIRELESS_EDGE, + ANTENNA, + NODE, + NODE_NAME, +] +ABOVE_SHAPE = [GRIDLINE, EDGE, LINK_INFO, WIRELESS_EDGE, ANTENNA, NODE, NODE_NAME] +COMPONENT_TAGS = [ + EDGE, + NODE, + NODE_NAME, + WALLPAPER, + LINK_INFO, + ANTENNA, + WIRELESS_EDGE, + SELECTION, + SHAPE, + SHAPE_TEXT, +] diff --git a/coretk/coretk/graph/tooltip.py b/coretk/coretk/graph/tooltip.py new file mode 100644 index 00000000..8ae528d8 --- /dev/null +++ b/coretk/coretk/graph/tooltip.py @@ -0,0 +1,103 @@ +import tkinter as tk +from tkinter import ttk + +from coretk.themes import Styles + + +class CanvasTooltip: + """ + It creates a tooltip for a given canvas tag or id as the mouse is + above it. + + This class has been derived from the original Tooltip class updated + and posted back to StackOverflow at the following link: + + https://stackoverflow.com/questions/3221956/ + what-is-the-simplest-way-to-make-tooltips-in-tkinter/ + 41079350#41079350 + + Alberto Vassena on 2016.12.10. + """ + + def __init__(self, canvas, *, pad=(5, 3, 5, 3), waittime=400, wraplength=600): + # in miliseconds, originally 500 + self.waittime = waittime + # in pixels, originally 180 + self.wraplength = wraplength + self.canvas = canvas + self.text = tk.StringVar() + self.pad = pad + self.id = None + self.tw = None + + def on_enter(self, event=None): + self.schedule() + + def on_leave(self, event=None): + self.unschedule() + self.hide() + + def schedule(self): + self.unschedule() + self.id = self.canvas.after(self.waittime, self.show) + + def unschedule(self): + id_ = self.id + self.id = None + if id_: + self.canvas.after_cancel(id_) + + def show(self, event=None): + def tip_pos_calculator(canvas, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): + c = canvas + s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight() + width, height = ( + pad[0] + label.winfo_reqwidth() + pad[2], + pad[1] + label.winfo_reqheight() + pad[3], + ) + mouse_x, mouse_y = c.winfo_pointerxy() + x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1] + x2, y2 = x1 + width, y1 + height + + x_delta = x2 - s_width + if x_delta < 0: + x_delta = 0 + y_delta = y2 - s_height + if y_delta < 0: + y_delta = 0 + + offscreen = (x_delta, y_delta) != (0, 0) + if offscreen: + if x_delta: + x1 = mouse_x - tip_delta[0] - width + if y_delta: + y1 = mouse_y - tip_delta[1] - height + offscreen_again = y1 < 0 # out on the top + if offscreen_again: + y1 = 0 + return x1, y1 + + pad = self.pad + canvas = self.canvas + + # creates a toplevel window + self.tw = tk.Toplevel(canvas.master) + + # Leaves only the label and removes the app window + self.tw.wm_overrideredirect(True) + win = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3) + win.grid() + label = ttk.Label( + win, + textvariable=self.text, + wraplength=self.wraplength, + style=Styles.tooltip, + ) + label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) + x, y = tip_pos_calculator(canvas, label, pad=pad) + self.tw.wm_geometry("+%d+%d" % (x, y)) + + def hide(self): + if self.tw: + self.tw.destroy() + self.tw = None diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py new file mode 100644 index 00000000..9d282dbb --- /dev/null +++ b/coretk/coretk/images.py @@ -0,0 +1,70 @@ +from enum import Enum + +from PIL import Image, ImageTk + +from coretk.appconfig import LOCAL_ICONS_PATH + + +class Images: + images = {} + + @classmethod + def create(cls, file_path, width, height=None): + if height is None: + height = width + image = Image.open(file_path) + image = image.resize((width, height), Image.ANTIALIAS) + return ImageTk.PhotoImage(image) + + @classmethod + def load_all(cls): + for image in LOCAL_ICONS_PATH.glob("*"): + cls.images[image.stem] = str(image) + + @classmethod + def get(cls, image_enum, width, height=None): + file_path = cls.images[image_enum.value] + return cls.create(file_path, width, height) + + @classmethod + def get_custom(cls, name, width, height=None): + file_path = cls.images[name] + return cls.create(file_path, width, height) + + +class ImageEnum(Enum): + SWITCH = "lanswitch" + CORE = "core-icon" + START = "start" + MARKER = "marker" + ROUTER = "router" + SELECT = "select" + LINK = "link" + HUB = "hub" + WLAN = "wlan" + EMANE = "emane" + RJ45 = "rj45" + TUNNEL = "tunnel" + OVAL = "oval" + RECTANGLE = "rectangle" + TEXT = "text" + HOST = "host" + PC = "pc" + MDR = "mdr" + PROUTER = "prouter" + OVS = "OVS" + EDITNODE = "edit-node" + PLOT = "plot" + TWONODE = "twonode" + PAUSE = "pause" + STOP = "stop" + OBSERVE = "observe" + RUN = "run" + DOCUMENTNEW = "document-new" + DOCUMENTSAVE = "document-save" + FILEOPEN = "fileopen" + EDITDELETE = "edit-delete" + ANTENNA = "antenna" + DOCKER = "docker" + LXC = "lxc" + ALERT = "alert" diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py new file mode 100644 index 00000000..f4a380ae --- /dev/null +++ b/coretk/coretk/interface.py @@ -0,0 +1,97 @@ +import logging +import random + +from netaddr import IPNetwork + +from coretk.nodeutils import NodeUtils + + +def random_mac(): + return ("{:02x}" * 6).format(*[random.randrange(256) for _ in range(6)]) + + +class InterfaceManager: + def __init__(self, app, address="10.0.0.0", mask=24): + self.app = app + self.mask = mask + self.base_prefix = max(self.mask - 8, 0) + self.subnets = IPNetwork(f"{address}/{self.base_prefix}") + self.current_subnet = None + + def next_subnet(self): + # define currently used subnets + used_subnets = set() + for link in self.app.core.links.values(): + if link.HasField("interface_one"): + subnet = self.get_subnet(link.interface_one) + used_subnets.add(subnet) + if link.HasField("interface_two"): + subnet = self.get_subnet(link.interface_two) + used_subnets.add(subnet) + + # find next available subnet + for i in self.subnets.subnet(self.mask): + if i not in used_subnets: + return i + + def reset(self): + self.current_subnet = None + + def get_ips(self, node_id): + ip4 = self.current_subnet[node_id] + ip6 = ip4.ipv6() + prefix = self.current_subnet.prefixlen + return str(ip4), str(ip6), prefix + + @classmethod + def get_subnet(cls, interface): + return IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr + + def determine_subnet(self, canvas_src_node, canvas_dst_node): + src_node = canvas_src_node.core_node + dst_node = canvas_dst_node.core_node + is_src_container = NodeUtils.is_container_node(src_node.type) + is_dst_container = NodeUtils.is_container_node(dst_node.type) + if is_src_container and is_dst_container: + self.current_subnet = self.next_subnet() + elif is_src_container and not is_dst_container: + subnet = self.find_subnet(canvas_dst_node, visited={src_node.id}) + if subnet: + self.current_subnet = subnet + else: + self.current_subnet = self.next_subnet() + elif not is_src_container and is_dst_container: + subnet = self.find_subnet(canvas_src_node, visited={dst_node.id}) + if subnet: + self.current_subnet = subnet + else: + self.current_subnet = self.next_subnet() + else: + logging.info("ignoring subnet change for link between network nodes") + + def find_subnet(self, canvas_node, visited=None): + logging.info("finding subnet for node: %s", canvas_node.core_node.name) + canvas = self.app.canvas + cidr = None + if not visited: + visited = set() + visited.add(canvas_node.core_node.id) + for edge in canvas_node.edges: + src_node = canvas.nodes[edge.src] + dst_node = canvas.nodes[edge.dst] + interface = edge.src_interface + check_node = src_node + if src_node == canvas_node: + interface = edge.dst_interface + check_node = dst_node + if check_node.core_node.id in visited: + continue + visited.add(check_node.core_node.id) + if interface: + cidr = self.get_subnet(interface) + else: + cidr = self.find_subnet(check_node, visited) + if cidr: + logging.info("found subnet: %s", cidr) + break + return cidr diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py new file mode 100644 index 00000000..ec7e7cb7 --- /dev/null +++ b/coretk/coretk/menuaction.py @@ -0,0 +1,157 @@ +""" +The actions taken when each menubar option is clicked +""" + +import logging +import threading +import time +import webbrowser +from tkinter import filedialog, messagebox + +import grpc + +from coretk.appconfig import XMLS_PATH +from coretk.dialogs.about import AboutDialog +from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog +from coretk.dialogs.canvaswallpaper import CanvasBackgroundDialog +from coretk.dialogs.hooks import HooksDialog +from coretk.dialogs.observers import ObserverDialog +from coretk.dialogs.preferences import PreferencesDialog +from coretk.dialogs.servers import ServersDialog +from coretk.dialogs.sessionoptions import SessionOptionsDialog +from coretk.dialogs.sessions import SessionsDialog + + +class MenuAction: + """ + Actions performed when choosing menu items + """ + + def __init__(self, app, master): + self.master = master + self.app = app + + def cleanup_old_session(self, quitapp=False): + logging.info("cleaning up old session") + start = time.perf_counter() + self.app.core.stop_session() + self.app.core.delete_session() + process_time = time.perf_counter() - start + self.app.statusbar.stop_session_callback(process_time) + if quitapp: + self.app.quit() + + def prompt_save_running_session(self, quitapp=False): + """ + Prompt use to stop running session before application is closed + + :return: nothing + """ + logging.info( + "menuaction.py: clean_nodes_links_and_set_configuration() Exiting the program" + ) + try: + if not self.app.core.is_runtime(): + self.app.core.delete_session() + if quitapp: + self.app.quit() + else: + result = messagebox.askyesnocancel("stop", "Stop the running session?") + if result: + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread( + target=self.cleanup_old_session, args=([quitapp]) + ) + thread.daemon = True + thread.start() + elif quitapp: + self.app.quit() + except grpc.RpcError: + logging.exception("error deleting session") + if quitapp: + self.app.quit() + + def on_quit(self, event=None): + """ + Prompt user whether so save running session, and then close the application + + :return: nothing + """ + self.prompt_save_running_session(quitapp=True) + + def file_save_as_xml(self, event=None): + logging.info("menuaction.py file_save_as_xml()") + file_path = filedialog.asksaveasfilename( + initialdir=str(XMLS_PATH), + title="Save As", + filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), + defaultextension=".xml", + ) + if file_path: + self.app.core.save_xml(file_path) + + def file_open_xml(self, event=None): + logging.info("menuaction.py file_open_xml()") + file_path = filedialog.askopenfilename( + initialdir=str(XMLS_PATH), + title="Open", + filetypes=(("XML Files", "*.xml"), ("All Files", "*")), + ) + if file_path: + logging.info("opening xml: %s", file_path) + self.prompt_save_running_session() + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread(target=self.app.core.open_xml, args=([file_path])) + thread.start() + + def gui_preferences(self): + dialog = PreferencesDialog(self.app, self.app) + dialog.show() + + def canvas_size_and_scale(self): + dialog = SizeAndScaleDialog(self.app, self.app) + dialog.show() + + def canvas_set_wallpaper(self): + dialog = CanvasBackgroundDialog(self.app, self.app) + dialog.show() + + def help_core_github(self): + webbrowser.open_new("https://github.com/coreemu/core") + + def help_core_documentation(self): + webbrowser.open_new("http://coreemu.github.io/core/") + + def session_options(self): + logging.debug("Click session options") + dialog = SessionOptionsDialog(self.app, self.app) + dialog.show() + + def session_change_sessions(self): + logging.debug("Click session change sessions") + dialog = SessionsDialog(self.app, self.app) + dialog.show() + + def session_hooks(self): + logging.debug("Click session hooks") + dialog = HooksDialog(self.app, self.app) + dialog.show() + + def session_servers(self): + logging.debug("Click session emulation servers") + dialog = ServersDialog(self.app, self.app) + dialog.show() + + def edit_observer_widgets(self): + dialog = ObserverDialog(self.app, self.app) + dialog.show() + + def show_about(self): + dialog = AboutDialog(self.app, self.app) + dialog.show() + + def throughput(self): + if not self.app.core.handling_throughputs: + self.app.core.enable_throughputs() + else: + self.app.core.cancel_throughputs() diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py new file mode 100644 index 00000000..460ce2bf --- /dev/null +++ b/coretk/coretk/menubar.py @@ -0,0 +1,467 @@ +import tkinter as tk +from functools import partial + +import coretk.menuaction as action +from coretk.coreclient import OBSERVERS + + +class Menubar(tk.Menu): + """ + Core menubar + """ + + def __init__(self, master, app, cnf={}, **kwargs): + """ + Create a CoreMenubar instance + + :param master: + :param tkinter.Menu menubar: menubar object + :param coretk.app.Application app: application object + """ + super().__init__(master, cnf, **kwargs) + self.master.config(menu=self) + self.app = app + self.menuaction = action.MenuAction(app, master) + self.draw() + + def draw(self): + """ + Create core menubar and bind the hot keys to their matching command + + :return: nothing + """ + self.draw_file_menu() + self.draw_edit_menu() + self.draw_canvas_menu() + self.draw_view_menu() + self.draw_tools_menu() + self.draw_widgets_menu() + self.draw_session_menu() + self.draw_help_menu() + + def draw_file_menu(self): + """ + Create file menu + + :return: nothing + """ + menu = tk.Menu(self) + menu.add_command( + label="New Session", + accelerator="Ctrl+N", + command=self.app.core.create_new_session, + ) + self.app.bind_all("", lambda e: self.app.core.create_new_session()) + menu.add_command( + label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O" + ) + self.app.bind_all("", self.menuaction.file_open_xml) + menu.add_command( + label="Save", accelerator="Ctrl+S", command=self.menuaction.file_save_as_xml + ) + menu.add_command(label="Reload", underline=0, state=tk.DISABLED) + self.app.bind_all("", self.menuaction.file_save_as_xml) + menu.add_separator() + menu.add_command(label="Export Python script...", state=tk.DISABLED) + menu.add_command(label="Execute XML or Python script...", state=tk.DISABLED) + menu.add_command( + label="Execute Python script with options...", state=tk.DISABLED + ) + menu.add_separator() + menu.add_command(label="Open current file in editor...", state=tk.DISABLED) + menu.add_command(label="Print...", underline=0, state=tk.DISABLED) + menu.add_command(label="Save screenshot...", state=tk.DISABLED) + menu.add_separator() + menu.add_command( + label="Quit", accelerator="Ctrl+Q", command=self.menuaction.on_quit + ) + self.app.bind_all("", self.menuaction.on_quit) + self.add_cascade(label="File", menu=menu) + + def draw_edit_menu(self): + """ + Create edit menu + + :return: nothing + """ + menu = tk.Menu(self) + menu.add_command(label="Preferences", command=self.menuaction.gui_preferences) + menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) + menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Cut", accelerator="Ctrl+X", state=tk.DISABLED) + menu.add_command(label="Copy", accelerator="Ctrl+C", state=tk.DISABLED) + menu.add_command(label="Paste", accelerator="Ctrl+V", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Select all", accelerator="Ctrl+A", state=tk.DISABLED) + menu.add_command( + label="Select Adjacent", accelerator="Ctrl+J", state=tk.DISABLED + ) + menu.add_separator() + menu.add_command(label="Find...", accelerator="Ctrl+F", state=tk.DISABLED) + menu.add_command(label="Clear marker", state=tk.DISABLED) + self.add_cascade(label="Edit", menu=menu) + + def draw_canvas_menu(self): + """ + Create canvas menu + + :return: nothing + """ + menu = tk.Menu(self) + menu.add_command( + label="Size/scale...", command=self.menuaction.canvas_size_and_scale + ) + menu.add_command( + label="Wallpaper...", command=self.menuaction.canvas_set_wallpaper + ) + menu.add_separator() + menu.add_command(label="New", state=tk.DISABLED) + menu.add_command(label="Manage...", state=tk.DISABLED) + menu.add_command(label="Delete", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Previous", accelerator="PgUp", state=tk.DISABLED) + menu.add_command(label="Next", accelerator="PgDown", state=tk.DISABLED) + menu.add_command(label="First", accelerator="Home", state=tk.DISABLED) + menu.add_command(label="Last", accelerator="End", state=tk.DISABLED) + self.add_cascade(label="Canvas", menu=menu) + + def draw_view_menu(self): + """ + Create view menu + + :return: nothing + """ + view_menu = tk.Menu(self) + self.create_show_menu(view_menu) + view_menu.add_command(label="Show hidden nodes", state=tk.DISABLED) + view_menu.add_command(label="Locked", state=tk.DISABLED) + view_menu.add_command(label="3D GUI...", state=tk.DISABLED) + view_menu.add_separator() + view_menu.add_command(label="Zoom in", accelerator="+", state=tk.DISABLED) + view_menu.add_command(label="Zoom out", accelerator="-", state=tk.DISABLED) + self.add_cascade(label="View", menu=view_menu) + + def create_show_menu(self, view_menu): + """ + Create the menu items in View/Show + + :param tkinter.Menu view_menu: the view menu + :return: nothing + """ + menu = tk.Menu(view_menu) + menu.add_command(label="All", state=tk.DISABLED) + menu.add_command(label="None", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Interface Names", state=tk.DISABLED) + menu.add_command(label="IPv4 Addresses", state=tk.DISABLED) + menu.add_command(label="IPv6 Addresses", state=tk.DISABLED) + menu.add_command(label="Node Labels", state=tk.DISABLED) + menu.add_command(label="Annotations", state=tk.DISABLED) + menu.add_command(label="Grid", state=tk.DISABLED) + menu.add_command(label="API Messages", state=tk.DISABLED) + view_menu.add_cascade(label="Show", menu=menu) + + def create_experimental_menu(self, tools_menu): + """ + Create experimental menu item and the sub menu items inside + + :param tkinter.Menu tools_menu: tools menu + :return: nothing + """ + menu = tk.Menu(tools_menu) + menu.add_command(label="Plugins...", state=tk.DISABLED) + menu.add_command(label="ns2immunes converter...", state=tk.DISABLED) + menu.add_command(label="Topology partitioning...", state=tk.DISABLED) + tools_menu.add_cascade(label="Experimental", menu=menu) + + def create_random_menu(self, topology_generator_menu): + """ + Create random menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + menu = tk.Menu(topology_generator_menu) + # list of number of random nodes to create + nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100] + for i in nums: + label = f"R({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Random", menu=menu) + + def create_grid_menu(self, topology_generator_menu): + """ + Create grid menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology_generator_menu + :return: nothing + """ + menu = tk.Menu(topology_generator_menu) + # list of number of nodes to create + nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] + for i in nums: + label = f"G({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Grid", menu=menu) + + def create_connected_grid_menu(self, topology_generator_menu): + """ + Create connected grid menu items and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + menu = tk.Menu(topology_generator_menu) + for i in range(1, 11, 1): + submenu = tk.Menu(menu) + for j in range(1, 11, 1): + label = f"{i} X {j}" + submenu.add_command(label=label, state=tk.DISABLED) + label = str(i) + " X N" + menu.add_cascade(label=label, menu=submenu) + topology_generator_menu.add_cascade(label="Connected Grid", menu=menu) + + def create_chain_menu(self, topology_generator_menu): + """ + Create chain menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + menu = tk.Menu(topology_generator_menu) + # number of nodes to create + nums = list(range(2, 25, 1)) + [32, 64, 128] + for i in nums: + label = f"P({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Chain", menu=menu) + + def create_star_menu(self, topology_generator_menu): + """ + Create star menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + menu = tk.Menu(topology_generator_menu) + for i in range(3, 26, 1): + label = f"C({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Star", menu=menu) + + def create_cycle_menu(self, topology_generator_menu): + """ + Create cycle menu item and the sub items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + menu = tk.Menu(topology_generator_menu) + for i in range(3, 25, 1): + label = f"C({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Cycle", menu=menu) + + def create_wheel_menu(self, topology_generator_menu): + """ + Create wheel menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + menu = tk.Menu(topology_generator_menu) + for i in range(4, 26, 1): + label = f"W({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Wheel", menu=menu) + + def create_cube_menu(self, topology_generator_menu): + """ + Create cube menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + menu = tk.Menu(topology_generator_menu) + for i in range(2, 7, 1): + label = f"Q({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Cube", menu=menu) + + def create_clique_menu(self, topology_generator_menu): + """ + Create clique menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + menu = tk.Menu(topology_generator_menu) + for i in range(3, 25, 1): + label = f"K({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Clique", menu=menu) + + def create_bipartite_menu(self, topology_generator_menu): + """ + Create bipartite menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology_generator_menu + :return: nothing + """ + menu = tk.Menu(topology_generator_menu) + temp = 24 + for i in range(1, 13, 1): + submenu = tk.Menu(menu) + for j in range(i, temp, 1): + label = f"K({i} X {j})" + submenu.add_command(label=label, state=tk.DISABLED) + label = f"K({i})" + menu.add_cascade(label=label, menu=submenu) + temp = temp - 1 + topology_generator_menu.add_cascade(label="Bipartite", menu=menu) + + def create_topology_generator_menu(self, tools_menu): + """ + Create topology menu item and its sub menu items + + :param tkinter.Menu tools_menu: tools menu + + :return: nothing + """ + menu = tk.Menu(tools_menu) + self.create_random_menu(menu) + self.create_grid_menu(menu) + self.create_connected_grid_menu(menu) + self.create_chain_menu(menu) + self.create_star_menu(menu) + self.create_cycle_menu(menu) + self.create_wheel_menu(menu) + self.create_cube_menu(menu) + self.create_clique_menu(menu) + self.create_bipartite_menu(menu) + tools_menu.add_cascade(label="Topology generator", menu=menu) + + def draw_tools_menu(self): + """ + Create tools menu + + :return: nothing + """ + menu = tk.Menu(self) + menu.add_command(label="Auto rearrange all", state=tk.DISABLED) + menu.add_command(label="Auto rearrange selected", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Align to grid", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Traffic...", state=tk.DISABLED) + menu.add_command(label="IP addresses...", state=tk.DISABLED) + menu.add_command(label="MAC addresses...", state=tk.DISABLED) + menu.add_command(label="Build hosts file...", state=tk.DISABLED) + menu.add_command(label="Renumber nodes...", state=tk.DISABLED) + self.create_experimental_menu(menu) + self.create_topology_generator_menu(menu) + menu.add_command(label="Debugger...", state=tk.DISABLED) + self.add_cascade(label="Tools", menu=menu) + + def create_observer_widgets_menu(self, widget_menu): + """ + Create observer widget menu item and create the sub menu items inside + + :param tkinter.Menu widget_menu: widget_menu + :return: nothing + """ + var = tk.StringVar(value="none") + menu = tk.Menu(widget_menu) + menu.var = var + menu.add_command( + label="Edit Observers", command=self.menuaction.edit_observer_widgets + ) + menu.add_separator() + menu.add_radiobutton( + label="None", + variable=var, + value="none", + command=lambda: self.app.core.set_observer(None), + ) + for name in sorted(OBSERVERS): + cmd = OBSERVERS[name] + menu.add_radiobutton( + label=name, + variable=var, + value=name, + command=partial(self.app.core.set_observer, cmd), + ) + for name in sorted(self.app.core.custom_observers): + observer = self.app.core.custom_observers[name] + menu.add_radiobutton( + label=name, + variable=var, + value=name, + command=partial(self.app.core.set_observer, observer.cmd), + ) + widget_menu.add_cascade(label="Observer Widgets", menu=menu) + + def create_adjacency_menu(self, widget_menu): + """ + Create adjacency menu item and the sub menu items inside + + :param tkinter.Menu widget_menu: widget menu + :return: nothing + """ + menu = tk.Menu(widget_menu) + menu.add_command(label="OSPFv2", state=tk.DISABLED) + menu.add_command(label="OSPFv3", state=tk.DISABLED) + menu.add_command(label="OSLR", state=tk.DISABLED) + menu.add_command(label="OSLRv2", state=tk.DISABLED) + widget_menu.add_cascade(label="Adjacency", menu=menu) + + def draw_widgets_menu(self): + """ + Create widget menu + + :return: nothing + """ + menu = tk.Menu(self) + self.create_observer_widgets_menu(menu) + self.create_adjacency_menu(menu) + menu.add_checkbutton(label="Throughput", command=self.menuaction.throughput) + menu.add_separator() + menu.add_command(label="Configure Adjacency...", state=tk.DISABLED) + menu.add_command(label="Configure Throughput...", state=tk.DISABLED) + self.add_cascade(label="Widgets", menu=menu) + + def draw_session_menu(self): + """ + Create session menu + + :return: nothing + """ + menu = tk.Menu(self) + menu.add_command( + label="Sessions...", command=self.menuaction.session_change_sessions + ) + menu.add_separator() + menu.add_command(label="Options...", command=self.menuaction.session_options) + menu.add_command(label="Servers...", command=self.menuaction.session_servers) + menu.add_command(label="Hooks...", command=self.menuaction.session_hooks) + menu.add_command(label="Reset Nodes", state=tk.DISABLED) + menu.add_command(label="Comments...", state=tk.DISABLED) + self.add_cascade(label="Session", menu=menu) + + def draw_help_menu(self): + """ + Create help menu + + :return: nothing + """ + menu = tk.Menu(self) + menu.add_command( + label="Core GitHub (www)", command=self.menuaction.help_core_github + ) + menu.add_command( + label="Core Documentation (www)", + command=self.menuaction.help_core_documentation, + ) + menu.add_command(label="About", command=self.menuaction.show_about) + self.add_cascade(label="Help", menu=menu) diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py new file mode 100644 index 00000000..cb0bc7a3 --- /dev/null +++ b/coretk/coretk/nodeutils.py @@ -0,0 +1,108 @@ +from core.api.grpc.core_pb2 import NodeType +from coretk.images import ImageEnum, Images + +ICON_SIZE = 48 +ANTENNA_SIZE = 32 + + +class NodeDraw: + def __init__(self): + self.custom = False + self.image = None + self.image_enum = None + self.image_file = None + self.node_type = None + self.model = None + self.services = set() + + @classmethod + def from_setup(cls, image_enum, node_type, label, model=None, tooltip=None): + node_draw = NodeDraw() + node_draw.image_enum = image_enum + node_draw.image = Images.get(image_enum, ICON_SIZE) + node_draw.node_type = node_type + node_draw.label = label + node_draw.model = model + node_draw.tooltip = tooltip + return node_draw + + @classmethod + def from_custom(cls, name, image_file, services): + node_draw = NodeDraw() + node_draw.custom = True + node_draw.image_file = image_file + node_draw.image = Images.get_custom(image_file, ICON_SIZE) + node_draw.node_type = NodeType.DEFAULT + node_draw.services = services + node_draw.label = name + node_draw.model = name + node_draw.tooltip = name + return node_draw + + +class NodeUtils: + NODES = [] + NETWORK_NODES = [] + NODE_ICONS = {} + CONTAINER_NODES = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} + IMAGE_NODES = {NodeType.DOCKER, NodeType.LXC} + WIRELESS_NODES = {NodeType.WIRELESS_LAN, NodeType.EMANE} + IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} + NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} + ANTENNA_ICON = None + + @classmethod + def is_ignore_node(cls, node_type): + return node_type in cls.IGNORE_NODES + + @classmethod + def is_container_node(cls, node_type): + return node_type in cls.CONTAINER_NODES + + @classmethod + def is_model_node(cls, node_type): + return node_type == NodeType.DEFAULT + + @classmethod + def is_image_node(cls, node_type): + return node_type in cls.IMAGE_NODES + + @classmethod + def is_wireless_node(cls, node_type): + return node_type in cls.WIRELESS_NODES + + @classmethod + def node_icon(cls, node_type, model): + if model == "": + model = None + return cls.NODE_ICONS[(node_type, model)] + + @classmethod + def setup(cls): + nodes = [ + (ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"), + (ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"), + (ImageEnum.PC, NodeType.DEFAULT, "PC", "PC"), + (ImageEnum.MDR, NodeType.DEFAULT, "MDR", "mdr"), + (ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"), + (ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None), + (ImageEnum.LXC, NodeType.LXC, "LXC", None), + ] + for image_enum, node_type, label, model in nodes: + node_draw = NodeDraw.from_setup(image_enum, node_type, label, model) + cls.NODES.append(node_draw) + cls.NODE_ICONS[(node_type, model)] = node_draw.image + + network_nodes = [ + (ImageEnum.HUB, NodeType.HUB, "Hub"), + (ImageEnum.SWITCH, NodeType.SWITCH, "Switch"), + (ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"), + (ImageEnum.EMANE, NodeType.EMANE, "EMANE"), + (ImageEnum.RJ45, NodeType.RJ45, "RJ45"), + (ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"), + ] + for image_enum, node_type, label in network_nodes: + node_draw = NodeDraw.from_setup(image_enum, node_type, label) + cls.NETWORK_NODES.append(node_draw) + cls.NODE_ICONS[(node_type, None)] = node_draw.image + cls.ANTENNA_ICON = Images.get(ImageEnum.ANTENNA, ANTENNA_SIZE) diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py new file mode 100644 index 00000000..5ed6f09d --- /dev/null +++ b/coretk/coretk/statusbar.py @@ -0,0 +1,77 @@ +"status bar" +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.alerts import AlertsDialog +from coretk.themes import Styles + + +class StatusBar(ttk.Frame): + def __init__(self, master, app, **kwargs): + super().__init__(master, **kwargs) + self.app = app + self.status = None + self.statusvar = tk.StringVar() + self.progress_bar = None + self.zoom = None + self.cpu_usage = None + self.memory = None + self.alerts_button = None + self.running = False + self.core_alarms = [] + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=7) + self.columnconfigure(2, weight=1) + self.columnconfigure(3, weight=1) + self.columnconfigure(4, weight=1) + + frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE) + frame.grid(row=0, column=0, sticky="ew") + frame.columnconfigure(0, weight=1) + self.progress_bar = ttk.Progressbar( + frame, orient="horizontal", mode="indeterminate" + ) + self.progress_bar.grid(sticky="ew") + + self.status = ttk.Label( + self, + textvariable=self.statusvar, + anchor=tk.CENTER, + borderwidth=1, + relief=tk.RIDGE, + ) + self.status.grid(row=0, column=1, sticky="ew") + + self.zoom = ttk.Label( + self, + text="%s" % (int(self.app.canvas.ratio * 100)) + "%", + anchor=tk.CENTER, + borderwidth=1, + relief=tk.RIDGE, + ) + self.zoom.grid(row=0, column=2, sticky="ew") + + self.cpu_usage = ttk.Label( + self, text="CPU TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + ) + self.cpu_usage.grid(row=0, column=3, sticky="ew") + + self.alerts_button = ttk.Button( + self, text="Alerts", command=self.click_alerts, style=Styles.green_alert + ) + self.alerts_button.grid(row=0, column=4, sticky="ew") + + def click_alerts(self): + dialog = AlertsDialog(self.app, self.app) + dialog.show() + + def start_session_callback(self, process_time): + self.progress_bar.stop() + self.statusvar.set(f"Session started in {process_time:.3f} seconds") + + def stop_session_callback(self, cleanup_time): + self.progress_bar.stop() + self.statusvar.set(f"Stopped session in {cleanup_time:.3f} seconds") diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py new file mode 100644 index 00000000..66565f8d --- /dev/null +++ b/coretk/coretk/themes.py @@ -0,0 +1,202 @@ +import tkinter as tk +from tkinter import ttk + +THEME_DARK = "black" +PADX = (0, 5) +PADY = (0, 5) +FRAME_PAD = 5 +DIALOG_PAD = 5 + + +class Styles: + tooltip = "Tooltip.TLabel" + tooltip_frame = "Tooltip.TFrame" + service_checkbutton = "Service.TCheckbutton" + picker_button = "Picker.TButton" + green_alert = "GAlert.TButton" + red_alert = "RAlert.TButton" + yellow_alert = "YAlert.TButton" + + +class Colors: + disabledfg = "DarkGrey" + frame = "#424242" + dark = "#222222" + darker = "#121212" + darkest = "black" + lighter = "#626262" + lightest = "#ffffff" + selectbg = "#4a6984" + selectfg = "#ffffff" + white = "white" + black = "black" + listboxbg = "#f2f1f0" + + +def load(style): + style.theme_create( + THEME_DARK, + "clam", + { + ".": { + "configure": { + "background": Colors.frame, + "foreground": Colors.white, + "bordercolor": Colors.darkest, + "darkcolor": Colors.dark, + "lightcolor": Colors.lighter, + "troughcolor": Colors.darker, + "selectbackground": Colors.selectbg, + "selectforeground": Colors.selectfg, + "selectborderwidth": 0, + "font": "TkDefaultFont", + }, + "map": { + "background": [ + ("disabled", Colors.frame), + ("active", Colors.lighter), + ], + "foreground": [("disabled", Colors.disabledfg)], + "selectbackground": [("!focus", Colors.darkest)], + "selectforeground": [("!focus", Colors.white)], + }, + }, + "TButton": { + "configure": { + "width": 8, + "padding": (5, 1), + "relief": tk.RAISED, + "anchor": tk.CENTER, + }, + "map": { + "relief": [("pressed", tk.SUNKEN)], + "shiftrelief": [("pressed", 1)], + }, + }, + "TMenubutton": { + "configure": {"width": 11, "padding": (5, 1), "relief": tk.RAISED} + }, + "TCheckbutton": { + "configure": { + "indicatorbackground": Colors.white, + "indicatormargin": (1, 1, 4, 1), + } + }, + "TRadiobutton": { + "configure": { + "indicatorbackground": Colors.white, + "indicatormargin": (1, 1, 4, 1), + } + }, + "TEntry": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + }, + "map": {"fieldbackground": [("disabled", Colors.frame)]}, + }, + "TSpinbox": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + }, + "map": {"fieldbackground": [("disabled", Colors.frame)]}, + }, + "TCombobox": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + } + }, + "TLabelframe": {"configure": {"relief": tk.GROOVE}}, + "TNotebook.Tab": { + "configure": {"padding": (6, 2, 6, 2)}, + "map": {"background": [("selected", Colors.lighter)]}, + }, + "Treeview": { + "configure": { + "fieldbackground": Colors.white, + "background": Colors.white, + "foreground": Colors.black, + }, + "map": { + "background": [("selected", Colors.selectbg)], + "foreground": [("selected", Colors.selectfg)], + }, + }, + Styles.tooltip: { + "configure": {"justify": tk.LEFT, "relief": tk.SOLID, "borderwidth": 0} + }, + Styles.tooltip_frame: {"configure": {}}, + Styles.service_checkbutton: { + "configure": { + "background": Colors.listboxbg, + "foreground": Colors.black, + } + }, + }, + ) + + +def theme_change_menu(event): + if not isinstance(event.widget, tk.Menu): + return + style_menu(event.widget) + + +def style_menu(widget): + style = ttk.Style() + bg = style.lookup(".", "background") + fg = style.lookup(".", "foreground") + abg = style.lookup(".", "lightcolor") + if not abg: + abg = bg + widget.config( + background=bg, foreground=fg, activebackground=abg, activeforeground=fg, bd=0 + ) + + +def style_listbox(widget): + style = ttk.Style() + bg = style.lookup(".", "background") + fg = style.lookup(".", "foreground") + bc = style.lookup(".", "bordercolor") + if not bc: + bc = "black" + widget.config( + background=bg, + foreground=fg, + highlightthickness=1, + highlightcolor=bc, + highlightbackground=bc, + bd=0, + ) + + +def theme_change(event): + style = ttk.Style() + style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal")) + style.configure( + Styles.green_alert, + background="green", + padding=0, + relief=tk.NONE, + font=("TkDefaultFont", 8, "normal"), + ) + style.configure( + Styles.yellow_alert, + background="yellow", + padding=0, + relief=tk.NONE, + font=("TkDefaultFont", 8, "normal"), + ) + style.configure( + Styles.red_alert, + background="red", + padding=0, + relief=tk.NONE, + font=("TkDefaultFont", 8, "normal"), + ) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py new file mode 100644 index 00000000..c32b0d75 --- /dev/null +++ b/coretk/coretk/toolbar.py @@ -0,0 +1,426 @@ +import logging +import threading +import tkinter as tk +from functools import partial +from tkinter import ttk +from tkinter.font import Font + +from coretk.dialogs.customnodes import CustomNodesDialog +from coretk.dialogs.marker import Marker +from coretk.graph import tags +from coretk.graph.enums import GraphMode +from coretk.graph.shapeutils import ShapeType, is_marker +from coretk.images import ImageEnum, Images +from coretk.nodeutils import NodeUtils +from coretk.themes import Styles +from coretk.tooltip import Tooltip + +TOOLBAR_SIZE = 32 +PICKER_SIZE = 24 + + +def icon(image_enum, width=TOOLBAR_SIZE): + return Images.get(image_enum, width) + + +class Toolbar(ttk.Frame): + """ + Core toolbar class + """ + + def __init__(self, master, app, **kwargs): + """ + Create a CoreToolbar instance + + :param tkinter.Frame edit_frame: edit frame + """ + super().__init__(master, **kwargs) + self.app = app + self.master = app.master + + # picker data + self.picker_font = Font(size=8) + + # design buttons + self.select_button = None + self.link_button = None + self.node_button = None + self.network_button = None + self.annotation_button = None + + # runtime buttons + self.runtime_select_button = None + + # frames + self.design_frame = None + self.runtime_frame = None + self.node_picker = None + self.network_picker = None + self.annotation_picker = None + + # dialog + self.marker_tool = None + + # draw components + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_design_frame() + self.draw_runtime_frame() + self.design_frame.tkraise() + + def draw_design_frame(self): + self.design_frame = ttk.Frame(self) + self.design_frame.grid(row=0, column=0, sticky="nsew") + self.design_frame.columnconfigure(0, weight=1) + self.create_button( + self.design_frame, + icon(ImageEnum.START), + self.click_start, + "start the session", + ) + self.select_button = self.create_button( + self.design_frame, + icon(ImageEnum.SELECT), + self.click_selection, + "selection tool", + ) + self.link_button = self.create_button( + self.design_frame, icon(ImageEnum.LINK), self.click_link, "link tool" + ) + self.create_node_button() + self.create_network_button() + self.create_annotation_button() + + def design_select(self, button): + logging.info("selecting design button: %s", button) + self.select_button.state(["!pressed"]) + self.link_button.state(["!pressed"]) + self.node_button.state(["!pressed"]) + self.network_button.state(["!pressed"]) + self.annotation_button.state(["!pressed"]) + button.state(["pressed"]) + + def runtime_select(self, button): + logging.info("selecting runtime button: %s", button) + self.runtime_select_button.state(["!pressed"]) + button.state(["pressed"]) + + def draw_runtime_frame(self): + self.runtime_frame = ttk.Frame(self) + self.runtime_frame.grid(row=0, column=0, sticky="nsew") + self.runtime_frame.columnconfigure(0, weight=1) + + self.create_button( + self.runtime_frame, + icon(ImageEnum.STOP), + self.click_stop, + "stop the session", + ) + self.runtime_select_button = self.create_button( + self.runtime_frame, + icon(ImageEnum.SELECT), + self.click_runtime_selection, + "selection tool", + ) + # self.create_observe_button() + self.create_button( + self.runtime_frame, icon(ImageEnum.PLOT), self.click_plot_button, "plot" + ) + self.create_button( + self.runtime_frame, + icon(ImageEnum.MARKER), + self.click_marker_button, + "marker", + ) + self.create_button( + self.runtime_frame, + icon(ImageEnum.TWONODE), + self.click_two_node_button, + "run command from one node to another", + ) + self.create_button( + self.runtime_frame, icon(ImageEnum.RUN), self.click_run_button, "run" + ) + + def draw_node_picker(self): + self.hide_pickers() + self.node_picker = ttk.Frame(self.master) + # draw default nodes + for node_draw in NodeUtils.NODES: + toolbar_image = icon(node_draw.image_enum) + image = icon(node_draw.image_enum, PICKER_SIZE) + func = partial( + self.update_button, self.node_button, toolbar_image, node_draw + ) + self.create_picker_button(image, func, self.node_picker, node_draw.label) + # draw custom nodes + for name in sorted(self.app.core.custom_nodes): + node_draw = self.app.core.custom_nodes[name] + toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE) + image = Images.get_custom(node_draw.image_file, PICKER_SIZE) + func = partial( + self.update_button, self.node_button, toolbar_image, node_draw + ) + self.create_picker_button(image, func, self.node_picker, name) + # draw edit node + image = icon(ImageEnum.EDITNODE, PICKER_SIZE) + self.create_picker_button( + image, self.click_edit_node, self.node_picker, "Custom" + ) + self.design_select(self.node_button) + self.node_button.after( + 0, lambda: self.show_picker(self.node_button, self.node_picker) + ) + + def show_picker(self, button, picker): + x = self.winfo_width() + 1 + y = button.winfo_rooty() - picker.master.winfo_rooty() - 1 + picker.place(x=x, y=y) + self.app.bind_all("", lambda e: self.hide_pickers()) + picker.wait_visibility() + picker.grab_set() + self.wait_window(picker) + self.app.unbind_all("") + + def create_picker_button(self, image, func, frame, label): + """ + Create button and put it on the frame + + :param PIL.Image image: button image + :param func: the command that is executed when button is clicked + :param tkinter.Frame frame: frame that contains the button + :param str label: button label + :return: nothing + """ + button = ttk.Button( + frame, image=image, text=label, compound=tk.TOP, style=Styles.picker_button + ) + button.image = image + button.bind("", lambda e: func()) + button.grid(pady=1) + + def create_button(self, frame, image, func, tooltip): + button = ttk.Button(frame, image=image, command=func) + button.image = image + button.grid(sticky="ew") + Tooltip(button, tooltip) + return button + + def click_selection(self): + logging.debug("clicked selection tool") + self.design_select(self.select_button) + self.app.canvas.mode = GraphMode.SELECT + + def click_runtime_selection(self): + logging.debug("clicked selection tool") + self.runtime_select(self.runtime_select_button) + self.app.canvas.mode = GraphMode.SELECT + + def click_start(self): + """ + Start session handler redraw buttons, send node and link messages to grpc + server. + + :return: nothing + """ + self.app.canvas.hide_context() + self.app.statusbar.core_alarms.clear() + self.app.statusbar.progress_bar.start(5) + self.app.canvas.mode = GraphMode.SELECT + thread = threading.Thread(target=self.app.core.start_session) + thread.start() + self.runtime_frame.tkraise() + self.click_runtime_selection() + + def click_link(self): + logging.debug("Click LINK button") + self.design_select(self.link_button) + self.app.canvas.mode = GraphMode.EDGE + + def click_edit_node(self): + self.hide_pickers() + dialog = CustomNodesDialog(self.app, self.app) + dialog.show() + + def update_button(self, button, image, node_draw): + logging.info("update button(%s): %s", button, node_draw) + self.hide_pickers() + button.configure(image=image) + button.image = image + self.app.canvas.mode = GraphMode.NODE + self.app.canvas.node_draw = node_draw + + def hide_pickers(self): + logging.info("hiding pickers") + if self.node_picker: + self.node_picker.destroy() + self.node_picker = None + if self.network_picker: + self.network_picker.destroy() + self.network_picker = None + if self.annotation_picker: + self.annotation_picker.destroy() + self.annotation_picker = None + + def create_node_button(self): + """ + Create network layer button + + :return: nothing + """ + image = icon(ImageEnum.ROUTER) + self.node_button = ttk.Button( + self.design_frame, image=image, command=self.draw_node_picker + ) + self.node_button.image = image + self.node_button.grid(sticky="ew") + Tooltip(self.node_button, "Network-layer virtual nodes") + + def draw_network_picker(self): + """ + Draw the options for link-layer button. + + :return: nothing + """ + self.hide_pickers() + self.network_picker = ttk.Frame(self.master) + for node_draw in NodeUtils.NETWORK_NODES: + toolbar_image = icon(node_draw.image_enum) + image = icon(node_draw.image_enum, PICKER_SIZE) + self.create_picker_button( + image, + partial( + self.update_button, self.network_button, toolbar_image, node_draw + ), + self.network_picker, + node_draw.label, + ) + self.design_select(self.network_button) + self.network_button.after( + 0, lambda: self.show_picker(self.network_button, self.network_picker) + ) + + def create_network_button(self): + """ + Create link-layer node button and the options that represent different + link-layer node types. + + :return: nothing + """ + image = icon(ImageEnum.HUB) + self.network_button = ttk.Button( + self.design_frame, image=image, command=self.draw_network_picker + ) + self.network_button.image = image + self.network_button.grid(sticky="ew") + Tooltip(self.network_button, "link-layer nodes") + + def draw_annotation_picker(self): + """ + Draw the options for marker button. + + :return: nothing + """ + self.hide_pickers() + self.annotation_picker = ttk.Frame(self.master) + nodes = [ + (ImageEnum.MARKER, ShapeType.MARKER), + (ImageEnum.OVAL, ShapeType.OVAL), + (ImageEnum.RECTANGLE, ShapeType.RECTANGLE), + (ImageEnum.TEXT, ShapeType.TEXT), + ] + for image_enum, shape_type in nodes: + toolbar_image = icon(image_enum) + image = icon(image_enum, PICKER_SIZE) + self.create_picker_button( + image, + partial(self.update_annotation, toolbar_image, shape_type), + self.annotation_picker, + shape_type.value, + ) + self.design_select(self.annotation_button) + self.annotation_button.after( + 0, lambda: self.show_picker(self.annotation_button, self.annotation_picker) + ) + + def create_annotation_button(self): + """ + Create marker button and options that represent different marker types + + :return: nothing + """ + image = icon(ImageEnum.MARKER) + self.annotation_button = ttk.Button( + self.design_frame, image=image, command=self.draw_annotation_picker + ) + self.annotation_button.image = image + self.annotation_button.grid(sticky="ew") + Tooltip(self.annotation_button, "background annotation tools") + + def create_observe_button(self): + menu_button = ttk.Menubutton( + self.runtime_frame, image=icon(ImageEnum.OBSERVE), direction=tk.RIGHT + ) + menu_button.grid(sticky="ew") + menu = tk.Menu(menu_button, tearoff=0) + menu_button["menu"] = menu + menu.add_command(label="None") + menu.add_command(label="processes") + menu.add_command(label="ifconfig") + menu.add_command(label="IPv4 routes") + menu.add_command(label="IPv6 routes") + menu.add_command(label="OSPFv2 neighbors") + menu.add_command(label="OSPFv3 neighbors") + menu.add_command(label="Listening sockets") + menu.add_command(label="IPv4 MFC entries") + menu.add_command(label="IPv6 MFC entries") + menu.add_command(label="firewall rules") + menu.add_command(label="IPSec policies") + menu.add_command(label="docker logs") + menu.add_command(label="OSPFv3 MDR level") + menu.add_command(label="PIM neighbors") + menu.add_command(label="Edit...") + + def click_stop(self): + """ + redraw buttons on the toolbar, send node and link messages to grpc server + + :return: nothing + """ + self.app.canvas.hide_context() + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread(target=self.app.core.stop_session) + thread.start() + self.app.canvas.delete(tags.WIRELESS_EDGE) + self.design_frame.tkraise() + self.click_selection() + + def update_annotation(self, image, shape_type): + logging.info("clicked annotation: ") + self.hide_pickers() + self.annotation_button.configure(image=image) + self.annotation_button.image = image + self.app.canvas.mode = GraphMode.ANNOTATION + self.app.canvas.annotation_type = shape_type + if is_marker(shape_type): + if not self.marker_tool: + self.marker_tool = Marker(self.master, self.app) + self.marker_tool.show() + + def click_run_button(self): + logging.debug("Click on RUN button") + + def click_plot_button(self): + logging.debug("Click on plot button") + + def click_marker_button(self): + logging.debug("Click on marker button") + dialog = Marker(self.master, self.app) + dialog.show() + # dialog.position() + + def click_two_node_button(self): + logging.debug("Click TWONODE button") diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py new file mode 100644 index 00000000..9a3f7ade --- /dev/null +++ b/coretk/coretk/tooltip.py @@ -0,0 +1,55 @@ +import tkinter as tk +from tkinter import ttk + +from coretk.themes import Styles + + +class Tooltip(object): + """ + Create tool tip for a given widget + """ + + def __init__(self, widget, text="widget info"): + self.widget = widget + self.text = text + self.widget.bind("", self.on_enter) + self.widget.bind("", self.on_leave) + self.waittime = 400 + self.id = None + self.tw = None + + def on_enter(self, event=None): + self.schedule() + + def on_leave(self, event=None): + self.unschedule() + self.close(event) + + def schedule(self): + self.unschedule() + self.id = self.widget.after(self.waittime, self.enter) + + def unschedule(self): + id_ = self.id + self.id = None + if id_: + self.widget.after_cancel(id_) + + def enter(self, event=None): + x, y, cx, cy = self.widget.bbox("insert") + x += self.widget.winfo_rootx() + y += self.widget.winfo_rooty() + 32 + + self.tw = tk.Toplevel(self.widget) + self.tw.wm_overrideredirect(True) + self.tw.wm_geometry("+%d+%d" % (x, y)) + self.tw.rowconfigure(0, weight=1) + self.tw.columnconfigure(0, weight=1) + frame = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3) + frame.grid(sticky="nsew") + label = ttk.Label(frame, text=self.text, style=Styles.tooltip) + label.grid() + + def close(self, event=None): + if self.tw: + self.tw.destroy() diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py new file mode 100644 index 00000000..955a7faf --- /dev/null +++ b/coretk/coretk/validation.py @@ -0,0 +1,140 @@ +""" +input validation +""" +import re +import tkinter as tk + +import netaddr +from netaddr import IPNetwork + + +class InputValidation: + def __init__(self, app): + self.master = app.master + self.positive_int = None + self.positive_float = None + self.name = None + self.ip4 = None + self.rgb = None + self.hex = None + self.register() + + def register(self): + self.positive_int = self.master.register(self.check_positive_int) + self.positive_float = self.master.register(self.check_positive_float) + self.name = self.master.register(self.check_node_name) + self.ip4 = self.master.register(self.check_ip4) + self.rgb = self.master.register(self.check_rbg) + self.hex = self.master.register(self.check_hex) + + def ip_focus_out(self, event): + value = event.widget.get() + try: + IPNetwork(value) + except netaddr.core.AddrFormatError: + event.widget.delete(0, tk.END) + event.widget.insert(tk.END, "invalid") + + def focus_out(self, event, default): + value = event.widget.get() + if value == "": + event.widget.insert(tk.END, default) + + def check_positive_int(self, s): + if len(s) == 0: + return True + try: + int_value = int(s) + if int_value >= 0: + return True + return False + except ValueError: + return False + + def check_positive_float(self, s): + if len(s) == 0: + return True + try: + float_value = float(s) + if float_value >= 0.0: + return True + return False + except ValueError: + return False + + def check_node_name(self, s): + if len(s) < 0: + return False + if len(s) == 0: + return True + for char in s: + if not char.isalnum() and char != "_": + return False + return True + + def check_canvas_int(sefl, s): + if len(s) == 0: + return True + try: + int_value = int(s) + if int_value >= 0: + return True + return False + except ValueError: + return False + + def check_canvas_float(self, s): + if not s: + return True + try: + float_value = float(s) + if float_value >= 0.0: + return True + return False + except ValueError: + return False + + def check_ip4(self, s): + if not s: + return True + pat = re.compile("^([0-9]+[.])*[0-9]*$") + if pat.match(s) is not None: + _32bits = s.split(".") + if len(_32bits) > 4: + return False + for _8bits in _32bits: + if ( + (_8bits and int(_8bits) > 255) + or len(_8bits) > 3 + or (_8bits.startswith("0") and len(_8bits) > 1) + ): + return False + return True + else: + return False + + def check_rbg(self, s): + if not s: + return True + if s.startswith("0") and len(s) >= 2: + return False + try: + value = int(s) + if 0 <= value <= 255: + return True + else: + return False + except ValueError: + return False + + def check_hex(self, s): + if not s: + return True + pat = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$") + if pat.match(s): + if 0 <= len(s) <= 7: + return True + else: + return False + else: + return False diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py new file mode 100644 index 00000000..649dce95 --- /dev/null +++ b/coretk/coretk/widgets.py @@ -0,0 +1,250 @@ +import logging +import tkinter as tk +from functools import partial +from tkinter import filedialog, font, ttk + +from core.api.grpc import core_pb2 +from coretk import themes +from coretk.themes import FRAME_PAD, PADX, PADY + +INT_TYPES = { + core_pb2.ConfigOptionType.UINT8, + core_pb2.ConfigOptionType.UINT16, + core_pb2.ConfigOptionType.UINT32, + core_pb2.ConfigOptionType.UINT64, + core_pb2.ConfigOptionType.INT8, + core_pb2.ConfigOptionType.INT16, + core_pb2.ConfigOptionType.INT32, + core_pb2.ConfigOptionType.INT64, +} + + +def file_button_click(value): + file_path = filedialog.askopenfilename(title="Select File") + if file_path: + value.set(file_path) + + +class FrameScroll(ttk.Frame): + def __init__(self, master, app, _cls=ttk.Frame, **kw): + super().__init__(master, **kw) + self.app = app + self.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + bg = self.app.style.lookup(".", "background") + self.canvas = tk.Canvas(self, highlightthickness=0, background=bg) + self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2) + self.canvas.columnconfigure(0, weight=1) + self.canvas.rowconfigure(0, weight=1) + self.scrollbar = ttk.Scrollbar( + self, orient="vertical", command=self.canvas.yview + ) + self.scrollbar.grid(row=0, column=1, sticky="ns") + self.frame = _cls(self.canvas) + self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) + self.canvas.update_idletasks() + self.canvas.configure( + scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set + ) + self.frame.bind("", self._configure_frame) + self.canvas.bind("", self._configure_canvas) + + def _configure_frame(self, event): + req_width = self.frame.winfo_reqwidth() + if req_width != self.canvas.winfo_reqwidth(): + self.canvas.configure(width=req_width) + self.canvas.configure(scrollregion=self.canvas.bbox("all")) + + def _configure_canvas(self, event): + self.canvas.itemconfig(self.frame_id, width=event.width) + + def clear(self): + for widget in self.frame.winfo_children(): + widget.destroy() + + +class ConfigFrame(ttk.Notebook): + def __init__(self, master, app, config, **kw): + super().__init__(master, **kw) + self.app = app + self.config = config + self.values = {} + + def draw_config(self): + group_mapping = {} + for key in self.config: + option = self.config[key] + group = group_mapping.setdefault(option.group, []) + group.append(option) + + for group_name in sorted(group_mapping): + group = group_mapping[group_name] + tab = FrameScroll(self, self.app, borderwidth=0, padding=FRAME_PAD) + tab.frame.columnconfigure(1, weight=1) + self.add(tab, text=group_name) + for index, option in enumerate(sorted(group, key=lambda x: x.name)): + label = ttk.Label(tab.frame, text=option.label) + label.grid(row=index, pady=PADY, padx=PADX, sticky="w") + value = tk.StringVar() + if option.type == core_pb2.ConfigOptionType.BOOL: + select = tuple(option.select) + combobox = ttk.Combobox( + tab.frame, textvariable=value, values=select, state="readonly" + ) + combobox.grid(row=index, column=1, sticky="ew") + if option.value == "1": + value.set("On") + else: + value.set("Off") + elif option.select: + value.set(option.value) + select = tuple(option.select) + combobox = ttk.Combobox( + tab.frame, textvariable=value, values=select, state="readonly" + ) + combobox.grid(row=index, column=1, sticky="ew") + elif option.type == core_pb2.ConfigOptionType.STRING: + value.set(option.value) + if "file" in option.label: + file_frame = ttk.Frame(tab.frame) + file_frame.grid(row=index, column=1, sticky="ew") + file_frame.columnconfigure(0, weight=1) + entry = ttk.Entry(file_frame, textvariable=value) + entry.grid(row=0, column=0, sticky="ew", padx=PADX) + func = partial(file_button_click, value) + button = ttk.Button(file_frame, text="...", command=func) + button.grid(row=0, column=1) + else: + if "controlnet" in option.name and "script" not in option.name: + entry = ttk.Entry( + tab.frame, + textvariable=value, + validate="key", + validatecommand=(self.app.validation.ip4, "%P"), + ) + entry.grid(row=index, column=1, sticky="ew") + else: + entry = ttk.Entry(tab.frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew") + + elif option.type in INT_TYPES: + value.set(option.value) + entry = ttk.Entry( + tab.frame, + textvariable=value, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.bind( + "", + lambda event: self.app.validation.focus_out(event, "0"), + ) + entry.grid(row=index, column=1, sticky="ew") + elif option.type == core_pb2.ConfigOptionType.FLOAT: + value.set(option.value) + entry = ttk.Entry( + tab.frame, + textvariable=value, + validate="key", + validatecommand=(self.app.validation.positive_float, "%P"), + ) + entry.bind( + "", + lambda event: self.app.validation.focus_out(event, "0"), + ) + entry.grid(row=index, column=1, sticky="ew") + else: + logging.error("unhandled config option type: %s", option.type) + self.values[option.name] = value + + def parse_config(self): + for key in self.config: + option = self.config[key] + value = self.values[key] + config_value = value.get() + if option.type == core_pb2.ConfigOptionType.BOOL: + if config_value == "On": + option.value = "1" + else: + option.value = "0" + else: + option.value = config_value + + return {x: self.config[x].value for x in self.config} + + +class ListboxScroll(ttk.Frame): + def __init__(self, master=None, **kw): + super().__init__(master, **kw) + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) + self.scrollbar.grid(row=0, column=1, sticky="ns") + self.listbox = tk.Listbox( + self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set + ) + themes.style_listbox(self.listbox) + self.listbox.grid(row=0, column=0, sticky="nsew") + self.scrollbar.config(command=self.listbox.yview) + + +class CheckboxList(FrameScroll): + def __init__(self, master, app, clicked=None, **kw): + super().__init__(master, app, **kw) + self.clicked = clicked + self.frame.columnconfigure(0, weight=1) + + def add(self, name, checked): + var = tk.BooleanVar(value=checked) + func = partial(self.clicked, name, var) + checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) + checkbox.grid(sticky="w") + + +class CodeFont(font.Font): + def __init__(self): + super().__init__(font="TkFixedFont", color="green") + + +class CodeText(ttk.Frame): + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + self.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + self.text = tk.Text( + self, + bd=0, + bg="black", + cursor="xterm lime lime", + fg="lime", + font=CodeFont(), + highlightbackground="black", + insertbackground="lime", + selectbackground="lime", + selectforeground="black", + relief=tk.FLAT, + ) + self.text.grid(row=0, column=0, sticky="nsew") + yscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") + self.text.configure(yscrollcommand=yscrollbar.set) + + +class Spinbox(ttk.Entry): + def __init__(self, master=None, **kwargs): + super().__init__(master, "ttk::spinbox", **kwargs) + + def set(self, value): + self.tk.call(self._w, "set", value) + + +def image_chooser(parent, path): + return filedialog.askopenfilename( + parent=parent, + initialdir=str(path), + title="Select", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) diff --git a/coretk/setup.cfg b/coretk/setup.cfg new file mode 100644 index 00000000..d9228b5f --- /dev/null +++ b/coretk/setup.cfg @@ -0,0 +1,15 @@ +[aliases] +test=pytest + +[isort] +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 + +[flake8] +ignore=E501,W503,E203 +max-line-length=100 +max-complexity=26 +select=B,C,E,F,W,T4 diff --git a/coretk/setup.py b/coretk/setup.py new file mode 100644 index 00000000..846ab074 --- /dev/null +++ b/coretk/setup.py @@ -0,0 +1,12 @@ +from setuptools import find_packages, setup + +setup( + name="coretk", + version="0.1.0", + packages=find_packages(), + install_requires=["netaddr", "pillow"], + description="CORE GUI", + url="https://github.com/coreemu/core", + author="Boeing Research & Technology", + license="BSD", +) diff --git a/daemon/.pre-commit-config.yaml b/daemon/.pre-commit-config.yaml index 73566c9d..ac6bc80b 100644 --- a/daemon/.pre-commit-config.yaml +++ b/daemon/.pre-commit-config.yaml @@ -21,3 +21,24 @@ repos: language: system entry: bash -c 'cd daemon && pipenv run flake8' types: [python] + + - id: isort-tk + name: coretk-isort + stages: [commit] + language: system + entry: bash -c 'cd coretk && pipenv run isort --atomic -y' + types: [python] + + - id: black-tk + name: coretk-black + stages: [commit] + language: system + entry: bash -c 'cd coretk && pipenv run black .' + types: [python] + + - id: flake8-tk + name: coretk-flake8 + stages: [commit] + language: system + entry: bash -c 'cd coretk && pipenv run flake8' + types: [python] diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 01a7b1f6..9aa99349 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -159,6 +159,9 @@ class CoreGrpcClient: emane_model_configs=None, wlan_configs=None, mobility_configs=None, + service_configs=None, + service_file_configs=None, + asymmetric_links=None, ): """ Start a session. @@ -169,9 +172,12 @@ class CoreGrpcClient: :param core_pb2.SessionLocation location: location to set :param list[core_pb2.Hook] hooks: session hooks to set :param dict emane_config: emane configuration to set - :param list emane_model_configs: emane model configurations to set - :param list wlan_configs: wlan configurations to set - :param list mobility_configs: mobility configurations to set + :param list emane_model_configs: node emane model configurations + :param list wlan_configs: node wlan configurations + :param list mobility_configs: node mobility configurations + :param list service_configs: node service configurations + :param list service_file_configs: node service file configurations + :param list asymmetric_links: asymmetric links to edit :return: start session response :rtype: core_pb2.StartSessionResponse """ @@ -185,6 +191,9 @@ class CoreGrpcClient: emane_model_configs=emane_model_configs, wlan_configs=wlan_configs, mobility_configs=mobility_configs, + service_configs=service_configs, + service_file_configs=service_file_configs, + asymmetric_links=asymmetric_links, ) return self.stub.StartSession(request) @@ -372,29 +381,34 @@ class CoreGrpcClient: ) return self.stub.AddSessionServer(request) - def events(self, session_id, handler): + def events(self, session_id, handler, events=None): """ Listen for session events. :param int session_id: id of session - :param handler: handler for every event - :return: nothing + :param handler: handler for received events + :param list events: events to listen to, defaults to all + :return: stream processing events, can be used to cancel stream :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.EventsRequest(session_id=session_id) + request = core_pb2.EventsRequest(session_id=session_id, events=events) stream = self.stub.Events(request) start_streamer(stream, handler) + return stream - def throughputs(self, handler): + def throughputs(self, session_id, handler): """ Listen for throughput events with information for interfaces and bridges. + :param int session_id: session id :param handler: handler for every event - :return: nothing + :return: stream processing events, can be used to cancel stream + :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.ThroughputsRequest() + request = core_pb2.ThroughputsRequest(session_id=session_id) stream = self.stub.Throughputs(request) start_streamer(stream, handler) + return stream def add_node(self, session_id, node): """ @@ -422,7 +436,7 @@ class CoreGrpcClient: request = core_pb2.GetNodeRequest(session_id=session_id, node_id=node_id) return self.stub.GetNode(request) - def edit_node(self, session_id, node_id, position, icon=None): + def edit_node(self, session_id, node_id, position, icon=None, source=None): """ Edit a node, currently only changes position. @@ -430,12 +444,17 @@ class CoreGrpcClient: :param int node_id: node id :param core_pb2.Position position: position to set node to :param str icon: path to icon for gui to use for node + :param str source: application source editing node :return: response with result of success or failure :rtype: core_pb2.EditNodeResponse :raises grpc.RpcError: when session or node doesn't exist """ request = core_pb2.EditNodeRequest( - session_id=session_id, node_id=node_id, position=position, icon=icon + session_id=session_id, + node_id=node_id, + position=position, + icon=icon, + source=source, ) return self.stub.EditNode(request) @@ -719,6 +738,18 @@ class CoreGrpcClient: ) return self.stub.SetServiceDefaults(request) + def get_node_service_configs(self, session_id): + """ + Get service data for a node. + + :param int session_id: session id + :return: response with all node service configs + :rtype: core_pb2.GetNodeServiceConfigsResponse + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetNodeServiceConfigsRequest(session_id=session_id) + return self.stub.GetNodeServiceConfigs(request) + def get_node_service(self, session_id, node_id, service): """ Get service data for a node. @@ -768,14 +799,14 @@ class CoreGrpcClient: :rtype: core_pb2.SetNodeServiceResponse :raises grpc.RpcError: when session or node doesn't exist """ - request = core_pb2.SetNodeServiceRequest( - session_id=session_id, + config = core_pb2.ServiceConfig( node_id=node_id, service=service, startup=startup, validate=validate, shutdown=shutdown, ) + request = core_pb2.SetNodeServiceRequest(session_id=session_id, config=config) return self.stub.SetNodeService(request) def set_node_service_file(self, session_id, node_id, service, file_name, data): @@ -791,12 +822,11 @@ class CoreGrpcClient: :rtype: core_pb2.SetNodeServiceFileResponse :raises grpc.RpcError: when session or node doesn't exist """ + config = core_pb2.ServiceFileConfig( + node_id=node_id, service=service, file=file_name, data=data + ) request = core_pb2.SetNodeServiceFileRequest( - session_id=session_id, - node_id=node_id, - service=service, - file=file_name, - data=data, + session_id=session_id, config=config ) return self.stub.SetNodeServiceFile(request) @@ -817,6 +847,18 @@ class CoreGrpcClient: ) return self.stub.ServiceAction(request) + def get_wlan_configs(self, session_id): + """ + Get all wlan configurations. + + :param int session_id: session id + :return: response with a dict of node ids to wlan configurations + :rtype: core_pb2.GetWlanConfigsResponse + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetWlanConfigsRequest(session_id=session_id) + return self.stub.GetWlanConfigs(request) + def get_wlan_config(self, session_id, node_id): """ Get wlan configuration for a node. diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py new file mode 100644 index 00000000..7b4756b1 --- /dev/null +++ b/daemon/core/api/grpc/events.py @@ -0,0 +1,259 @@ +import logging +from queue import Empty, Queue + +from core.api.grpc import core_pb2 +from core.api.grpc.grpcutils import convert_value +from core.emulator.data import ( + ConfigData, + EventData, + ExceptionData, + FileData, + LinkData, + NodeData, +) + + +def handle_node_event(event): + """ + Handle node event when there is a node event + + :param core.emulator.data.NodeData event: node data + :return: node event that contains node id, name, model, position, and services + :rtype: core.api.grpc.core_pb2.NodeEvent + """ + position = core_pb2.Position(x=event.x_position, y=event.y_position) + services = event.services or "" + services = services.split("|") + node_proto = core_pb2.Node( + id=event.id, + name=event.name, + model=event.model, + position=position, + services=services, + ) + return core_pb2.NodeEvent(node=node_proto, source=event.source) + + +def handle_link_event(event): + """ + Handle link event when there is a link event + + :param core.emulator.data.LinkData event: link data + :return: link event that has message type and link information + :rtype: core.api.grpc.core_pb2.LinkEvent + """ + interface_one = None + if event.interface1_id is not None: + interface_one = core_pb2.Interface( + id=event.interface1_id, + name=event.interface1_name, + mac=convert_value(event.interface1_mac), + ip4=convert_value(event.interface1_ip4), + ip4mask=event.interface1_ip4_mask, + ip6=convert_value(event.interface1_ip6), + ip6mask=event.interface1_ip6_mask, + ) + + interface_two = None + if event.interface2_id is not None: + interface_two = core_pb2.Interface( + id=event.interface2_id, + name=event.interface2_name, + mac=convert_value(event.interface2_mac), + ip4=convert_value(event.interface2_ip4), + ip4mask=event.interface2_ip4_mask, + ip6=convert_value(event.interface2_ip6), + ip6mask=event.interface2_ip6_mask, + ) + + options = core_pb2.LinkOptions( + opaque=event.opaque, + jitter=event.jitter, + key=event.key, + mburst=event.mburst, + mer=event.mer, + per=event.per, + bandwidth=event.bandwidth, + burst=event.burst, + delay=event.delay, + dup=event.dup, + unidirectional=event.unidirectional, + ) + link = core_pb2.Link( + type=event.link_type, + node_one_id=event.node1_id, + node_two_id=event.node2_id, + interface_one=interface_one, + interface_two=interface_two, + options=options, + ) + return core_pb2.LinkEvent(message_type=event.message_type, link=link) + + +def handle_session_event(event): + """ + Handle session event when there is a session event + + :param core.emulator.data.EventData event: event data + :return: session event + :rtype: core.api.grpc.core_pb2.SessionEvent + """ + event_time = event.time + if event_time is not None: + event_time = float(event_time) + return core_pb2.SessionEvent( + node_id=event.node, + event=event.event_type, + name=event.name, + data=event.data, + time=event_time, + ) + + +def handle_config_event(event): + """ + Handle configuration event when there is configuration event + + :param core.emulator.data.ConfigData event: configuration data + :return: configuration event + :rtype: core.api.grpc.core_pb2.ConfigEvent + """ + return core_pb2.ConfigEvent( + message_type=event.message_type, + node_id=event.node, + object=event.object, + type=event.type, + captions=event.captions, + bitmap=event.bitmap, + data_values=event.data_values, + possible_values=event.possible_values, + groups=event.groups, + interface=event.interface_number, + network_id=event.network_id, + opaque=event.opaque, + data_types=event.data_types, + ) + + +def handle_exception_event(event): + """ + Handle exception event when there is exception event + + :param core.emulator.data.ExceptionData event: exception data + :return: exception event + :rtype: core.api.grpc.core_pb2.ExceptionEvent + """ + return core_pb2.ExceptionEvent( + node_id=event.node, + level=event.level, + source=event.source, + date=event.date, + text=event.text, + opaque=event.opaque, + ) + + +def handle_file_event(event): + """ + Handle file event + + :param core.emulator.data.FileData event: file data + :return: file event + :rtype: core.api.grpc.core_pb2.FileEvent + """ + return core_pb2.FileEvent( + message_type=event.message_type, + node_id=event.node, + name=event.name, + mode=event.mode, + number=event.number, + type=event.type, + source=event.source, + data=event.data, + compressed_data=event.compressed_data, + ) + + +class EventStreamer: + """ + Processes session events to generate grpc events. + """ + + def __init__(self, session, event_types): + """ + Create a EventStreamer instance. + + :param core.emulator.session.Session session: session to process events for + :param set event_types: types of events to process + """ + self.session = session + self.event_types = event_types + self.queue = Queue() + self.add_handlers() + + def add_handlers(self): + """ + Add a session event handler for desired event types. + + :return: nothing + """ + if core_pb2.EventType.NODE in self.event_types: + self.session.node_handlers.append(self.queue.put) + if core_pb2.EventType.LINK in self.event_types: + self.session.link_handlers.append(self.queue.put) + if core_pb2.EventType.CONFIG in self.event_types: + self.session.config_handlers.append(self.queue.put) + if core_pb2.EventType.FILE in self.event_types: + self.session.file_handlers.append(self.queue.put) + if core_pb2.EventType.EXCEPTION in self.event_types: + self.session.exception_handlers.append(self.queue.put) + if core_pb2.EventType.SESSION in self.event_types: + self.session.event_handlers.append(self.queue.put) + + def process(self): + """ + Process the next event in the queue. + + :return: grpc event, or None when invalid event or queue timeout + :rtype: core.api.grpc.core_pb2.Event + """ + event = core_pb2.Event(session_id=self.session.id) + try: + data = self.queue.get(timeout=1) + if isinstance(data, NodeData): + event.node_event.CopyFrom(handle_node_event(data)) + elif isinstance(data, LinkData): + event.link_event.CopyFrom(handle_link_event(data)) + elif isinstance(data, EventData): + event.session_event.CopyFrom(handle_session_event(data)) + elif isinstance(data, ConfigData): + event.config_event.CopyFrom(handle_config_event(data)) + elif isinstance(data, ExceptionData): + event.exception_event.CopyFrom(handle_exception_event(data)) + elif isinstance(data, FileData): + event.file_event.CopyFrom(handle_file_event(data)) + else: + logging.error("unknown event: %s", data) + event = None + except Empty: + event = None + return event + + def remove_handlers(self): + """ + Remove session event handlers for events being watched. + + :return: nothing + """ + if core_pb2.EventType.NODE in self.event_types: + self.session.node_handlers.remove(self.queue.put) + if core_pb2.EventType.LINK in self.event_types: + self.session.link_handlers.remove(self.queue.put) + if core_pb2.EventType.CONFIG in self.event_types: + self.session.config_handlers.remove(self.queue.put) + if core_pb2.EventType.FILE in self.event_types: + self.session.file_handlers.remove(self.queue.put) + if core_pb2.EventType.EXCEPTION in self.event_types: + self.session.exception_handlers.remove(self.queue.put) + if core_pb2.EventType.SESSION in self.event_types: + self.session.event_handlers.remove(self.queue.put) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 166807d0..a3b25541 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -30,6 +30,8 @@ def add_node_data(node_proto): options.opaque = node_proto.opaque options.image = node_proto.image options.services = node_proto.services + if node_proto.emane: + options.emane = node_proto.emane if node_proto.server: options.server = node_proto.server @@ -126,7 +128,7 @@ def create_nodes(session, node_protos): def create_links(session, link_protos): """ - Create nodes using a thread pool and wait for completion. + Create links using a thread pool and wait for completion. :param core.emulator.session.Session session: session to create nodes in :param list[core_pb2.Link] link_protos: link proto messages @@ -147,6 +149,29 @@ def create_links(session, link_protos): return results, exceptions +def edit_links(session, link_protos): + """ + Edit links using a thread pool and wait for completion. + + :param core.emulator.session.Session session: session to create nodes in + :param list[core_pb2.Link] link_protos: link proto messages + :return: results and exceptions for created links + :rtype: tuple + """ + funcs = [] + for link_proto in link_protos: + node_one_id = link_proto.node_one_id + node_two_id = link_proto.node_two_id + interface_one, interface_two, options = add_link_data(link_proto) + args = (node_one_id, node_two_id, interface_one.id, interface_two.id, options) + funcs.append((session.update_link, args, {})) + start = time.monotonic() + results, exceptions = utils.threadpool(funcs) + total = time.monotonic() - start + logging.debug("grpc edit links time: %s", total) + return results, exceptions + + def convert_value(value): """ Convert value into string. @@ -219,6 +244,22 @@ def get_emane_model_id(node_id, interface_id): return node_id +def parse_emane_model_id(_id): + """ + Parses EMANE model id to get true node id and interface id. + + :param _id: id to parse + :return: node id and interface id + :rtype: tuple + """ + interface = -1 + node_id = _id + if _id >= 1000: + interface = _id % 1000 + node_id = int(_id / 1000) + return node_id, interface + + def convert_link(session, link_data): """ Convert link_data into core protobuf Link @@ -319,3 +360,40 @@ def session_location(session, location): session.location.refxyz = (location.x, location.y, location.z) session.location.setrefgeo(location.lat, location.lon, location.alt) session.location.refscale = location.scale + + +def service_configuration(session, config): + """ + Convenience method for setting a node service configuration. + + :param core.emulator.session.Session session: session for service configuration + :param core_pb2.ServiceConfig config: service configuration + :return: + """ + session.services.set_service(config.node_id, config.service) + service = session.services.get_service(config.node_id, config.service) + service.startup = tuple(config.startup) + service.validate = tuple(config.validate) + service.shutdown = tuple(config.shutdown) + + +def get_service_configuration(service): + """ + Convenience for converting a service to service data proto. + + :param service: service to get proto data for + :return: service proto data + :rtype: core.api.grpc.core_pb2.NodeServiceData + """ + return core_pb2.NodeServiceData( + executables=service.executables, + dependencies=service.dependencies, + dirs=service.dirs, + configs=service.configs, + startup=service.startup, + validate=service.validate, + validation_mode=service.validation_mode.value, + validation_timer=service.validation_timer, + shutdown=service.shutdown, + meta=service.meta, + ) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index e1e1f24d..bf2a8ac8 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -5,27 +5,19 @@ import re import tempfile import time from concurrent import futures -from queue import Empty, Queue import grpc from core.api.grpc import core_pb2, core_pb2_grpc, grpcutils +from core.api.grpc.events import EventStreamer from core.api.grpc.grpcutils import ( - convert_value, get_config_options, get_emane_model_id, get_links, get_net_stats, ) from core.emane.nodes import EmaneNet -from core.emulator.data import ( - ConfigData, - EventData, - ExceptionData, - FileData, - LinkData, - NodeData, -) +from core.emulator.data import LinkData from core.emulator.emudata import LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.errors import CoreCommandError, CoreError @@ -35,7 +27,7 @@ from core.nodes.lxd import LxcNode from core.services.coreservices import ServiceManager _ONE_DAY_IN_SECONDS = 60 * 60 * 24 -_INTERFACE_REGEX = re.compile(r"\d+") +_INTERFACE_REGEX = re.compile(r"veth(?P[0-9a-fA-F]+)") class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): @@ -153,9 +145,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): config.node_id, Ns2ScriptedMobility.name, config.config ) + # service configs + for config in request.service_configs: + grpcutils.service_configuration(session, config) + + # service file configs + for config in request.service_file_configs: + session.services.set_service_file( + config.node_id, config.service, config.file, config.data + ) + # create links grpcutils.create_links(session, request.links) + # asymmetric links + grpcutils.edit_links(session, request.asymmetric_links) + # set to instantiation and start session.set_state(EventTypes.INSTANTIATION_STATE) session.instantiate() @@ -174,9 +179,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("stop session: %s", request) session = self.get_session(request.session_id, context) session.data_collect() - session.set_state(EventTypes.DATACOLLECT_STATE) + session.set_state(EventTypes.DATACOLLECT_STATE, send_event=True) session.clear() - session.set_state(EventTypes.SHUTDOWN_STATE) + session.set_state(EventTypes.SHUTDOWN_STATE, send_event=True) return core_pb2.StopSessionResponse(result=True) def CreateSession(self, request, context): @@ -383,15 +388,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): position = core_pb2.Position( x=node.position.x, y=node.position.y, z=node.position.z ) - services = getattr(node, "services", []) if services is None: services = [] services = [x.name for x in services] - emane_model = None if isinstance(node, EmaneNet): emane_model = node.model.name + image = getattr(node, "image", None) node_proto = core_pb2.Node( id=node.id, @@ -401,6 +405,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): type=node_type.value, position=position, services=services, + icon=node.icon, + image=image, ) if isinstance(node, (DockerNode, LxcNode)): node_proto.image = node.image @@ -428,210 +434,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): def Events(self, request, context): session = self.get_session(request.session_id, context) - queue = Queue() - session.node_handlers.append(queue.put) - session.link_handlers.append(queue.put) - session.config_handlers.append(queue.put) - session.file_handlers.append(queue.put) - session.exception_handlers.append(queue.put) - session.event_handlers.append(queue.put) + event_types = set(request.events) + if not event_types: + event_types = set(core_pb2.EventType.Enum.values()) + streamer = EventStreamer(session, event_types) while self._is_running(context): - event = core_pb2.Event() - try: - data = queue.get(timeout=1) - if isinstance(data, NodeData): - event.node_event.CopyFrom(self._handle_node_event(data)) - elif isinstance(data, LinkData): - event.link_event.CopyFrom(self._handle_link_event(data)) - elif isinstance(data, EventData): - event.session_event.CopyFrom(self._handle_session_event(data)) - elif isinstance(data, ConfigData): - event.config_event.CopyFrom(self._handle_config_event(data)) - # TODO: remove when config events are fixed - event.config_event.session_id = session.id - elif isinstance(data, ExceptionData): - event.exception_event.CopyFrom(self._handle_exception_event(data)) - elif isinstance(data, FileData): - event.file_event.CopyFrom(self._handle_file_event(data)) - else: - logging.error("unknown event: %s", data) - continue - + event = streamer.process() + if event: yield event - except Empty: - continue - session.node_handlers.remove(queue.put) - session.link_handlers.remove(queue.put) - session.config_handlers.remove(queue.put) - session.file_handlers.remove(queue.put) - session.exception_handlers.remove(queue.put) - session.event_handlers.remove(queue.put) + streamer.remove_handlers() self._cancel_stream(context) - def _handle_node_event(self, event): - """ - Handle node event when there is a node event - - :param core.emulator.data.NodeData event: node data - :return: node event that contains node id, name, model, position, and services - :rtype: core.api.grpc.core_pb2.NodeEvent - """ - position = core_pb2.Position(x=event.x_position, y=event.y_position) - services = event.services or "" - services = services.split("|") - node_proto = core_pb2.Node( - id=event.id, - name=event.name, - model=event.model, - position=position, - services=services, - ) - return core_pb2.NodeEvent(node=node_proto) - - def _handle_link_event(self, event): - """ - Handle link event when there is a link event - - :param core.emulator.data.LinkData event: link data - :return: link event that has message type and link information - :rtype: core.api.grpc.core_pb2.LinkEvent - """ - interface_one = None - if event.interface1_id is not None: - interface_one = core_pb2.Interface( - id=event.interface1_id, - name=event.interface1_name, - mac=convert_value(event.interface1_mac), - ip4=convert_value(event.interface1_ip4), - ip4mask=event.interface1_ip4_mask, - ip6=convert_value(event.interface1_ip6), - ip6mask=event.interface1_ip6_mask, - ) - - interface_two = None - if event.interface2_id is not None: - interface_two = core_pb2.Interface( - id=event.interface2_id, - name=event.interface2_name, - mac=convert_value(event.interface2_mac), - ip4=convert_value(event.interface2_ip4), - ip4mask=event.interface2_ip4_mask, - ip6=convert_value(event.interface2_ip6), - ip6mask=event.interface2_ip6_mask, - ) - - options = core_pb2.LinkOptions( - opaque=event.opaque, - jitter=event.jitter, - key=event.key, - mburst=event.mburst, - mer=event.mer, - per=event.per, - bandwidth=event.bandwidth, - burst=event.burst, - delay=event.delay, - dup=event.dup, - unidirectional=event.unidirectional, - ) - link = core_pb2.Link( - type=event.link_type, - node_one_id=event.node1_id, - node_two_id=event.node2_id, - interface_one=interface_one, - interface_two=interface_two, - options=options, - ) - return core_pb2.LinkEvent(message_type=event.message_type, link=link) - - def _handle_session_event(self, event): - """ - Handle session event when there is a session event - - :param core.emulator.data.EventData event: event data - :return: session event - :rtype: core.api.grpc.core_pb2.SessionEvent - """ - event_time = event.time - if event_time is not None: - event_time = float(event_time) - return core_pb2.SessionEvent( - node_id=event.node, - event=event.event_type, - name=event.name, - data=event.data, - time=event_time, - session_id=event.session, - ) - - def _handle_config_event(self, event): - """ - Handle configuration event when there is configuration event - - :param core.emulator.data.ConfigData event: configuration data - :return: configuration event - :rtype: core.api.grpc.core_pb2.ConfigEvent - """ - session_id = None - if event.session is not None: - session_id = int(event.session) - return core_pb2.ConfigEvent( - message_type=event.message_type, - node_id=event.node, - object=event.object, - type=event.type, - captions=event.captions, - bitmap=event.bitmap, - data_values=event.data_values, - possible_values=event.possible_values, - groups=event.groups, - session_id=session_id, - interface=event.interface_number, - network_id=event.network_id, - opaque=event.opaque, - data_types=event.data_types, - ) - - def _handle_exception_event(self, event): - """ - Handle exception event when there is exception event - - :param core.emulator.data.ExceptionData event: exception data - :return: exception event - :rtype: core.api.grpc.core_pb2.ExceptionEvent - """ - return core_pb2.ExceptionEvent( - node_id=event.node, - session_id=int(event.session), - level=event.level.value, - source=event.source, - date=event.date, - text=event.text, - opaque=event.opaque, - ) - - def _handle_file_event(self, event): - """ - Handle file event - - :param core.emulator.data.FileData event: file data - :return: file event - :rtype: core.api.grpc.core_pb2.FileEvent - """ - return core_pb2.FileEvent( - message_type=event.message_type, - node_id=event.node, - name=event.name, - mode=event.mode, - number=event.number, - type=event.type, - source=event.source, - session_id=event.session, - data=event.data, - compressed_data=event.compressed_data, - ) - def Throughputs(self, request, context): """ Calculate average throughput after every certain amount of delay time @@ -640,17 +455,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param grpc.SrevicerContext context: context object :return: nothing """ + session = self.get_session(request.session_id, context) delay = 3 last_check = None last_stats = None + while self._is_running(context): - now = time.time() + now = time.monotonic() stats = get_net_stats() # calculate average if last_check is not None: interval = now - last_check - throughputs_event = core_pb2.ThroughputsEvent() + throughputs_event = core_pb2.ThroughputsEvent(session_id=session.id) for key in stats: current_rxtx = stats[key] previous_rxtx = last_stats.get(key) @@ -665,8 +482,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): throughput = rx_kbps + tx_kbps if key.startswith("veth"): key = key.split(".") - node_id = int(_INTERFACE_REGEX.search(key[0]).group()) - interface_id = int(key[1]) + node_id = _INTERFACE_REGEX.search(key[0]).group("node") + node_id = int(node_id, base=16) + interface_id = int(key[1], base=16) + session_id = int(key[2], base=16) + if session.id != session_id: + continue interface_throughput = ( throughputs_event.interface_throughputs.add() ) @@ -675,7 +496,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): interface_throughput.throughput = throughput elif key.startswith("b."): try: - node_id = int(key.split(".")[1]) + key = key.split(".") + node_id = int(key[1], base=16) + session_id = int(key[2], base=16) + if session.id != session_id: + continue bridge_throughput = ( throughputs_event.bridge_throughputs.add() ) @@ -703,10 +528,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) _type, _id, options = grpcutils.add_node_data(request.node) node = session.add_node(_type=_type, _id=_id, options=options) - # configure emane if provided - emane_model = request.node.emane - if emane_model: - session.emane.set_model_config(id, emane_model) return core_pb2.AddNodeResponse(node_id=node.id) def GetNode(self, request, context): @@ -788,7 +609,10 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): result = True try: session.edit_node(node.id, options) - node_data = node.data(0) + source = None + if request.source: + source = request.source + node_data = node.data(0, source=source) session.broadcast_node(node_data) except CoreError: result = False @@ -1106,6 +930,32 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ] = service_defaults.services return core_pb2.SetServiceDefaultsResponse(result=True) + def GetNodeServiceConfigs(self, request, context): + """ + Retrieve all node service configurations. + + :param core.api.grpc.core_pb2.GetNodeServiceConfigsRequest request: + get-node-service request + :param grpc.ServicerContext context: context object + :return: all node service configs response + :rtype: core.api.grpc.core_pb2.GetNodeServiceConfigsResponse + """ + logging.debug("get node service configs: %s", request) + session = self.get_session(request.session_id, context) + configs = [] + for node_id, service_configs in session.services.custom_services.items(): + for name in service_configs: + service = session.services.get_service(node_id, name) + service_proto = grpcutils.get_service_configuration(service) + config = core_pb2.GetNodeServiceConfigsResponse.ServiceConfig( + node_id=node_id, + service=name, + data=service_proto, + files=service.config_data, + ) + configs.append(config) + return core_pb2.GetNodeServiceConfigsResponse(configs=configs) + def GetNodeService(self, request, context): """ Retrieve a requested service from a node @@ -1121,18 +971,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): service = session.services.get_service( request.node_id, request.service, default_service=True ) - service_proto = core_pb2.NodeServiceData( - executables=service.executables, - dependencies=service.dependencies, - dirs=service.dirs, - configs=service.configs, - startup=service.startup, - validate=service.validate, - validation_mode=service.validation_mode.value, - validation_timer=service.validation_timer, - shutdown=service.shutdown, - meta=service.meta, - ) + service_proto = grpcutils.get_service_configuration(service) return core_pb2.GetNodeServiceResponse(service=service_proto) def GetNodeServiceFile(self, request, context): @@ -1148,13 +987,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("get node service file: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context) - service = None - for current_service in node.services: - if current_service.name == request.service: - service = current_service - break - if not service: - context.abort(grpc.StatusCode.NOT_FOUND, "service not found") file_data = session.services.get_service_file( node, request.service, request.file ) @@ -1172,11 +1004,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set node service: %s", request) session = self.get_session(request.session_id, context) - session.services.set_service(request.node_id, request.service) - service = session.services.get_service(request.node_id, request.service) - service.startup = tuple(request.startup) - service.validate = tuple(request.validate) - service.shutdown = tuple(request.shutdown) + config = request.config + grpcutils.service_configuration(session, config) return core_pb2.SetNodeServiceResponse(result=True) def SetNodeServiceFile(self, request, context): @@ -1191,8 +1020,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set node service file: %s", request) session = self.get_session(request.session_id, context) + config = request.config session.services.set_service_file( - request.node_id, request.service, request.file, request.data + config.node_id, config.service, config.file, config.data ) return core_pb2.SetNodeServiceFileResponse(result=True) @@ -1235,6 +1065,31 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.ServiceActionResponse(result=result) + def GetWlanConfigs(self, request, context): + """ + Retrieve all wireless-lan configurations. + + :param core.api.grpc.core_pb2.GetWlanConfigsRequest request: request + :param context: core.api.grpc.core_pb2.GetWlanConfigResponse + :return: all wlan configurations + :rtype: core.api.grpc.core_pb2.GetWlanConfigsResponse + """ + logging.debug("get wlan configs: %s", request) + session = self.get_session(request.session_id, context) + response = core_pb2.GetWlanConfigsResponse() + for node_id in session.mobility.node_configurations: + model_config = session.mobility.node_configurations[node_id] + if node_id == -1: + continue + for model_name in model_config: + if model_name != BasicRangeModel.name: + continue + current_config = session.mobility.get_model_config(node_id, model_name) + config = get_config_options(current_config, BasicRangeModel) + mapped_config = core_pb2.MappedConfig(config=config) + response.configs[node_id].CopyFrom(mapped_config) + return response + def GetWlanConfig(self, request, context): """ Retrieve wireless-lan configuration of a node @@ -1368,21 +1223,26 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get emane model configs: %s", request) session = self.get_session(request.session_id, context) - response = core_pb2.GetEmaneModelConfigsResponse() - for node_id in session.emane.node_configurations: - model_config = session.emane.node_configurations[node_id] - if node_id == -1: + + configs = [] + for _id in session.emane.node_configurations: + if _id == -1: continue - for model_name in model_config: + model_configs = session.emane.node_configurations[_id] + for model_name in model_configs: model = session.emane.models[model_name] - current_config = session.emane.get_model_config(node_id, model_name) + current_config = session.emane.get_model_config(_id, model_name) config = get_config_options(current_config, model) + node_id, interface = grpcutils.parse_emane_model_id(_id) model_config = core_pb2.GetEmaneModelConfigsResponse.ModelConfig( - model=model_name, config=config + node_id=node_id, + model=model_name, + interface=interface, + config=config, ) - response.configs[node_id].CopyFrom(model_config) - return response + configs.append(model_config) + return core_pb2.GetEmaneModelConfigsResponse(configs=configs) def SaveXml(self, request, context): """ diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 321306a2..009cb067 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1710,7 +1710,7 @@ class CoreHandler(socketserver.BaseRequestHandler): event_type=event_type, name=name, data=fail_data + ";" + unknown_data, - time=str(time.time()), + time=str(time.monotonic()), ) self.session.broadcast_event(event_data) diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 8e1c270d..8228b536 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -26,8 +26,8 @@ def convert_node(node_data): (NodeTlvs.EMULATION_ID, node_data.emulation_id), (NodeTlvs.EMULATION_SERVER, node_data.server), (NodeTlvs.SESSION, node_data.session), - (NodeTlvs.X_POSITION, node_data.x_position), - (NodeTlvs.Y_POSITION, node_data.y_position), + (NodeTlvs.X_POSITION, int(node_data.x_position)), + (NodeTlvs.Y_POSITION, int(node_data.y_position)), (NodeTlvs.CANVAS, node_data.canvas), (NodeTlvs.NETWORK_ID, node_data.network_id), (NodeTlvs.SERVICES, node_data.services), diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index c7817b41..bd76ed81 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -25,7 +25,7 @@ class EmaneNet(CoreNetworkBase): """ apitype = NodeTypes.EMANE.value - linktype = LinkTypes.WIRELESS.value + linktype = LinkTypes.WIRED.value type = "wlan" is_emane = True diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index cdba4e44..158dc296 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -5,7 +5,6 @@ import signal import sys import core.services -from core.emulator.emudata import IdGen from core.emulator.session import Session from core.services.coreservices import ServiceManager @@ -49,7 +48,6 @@ class CoreEmu: self.config = config # session management - self.session_id_gen = IdGen() self.sessions = {} # load services @@ -79,7 +77,6 @@ class CoreEmu: :return: nothing """ logging.info("shutting down all sessions") - self.session_id_gen.id = 0 sessions = self.sessions.copy() self.sessions.clear() for _id in sessions: @@ -96,11 +93,9 @@ class CoreEmu: :rtype: EmuSession """ if not _id: - while True: - _id = self.session_id_gen.next() - if _id not in self.sessions: - break - + _id = 1 + while _id in self.sessions: + _id += 1 session = _cls(_id, config=self.config) logging.info("created session: %s", _id) self.sessions[_id] = session diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index ba0dd457..0ed1fa67 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -76,6 +76,7 @@ NodeData = collections.namedtuple( "altitude", "icon", "opaque", + "source", ], ) NodeData.__new__.__defaults__ = (None,) * len(NodeData._fields) diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 5e59eaae..2a8f9bf6 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -89,6 +89,7 @@ class NodeOptions: self.emulation_id = None self.server = None self.image = image + self.emane = None def set_position(self, x, y): """ diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index cba99e8e..1a01bdaa 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -30,7 +30,7 @@ from core.emulator.sessionconfig import SessionConfig from core.errors import CoreError from core.location.corelocation import CoreLocation from core.location.event import EventLoop -from core.location.mobility import MobilityManager +from core.location.mobility import BasicRangeModel, MobilityManager from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase from core.nodes.docker import DockerNode from core.nodes.ipaddress import MacAddress @@ -103,7 +103,7 @@ class Session: # TODO: should the default state be definition? self.state = EventTypes.NONE.value - self._state_time = time.time() + self._state_time = time.monotonic() self._state_file = os.path.join(self.session_dir, "state") # hooks handlers @@ -703,6 +703,13 @@ class Session: logging.debug("set node type: %s", node.type) self.services.add_services(node, node.type, options.services) + # ensure default emane configuration + if isinstance(node, EmaneNet) and options.emane: + self.emane.set_model_config(_id, options.emane) + # set default wlan config if needed + if isinstance(node, WlanNode): + self.mobility.set_model_config(_id, BasicRangeModel.name) + # boot nodes after runtime, CoreNodes, Physical, and RJ45 are all nodes is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node) if self.state == EventTypes.RUNTIME_STATE.value and is_boot_node: @@ -1023,7 +1030,7 @@ class Session: return self.state = state_value - self._state_time = time.time() + self._state_time = time.monotonic() logging.info("changing session(%s) to state %s", self.id, state_name) self.write_state(state_value) @@ -1031,7 +1038,7 @@ class Session: self.run_state_hooks(state_value) if send_event: - event_data = EventData(event_type=state_value, time=str(time.time())) + event_data = EventData(event_type=state_value, time=str(time.monotonic())) self.broadcast_event(event_data) def write_state(self, state): @@ -1814,7 +1821,7 @@ class Session: if not in runtime. """ if self.state == EventTypes.RUNTIME_STATE.value: - return time.time() - self._state_time + return time.monotonic() - self._state_time else: return 0.0 diff --git a/daemon/core/location/event.py b/daemon/core/location/event.py index 11e535d3..f930d9b7 100644 --- a/daemon/core/location/event.py +++ b/daemon/core/location/event.py @@ -145,7 +145,7 @@ class EventLoop: with self.lock: if not self.running or not self.queue: break - now = time.time() + now = time.monotonic() if self.queue[0].time > now: schedule = True break @@ -170,7 +170,7 @@ class EventLoop: raise ValueError("scheduling event while not running") if not self.queue: return - delay = self.queue[0].time - time.time() + delay = self.queue[0].time - time.monotonic() if self.timer: raise ValueError("timer was already set") self.timer = Timer(delay, self.__run_events) @@ -187,7 +187,7 @@ class EventLoop: if self.running: return self.running = True - self.start = time.time() + self.start = time.monotonic() for event in self.queue: event.time += self.start self.__schedule_event() @@ -225,7 +225,7 @@ class EventLoop: self.eventnum += 1 evtime = float(delaysec) if self.running: - evtime += time.time() + evtime += time.monotonic() event = Event(eventnum, evtime, func, *args, **kwds) if self.queue: diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 085962ff..b3eb4884 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -174,7 +174,7 @@ class MobilityManager(ModelManager): event_type=event_type, name=f"mobility:{model.name}", data=data, - time=str(time.time()), + time=str(time.monotonic()), ) self.session.broadcast_event(event_data) @@ -612,7 +612,7 @@ class WayPointMobility(WirelessModel): if self.state != self.STATE_RUNNING: return t = self.lasttime - self.lasttime = time.time() + self.lasttime = time.monotonic() now = self.lasttime - self.timezero dt = self.lasttime - t @@ -664,7 +664,7 @@ class WayPointMobility(WirelessModel): :return: nothing """ logging.info("running mobility scenario") - self.timezero = time.time() + self.timezero = time.monotonic() self.lasttime = self.timezero - (0.001 * self.refresh_ms) self.movenodesinitial() self.runround() @@ -811,12 +811,6 @@ class WayPointMobility(WirelessModel): :param z: z position :return: nothing """ - if x is not None: - x = int(x) - if y is not None: - y = int(y) - if z is not None: - z = int(z) node.position.set(x, y, z) node_data = node.data(message_type=0) self.session.broadcast_node(node_data) @@ -850,7 +844,7 @@ class WayPointMobility(WirelessModel): self.lasttime = 0 self.run() elif laststate == self.STATE_PAUSED: - now = time.time() + now = time.monotonic() self.timezero += now - self.lasttime self.lasttime = now - (0.001 * self.refresh_ms) self.runround() @@ -877,7 +871,7 @@ class WayPointMobility(WirelessModel): :return: nothing """ self.state = self.STATE_PAUSED - self.lasttime = time.time() + self.lasttime = time.monotonic() class Ns2ScriptedMobility(WayPointMobility): diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 72fc0fe1..3193d954 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -176,7 +176,7 @@ class NodeBase: self.ifindex += 1 return ifindex - def data(self, message_type, lat=None, lon=None, alt=None): + def data(self, message_type, lat=None, lon=None, alt=None, source=None): """ Build a data object for this node. @@ -184,6 +184,7 @@ class NodeBase: :param str lat: latitude :param str lon: longitude :param str alt: altitude + :param str source: source of node data :return: node data object :rtype: core.emulator.data.NodeData """ @@ -217,6 +218,7 @@ class NodeBase: model=model, server=server, services=services, + source=source, ) return node_data @@ -1051,6 +1053,7 @@ class CoreNetworkBase(NodeBase): message_type=0, node1_id=linked_node.id, node2_id=self.id, + link_type=self.linktype, unidirectional=1, delay=netif.getparam("delay"), bandwidth=netif.getparam("bw"), diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 80a730e2..5342215b 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -57,7 +57,7 @@ class EbtablesQueue: :return: nothing """ with self.updatelock: - self.last_update_time[wlan] = time.time() + self.last_update_time[wlan] = time.monotonic() if self.doupdateloop: return @@ -108,9 +108,9 @@ class EbtablesQueue: :rtype: float """ try: - elapsed = time.time() - self.last_update_time[wlan] + elapsed = time.monotonic() - self.last_update_time[wlan] except KeyError: - self.last_update_time[wlan] = time.time() + self.last_update_time[wlan] = time.monotonic() elapsed = 0.0 return elapsed @@ -122,7 +122,7 @@ class EbtablesQueue: :param wlan: wlan entity :return: nothing """ - self.last_update_time[wlan] = time.time() + self.last_update_time[wlan] = time.monotonic() self.updates.remove(wlan) def updateloop(self): @@ -965,6 +965,7 @@ class PtpNet(CoreNetwork): if unidirectional: link_data = LinkData( message_type=0, + link_type=self.linktype, node1_id=if2.node.id, node2_id=if1.node.id, delay=if2.getparam("delay"), @@ -1017,7 +1018,7 @@ class WlanNode(CoreNetwork): """ apitype = NodeTypes.WIRELESS_LAN.value - linktype = LinkTypes.WIRELESS.value + linktype = LinkTypes.WIRED.value policy = "DROP" type = "wlan" diff --git a/daemon/core/services/bird.py b/daemon/core/services/bird.py index 80774201..a0f4c640 100644 --- a/daemon/core/services/bird.py +++ b/daemon/core/services/bird.py @@ -1,7 +1,7 @@ """ bird.py: defines routing services provided by the BIRD Internet Routing Daemon. """ - +from core.nodes import ipaddress from core.services.coreservices import CoreService @@ -38,8 +38,9 @@ class Bird(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 80168425..684ccbb9 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -396,7 +396,7 @@ class CoreServices: """ Add services to a node. - :param core.coreobj.PyCoreNode node: node to add services to + :param core.nodes.base.CoreNode node: node to add services to :param str node_type: node type to add services to :param list[str] services: names of services to add to node :return: nothing @@ -461,8 +461,8 @@ class CoreServices: :param core.netns.vnode.LxcNode node: node to start services on :return: nothing """ - funcs = [] boot_paths = ServiceDependencies(node.services).boot_paths() + funcs = [] for boot_path in boot_paths: args = (node, boot_path) funcs.append((self._start_boot_paths, args, {})) @@ -484,6 +484,7 @@ class CoreServices: " -> ".join([x.name for x in boot_path]), ) for service in boot_path: + service = self.get_service(node.id, service.name, default_service=True) try: self.boot_service(node, service) except Exception: @@ -538,13 +539,13 @@ class CoreServices: time.sleep(service.validation_timer) # non-blocking, attempt to validate periodically, up to validation_timer time elif service.validation_mode == ServiceMode.NON_BLOCKING: - start = time.time() + start = time.monotonic() while True: status = self.validate_service(node, service) if not status: break - if time.time() - start > service.validation_timer: + if time.monotonic() - start > service.validation_timer: break time.sleep(service.validation_period) @@ -744,7 +745,9 @@ class CoreServices: config_files = service.get_configs(node) for file_name in config_files: - logging.debug("generating service config: %s", file_name) + logging.debug( + "generating service config custom(%s): %s", service.custom, file_name + ) if service.custom: cfg = service.config_data.get(file_name) if cfg is None: diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index b4332009..95ffb0b5 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -112,9 +112,10 @@ class FRRZebra(CoreService): """ helper for mapping IP addresses to zebra config statements """ - if x.find(".") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv4_address(addr): return "ip address %s" % x - elif x.find(":") >= 0: + elif ipaddress.is_ipv6_address(addr): return "ipv6 address %s" % x else: raise ValueError("invalid address: %s", x) @@ -328,8 +329,9 @@ class FrrService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @@ -411,7 +413,8 @@ class FRROspfv2(FrrService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") < 0: + addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): continue net = ipaddress.Ipv4Prefix(a) cfg += " network %s area 0\n" % net diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py index a610f1cc..0a6b1f92 100644 --- a/daemon/core/services/nrl.py +++ b/daemon/core/services/nrl.py @@ -4,6 +4,7 @@ nrl.py: defines services provided by NRL protolib tools hosted here: """ from core import utils +from core.nodes import ipaddress from core.nodes.ipaddress import Ipv4Prefix from core.services.coreservices import CoreService @@ -36,9 +37,9 @@ class NrlService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - addr = a.split("/")[0] - pre = Ipv4Prefix("%s/%s" % (addr, prefixlen)) + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + pre = Ipv4Prefix("%s/%s" % (a, prefixlen)) return str(pre) # raise ValueError, "no IPv4 address found" return "0.0.0.0/%s" % prefixlen diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index 5b5cf0ba..267bbcdd 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -109,9 +109,10 @@ class Zebra(CoreService): """ helper for mapping IP addresses to zebra config statements """ - if x.find(".") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv4_address(addr): return "ip address %s" % x - elif x.find(":") >= 0: + elif ipaddress.is_ipv6_address(addr): return "ipv6 address %s" % x else: raise ValueError("invalid address: %s", x) @@ -255,8 +256,9 @@ class QuaggaService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @@ -338,10 +340,10 @@ class Ospfv2(QuaggaService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") < 0: - continue - net = ipaddress.Ipv4Prefix(a) - cfg += " network %s area 0\n" % net + addr = a.split("/")[0] + if ipaddress.is_ipv4_address(addr): + net = ipaddress.Ipv4Prefix(a) + cfg += " network %s area 0\n" % net cfg += "!\n" return cfg diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index c837de53..d924abd7 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -4,6 +4,7 @@ sdn.py defines services to start Open vSwitch and the Ryu SDN Controller. import re +from core.nodes import ipaddress from core.services.coreservices import CoreService @@ -56,11 +57,12 @@ class OvsService(SdnService): # remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces # or assign them manually to rtr interfaces if zebra is not running for ifcaddr in ifc.addrlist: - if ifcaddr.find(".") >= 0: + addr = ifcaddr.split("/")[0] + if ipaddress.is_ipv4_address(addr): cfg += "ip addr del %s dev %s\n" % (ifcaddr, ifc.name) if has_zebra == 0: cfg += "ip addr add %s dev rtr%s\n" % (ifcaddr, ifnum) - elif ifcaddr.find(":") >= 0: + elif ipaddress.is_ipv6_address(addr): cfg += "ip -6 addr del %s dev %s\n" % (ifcaddr, ifc.name) if has_zebra == 0: cfg += "ip -6 addr add %s dev rtr%s\n" % (ifcaddr, ifnum) diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index e408b182..16dc6906 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -6,6 +6,7 @@ import os from core import constants, utils from core.errors import CoreCommandError +from core.nodes import ipaddress from core.nodes.ipaddress import Ipv4Prefix, Ipv6Prefix from core.services.coreservices import CoreService @@ -87,7 +88,8 @@ class DefaultRouteService(UtilService): @staticmethod def addrstr(x): - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): net = Ipv6Prefix(x) else: net = Ipv4Prefix(x) @@ -147,7 +149,8 @@ class StaticRouteService(UtilService): @staticmethod def routestr(x): - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): net = Ipv6Prefix(x) dst = "3ffe:4::/64" else: @@ -280,7 +283,8 @@ ddns-update-style none; Generate a subnet declaration block given an IPv4 prefix string for inclusion in the dhcpd3 config file. """ - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): return "" else: addr = x.split("/")[0] @@ -702,7 +706,8 @@ interface %s Generate a subnet declaration block given an IPv6 prefix string for inclusion in the RADVD config file. """ - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): net = Ipv6Prefix(x) return str(net) else: diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 1cd62620..3c1de852 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -4,6 +4,7 @@ xorp.py: defines routing services provided by the XORP routing suite. import logging +from core.nodes import ipaddress from core.services.coreservices import CoreService @@ -150,8 +151,9 @@ class XorpService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @@ -187,9 +189,9 @@ class XorpOspfv2(XorpService): cfg += "\t interface %s {\n" % ifc.name cfg += "\t\tvif %s {\n" % ifc.name for a in ifc.addrlist: - if a.find(".") < 0: - continue addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): + continue cfg += "\t\t address %s {\n" % addr cfg += "\t\t }\n" cfg += "\t\t}\n" @@ -280,9 +282,9 @@ class XorpRip(XorpService): cfg += "\tinterface %s {\n" % ifc.name cfg += "\t vif %s {\n" % ifc.name for a in ifc.addrlist: - if a.find(".") < 0: - continue addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): + continue cfg += "\t\taddress %s {\n" % addr cfg += "\t\t disable: false\n" cfg += "\t\t}\n" @@ -462,9 +464,9 @@ class XorpOlsr(XorpService): cfg += "\tinterface %s {\n" % ifc.name cfg += "\t vif %s {\n" % ifc.name for a in ifc.addrlist: - if a.find(".") < 0: - continue addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): + continue cfg += "\t\taddress %s {\n" % addr cfg += "\t\t}\n" cfg += "\t }\n" diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index dcca6b80..285b7a3b 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -745,8 +745,8 @@ class CoreXmlReader: position_element = device_element.find("position") if position_element is not None: - x = get_int(position_element, "x") - y = get_int(position_element, "y") + x = get_float(position_element, "x") + y = get_float(position_element, "y") if all([x, y]): options.set_position(x, y) @@ -767,8 +767,8 @@ class CoreXmlReader: position_element = network_element.find("position") if position_element is not None: - x = get_int(position_element, "x") - y = get_int(position_element, "y") + x = get_float(position_element, "x") + y = get_float(position_element, "y") if all([x, y]): options.set_position(x, y) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 19a1ff97..050d5e75 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -89,6 +89,8 @@ service CoreApi { } rpc SetServiceDefaults (SetServiceDefaultsRequest) returns (SetServiceDefaultsResponse) { } + rpc GetNodeServiceConfigs (GetNodeServiceConfigsRequest) returns (GetNodeServiceConfigsResponse) { + } rpc GetNodeService (GetNodeServiceRequest) returns (GetNodeServiceResponse) { } rpc GetNodeServiceFile (GetNodeServiceFileRequest) returns (GetNodeServiceFileResponse) { @@ -101,6 +103,8 @@ service CoreApi { } // wlan rpc + rpc GetWlanConfigs (GetWlanConfigsRequest) returns (GetWlanConfigsResponse) { + } rpc GetWlanConfig (GetWlanConfigRequest) returns (GetWlanConfigResponse) { } rpc SetWlanConfig (SetWlanConfigRequest) returns (SetWlanConfigResponse) { @@ -144,6 +148,9 @@ message StartSessionRequest { repeated WlanConfig wlan_configs = 7; repeated EmaneModelConfig emane_model_configs = 8; repeated MobilityConfig mobility_configs = 9; + repeated ServiceConfig service_configs = 10; + repeated ServiceFileConfig service_file_configs = 11; + repeated Link asymmetric_links = 12; } message StartSessionResponse { @@ -262,14 +269,17 @@ message AddSessionServerResponse { message EventsRequest { int32 session_id = 1; + repeated EventType.Enum events = 2; } message ThroughputsRequest { + int32 session_id = 1; } message ThroughputsEvent { - repeated BridgeThroughput bridge_throughputs = 1; - repeated InterfaceThroughput interface_throughputs = 2; + int32 session_id = 1; + repeated BridgeThroughput bridge_throughputs = 2; + repeated InterfaceThroughput interface_throughputs = 3; } message InterfaceThroughput { @@ -292,10 +302,12 @@ message Event { ExceptionEvent exception_event = 5; FileEvent file_event = 6; } + int32 session_id = 7; } message NodeEvent { Node node = 1; + string source = 2; } message LinkEvent { @@ -309,7 +321,6 @@ message SessionEvent { string name = 3; string data = 4; float time = 5; - int32 session_id = 6; } message ConfigEvent { @@ -323,20 +334,18 @@ message ConfigEvent { string bitmap = 8; string possible_values = 9; string groups = 10; - int32 session_id = 11; - int32 interface = 12; - int32 network_id = 13; - string opaque = 14; + int32 interface = 11; + int32 network_id = 12; + string opaque = 13; } message ExceptionEvent { int32 node_id = 1; - int32 session_id = 2; - ExceptionLevel.Enum level = 3; - string source = 4; - string date = 5; - string text = 6; - string opaque = 7; + ExceptionLevel.Enum level = 2; + string source = 3; + string date = 4; + string text = 5; + string opaque = 6; } message FileEvent { @@ -347,9 +356,8 @@ message FileEvent { int32 number = 5; string type = 6; string source = 7; - int32 session_id = 8; - string data = 9; - string compressed_data = 10; + string data = 8; + string compressed_data = 9; } message AddNodeRequest { @@ -376,6 +384,7 @@ message EditNodeRequest { int32 node_id = 2; Position position = 3; string icon = 4; + string source = 5; } message EditNodeResponse { @@ -531,6 +540,20 @@ message SetServiceDefaultsResponse { bool result = 1; } +message GetNodeServiceConfigsRequest { + int32 session_id = 1; +} + +message GetNodeServiceConfigsResponse { + message ServiceConfig { + int32 node_id = 1; + string service = 2; + NodeServiceData data = 3; + map files = 4; + } + repeated ServiceConfig configs = 1; +} + message GetNodeServiceRequest { int32 session_id = 1; int32 node_id = 2; @@ -554,11 +577,7 @@ message GetNodeServiceFileResponse { message SetNodeServiceRequest { int32 session_id = 1; - int32 node_id = 2; - string service = 3; - repeated string startup = 4; - repeated string validate = 5; - repeated string shutdown = 6; + ServiceConfig config = 2; } message SetNodeServiceResponse { @@ -567,10 +586,7 @@ message SetNodeServiceResponse { message SetNodeServiceFileRequest { int32 session_id = 1; - int32 node_id = 2; - string service = 3; - string file = 4; - string data = 5; + ServiceFileConfig config = 2; } message SetNodeServiceFileResponse { @@ -588,6 +604,14 @@ message ServiceActionResponse { bool result = 1; } +message GetWlanConfigsRequest { + int32 session_id = 1; +} + +message GetWlanConfigsResponse { + map configs = 1; +} + message GetWlanConfigRequest { int32 session_id = 1; int32 node_id = 2; @@ -657,10 +681,12 @@ message GetEmaneModelConfigsRequest { message GetEmaneModelConfigsResponse { message ModelConfig { - string model = 1; - map config = 2; + int32 node_id = 1; + string model = 2; + int32 interface = 3; + map config = 4; } - map configs = 1; + repeated ModelConfig configs = 1; } message SaveXmlRequest { @@ -718,6 +744,32 @@ message EmaneModelConfig { map config = 4; } +message ServiceConfig { + int32 node_id = 1; + string service = 2; + repeated string startup = 3; + repeated string validate = 4; + repeated string shutdown = 5; +} + +message ServiceFileConfig { + int32 node_id = 1; + string service = 2; + string file = 3; + string data = 4; +} + +message EventType { + enum Enum { + SESSION = 0; + NODE = 1; + LINK = 2; + CONFIG = 3; + EXCEPTION = 4; + FILE = 5; + } +} + message MessageType { enum Enum { NONE = 0; @@ -768,6 +820,23 @@ message NodeType { } } +message ConfigOptionType { + enum Enum { + NONE = 0; + UINT8 = 1; + UINT16 = 2; + UINT32 = 3; + UINT64 = 4; + INT8 = 5; + INT16 = 6; + INT32 = 7; + INT64 = 8; + FLOAT = 9; + STRING = 10; + BOOL = 11; + } +} + message ServiceValidationMode { enum Enum { BLOCKING = 0; @@ -920,9 +989,9 @@ message SessionLocation { } message Position { - int32 x = 1; - int32 y = 2; - int32 z = 3; + float x = 1; + float y = 2; + float z = 3; float lat = 4; float lon = 5; float alt = 6; diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 62ff3a22..89e46f9b 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -27,7 +27,6 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - nodes = [] position = core_pb2.Position(x=50, y=100) node_one = core_pb2.Node(id=1, position=position, model="PC") position = core_pb2.Position(x=100, y=100) @@ -36,8 +35,7 @@ class TestGrpc: wlan_node = core_pb2.Node( id=3, type=NodeTypes.WIRELESS_LAN.value, position=position ) - nodes.extend([node_one, node_two, wlan_node]) - links = [] + nodes = [node_one, node_two, wlan_node] interface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") interface_one = interface_helper.create_interface(node_one.id, 0) interface_two = interface_helper.create_interface(node_two.id, 0) @@ -48,12 +46,11 @@ class TestGrpc: interface_one=interface_one, interface_two=interface_two, ) - links.append(link) - hooks = [] + links = [link] hook = core_pb2.Hook( state=core_pb2.SessionState.RUNTIME, file="echo.sh", data="echo hello" ) - hooks.append(hook) + hooks = [hook] location_x = 5 location_y = 10 location_z = 15 @@ -73,7 +70,6 @@ class TestGrpc: emane_config_key = "platform_id_start" emane_config_value = "2" emane_config = {emane_config_key: emane_config_value} - model_configs = [] model_node_id = 20 model_config_key = "bandwidth" model_config_value = "500000" @@ -83,21 +79,30 @@ class TestGrpc: model=EmaneIeee80211abgModel.name, config={model_config_key: model_config_value}, ) - model_configs.append(model_config) - wlan_configs = [] + model_configs = [model_config] wlan_config_key = "range" wlan_config_value = "333" wlan_config = core_pb2.WlanConfig( node_id=wlan_node.id, config={wlan_config_key: wlan_config_value} ) - wlan_configs.append(wlan_config) + wlan_configs = [wlan_config] mobility_config_key = "refresh_ms" mobility_config_value = "60" - mobility_configs = [] mobility_config = core_pb2.MobilityConfig( node_id=wlan_node.id, config={mobility_config_key: mobility_config_value} ) - mobility_configs.append(mobility_config) + mobility_configs = [mobility_config] + service_config = core_pb2.ServiceConfig( + node_id=node_one.id, service="DefaultRoute", validate=["echo hello"] + ) + service_configs = [service_config] + service_file_config = core_pb2.ServiceFileConfig( + node_id=node_one.id, + service="DefaultRoute", + file="defaultroute.sh", + data="echo hello", + ) + service_file_configs = [service_file_config] # when with patch.object(CoreXmlWriter, "write"): @@ -112,6 +117,8 @@ class TestGrpc: model_configs, wlan_configs, mobility_configs, + service_configs, + service_file_configs, ) # then @@ -139,6 +146,14 @@ class TestGrpc: model_node_id, EmaneIeee80211abgModel.name ) assert set_model_config[model_config_key] == model_config_value + service = session.services.get_service( + node_one.id, service_config.service, default_service=True + ) + assert service.validate == tuple(service_config.validate) + service_file = session.services.get_service_file( + node_one, service_file_config.service, service_file_config.file + ) + assert service_file.data == service_file_config.data @pytest.mark.parametrize("session_id", [None, 6013]) def test_create_session(self, grpc_server, session_id): @@ -677,7 +692,9 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - emane_network = session.add_node(_type=NodeTypes.EMANE) + options = NodeOptions() + options.emane = EmaneIeee80211abgModel.name + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "platform_id_start" config_value = "2" @@ -691,17 +708,20 @@ class TestGrpc: # then assert len(response.configs) == 1 - assert emane_network.id in response.configs - model_config = response.configs[emane_network.id] + model_config = response.configs[0] + assert emane_network.id == model_config.node_id assert model_config.model == EmaneIeee80211abgModel.name assert len(model_config.config) > 0 + assert model_config.interface == -1 def test_set_emane_model_config(self, grpc_server): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - emane_network = session.add_node(_type=NodeTypes.EMANE) + options = NodeOptions() + options.emane = EmaneIeee80211abgModel.name + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "bandwidth" config_value = "900000" @@ -727,7 +747,9 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - emane_network = session.add_node(_type=NodeTypes.EMANE) + options = NodeOptions() + options.emane = EmaneIeee80211abgModel.name + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) # then @@ -856,6 +878,24 @@ class TestGrpc: assert response.result is True assert session.services.default_services[node_type] == services + def test_get_node_service_configs(self, grpc_server): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + node = session.add_node() + service_name = "DefaultRoute" + session.services.set_service(node.id, service_name) + + # then + with client.context_connect(): + response = client.get_node_service_configs(session.id) + + # then + assert len(response.configs) == 1 + service_config = response.configs[0] + assert service_config.node_id == node.id + assert service_config.service == service_name + def test_get_node_service(self, grpc_server): # given client = CoreGrpcClient() @@ -950,6 +990,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("node_event") queue.put(event_data) @@ -974,6 +1015,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("link_event") queue.put(event_data) @@ -992,15 +1034,16 @@ class TestGrpc: # given client = CoreGrpcClient() - grpc_server.coreemu.create_session() + session = grpc_server.coreemu.create_session() queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id queue.put(event_data) # then with client.context_connect(): - client.throughputs(handle_event) + client.throughputs(session.id, handle_event) time.sleep(0.1) # then @@ -1013,6 +1056,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("session_event") queue.put(event_data) @@ -1021,7 +1065,7 @@ class TestGrpc: client.events(session.id, handle_event) time.sleep(0.1) event = EventData( - event_type=EventTypes.RUNTIME_STATE.value, time=str(time.time()) + event_type=EventTypes.RUNTIME_STATE.value, time=str(time.monotonic()) ) session.broadcast_event(event) @@ -1035,6 +1079,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("config_event") queue.put(event_data) @@ -1058,6 +1103,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("exception_event") queue.put(event_data) @@ -1065,7 +1111,9 @@ class TestGrpc: with client.context_connect(): client.events(session.id, handle_event) time.sleep(0.1) - session.exception(ExceptionLevels.FATAL, "test", None, "exception message") + session.exception( + ExceptionLevels.FATAL.value, "test", None, "exception message" + ) # then queue.get(timeout=5) @@ -1078,6 +1126,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("file_event") queue.put(event_data) diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index c2a8c9fc..91324d08 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -523,7 +523,7 @@ class TestGui: MessageFlags.ADD.value, [ (EventTlvs.TYPE, EventTypes.SCHEDULED.value), - (EventTlvs.TIME, str(time.time() + 100)), + (EventTlvs.TIME, str(time.monotonic() + 100)), (EventTlvs.NODE, node.id), (EventTlvs.NAME, "event"), (EventTlvs.DATA, "data"), diff --git a/docs/install.md b/docs/install.md index 12242b4b..e10c1450 100644 --- a/docs/install.md +++ b/docs/install.md @@ -68,7 +68,7 @@ Virtual networks generally require some form of routing in order to work (e.g. t tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by default when the blue router node type is used. -* [OSPF MANET Designated Routers](http://www.nrl.navy.mil/itd/ncs/products/ospf-manet) (MDR) - the Quagga routing +* [OSPF MANET Designated Routers](https://github.com/USNavalResearchLaboratory/ospf-mdr) (MDR) - the Quagga routing suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type (and the MDR service) requires this variant of Quagga. @@ -77,7 +77,7 @@ suite with a modified version of OSPFv3, optimized for use with mobile wireless There is a built package which can be used. ```shell -wget https://downloads.pf.itd.nrl.navy.mil/ospf-manet/quagga-0.99.21mr2.2/quagga-mr_0.99.21mr2.2_amd64.deb +wget https://github.com/USNavalResearchLaboratory/ospf-mdr/releases/download/v0.99.21mr2.2/quagga-mr_0.99.21mr2.2_amd64.deb sudo dpkg -i quagga-mr_0.99.21mr2.2_amd64.deb ``` @@ -89,9 +89,8 @@ Requires building from source, from the latest nightly snapshot. # packages needed beyond what's normally required to build core on ubuntu sudo apt install libtool libreadline-dev autoconf -wget https://downloads.pf.itd.nrl.navy.mil/ospf-manet/nightly_snapshots/quagga-svnsnap.tgz -tar xzf quagga-svnsnap.tgz -cd quagga +git clone https://github.com/USNavalResearchLaboratory/ospf-mdr +cd ospf-mdr ./bootstrap.sh ./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ diff --git a/docs/services.md b/docs/services.md index f2a1a38a..692bcedd 100644 --- a/docs/services.md +++ b/docs/services.md @@ -312,17 +312,17 @@ Currently the Naval Research Laboratory uses this library to develop a wide vari * arouted #### NRL Installation -In order to be able to use the different protocols that NRL offers, you must first download the support library itself. You can get the source code from their [official nightly snapshots website](https://downloads.pf.itd.nrl.navy.mil/protolib/nightly_snapshots/). +In order to be able to use the different protocols that NRL offers, you must first download the support library itself. You can get the source code from their [NRL Protolib Repo](https://github.com/USNavalResearchLaboratory/protolib). #### Multi-Generator (MGEN) -Download MGEN from the [NRL MGEN nightly snapshots](https://downloads.pf.itd.nrl.navy.mil/mgen/nightly_snapshots/), unpack it and copy the protolib library into the main folder *mgen*. Execute the following commands to build the protocol. +Download MGEN from the [NRL MGEN Repo](https://github.com/USNavalResearchLaboratory/mgen), unpack it and copy the protolib library into the main folder *mgen*. Execute the following commands to build the protocol. ```shell cd mgen/makefiles make -f Makefile.{os} mgen ``` #### Neighborhood Discovery Protocol (NHDP) -Download NHDP from the [NRL NHDP nightly snapshots](https://downloads.pf.itd.nrl.navy.mil/nhdp/nightly_snapshots/). +Download NHDP from the [NRL NHDP Repo](https://github.com/USNavalResearchLaboratory/NCS-Downloads/tree/master/nhdp). ```shell sudo apt-get install libpcap-dev libboost-all-dev wget https://github.com/protocolbuffers/protobuf/releases/download/v3.8.0/protoc-3.8.0-linux-x86_64.zip @@ -339,14 +339,14 @@ make -f Makefile.{os} ``` #### Simplified Multicast Forwarding (SMF) -Download SMF from the [NRL SMF nightly snapshot](https://downloads.pf.itd.nrl.navy.mil/smf/nightly_snapshots/) , unpack it and place the protolib library inside the *smf* main folder. +Download SMF from the [NRL SMF Repo](https://github.com/USNavalResearchLaboratory/nrlsmf) , unpack it and place the protolib library inside the *smf* main folder. ```shell cd mgen/makefiles make -f Makefile.{os} ``` #### Optimized Link State Routing Protocol (OLSR) -To install the OLSR protocol, download their source code from their [nightly snapshots](https://downloads.pf.itd.nrl.navy.mil/olsr/nightly_snapshots/nrlolsr-svnsnap.tgz). Unpack it and place the previously downloaded protolib library inside the *nrlolsr* main directory. Then execute the following commands: +To install the OLSR protocol, download their source code from their [NRL OLSR Repo](https://github.com/USNavalResearchLaboratory/nrlolsr). Unpack it and place the previously downloaded protolib library inside the *nrlolsr* main directory. Then execute the following commands: ```shell cd ./unix make -f Makefile.{os} diff --git a/docs/usage.md b/docs/usage.md index 7f8cf672..02e1295f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -650,7 +650,7 @@ Any time you edit the topology file, you will need to stop the emulation if it were running and reload the file. -The **.xml** [file schema is specified by NRL](http://www.nrl.navy.mil/itd/ncs/products/mnmtools) and there are two versions to date: +The **.xml** [file schema is specified by NRL](https://github.com/USNavalResearchLaboratory/NCS-Downloads/blob/master/mnmtools/EmulationScriptSchemaDescription.pdf) and there are two versions to date: version 0.0 and version 1.0, with 1.0 as the current default. CORE can open either XML version. However, the xmlfilever line in **/etc/core/core.conf** controls the version of the XML file diff --git a/output.txt b/output.txt new file mode 100644 index 00000000..ee11dc37 --- /dev/null +++ b/output.txt @@ -0,0 +1,23 @@ +./netns/vcmdmodule.c:static PyObject *VCmd_popen(VCmd *self, PyObject *args, PyObject *kwds) +./netns/vcmdmodule.c: {"popen", (PyCFunction)VCmd_popen, METH_VARARGS | METH_KEYWORDS, +./netns/vcmdmodule.c: "popen(args...) -> (VCmdWait, cmdin, cmdout, cmderr)\n\n" +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: def popen(self, args): +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: Execute a popen command against the node. +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: :return: popen object, stdin, stdout, and stderr +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: logging.debug("popen: %s", cmd) +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/tests/test_core.py: p, stdin, stdout, stderr = client.popen(command) +./daemon/tests/test_core.py: p, stdin, stdout, stderr = client.popen(command) +./daemon/examples/netns/ospfmanetmdrtest.py: """ Exceute call to node.popen(). """ +./daemon/examples/netns/ospfmanetmdrtest.py: self.id, self.stdin, self.out, self.err = self.node.client.popen(self.args) +./daemon/examples/netns/ospfmanetmdrtest.py: self.id, self.stdin, self.out, self.err = self.node.client.popen(args) +./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/core/nodes/client.py: def popen(self, args): +./daemon/core/nodes/client.py: Execute a popen command against the node. +./daemon/core/nodes/client.py: :return: popen object, stdin, stdout, and stderr +./daemon/core/nodes/client.py: logging.debug("popen: %s", cmd) +./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./corefx/src/main/resources/js/leaflet.js:!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i(t.L={})}(this,function(t){"use strict";function i(t){var i,e,n,o;for(e=1,n=arguments.length;e=0}function B(t,i,e,n){return"touchstart"===i?O(t,e,n):"touchmove"===i?W(t,e,n):"touchend"===i&&H(t,e,n),this}function I(t,i,e){var n=t["_leaflet_"+i+e];return"touchstart"===i?t.removeEventListener(te,n,!1):"touchmove"===i?t.removeEventListener(ie,n,!1):"touchend"===i&&(t.removeEventListener(ee,n,!1),t.removeEventListener(ne,n,!1)),this}function O(t,i,n){var o=e(function(t){if("mouse"!==t.pointerType&&t.MSPOINTER_TYPE_MOUSE&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE){if(!(oe.indexOf(t.target.tagName)<0))return;Pt(t)}j(t,i)});t["_leaflet_touchstart"+n]=o,t.addEventListener(te,o,!1),re||(document.documentElement.addEventListener(te,R,!0),document.documentElement.addEventListener(ie,N,!0),document.documentElement.addEventListener(ee,D,!0),document.documentElement.addEventListener(ne,D,!0),re=!0)}function R(t){se[t.pointerId]=t,ae++}function N(t){se[t.pointerId]&&(se[t.pointerId]=t)}function D(t){delete se[t.pointerId],ae--}function j(t,i){t.touches=[];for(var e in se)t.touches.push(se[e]);t.changedTouches=[t],i(t)}function W(t,i,e){var n=function(t){(t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&"mouse"!==t.pointerType||0!==t.buttons)&&j(t,i)};t["_leaflet_touchmove"+e]=n,t.addEventListener(ie,n,!1)}function H(t,i,e){var n=function(t){j(t,i)};t["_leaflet_touchend"+e]=n,t.addEventListener(ee,n,!1),t.addEventListener(ne,n,!1)}function F(t,i,e){function n(t){var i;if(Vi){if(!bi||"mouse"===t.pointerType)return;i=ae}else i=t.touches.length;if(!(i>1)){var e=Date.now(),n=e-(s||e);r=t.touches?t.touches[0]:t,a=n>0&&n<=h,s=e}}function o(t){if(a&&!r.cancelBubble){if(Vi){if(!bi||"mouse"===t.pointerType)return;var e,n,o={};for(n in r)e=r[n],o[n]=e&&e.bind?e.bind(r):e;r=o}r.type="dblclick",i(r),s=null}}var s,r,a=!1,h=250;return t[le+he+e]=n,t[le+ue+e]=o,t[le+"dblclick"+e]=i,t.addEventListener(he,n,!1),t.addEventListener(ue,o,!1),t.addEventListener("dblclick",i,!1),this}function U(t,i){var e=t[le+he+i],n=t[le+ue+i],o=t[le+"dblclick"+i];return t.removeEventListener(he,e,!1),t.removeEventListener(ue,n,!1),bi||t.removeEventListener("dblclick",o,!1),this}function V(t){return"string"==typeof t?document.getElementById(t):t}function q(t,i){var e=t.style[i]||t.currentStyle&&t.currentStyle[i];if((!e||"auto"===e)&&document.defaultView){var n=document.defaultView.getComputedStyle(t,null);e=n?n[i]:null}return"auto"===e?null:e}function G(t,i,e){var n=document.createElement(t);return n.className=i||"",e&&e.appendChild(n),n}function K(t){var i=t.parentNode;i&&i.removeChild(t)}function Y(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function X(t){var i=t.parentNode;i.lastChild!==t&&i.appendChild(t)}function J(t){var i=t.parentNode;i.firstChild!==t&&i.insertBefore(t,i.firstChild)}function $(t,i){if(void 0!==t.classList)return t.classList.contains(i);var e=et(t);return e.length>0&&new RegExp("(^|\\s)"+i+"(\\s|$)").test(e)}function Q(t,i){if(void 0!==t.classList)for(var e=u(i),n=0,o=e.length;n100&&n<500||t.target._simulatedClick&&!t._simulated?Lt(t):(ge=e,i(t))}function Zt(t,i){if(!i||!t.length)return t.slice();var e=i*i;return t=Bt(t,e),t=kt(t,e)}function Et(t,i,e){return Math.sqrt(Dt(t,i,e,!0))}function kt(t,i){var e=t.length,n=new(typeof Uint8Array!=void 0+""?Uint8Array:Array)(e);n[0]=n[e-1]=1,At(t,n,i,0,e-1);var o,s=[];for(o=0;oh&&(s=r,h=a);h>e&&(i[s]=1,At(t,i,e,n,s),At(t,i,e,s,o))}function Bt(t,i){for(var e=[t[0]],n=1,o=0,s=t.length;ni&&(e.push(t[n]),o=n);return oi.max.x&&(e|=2),t.yi.max.y&&(e|=8),e}function Nt(t,i){var e=i.x-t.x,n=i.y-t.y;return e*e+n*n}function Dt(t,i,e,n){var o,s=i.x,r=i.y,a=e.x-s,h=e.y-r,u=a*a+h*h;return u>0&&((o=((t.x-s)*a+(t.y-r)*h)/u)>1?(s=e.x,r=e.y):o>0&&(s+=a*o,r+=h*o)),a=t.x-s,h=t.y-r,n?a*a+h*h:new x(s,r)}function jt(t){return!oi(t[0])||"object"!=typeof t[0][0]&&void 0!==t[0][0]}function Wt(t){return console.warn("Deprecated use of _flat, please use L.LineUtil.isFlat instead."),jt(t)}function Ht(t,i,e){var n,o,s,r,a,h,u,l,c,_=[1,4,2,8];for(o=0,u=t.length;o0?Math.floor(t):Math.ceil(t)};x.prototype={clone:function(){return new x(this.x,this.y)},add:function(t){return this.clone()._add(w(t))},_add:function(t){return this.x+=t.x,this.y+=t.y,this},subtract:function(t){return this.clone()._subtract(w(t))},_subtract:function(t){return this.x-=t.x,this.y-=t.y,this},divideBy:function(t){return this.clone()._divideBy(t)},_divideBy:function(t){return this.x/=t,this.y/=t,this},multiplyBy:function(t){return this.clone()._multiplyBy(t)},_multiplyBy:function(t){return this.x*=t,this.y*=t,this},scaleBy:function(t){return new x(this.x*t.x,this.y*t.y)},unscaleBy:function(t){return new x(this.x/t.x,this.y/t.y)},round:function(){return this.clone()._round()},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},floor:function(){return this.clone()._floor()},_floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},ceil:function(){return this.clone()._ceil()},_ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this},trunc:function(){return this.clone()._trunc()},_trunc:function(){return this.x=_i(this.x),this.y=_i(this.y),this},distanceTo:function(t){var i=(t=w(t)).x-this.x,e=t.y-this.y;return Math.sqrt(i*i+e*e)},equals:function(t){return(t=w(t)).x===this.x&&t.y===this.y},contains:function(t){return t=w(t),Math.abs(t.x)<=Math.abs(this.x)&&Math.abs(t.y)<=Math.abs(this.y)},toString:function(){return"Point("+a(this.x)+", "+a(this.y)+")"}},P.prototype={extend:function(t){return t=w(t),this.min||this.max?(this.min.x=Math.min(t.x,this.min.x),this.max.x=Math.max(t.x,this.max.x),this.min.y=Math.min(t.y,this.min.y),this.max.y=Math.max(t.y,this.max.y)):(this.min=t.clone(),this.max=t.clone()),this},getCenter:function(t){return new x((this.min.x+this.max.x)/2,(this.min.y+this.max.y)/2,t)},getBottomLeft:function(){return new x(this.min.x,this.max.y)},getTopRight:function(){return new x(this.max.x,this.min.y)},getTopLeft:function(){return this.min},getBottomRight:function(){return this.max},getSize:function(){return this.max.subtract(this.min)},contains:function(t){var i,e;return(t="number"==typeof t[0]||t instanceof x?w(t):b(t))instanceof P?(i=t.min,e=t.max):i=e=t,i.x>=this.min.x&&e.x<=this.max.x&&i.y>=this.min.y&&e.y<=this.max.y},intersects:function(t){t=b(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>=i.x&&n.x<=e.x,r=o.y>=i.y&&n.y<=e.y;return s&&r},overlaps:function(t){t=b(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>i.x&&n.xi.y&&n.y=n.lat&&e.lat<=o.lat&&i.lng>=n.lng&&e.lng<=o.lng},intersects:function(t){t=z(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>=i.lat&&n.lat<=e.lat,r=o.lng>=i.lng&&n.lng<=e.lng;return s&&r},overlaps:function(t){t=z(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>i.lat&&n.lati.lng&&n.lng1,Xi=!!document.createElement("canvas").getContext,Ji=!(!document.createElementNS||!E("svg").createSVGRect),$i=!Ji&&function(){try{var t=document.createElement("div");t.innerHTML='';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(t){return!1}}(),Qi=(Object.freeze||Object)({ie:Pi,ielt9:Li,edge:bi,webkit:Ti,android:zi,android23:Mi,androidStock:Si,opera:Zi,chrome:Ei,gecko:ki,safari:Ai,phantom:Bi,opera12:Ii,win:Oi,ie3d:Ri,webkit3d:Ni,gecko3d:Di,any3d:ji,mobile:Wi,mobileWebkit:Hi,mobileWebkit3d:Fi,msPointer:Ui,pointer:Vi,touch:qi,mobileOpera:Gi,mobileGecko:Ki,retina:Yi,canvas:Xi,svg:Ji,vml:$i}),te=Ui?"MSPointerDown":"pointerdown",ie=Ui?"MSPointerMove":"pointermove",ee=Ui?"MSPointerUp":"pointerup",ne=Ui?"MSPointerCancel":"pointercancel",oe=["INPUT","SELECT","OPTION"],se={},re=!1,ae=0,he=Ui?"MSPointerDown":Vi?"pointerdown":"touchstart",ue=Ui?"MSPointerUp":Vi?"pointerup":"touchend",le="_leaflet_",ce=st(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),_e=st(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===_e||"OTransition"===_e?_e+"End":"transitionend";if("onselectstart"in document)fi=function(){mt(window,"selectstart",Pt)},gi=function(){ft(window,"selectstart",Pt)};else{var pe=st(["userSelect","WebkitUserSelect","OUserSelect","MozUserSelect","msUserSelect"]);fi=function(){if(pe){var t=document.documentElement.style;vi=t[pe],t[pe]="none"}},gi=function(){pe&&(document.documentElement.style[pe]=vi,vi=void 0)}}var me,fe,ge,ve=(Object.freeze||Object)({TRANSFORM:ce,TRANSITION:_e,TRANSITION_END:de,get:V,getStyle:q,create:G,remove:K,empty:Y,toFront:X,toBack:J,hasClass:$,addClass:Q,removeClass:tt,setClass:it,getClass:et,setOpacity:nt,testProp:st,setTransform:rt,setPosition:at,getPosition:ht,disableTextSelection:fi,enableTextSelection:gi,disableImageDrag:ut,enableImageDrag:lt,preventOutline:ct,restoreOutline:_t,getSizedParentNode:dt,getScale:pt}),ye="_leaflet_events",xe=Oi&&Ei?2*window.devicePixelRatio:ki?window.devicePixelRatio:1,we={},Pe=(Object.freeze||Object)({on:mt,off:ft,stopPropagation:yt,disableScrollPropagation:xt,disableClickPropagation:wt,preventDefault:Pt,stop:Lt,getMousePosition:bt,getWheelDelta:Tt,fakeStop:zt,skipped:Mt,isExternalTarget:Ct,addListener:mt,removeListener:ft}),Le=ci.extend({run:function(t,i,e,n){this.stop(),this._el=t,this._inProgress=!0,this._duration=e||.25,this._easeOutPower=1/Math.max(n||.5,.2),this._startPos=ht(t),this._offset=i.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(!0),this._complete())},_animate:function(){this._animId=f(this._animate,this),this._step()},_step:function(t){var i=+new Date-this._startTime,e=1e3*this._duration;ithis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,i){this._enforcingBounds=!0;var e=this.getCenter(),n=this._limitCenter(e,this._zoom,z(t));return e.equals(n)||this.panTo(n,i),this._enforcingBounds=!1,this},invalidateSize:function(t){if(!this._loaded)return this;t=i({animate:!1,pan:!0},!0===t?{animate:!0}:t);var n=this.getSize();this._sizeChanged=!0,this._lastCenter=null;var o=this.getSize(),s=n.divideBy(2).round(),r=o.divideBy(2).round(),a=s.subtract(r);return a.x||a.y?(t.animate&&t.pan?this.panBy(a):(t.pan&&this._rawPanBy(a),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(e(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:n,newSize:o})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){if(t=this._locateOptions=i({timeout:1e4,watch:!1},t),!("geolocation"in navigator))return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var n=e(this._handleGeolocationResponse,this),o=e(this._handleGeolocationError,this);return t.watch?this._locationWatchId=navigator.geolocation.watchPosition(n,o,t):navigator.geolocation.getCurrentPosition(n,o,t),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var i=t.code,e=t.message||(1===i?"permission denied":2===i?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:i,message:"Geolocation error: "+e+"."})},_handleGeolocationResponse:function(t){var i=new M(t.coords.latitude,t.coords.longitude),e=i.toBounds(2*t.coords.accuracy),n=this._locateOptions;if(n.setView){var o=this.getBoundsZoom(e);this.setView(i,n.maxZoom?Math.min(o,n.maxZoom):o)}var s={latlng:i,bounds:e,timestamp:t.timestamp};for(var r in t.coords)"number"==typeof t.coords[r]&&(s[r]=t.coords[r]);this.fire("locationfound",s)},addHandler:function(t,i){if(!i)return this;var e=this[t]=new i(this);return this._handlers.push(e),this.options[t]&&e.enable(),this},remove:function(){if(this._initEvents(!0),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),K(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(g(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload");var t;for(t in this._layers)this._layers[t].remove();for(t in this._panes)K(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,i){var e=G("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),i||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter:this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new T(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,i,e){t=z(t),e=w(e||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),a=t.getSouthEast(),h=this.getSize().subtract(e),u=b(this.project(a,n),this.project(r,n)).getSize(),l=ji?this.options.zoomSnap:1,c=h.x/u.x,_=h.y/u.y,d=i?Math.max(c,_):Math.min(c,_);return n=this.getScaleZoom(d,n),l&&(n=Math.round(n/(l/100))*(l/100),n=i?Math.ceil(n/l)*l:Math.floor(n/l)*l),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new x(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,i){var e=this._getTopLeftPoint(t,i);return new P(e,e.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,i){var e=this.options.crs;return i=void 0===i?this._zoom:i,e.scale(t)/e.scale(i)},getScaleZoom:function(t,i){var e=this.options.crs;i=void 0===i?this._zoom:i;var n=e.zoom(t*e.scale(i));return isNaN(n)?1/0:n},project:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.latLngToPoint(C(t),i)},unproject:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.pointToLatLng(w(t),i)},layerPointToLatLng:function(t){var i=w(t).add(this.getPixelOrigin());return this.unproject(i)},latLngToLayerPoint:function(t){return this.project(C(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(C(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(z(t))},distance:function(t,i){return this.options.crs.distance(C(t),C(i))},containerPointToLayerPoint:function(t){return w(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return w(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){var i=this.containerPointToLayerPoint(w(t));return this.layerPointToLatLng(i)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(C(t)))},mouseEventToContainerPoint:function(t){return bt(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){var i=this._container=V(t);if(!i)throw new Error("Map container not found.");if(i._leaflet_id)throw new Error("Map container is already initialized.");mt(i,"scroll",this._onScroll,this),this._containerId=n(i)},_initLayout:function(){var t=this._container;this._fadeAnimated=this.options.fadeAnimation&&ji,Q(t,"leaflet-container"+(qi?" leaflet-touch":"")+(Yi?" leaflet-retina":"")+(Li?" leaflet-oldie":"")+(Ai?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":""));var i=q(t,"position");"absolute"!==i&&"relative"!==i&&"fixed"!==i&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),at(this._mapPane,new x(0,0)),this.createPane("tilePane"),this.createPane("shadowPane"),this.createPane("overlayPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(Q(t.markerPane,"leaflet-zoom-hide"),Q(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,i){at(this._mapPane,new x(0,0));var e=!this._loaded;this._loaded=!0,i=this._limitZoom(i),this.fire("viewprereset");var n=this._zoom!==i;this._moveStart(n,!1)._move(t,i)._moveEnd(n),this.fire("viewreset"),e&&this.fire("load")},_moveStart:function(t,i){return t&&this.fire("zoomstart"),i||this.fire("movestart"),this},_move:function(t,i,e){void 0===i&&(i=this._zoom);var n=this._zoom!==i;return this._zoom=i,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),(n||e&&e.pinch)&&this.fire("zoom",e),this.fire("move",e)},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return g(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){at(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={},this._targets[n(this._container)]=this;var i=t?ft:mt;i(this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress",this._handleDOMEvent,this),this.options.trackResize&&i(window,"resize",this._onResize,this),ji&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){g(this._resizeRequest),this._resizeRequest=f(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,i){for(var e,o=[],s="mouseout"===i||"mouseover"===i,r=t.target||t.srcElement,a=!1;r;){if((e=this._targets[n(r)])&&("click"===i||"preclick"===i)&&!t._simulated&&this._draggableMoved(e)){a=!0;break}if(e&&e.listens(i,!0)){if(s&&!Ct(r,t))break;if(o.push(e),s)break}if(r===this._container)break;r=r.parentNode}return o.length||a||s||!Ct(r,t)||(o=[this]),o},_handleDOMEvent:function(t){if(this._loaded&&!Mt(t)){var i=t.type;"mousedown"!==i&&"keypress"!==i||ct(t.target||t.srcElement),this._fireDOMEvent(t,i)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,n){if("click"===t.type){var o=i({},t);o.type="preclick",this._fireDOMEvent(o,o.type,n)}if(!t._stopped&&(n=(n||[]).concat(this._findEventTargets(t,e))).length){var s=n[0];"contextmenu"===e&&s.listens(e,!0)&&Pt(t);var r={originalEvent:t};if("keypress"!==t.type){var a=s.getLatLng&&(!s._radius||s._radius<=10);r.containerPoint=a?this.latLngToContainerPoint(s.getLatLng()):this.mouseEventToContainerPoint(t),r.layerPoint=this.containerPointToLayerPoint(r.containerPoint),r.latlng=a?s.getLatLng():this.layerPointToLatLng(r.layerPoint)}for(var h=0;h0?Math.round(t-i)/2:Math.max(0,Math.ceil(t))-Math.max(0,Math.floor(i))},_limitZoom:function(t){var i=this.getMinZoom(),e=this.getMaxZoom(),n=ji?this.options.zoomSnap:1;return n&&(t=Math.round(t/n)*n),Math.max(i,Math.min(e,t))},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){tt(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(t,i){var e=this._getCenterOffset(t)._trunc();return!(!0!==(i&&i.animate)&&!this.getSize().contains(e))&&(this.panBy(e,i),!0)},_createAnimProxy:function(){var t=this._proxy=G("div","leaflet-proxy leaflet-zoom-animated");this._panes.mapPane.appendChild(t),this.on("zoomanim",function(t){var i=ce,e=this._proxy.style[i];rt(this._proxy,this.project(t.center,t.zoom),this.getZoomScale(t.zoom,1)),e===this._proxy.style[i]&&this._animatingZoom&&this._onZoomTransitionEnd()},this),this.on("load moveend",function(){var t=this.getCenter(),i=this.getZoom();rt(this._proxy,this.project(t,i),this.getZoomScale(i,1))},this),this._on("unload",this._destroyAnimProxy,this)},_destroyAnimProxy:function(){K(this._proxy),delete this._proxy},_catchTransitionEnd:function(t){this._animatingZoom&&t.propertyName.indexOf("transform")>=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(t,i,e){if(this._animatingZoom)return!0;if(e=e||{},!this._zoomAnimated||!1===e.animate||this._nothingToAnimate()||Math.abs(i-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(i),o=this._getCenterOffset(t)._divideBy(1-1/n);return!(!0!==e.animate&&!this.getSize().contains(o))&&(f(function(){this._moveStart(!0,!1)._animateZoom(t,i,!0)},this),!0)},_animateZoom:function(t,i,n,o){this._mapPane&&(n&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=i,Q(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:i,noUpdate:o}),setTimeout(e(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&tt(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),f(function(){this._moveEnd(!0)},this))}}),Te=v.extend({options:{position:"topright"},initialize:function(t){l(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var i=this._map;return i&&i.removeControl(this),this.options.position=t,i&&i.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var i=this._container=this.onAdd(t),e=this.getPosition(),n=t._controlCorners[e];return Q(i,"leaflet-control"),-1!==e.indexOf("bottom")?n.insertBefore(i,n.firstChild):n.appendChild(i),this},remove:function(){return this._map?(K(this._container),this.onRemove&&this.onRemove(this._map),this._map=null,this):this},_refocusOnMap:function(t){this._map&&t&&t.screenX>0&&t.screenY>0&&this._map.getContainer().focus()}}),ze=function(t){return new Te(t)};be.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.remove(),this},_initControlPos:function(){function t(t,o){var s=e+t+" "+e+o;i[t+o]=G("div",s,n)}var i=this._controlCorners={},e="leaflet-",n=this._controlContainer=G("div",e+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){for(var t in this._controlCorners)K(this._controlCorners[t]);K(this._controlContainer),delete this._controlCorners,delete this._controlContainer}});var Me=Te.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1,sortLayers:!1,sortFunction:function(t,i,e,n){return e1,this._baseLayersList.style.display=t?"":"none"),this._separator.style.display=i&&t?"":"none",this},_onLayerChange:function(t){this._handlingClick||this._update();var i=this._getLayer(n(t.target)),e=i.overlay?"add"===t.type?"overlayadd":"overlayremove":"add"===t.type?"baselayerchange":null;e&&this._map.fire(e,i)},_createRadioElement:function(t,i){var e='",n=document.createElement("div");return n.innerHTML=e,n.firstChild},_addItem:function(t){var i,e=document.createElement("label"),o=this._map.hasLayer(t.layer);t.overlay?((i=document.createElement("input")).type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=o):i=this._createRadioElement("leaflet-base-layers",o),this._layerControlInputs.push(i),i.layerId=n(t.layer),mt(i,"click",this._onInputClick,this);var s=document.createElement("span");s.innerHTML=" "+t.name;var r=document.createElement("div");return e.appendChild(r),r.appendChild(i),r.appendChild(s),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(e),this._checkDisabledLayers(),e},_onInputClick:function(){var t,i,e=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=e.length-1;s>=0;s--)t=e[s],i=this._getLayer(t.layerId).layer,t.checked?n.push(i):t.checked||o.push(i);for(s=0;s=0;o--)t=e[o],i=this._getLayer(t.layerId).layer,t.disabled=void 0!==i.options.minZoom&&ni.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),Ce=Te.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"−",zoomOutTitle:"Zoom out"},onAdd:function(t){var i="leaflet-control-zoom",e=G("div",i+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,i+"-in",e,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,i+"-out",e,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),e},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,i,e,n,o){var s=G("a",e,n);return s.innerHTML=t,s.href="#",s.title=i,s.setAttribute("role","button"),s.setAttribute("aria-label",i),wt(s),mt(s,"click",Lt),mt(s,"click",o,this),mt(s,"click",this._refocusOnMap,this),s},_updateDisabled:function(){var t=this._map,i="leaflet-disabled";tt(this._zoomInButton,i),tt(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMinZoom())&&Q(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMaxZoom())&&Q(this._zoomInButton,i)}});be.mergeOptions({zoomControl:!0}),be.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new Ce,this.addControl(this.zoomControl))});var Se=Te.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var i=G("div","leaflet-control-scale"),e=this.options;return this._addScales(e,"leaflet-control-scale-line",i),t.on(e.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,i,e){t.metric&&(this._mScale=G("div",i,e)),t.imperial&&(this._iScale=G("div",i,e))},_update:function(){var t=this._map,i=t.getSize().y/2,e=t.distance(t.containerPointToLatLng([0,i]),t.containerPointToLatLng([this.options.maxWidth,i]));this._updateScales(e)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var i=this._getRoundNum(t),e=i<1e3?i+" m":i/1e3+" km";this._updateScale(this._mScale,e,i/t)},_updateImperial:function(t){var i,e,n,o=3.2808399*t;o>5280?(i=o/5280,e=this._getRoundNum(i),this._updateScale(this._iScale,e+" mi",e/i)):(n=this._getRoundNum(o),this._updateScale(this._iScale,n+" ft",n/o))},_updateScale:function(t,i,e){t.style.width=Math.round(this.options.maxWidth*e)+"px",t.innerHTML=i},_getRoundNum:function(t){var i=Math.pow(10,(Math.floor(t)+"").length-1),e=t/i;return e=e>=10?10:e>=5?5:e>=3?3:e>=2?2:1,i*e}}),Ze=Te.extend({options:{position:"bottomright",prefix:'Leaflet'},initialize:function(t){l(this,t),this._attributions={}},onAdd:function(t){t.attributionControl=this,this._container=G("div","leaflet-control-attribution"),wt(this._container);for(var i in t._layers)t._layers[i].getAttribution&&this.addAttribution(t._layers[i].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t?(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update(),this):this},removeAttribution:function(t){return t?(this._attributions[t]&&(this._attributions[t]--,this._update()),this):this},_update:function(){if(this._map){var t=[];for(var i in this._attributions)this._attributions[i]&&t.push(i);var e=[];this.options.prefix&&e.push(this.options.prefix),t.length&&e.push(t.join(", ")),this._container.innerHTML=e.join(" | ")}}});be.mergeOptions({attributionControl:!0}),be.addInitHook(function(){this.options.attributionControl&&(new Ze).addTo(this)});Te.Layers=Me,Te.Zoom=Ce,Te.Scale=Se,Te.Attribution=Ze,ze.layers=function(t,i,e){return new Me(t,i,e)},ze.zoom=function(t){return new Ce(t)},ze.scale=function(t){return new Se(t)},ze.attribution=function(t){return new Ze(t)};var Ee=v.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled?this:(this._enabled=!0,this.addHooks(),this)},disable:function(){return this._enabled?(this._enabled=!1,this.removeHooks(),this):this},enabled:function(){return!!this._enabled}});Ee.addTo=function(t,i){return t.addHandler(i,this),this};var ke,Ae={Events:li},Be=qi?"touchstart mousedown":"mousedown",Ie={mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},Oe={mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"},Re=ci.extend({options:{clickTolerance:3},initialize:function(t,i,e,n){l(this,n),this._element=t,this._dragStartTarget=i||t,this._preventOutline=e},enable:function(){this._enabled||(mt(this._dragStartTarget,Be,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Re._dragging===this&&this.finishDrag(),ft(this._dragStartTarget,Be,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){if(!t._simulated&&this._enabled&&(this._moved=!1,!$(this._element,"leaflet-zoom-anim")&&!(Re._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||(Re._dragging=this,this._preventOutline&&ct(this._element),ut(),fi(),this._moving)))){this.fire("down");var i=t.touches?t.touches[0]:t,e=dt(this._element);this._startPoint=new x(i.clientX,i.clientY),this._parentScale=pt(e),mt(document,Oe[t.type],this._onMove,this),mt(document,Ie[t.type],this._onUp,this)}},_onMove:function(t){if(!t._simulated&&this._enabled)if(t.touches&&t.touches.length>1)this._moved=!0;else{var i=t.touches&&1===t.touches.length?t.touches[0]:t,e=new x(i.clientX,i.clientY)._subtract(this._startPoint);(e.x||e.y)&&(Math.abs(e.x)+Math.abs(e.y)1e-7;h++)i=s*Math.sin(a),i=Math.pow((1-i)/(1+i),s/2),a+=u=Math.PI/2-2*Math.atan(r*i)-a;return new M(a*e,t.x*e/n)}},He=(Object.freeze||Object)({LonLat:je,Mercator:We,SphericalMercator:mi}),Fe=i({},pi,{code:"EPSG:3395",projection:We,transformation:function(){var t=.5/(Math.PI*We.R);return Z(t,.5,-t,.5)}()}),Ue=i({},pi,{code:"EPSG:4326",projection:je,transformation:Z(1/180,1,-1/180,.5)}),Ve=i({},di,{projection:je,transformation:Z(1,0,-1,0),scale:function(t){return Math.pow(2,t)},zoom:function(t){return Math.log(t)/Math.LN2},distance:function(t,i){var e=i.lng-t.lng,n=i.lat-t.lat;return Math.sqrt(e*e+n*n)},infinite:!0});di.Earth=pi,di.EPSG3395=Fe,di.EPSG3857=yi,di.EPSG900913=xi,di.EPSG4326=Ue,di.Simple=Ve;var qe=ci.extend({options:{pane:"overlayPane",attribution:null,bubblingMouseEvents:!0},addTo:function(t){return t.addLayer(this),this},remove:function(){return this.removeFrom(this._map||this._mapToAdd)},removeFrom:function(t){return t&&t.removeLayer(this),this},getPane:function(t){return this._map.getPane(t?this.options[t]||t:this.options.pane)},addInteractiveTarget:function(t){return this._map._targets[n(t)]=this,this},removeInteractiveTarget:function(t){return delete this._map._targets[n(t)],this},getAttribution:function(){return this.options.attribution},_layerAdd:function(t){var i=t.target;if(i.hasLayer(this)){if(this._map=i,this._zoomAnimated=i._zoomAnimated,this.getEvents){var e=this.getEvents();i.on(e,this),this.once("remove",function(){i.off(e,this)},this)}this.onAdd(i),this.getAttribution&&i.attributionControl&&i.attributionControl.addAttribution(this.getAttribution()),this.fire("add"),i.fire("layeradd",{layer:this})}}});be.include({addLayer:function(t){if(!t._layerAdd)throw new Error("The provided object is not a Layer.");var i=n(t);return this._layers[i]?this:(this._layers[i]=t,t._mapToAdd=this,t.beforeAdd&&t.beforeAdd(this),this.whenReady(t._layerAdd,t),this)},removeLayer:function(t){var i=n(t);return this._layers[i]?(this._loaded&&t.onRemove(this),t.getAttribution&&this.attributionControl&&this.attributionControl.removeAttribution(t.getAttribution()),delete this._layers[i],this._loaded&&(this.fire("layerremove",{layer:t}),t.fire("remove")),t._map=t._mapToAdd=null,this):this},hasLayer:function(t){return!!t&&n(t)in this._layers},eachLayer:function(t,i){for(var e in this._layers)t.call(i,this._layers[e]);return this},_addLayers:function(t){for(var i=0,e=(t=t?oi(t)?t:[t]:[]).length;ithis._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()i)return r=(n-i)/e,this._map.layerPointToLatLng([s.x-r*(s.x-o.x),s.y-r*(s.y-o.y)])},getBounds:function(){return this._bounds},addLatLng:function(t,i){return i=i||this._defaultShape(),t=C(t),i.push(t),this._bounds.extend(t),this.redraw()},_setLatLngs:function(t){this._bounds=new T,this._latlngs=this._convertLatLngs(t)},_defaultShape:function(){return jt(this._latlngs)?this._latlngs:this._latlngs[0]},_convertLatLngs:function(t){for(var i=[],e=jt(t),n=0,o=t.length;n=2&&i[0]instanceof M&&i[0].equals(i[e-1])&&i.pop(),i},_setLatLngs:function(t){nn.prototype._setLatLngs.call(this,t),jt(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return jt(this._latlngs[0])?this._latlngs[0]:this._latlngs[0][0]},_clipPoints:function(){var t=this._renderer._bounds,i=this.options.weight,e=new x(i,i);if(t=new P(t.min.subtract(e),t.max.add(e)),this._parts=[],this._pxBounds&&this._pxBounds.intersects(t))if(this.options.noClip)this._parts=this._rings;else for(var n,o=0,s=this._rings.length;ot.y!=n.y>t.y&&t.x<(n.x-e.x)*(t.y-e.y)/(n.y-e.y)+e.x&&(u=!u);return u||nn.prototype._containsPoint.call(this,t,!0)}}),sn=Ke.extend({initialize:function(t,i){l(this,i),this._layers={},t&&this.addData(t)},addData:function(t){var i,e,n,o=oi(t)?t:t.features;if(o){for(i=0,e=o.length;i0?o:[i.src]}else{oi(this._url)||(this._url=[this._url]),i.autoplay=!!this.options.autoplay,i.loop=!!this.options.loop;for(var a=0;ao?(i.height=o+"px",Q(t,"leaflet-popup-scrolled")):tt(t,"leaflet-popup-scrolled"),this._containerWidth=this._container.offsetWidth},_animateZoom:function(t){var i=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center),e=this._getAnchor();at(this._container,i.add(e))},_adjustPan:function(){if(!(!this.options.autoPan||this._map._panAnim&&this._map._panAnim._inProgress)){var t=this._map,i=parseInt(q(this._container,"marginBottom"),10)||0,e=this._container.offsetHeight+i,n=this._containerWidth,o=new x(this._containerLeft,-e-this._containerBottom);o._add(ht(this._container));var s=t.layerPointToContainerPoint(o),r=w(this.options.autoPanPadding),a=w(this.options.autoPanPaddingTopLeft||r),h=w(this.options.autoPanPaddingBottomRight||r),u=t.getSize(),l=0,c=0;s.x+n+h.x>u.x&&(l=s.x+n-u.x+h.x),s.x-l-a.x<0&&(l=s.x-a.x),s.y+e+h.y>u.y&&(c=s.y+e-u.y+h.y),s.y-c-a.y<0&&(c=s.y-a.y),(l||c)&&t.fire("autopanstart").panBy([l,c])}},_onCloseButtonClick:function(t){this._close(),Lt(t)},_getAnchor:function(){return w(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}});be.mergeOptions({closePopupOnClick:!0}),be.include({openPopup:function(t,i,e){return t instanceof cn||(t=new cn(e).setContent(t)),i&&t.setLatLng(i),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),qe.include({bindPopup:function(t,i){return t instanceof cn?(l(t,i),this._popup=t,t._source=this):(this._popup&&!i||(this._popup=new cn(i,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,i){if(t instanceof qe||(i=t,t=this),t instanceof Ke)for(var e in this._layers){t=this._layers[e];break}return i||(i=t.getCenter?t.getCenter():t.getLatLng()),this._popup&&this._map&&(this._popup._source=t,this._popup.update(),this._map.openPopup(this._popup,i)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var i=t.layer||t.target;this._popup&&this._map&&(Lt(t),i instanceof Qe?this.openPopup(t.layer||t.target,t.latlng):this._map.hasLayer(this._popup)&&this._popup._source===i?this.closePopup():this.openPopup(i,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}});var _n=ln.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){ln.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){ln.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=ln.prototype.getEvents.call(this);return qi&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=G("div",t)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var i=this._map,e=this._container,n=i.latLngToContainerPoint(i.getCenter()),o=i.layerPointToContainerPoint(t),s=this.options.direction,r=e.offsetWidth,a=e.offsetHeight,h=w(this.options.offset),u=this._getAnchor();"top"===s?t=t.add(w(-r/2+h.x,-a+h.y+u.y,!0)):"bottom"===s?t=t.subtract(w(r/2-h.x,-h.y,!0)):"center"===s?t=t.subtract(w(r/2+h.x,a/2-u.y+h.y,!0)):"right"===s||"auto"===s&&o.xthis.options.maxZoom||en&&this._retainParent(o,s,r,n))},_retainChildren:function(t,i,e,n){for(var o=2*t;o<2*t+2;o++)for(var s=2*i;s<2*i+2;s++){var r=new x(o,s);r.z=e+1;var a=this._tileCoordsToKey(r),h=this._tiles[a];h&&h.active?h.retain=!0:(h&&h.loaded&&(h.retain=!0),e+1this.options.maxZoom||void 0!==this.options.minZoom&&o1)this._setView(t,e);else{for(var c=o.min.y;c<=o.max.y;c++)for(var _=o.min.x;_<=o.max.x;_++){var d=new x(_,c);if(d.z=this._tileZoom,this._isValidTile(d)){var p=this._tiles[this._tileCoordsToKey(d)];p?p.current=!0:r.push(d)}}if(r.sort(function(t,i){return t.distanceTo(s)-i.distanceTo(s)}),0!==r.length){this._loading||(this._loading=!0,this.fire("loading"));var m=document.createDocumentFragment();for(_=0;_e.max.x)||!i.wrapLat&&(t.ye.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return z(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var i=this._map,e=this.getTileSize(),n=t.scaleBy(e),o=n.add(e);return[i.unproject(n,t.z),i.unproject(o,t.z)]},_tileCoordsToBounds:function(t){var i=this._tileCoordsToNwSe(t),e=new T(i[0],i[1]);return this.options.noWrap||(e=this._map.wrapLatLngBounds(e)),e},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var i=t.split(":"),e=new x(+i[0],+i[1]);return e.z=+i[2],e},_removeTile:function(t){var i=this._tiles[t];i&&(K(i.el),delete this._tiles[t],this.fire("tileunload",{tile:i.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){Q(t,"leaflet-tile");var i=this.getTileSize();t.style.width=i.x+"px",t.style.height=i.y+"px",t.onselectstart=r,t.onmousemove=r,Li&&this.options.opacity<1&&nt(t,this.options.opacity),zi&&!Mi&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,i){var n=this._getTilePos(t),o=this._tileCoordsToKey(t),s=this.createTile(this._wrapCoords(t),e(this._tileReady,this,t));this._initTile(s),this.createTile.length<2&&f(e(this._tileReady,this,t,null,s)),at(s,n),this._tiles[o]={el:s,coords:t,current:!0},i.appendChild(s),this.fire("tileloadstart",{tile:s,coords:t})},_tileReady:function(t,i,n){i&&this.fire("tileerror",{error:i,tile:n,coords:t});var o=this._tileCoordsToKey(t);(n=this._tiles[o])&&(n.loaded=+new Date,this._map._fadeAnimated?(nt(n.el,0),g(this._fadeFrame),this._fadeFrame=f(this._updateOpacity,this)):(n.active=!0,this._pruneTiles()),i||(Q(n.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:n.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),Li||!this._map._fadeAnimated?f(this._pruneTiles,this):setTimeout(e(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var i=new x(this._wrapX?s(t.x,this._wrapX):t.x,this._wrapY?s(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToTileRange:function(t){var i=this.getTileSize();return new P(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}}),mn=pn.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,i){this._url=t,(i=l(this,i)).detectRetina&&Yi&&i.maxZoom>0&&(i.tileSize=Math.floor(i.tileSize/2),i.zoomReverse?(i.zoomOffset--,i.minZoom++):(i.zoomOffset++,i.maxZoom--),i.minZoom=Math.max(0,i.minZoom)),"string"==typeof i.subdomains&&(i.subdomains=i.subdomains.split("")),zi||this.on("tileunload",this._onTileRemove)},setUrl:function(t,i){return this._url=t,i||this.redraw(),this},createTile:function(t,i){var n=document.createElement("img");return mt(n,"load",e(this._tileOnLoad,this,i,n)),mt(n,"error",e(this._tileOnError,this,i,n)),(this.options.crossOrigin||""===this.options.crossOrigin)&&(n.crossOrigin=!0===this.options.crossOrigin?"":this.options.crossOrigin),n.alt="",n.setAttribute("role","presentation"),n.src=this.getTileUrl(t),n},getTileUrl:function(t){var e={r:Yi?"@2x":"",s:this._getSubdomain(t),x:t.x,y:t.y,z:this._getZoomForUrl()};if(this._map&&!this._map.options.crs.infinite){var n=this._globalTileRange.max.y-t.y;this.options.tms&&(e.y=n),e["-y"]=n}return _(this._url,i(e,this.options))},_tileOnLoad:function(t,i){Li?setTimeout(e(t,this,null,i),0):t(null,i)},_tileOnError:function(t,i,e){var n=this.options.errorTileUrl;n&&i.getAttribute("src")!==n&&(i.src=n),t(e,i)},_onTileRemove:function(t){t.tile.onload=null},_getZoomForUrl:function(){var t=this._tileZoom,i=this.options.maxZoom,e=this.options.zoomReverse,n=this.options.zoomOffset;return e&&(t=i-t),t+n},_getSubdomain:function(t){var i=Math.abs(t.x+t.y)%this.options.subdomains.length;return this.options.subdomains[i]},_abortLoading:function(){var t,i;for(t in this._tiles)this._tiles[t].coords.z!==this._tileZoom&&((i=this._tiles[t].el).onload=r,i.onerror=r,i.complete||(i.src=si,K(i),delete this._tiles[t]))},_removeTile:function(t){var i=this._tiles[t];if(i)return Si||i.el.setAttribute("src",si),pn.prototype._removeTile.call(this,t)},_tileReady:function(t,i,e){if(this._map&&(!e||e.getAttribute("src")!==si))return pn.prototype._tileReady.call(this,t,i,e)}}),fn=mn.extend({defaultWmsParams:{service:"WMS",request:"GetMap",layers:"",styles:"",format:"image/jpeg",transparent:!1,version:"1.1.1"},options:{crs:null,uppercase:!1},initialize:function(t,e){this._url=t;var n=i({},this.defaultWmsParams);for(var o in e)o in this.options||(n[o]=e[o]);var s=(e=l(this,e)).detectRetina&&Yi?2:1,r=this.getTileSize();n.width=r.x*s,n.height=r.y*s,this.wmsParams=n},onAdd:function(t){this._crs=this.options.crs||t.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var i=this._wmsVersion>=1.3?"crs":"srs";this.wmsParams[i]=this._crs.code,mn.prototype.onAdd.call(this,t)},getTileUrl:function(t){var i=this._tileCoordsToNwSe(t),e=this._crs,n=b(e.project(i[0]),e.project(i[1])),o=n.min,s=n.max,r=(this._wmsVersion>=1.3&&this._crs===Ue?[o.y,o.x,s.y,s.x]:[o.x,o.y,s.x,s.y]).join(","),a=mn.prototype.getTileUrl.call(this,t);return a+c(this.wmsParams,a,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+r},setParams:function(t,e){return i(this.wmsParams,t),e||this.redraw(),this}});mn.WMS=fn,Jt.wms=function(t,i){return new fn(t,i)};var gn=qe.extend({options:{padding:.1,tolerance:0},initialize:function(t){l(this,t),n(this),this._layers=this._layers||{}},onAdd:function(){this._container||(this._initContainer(),this._zoomAnimated&&Q(this._container,"leaflet-zoom-animated")),this.getPane().appendChild(this._container),this._update(),this.on("update",this._updatePaths,this)},onRemove:function(){this.off("update",this._updatePaths,this),this._destroyContainer()},getEvents:function(){var t={viewreset:this._reset,zoom:this._onZoom,moveend:this._update,zoomend:this._onZoomEnd};return this._zoomAnimated&&(t.zoomanim=this._onAnimZoom),t},_onAnimZoom:function(t){this._updateTransform(t.center,t.zoom)},_onZoom:function(){this._updateTransform(this._map.getCenter(),this._map.getZoom())},_updateTransform:function(t,i){var e=this._map.getZoomScale(i,this._zoom),n=ht(this._container),o=this._map.getSize().multiplyBy(.5+this.options.padding),s=this._map.project(this._center,i),r=this._map.project(t,i).subtract(s),a=o.multiplyBy(-e).add(n).add(o).subtract(r);ji?rt(this._container,a,e):at(this._container,a)},_reset:function(){this._update(),this._updateTransform(this._center,this._zoom);for(var t in this._layers)this._layers[t]._reset()},_onZoomEnd:function(){for(var t in this._layers)this._layers[t]._project()},_updatePaths:function(){for(var t in this._layers)this._layers[t]._update()},_update:function(){var t=this.options.padding,i=this._map.getSize(),e=this._map.containerPointToLayerPoint(i.multiplyBy(-t)).round();this._bounds=new P(e,e.add(i.multiplyBy(1+2*t)).round()),this._center=this._map.getCenter(),this._zoom=this._map.getZoom()}}),vn=gn.extend({getEvents:function(){var t=gn.prototype.getEvents.call(this);return t.viewprereset=this._onViewPreReset,t},_onViewPreReset:function(){this._postponeUpdatePaths=!0},onAdd:function(){gn.prototype.onAdd.call(this),this._draw()},_initContainer:function(){var t=this._container=document.createElement("canvas");mt(t,"mousemove",o(this._onMouseMove,32,this),this),mt(t,"click dblclick mousedown mouseup contextmenu",this._onClick,this),mt(t,"mouseout",this._handleMouseOut,this),this._ctx=t.getContext("2d")},_destroyContainer:function(){g(this._redrawRequest),delete this._ctx,K(this._container),ft(this._container),delete this._container},_updatePaths:function(){if(!this._postponeUpdatePaths){this._redrawBounds=null;for(var t in this._layers)this._layers[t]._update();this._redraw()}},_update:function(){if(!this._map._animatingZoom||!this._bounds){this._drawnLayers={},gn.prototype._update.call(this);var t=this._bounds,i=this._container,e=t.getSize(),n=Yi?2:1;at(i,t.min),i.width=n*e.x,i.height=n*e.y,i.style.width=e.x+"px",i.style.height=e.y+"px",Yi&&this._ctx.scale(2,2),this._ctx.translate(-t.min.x,-t.min.y),this.fire("update")}},_reset:function(){gn.prototype._reset.call(this),this._postponeUpdatePaths&&(this._postponeUpdatePaths=!1,this._updatePaths())},_initPath:function(t){this._updateDashArray(t),this._layers[n(t)]=t;var i=t._order={layer:t,prev:this._drawLast,next:null};this._drawLast&&(this._drawLast.next=i),this._drawLast=i,this._drawFirst=this._drawFirst||this._drawLast},_addPath:function(t){this._requestRedraw(t)},_removePath:function(t){var i=t._order,e=i.next,o=i.prev;e?e.prev=o:this._drawLast=o,o?o.next=e:this._drawFirst=e,delete this._drawnLayers[t._leaflet_id],delete t._order,delete this._layers[n(t)],this._requestRedraw(t)},_updatePath:function(t){this._extendRedrawBounds(t),t._project(),t._update(),this._requestRedraw(t)},_updateStyle:function(t){this._updateDashArray(t),this._requestRedraw(t)},_updateDashArray:function(t){if("string"==typeof t.options.dashArray){var i,e=t.options.dashArray.split(/[, ]+/),n=[];for(i=0;i')}}catch(t){return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),xn={_initContainer:function(){this._container=G("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(gn.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var i=t._container=yn("shape");Q(i,"leaflet-vml-shape "+(this.options.className||"")),i.coordsize="1 1",t._path=yn("path"),i.appendChild(t._path),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){var i=t._container;this._container.appendChild(i),t.options.interactive&&t.addInteractiveTarget(i)},_removePath:function(t){var i=t._container;K(i),t.removeInteractiveTarget(i),delete this._layers[n(t)]},_updateStyle:function(t){var i=t._stroke,e=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(i||(i=t._stroke=yn("stroke")),o.appendChild(i),i.weight=n.weight+"px",i.color=n.color,i.opacity=n.opacity,n.dashArray?i.dashStyle=oi(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):i.dashStyle="",i.endcap=n.lineCap.replace("butt","flat"),i.joinstyle=n.lineJoin):i&&(o.removeChild(i),t._stroke=null),n.fill?(e||(e=t._fill=yn("fill")),o.appendChild(e),e.color=n.fillColor||n.color,e.opacity=n.fillOpacity):e&&(o.removeChild(e),t._fill=null)},_updateCircle:function(t){var i=t._point.round(),e=Math.round(t._radius),n=Math.round(t._radiusY||e);this._setPath(t,t._empty()?"M0 0":"AL "+i.x+","+i.y+" "+e+","+n+" 0,23592600")},_setPath:function(t,i){t._path.v=i},_bringToFront:function(t){X(t._container)},_bringToBack:function(t){J(t._container)}},wn=$i?yn:E,Pn=gn.extend({getEvents:function(){var t=gn.prototype.getEvents.call(this);return t.zoomstart=this._onZoomStart,t},_initContainer:function(){this._container=wn("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=wn("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){K(this._container),ft(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_onZoomStart:function(){this._update()},_update:function(){if(!this._map._animatingZoom||!this._bounds){gn.prototype._update.call(this);var t=this._bounds,i=t.getSize(),e=this._container;this._svgSize&&this._svgSize.equals(i)||(this._svgSize=i,e.setAttribute("width",i.x),e.setAttribute("height",i.y)),at(e,t.min),e.setAttribute("viewBox",[t.min.x,t.min.y,i.x,i.y].join(" ")),this.fire("update")}},_initPath:function(t){var i=t._path=wn("path");t.options.className&&Q(i,t.options.className),t.options.interactive&&Q(i,"leaflet-interactive"),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){K(t._path),t.removeInteractiveTarget(t._path),delete this._layers[n(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var i=t._path,e=t.options;i&&(e.stroke?(i.setAttribute("stroke",e.color),i.setAttribute("stroke-opacity",e.opacity),i.setAttribute("stroke-width",e.weight),i.setAttribute("stroke-linecap",e.lineCap),i.setAttribute("stroke-linejoin",e.lineJoin),e.dashArray?i.setAttribute("stroke-dasharray",e.dashArray):i.removeAttribute("stroke-dasharray"),e.dashOffset?i.setAttribute("stroke-dashoffset",e.dashOffset):i.removeAttribute("stroke-dashoffset")):i.setAttribute("stroke","none"),e.fill?(i.setAttribute("fill",e.fillColor||e.color),i.setAttribute("fill-opacity",e.fillOpacity),i.setAttribute("fill-rule",e.fillRule||"evenodd")):i.setAttribute("fill","none"))},_updatePoly:function(t,i){this._setPath(t,k(t._parts,i))},_updateCircle:function(t){var i=t._point,e=Math.max(Math.round(t._radius),1),n="a"+e+","+(Math.max(Math.round(t._radiusY),1)||e)+" 0 1,0 ",o=t._empty()?"M0 0":"M"+(i.x-e)+","+i.y+n+2*e+",0 "+n+2*-e+",0 ";this._setPath(t,o)},_setPath:function(t,i){t._path.setAttribute("d",i)},_bringToFront:function(t){X(t._path)},_bringToBack:function(t){J(t._path)}});$i&&Pn.include(xn),be.include({getRenderer:function(t){var i=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer;return i||(i=this._renderer=this._createRenderer()),this.hasLayer(i)||this.addLayer(i),i},_getPaneRenderer:function(t){if("overlayPane"===t||void 0===t)return!1;var i=this._paneRenderers[t];return void 0===i&&(i=this._createRenderer({pane:t}),this._paneRenderers[t]=i),i},_createRenderer:function(t){return this.options.preferCanvas&&$t(t)||Qt(t)}});var Ln=on.extend({initialize:function(t,i){on.prototype.initialize.call(this,this._boundsToLatLngs(t),i)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return t=z(t),[t.getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Pn.create=wn,Pn.pointsToPath=k,sn.geometryToLayer=Ft,sn.coordsToLatLng=Ut,sn.coordsToLatLngs=Vt,sn.latLngToCoords=qt,sn.latLngsToCoords=Gt,sn.getFeature=Kt,sn.asFeature=Yt,be.mergeOptions({boxZoom:!0});var bn=Ee.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){mt(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){ft(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){K(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),fi(),ut(),this._startPoint=this._map.mouseEventToContainerPoint(t),mt(document,{contextmenu:Lt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=G("div","leaflet-zoom-box",this._container),Q(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var i=new P(this._point,this._startPoint),e=i.getSize();at(this._box,i.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(K(this._box),tt(this._container,"leaflet-crosshair")),gi(),lt(),ft(document,{contextmenu:Lt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(e(this._resetState,this),0);var i=new T(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(i).fire("boxzoomend",{boxZoomBounds:i})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}});be.addInitHook("addHandler","boxZoom",bn),be.mergeOptions({doubleClickZoom:!0});var Tn=Ee.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var i=this._map,e=i.getZoom(),n=i.options.zoomDelta,o=t.originalEvent.shiftKey?e-n:e+n;"center"===i.options.doubleClickZoom?i.setZoom(o):i.setZoomAround(t.containerPoint,o)}});be.addInitHook("addHandler","doubleClickZoom",Tn),be.mergeOptions({dragging:!0,inertia:!Mi,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var zn=Ee.extend({addHooks:function(){if(!this._draggable){var t=this._map;this._draggable=new Re(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))}Q(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){tt(this._map._container,"leaflet-grab"),tt(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t=this._map;if(t._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var i=z(this._map.options.maxBounds);this._offsetLimit=b(this._map.latLngToContainerPoint(i.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(i.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;t.fire("movestart").fire("dragstart"),t.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){if(this._map.options.inertia){var i=this._lastTime=+new Date,e=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(e),this._times.push(i),this._prunePositions(i)}this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;this._positions.length>1&&t-this._times[0]>50;)this._positions.shift(),this._times.shift()},_onZoomEnd:function(){var t=this._map.getSize().divideBy(2),i=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=i.subtract(t).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(t,i){return t-(t-i)*this._viscosity},_onPreDragLimit:function(){if(this._viscosity&&this._offsetLimit){var t=this._draggable._newPos.subtract(this._draggable._startPos),i=this._offsetLimit;t.xi.max.x&&(t.x=this._viscousLimit(t.x,i.max.x)),t.y>i.max.y&&(t.y=this._viscousLimit(t.y,i.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)0?s:-s))-i;this._delta=0,this._startTime=null,r&&("center"===t.options.scrollWheelZoom?t.setZoom(i+r):t.setZoomAround(this._lastMousePos,i+r))}});be.addInitHook("addHandler","scrollWheelZoom",Cn),be.mergeOptions({tap:!0,tapTolerance:15});var Sn=Ee.extend({addHooks:function(){mt(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){ft(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if(Pt(t),this._fireClick=!0,t.touches.length>1)return this._fireClick=!1,void clearTimeout(this._holdTimeout);var i=t.touches[0],n=i.target;this._startPos=this._newPos=new x(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&Q(n,"leaflet-active"),this._holdTimeout=setTimeout(e(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),this._simulateEvent("mousedown",i),mt(document,{touchmove:this._onMove,touchend:this._onUp},this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),ft(document,{touchmove:this._onMove,touchend:this._onUp},this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],e=i.target;e&&e.tagName&&"a"===e.tagName.toLowerCase()&&tt(e,"leaflet-active"),this._simulateEvent("mouseup",i),this._isTapValid()&&this._simulateEvent("click",i)}},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_onMove:function(t){var i=t.touches[0];this._newPos=new x(i.clientX,i.clientY),this._simulateEvent("mousemove",i)},_simulateEvent:function(t,i){var e=document.createEvent("MouseEvents");e._simulated=!0,i.target._simulatedClick=!0,e.initMouseEvent(t,!0,!0,window,1,i.screenX,i.screenY,i.clientX,i.clientY,!1,!1,!1,!1,0,null),i.target.dispatchEvent(e)}});qi&&!Vi&&be.addInitHook("addHandler","tap",Sn),be.mergeOptions({touchZoom:qi&&!Mi,bounceAtZoomLimits:!0});var Zn=Ee.extend({addHooks:function(){Q(this._map._container,"leaflet-touch-zoom"),mt(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){tt(this._map._container,"leaflet-touch-zoom"),ft(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(t){var i=this._map;if(t.touches&&2===t.touches.length&&!i._animatingZoom&&!this._zooming){var e=i.mouseEventToContainerPoint(t.touches[0]),n=i.mouseEventToContainerPoint(t.touches[1]);this._centerPoint=i.getSize()._divideBy(2),this._startLatLng=i.containerPointToLatLng(this._centerPoint),"center"!==i.options.touchZoom&&(this._pinchStartLatLng=i.containerPointToLatLng(e.add(n)._divideBy(2))),this._startDist=e.distanceTo(n),this._startZoom=i.getZoom(),this._moved=!1,this._zooming=!0,i._stop(),mt(document,"touchmove",this._onTouchMove,this),mt(document,"touchend",this._onTouchEnd,this),Pt(t)}},_onTouchMove:function(t){if(t.touches&&2===t.touches.length&&this._zooming){var i=this._map,n=i.mouseEventToContainerPoint(t.touches[0]),o=i.mouseEventToContainerPoint(t.touches[1]),s=n.distanceTo(o)/this._startDist;if(this._zoom=i.getScaleZoom(s,this._startZoom),!i.options.bounceAtZoomLimits&&(this._zoomi.getMaxZoom()&&s>1)&&(this._zoom=i._limitZoom(this._zoom)),"center"===i.options.touchZoom){if(this._center=this._startLatLng,1===s)return}else{var r=n._add(o)._divideBy(2)._subtract(this._centerPoint);if(1===s&&0===r.x&&0===r.y)return;this._center=i.unproject(i.project(this._pinchStartLatLng,this._zoom).subtract(r),this._zoom)}this._moved||(i._moveStart(!0,!1),this._moved=!0),g(this._animRequest);var a=e(i._move,i,this._center,this._zoom,{pinch:!0,round:!1});this._animRequest=f(a,this,!0),Pt(t)}},_onTouchEnd:function(){this._moved&&this._zooming?(this._zooming=!1,g(this._animRequest),ft(document,"touchmove",this._onTouchMove),ft(document,"touchend",this._onTouchEnd),this._map.options.zoomAnimation?this._map._animateZoom(this._center,this._map._limitZoom(this._zoom),!0,this._map.options.zoomSnap):this._map._resetView(this._center,this._map._limitZoom(this._zoom))):this._zooming=!1}});be.addInitHook("addHandler","touchZoom",Zn),be.BoxZoom=bn,be.DoubleClickZoom=Tn,be.Drag=zn,be.Keyboard=Mn,be.ScrollWheelZoom=Cn,be.Tap=Sn,be.TouchZoom=Zn,Object.freeze=ti,t.version="1.3.4+HEAD.0e566b2",t.Control=Te,t.control=ze,t.Browser=Qi,t.Evented=ci,t.Mixin=Ae,t.Util=ui,t.Class=v,t.Handler=Ee,t.extend=i,t.bind=e,t.stamp=n,t.setOptions=l,t.DomEvent=Pe,t.DomUtil=ve,t.PosAnimation=Le,t.Draggable=Re,t.LineUtil=Ne,t.PolyUtil=De,t.Point=x,t.point=w,t.Bounds=P,t.bounds=b,t.Transformation=S,t.transformation=Z,t.Projection=He,t.LatLng=M,t.latLng=C,t.LatLngBounds=T,t.latLngBounds=z,t.CRS=di,t.GeoJSON=sn,t.geoJSON=Xt,t.geoJson=an,t.Layer=qe,t.LayerGroup=Ge,t.layerGroup=function(t,i){return new Ge(t,i)},t.FeatureGroup=Ke,t.featureGroup=function(t){return new Ke(t)},t.ImageOverlay=hn,t.imageOverlay=function(t,i,e){return new hn(t,i,e)},t.VideoOverlay=un,t.videoOverlay=function(t,i,e){return new un(t,i,e)},t.DivOverlay=ln,t.Popup=cn,t.popup=function(t,i){return new cn(t,i)},t.Tooltip=_n,t.tooltip=function(t,i){return new _n(t,i)},t.Icon=Ye,t.icon=function(t){return new Ye(t)},t.DivIcon=dn,t.divIcon=function(t){return new dn(t)},t.Marker=$e,t.marker=function(t,i){return new $e(t,i)},t.TileLayer=mn,t.tileLayer=Jt,t.GridLayer=pn,t.gridLayer=function(t){return new pn(t)},t.SVG=Pn,t.svg=Qt,t.Renderer=gn,t.Canvas=vn,t.canvas=$t,t.Path=Qe,t.CircleMarker=tn,t.circleMarker=function(t,i){return new tn(t,i)},t.Circle=en,t.circle=function(t,i,e){return new en(t,i,e)},t.Polyline=nn,t.polyline=function(t,i){return new nn(t,i)},t.Polygon=on,t.polygon=function(t,i){return new on(t,i)},t.Rectangle=Ln,t.rectangle=function(t,i){return new Ln(t,i)},t.Map=be,t.map=function(t,i){return new be(t,i)};var En=window.L;t.noConflict=function(){return window.L=En,this},window.L=t});