commit
3be162b0b0
49 changed files with 1934 additions and 1597 deletions
|
@ -2,7 +2,7 @@
|
||||||
# Process this file with autoconf to produce a configure script.
|
# Process this file with autoconf to produce a configure script.
|
||||||
|
|
||||||
# this defines the CORE version number, must be static for AC_INIT
|
# this defines the CORE version number, must be static for AC_INIT
|
||||||
AC_INIT(core, 6.1.0)
|
AC_INIT(core, 6.2.0)
|
||||||
|
|
||||||
# autoconf and automake initialization
|
# autoconf and automake initialization
|
||||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||||
|
|
581
daemon/Pipfile.lock
generated
581
daemon/Pipfile.lock
generated
|
@ -39,41 +39,36 @@
|
||||||
},
|
},
|
||||||
"cffi": {
|
"cffi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42",
|
"sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
|
||||||
"sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04",
|
"sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
|
||||||
"sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5",
|
"sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
|
||||||
"sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54",
|
"sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
|
||||||
"sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba",
|
"sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
|
||||||
"sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57",
|
"sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
|
||||||
"sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396",
|
"sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
|
||||||
"sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12",
|
"sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
|
||||||
"sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97",
|
"sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
|
||||||
"sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43",
|
"sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
|
||||||
"sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db",
|
"sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
|
||||||
"sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3",
|
"sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
|
||||||
"sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b",
|
"sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
|
||||||
"sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579",
|
"sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
|
||||||
"sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346",
|
"sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
|
||||||
"sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159",
|
"sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
|
||||||
"sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652",
|
"sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
|
||||||
"sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e",
|
"sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
|
||||||
"sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a",
|
"sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
|
||||||
"sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506",
|
"sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
|
||||||
"sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f",
|
"sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
|
||||||
"sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d",
|
"sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
|
||||||
"sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c",
|
"sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
|
||||||
"sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20",
|
"sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
|
||||||
"sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858",
|
"sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
|
||||||
"sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc",
|
"sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
|
||||||
"sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a",
|
"sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
|
||||||
"sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3",
|
"sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
|
||||||
"sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e",
|
|
||||||
"sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410",
|
|
||||||
"sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25",
|
|
||||||
"sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b",
|
|
||||||
"sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"
|
|
||||||
],
|
],
|
||||||
"version": "==1.13.2"
|
"version": "==1.14.0"
|
||||||
},
|
},
|
||||||
"core": {
|
"core": {
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
@ -114,90 +109,91 @@
|
||||||
},
|
},
|
||||||
"grpcio": {
|
"grpcio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:066630f6b62bffa291dacbee56994279a6a3682b8a11967e9ccaf3cc770fc11e",
|
"sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6",
|
||||||
"sha256:07e95762ca6b18afbeb3aa2793e827c841152d5e507089b1db0b18304edda105",
|
"sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045",
|
||||||
"sha256:0a0fb2f8e3a13537106bc77e4c63005bc60124a6203034304d9101921afa4e90",
|
"sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1",
|
||||||
"sha256:0c61b74dcfb302613926e785cb3542a0905b9a3a86e9410d8cf5d25e25e10104",
|
"sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00",
|
||||||
"sha256:13383bd70618da03684a8aafbdd9e3d9a6720bf8c07b85d0bc697afed599d8f0",
|
"sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942",
|
||||||
"sha256:1c6e0f6b9d091e3717e9a58d631c8bb4898be3b261c2a01fe46371fdc271052f",
|
"sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8",
|
||||||
"sha256:1cf710c04689daa5cc1e598efba00b028215700dcc1bf66fcb7b4f64f2ea5d5f",
|
"sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a",
|
||||||
"sha256:2da5cee9faf17bb8daf500cd0d28a17ae881ab5500f070a6aace457f4c08cac4",
|
"sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c",
|
||||||
"sha256:2f78ebf340eaf28fa09aba0f836a8b869af1716078dfe8f3b3f6ff785d8f2b0f",
|
"sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6",
|
||||||
"sha256:33a07a1a8e817d733588dbd18e567caad1a6fe0d440c165619866cd490c7911a",
|
"sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a",
|
||||||
"sha256:3d090c66af9c065b7228b07c3416f93173e9839b1d40bb0ce3dd2aa783645026",
|
"sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1",
|
||||||
"sha256:42b903a3596a10e2a3727bae2a76f8aefd324d498424b843cfa9606847faea7b",
|
"sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961",
|
||||||
"sha256:4fffbb58134c4f23e5a8312ac3412db6f5e39e961dc0eb5e3115ce5aa16bf927",
|
"sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7",
|
||||||
"sha256:57be5a6c509a406fe0ffa6f8b86904314c77b5e2791be8123368ad2ebccec874",
|
"sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386",
|
||||||
"sha256:5b0fa09efb33e2af4e8822b4eb8b2cbc201d562e3e185c439be7eaeee2e8b8aa",
|
"sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e",
|
||||||
"sha256:5ef42dfc18f9a63a06aca938770b69470bb322e4c137cf08cf21703d1ef4ae5c",
|
"sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866",
|
||||||
"sha256:6a43d2f2ff8250f200fdf7aa31fa191a997922aa9ea1182453acd705ad83ab72",
|
"sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d",
|
||||||
"sha256:6d8ab28559be98b02f8b3a154b53239df1aa5b0d28ff865ae5be4f30e7ed4d3f",
|
"sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371",
|
||||||
"sha256:6e47866b7dc14ca3a12d40c1d6082e7bea964670f1c5315ea0fb8b0550244d64",
|
"sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6",
|
||||||
"sha256:6edda1b96541187f73aab11800d25f18ee87e53d5f96bb74473873072bf28a0e",
|
"sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb",
|
||||||
"sha256:7109c8738a8a3c98cfb5dda1c45642a8d6d35dc00d257ab7a175099b2b4daecd",
|
"sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3",
|
||||||
"sha256:8d866aafb08657c456a18c4a31c8526ea62de42427c242b58210b9eae6c64559",
|
"sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6",
|
||||||
"sha256:9939727d9ae01690b24a2b159ac9dbca7b7e8e6edd5af6a6eb709243cae7b52b",
|
"sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47",
|
||||||
"sha256:99fd873699df17cb11c542553270ae2b32c169986e475df0d68a8629b8ef4df7",
|
"sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454",
|
||||||
"sha256:b6fda5674f990e15e1bcaacf026428cf50bce36e708ddcbd1de9673b14aab760",
|
"sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e",
|
||||||
"sha256:bdb2f3dcb664f0c39ef1312cd6acf6bc6375252e4420cf8f36fff4cb4fa55c71",
|
"sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8",
|
||||||
"sha256:bfd7d3130683a1a0a50c456273c21ec8a604f2d043b241a55235a78a0090ee06",
|
"sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c",
|
||||||
"sha256:c6c2db348ac73d73afe14e0833b18abbbe920969bf2c5c03c0922719f8020d06",
|
"sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4",
|
||||||
"sha256:cb7a4b41b5e2611f85c3402ac364f1d689f5d7ecbc24a55ef010eedcd6cf460f",
|
"sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7",
|
||||||
"sha256:cd3d3e328f20f7c807a862620c6ee748e8d57ba2a8fc960d48337ed71c6d9d32",
|
"sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6",
|
||||||
"sha256:d1a481777952e4f99b8a6956581f3ee866d7614100d70ae6d7e07327570b85ce",
|
"sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85",
|
||||||
"sha256:d1d49720ed636920bb3d74cedf549382caa9ad55aea89d1de99d817068d896b2",
|
"sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571",
|
||||||
"sha256:d42433f0086cccd192114343473d7dbd4aae9141794f939e2b7b83efc57543db",
|
"sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345",
|
||||||
"sha256:d44c34463a7c481e076f691d8fa25d080c3486978c2c41dca09a8dd75296c2d7",
|
"sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb",
|
||||||
"sha256:d7e5b7af1350e9c8c17a7baf99d575fbd2de69f7f0b0e6ebd47b57506de6493a",
|
"sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49",
|
||||||
"sha256:d9542366a0917b9b48bab1fee481ac01f56bdffc52437b598c09e7840148a6a9",
|
"sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8",
|
||||||
"sha256:df7cdfb40179acc9790a462c049e0b8e109481164dd7ad1a388dd67ff1528759",
|
"sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844",
|
||||||
"sha256:e1a9d9d2e7224d981aea8da79260c7f6932bf31ce1f99b7ccfa5eceeb30dc5d0",
|
"sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54",
|
||||||
"sha256:ed10e5fad105ecb0b12822f924e62d0deb07f46683a0b64416b17fd143daba1d",
|
"sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca",
|
||||||
"sha256:f0ec5371ce2363b03531ed522bfbe691ec940f51f0e111f0500fc0f44518c69d",
|
"sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5",
|
||||||
"sha256:f6580a8a4f5e701289b45fd62a8f6cb5ec41e4d77082424f8b676806dcd22564",
|
"sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3",
|
||||||
"sha256:f7b83e4b2842d44fce3cdc0d54db7a7e0d169a598751bf393601efaa401c83e0",
|
"sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed",
|
||||||
"sha256:ffec45b0db18a555fdfe0c6fa2d0a3fceb751b22b31e8fcd14ceed7bde05481e"
|
"sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b"
|
||||||
],
|
],
|
||||||
"version": "==1.26.0"
|
"version": "==1.27.2"
|
||||||
},
|
},
|
||||||
"invoke": {
|
"invoke": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4668a4a594a47f2da2f0672ec2f7b1566f809cebf10bcd95ce2de9ecf39b95d1",
|
"sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132",
|
||||||
"sha256:ae7b4513638bde9afcda0825e9535599637a3f65bd819a27098356027bb17c8a",
|
"sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134",
|
||||||
"sha256:e04faba8ea7cdf6f5c912be42dcafd5c1074b7f2f306998992c4bfb40a9a690b"
|
"sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d"
|
||||||
],
|
],
|
||||||
"version": "==1.4.0"
|
"version": "==1.4.1"
|
||||||
},
|
},
|
||||||
"lxml": {
|
"lxml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00ac0d64949fef6b3693813fe636a2d56d97a5a49b5bbb86e4cc4cc50ebc9ea2",
|
"sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd",
|
||||||
"sha256:0571e607558665ed42e450d7bf0e2941d542c18e117b1ebbf0ba72f287ad841c",
|
"sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c",
|
||||||
"sha256:0e3f04a7615fdac0be5e18b2406529521d6dbdb0167d2a690ee328bef7807487",
|
"sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081",
|
||||||
"sha256:13cf89be53348d1c17b453867da68704802966c433b2bb4fa1f970daadd2ef70",
|
"sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f",
|
||||||
"sha256:217262fcf6a4c2e1c7cb1efa08bd9ebc432502abc6c255c4abab611e8be0d14d",
|
"sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261",
|
||||||
"sha256:223e544828f1955daaf4cefbb4853bc416b2ec3fd56d4f4204a8b17007c21250",
|
"sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a",
|
||||||
"sha256:277cb61fede2f95b9c61912fefb3d43fbd5f18bf18a14fae4911b67984486f5d",
|
"sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9",
|
||||||
"sha256:3213f753e8ae86c396e0e066866e64c6b04618e85c723b32ecb0909885211f74",
|
"sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a",
|
||||||
"sha256:4690984a4dee1033da0af6df0b7a6bde83f74e1c0c870623797cec77964de34d",
|
"sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb",
|
||||||
"sha256:4fcc472ef87f45c429d3b923b925704aa581f875d65bac80f8ab0c3296a63f78",
|
"sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60",
|
||||||
"sha256:61409bd745a265a742f2693e4600e4dbd45cc1daebe1d5fad6fcb22912d44145",
|
"sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128",
|
||||||
"sha256:678f1963f755c5d9f5f6968dded7b245dd1ece8cf53c1aa9d80e6734a8c7f41d",
|
"sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a",
|
||||||
"sha256:6c6d03549d4e2734133badb9ab1c05d9f0ef4bcd31d83e5d2b4747c85cfa21da",
|
"sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717",
|
||||||
"sha256:6e74d5f4d6ecd6942375c52ffcd35f4318a61a02328f6f1bd79fcb4ffedf969e",
|
"sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89",
|
||||||
"sha256:7b4fc7b1ecc987ca7aaf3f4f0e71bbfbd81aaabf87002558f5bc95da3a865bcd",
|
"sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72",
|
||||||
"sha256:7ed386a40e172ddf44c061ad74881d8622f791d9af0b6f5be20023029129bc85",
|
"sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8",
|
||||||
"sha256:8f54f0924d12c47a382c600c880770b5ebfc96c9fd94cf6f6bdc21caf6163ea7",
|
"sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3",
|
||||||
"sha256:ad9b81351fdc236bda538efa6879315448411a81186c836d4b80d6ca8217cdb9",
|
"sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7",
|
||||||
"sha256:bbd00e21ea17f7bcc58dccd13869d68441b32899e89cf6cfa90d624a9198ce85",
|
"sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8",
|
||||||
"sha256:c3c289762cc09735e2a8f8a49571d0e8b4f57ea831ea11558247b5bdea0ac4db",
|
"sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77",
|
||||||
"sha256:cf4650942de5e5685ad308e22bcafbccfe37c54aa7c0e30cd620c2ee5c93d336",
|
"sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1",
|
||||||
"sha256:cfcbc33c9c59c93776aa41ab02e55c288a042211708b72fdb518221cc803abc8",
|
"sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15",
|
||||||
"sha256:e301055deadfedbd80cf94f2f65ff23126b232b0d1fea28f332ce58137bcdb18",
|
"sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679",
|
||||||
"sha256:ebbfe24df7f7b5c6c7620702496b6419f6a9aa2fd7f005eb731cc80d7b4692b9",
|
"sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012",
|
||||||
"sha256:eff69ddbf3ad86375c344339371168640951c302450c5d3e9936e98d6459db06",
|
"sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6",
|
||||||
"sha256:f6ed60a62c5f1c44e789d2cf14009423cb1646b44a43e40a9cf6a21f077678a1"
|
"sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc",
|
||||||
|
"sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca"
|
||||||
],
|
],
|
||||||
"version": "==4.4.2"
|
"version": "==4.5.0"
|
||||||
},
|
},
|
||||||
"mako": {
|
"mako": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -211,13 +207,16 @@
|
||||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||||
|
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
|
||||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||||
|
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
|
||||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||||
|
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
|
||||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||||
|
@ -234,7 +233,9 @@
|
||||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
|
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
|
||||||
|
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||||
|
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||||
],
|
],
|
||||||
"version": "==1.1.1"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
|
@ -281,26 +282,26 @@
|
||||||
},
|
},
|
||||||
"protobuf": {
|
"protobuf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37",
|
"sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab",
|
||||||
"sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36",
|
"sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f",
|
||||||
"sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4",
|
"sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a",
|
||||||
"sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d",
|
"sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0",
|
||||||
"sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574",
|
"sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4",
|
||||||
"sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0",
|
"sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2",
|
||||||
"sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5",
|
"sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee",
|
||||||
"sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f",
|
"sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07",
|
||||||
"sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946",
|
"sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151",
|
||||||
"sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104",
|
"sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a",
|
||||||
"sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e",
|
"sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f",
|
||||||
"sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7",
|
"sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7",
|
||||||
"sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a",
|
"sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956",
|
||||||
"sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507",
|
"sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306",
|
||||||
"sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1",
|
"sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961",
|
||||||
"sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593",
|
"sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481",
|
||||||
"sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c",
|
"sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a",
|
||||||
"sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1"
|
"sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80"
|
||||||
],
|
],
|
||||||
"version": "==3.11.2"
|
"version": "==3.11.3"
|
||||||
},
|
},
|
||||||
"pycparser": {
|
"pycparser": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -334,6 +335,36 @@
|
||||||
],
|
],
|
||||||
"version": "==1.3.0"
|
"version": "==1.3.0"
|
||||||
},
|
},
|
||||||
|
"pyproj": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0a12982df36f55412597431676e51d3e8fcf9b3e41f18103c31edfb1fc5fa4c0",
|
||||||
|
"sha256:0b57669a568e4235f09fea9c4e498b9beca2673ea7318989569dbb750ed299c5",
|
||||||
|
"sha256:155064fde6a95f6328962386ebde043679fd744f1415e512ed88ec47760ed47c",
|
||||||
|
"sha256:189b8278784655ee2a3bfc51bde3091b5615cc982d0017edabcb10099b2ccb3f",
|
||||||
|
"sha256:1db407591f99877b551a655897da1fd95f4e82e089c8b0d29bcd8beffcffedb8",
|
||||||
|
"sha256:226e0c126d6db158dd3da8879e5efab9f05b1d67989c33fc6aa73bf70409bb12",
|
||||||
|
"sha256:2842412ea3f99383850df92dbbca837847f3e574f98f81eaa8caebc6514a26e2",
|
||||||
|
"sha256:2d2884e85b1e69ff829bfd54872c322d3d5662dc2120a17fbd1094b9c08f9dc5",
|
||||||
|
"sha256:341dc836a1a57b74494a95cff0f05029988d93e1f96ba6c190384ec757d482b2",
|
||||||
|
"sha256:3d69b6a197fc8cf3585290e272e1cdd641d6834a3c71894ec4f2b800d2210d2a",
|
||||||
|
"sha256:447d5b18d941bea180f04179946d1d4f4aa5012697d78c9a4ceac6081dd32465",
|
||||||
|
"sha256:4e8f18a8be5653e90f24b0aea74e85e10271d1c537742ede8a11b569d3583125",
|
||||||
|
"sha256:659b1d748cd7480324841da93f91097a726b898a2de0d192bc771d374006ceb4",
|
||||||
|
"sha256:6972adfe6bb40da0423c12c38617809bf50ca8b7411a20795a1c6c3d96f10942",
|
||||||
|
"sha256:75d7ed27e2e081d2036647f7b40a9e3d4f9ec4bde795925f3f7b4c6bb85f742e",
|
||||||
|
"sha256:7b623a18f70e70cbe594fa429283027c1a73d6d31c70cd04eea65845cd060b76",
|
||||||
|
"sha256:8112da72b47af9ffcc8f0f42224898ba6371680501b3657091bb7420b7dd5c03",
|
||||||
|
"sha256:9686c611893d1c182befa63157f4a1d629e7caa464adf21309cf4da5d422a264",
|
||||||
|
"sha256:98bb690ca7ea50148792f656c0366e799d70dd7e43ab8f0c733b64bd96842e1c",
|
||||||
|
"sha256:a6ede79fd7ddd176d824e0366f8d326ff8bc082d7332c9b40baf8cb8ae7d51fe",
|
||||||
|
"sha256:c7e7b6a00a701e166e5ce903159282f2969eef689fd7fb9d7bcf92aaf167e150",
|
||||||
|
"sha256:cb8c57faf91173c219739a37b909edc1c35a48a86d26be17f1a21ffd9f8728c3",
|
||||||
|
"sha256:ea6c7cbe2f277ca6b32ebad77d713681819e23b07b17a4a892878ffe245826b7",
|
||||||
|
"sha256:ec4b2146ec8fcc93c38fbd1dcb0df06e5737d588fe28d833dfb2b241d2736f54",
|
||||||
|
"sha256:f540f4af0223cb2195b0953db6c5cb45256137da430657db42ad1b076caca361"
|
||||||
|
],
|
||||||
|
"version": "==2.5.0"
|
||||||
|
},
|
||||||
"pyyaml": {
|
"pyyaml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
|
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
|
||||||
|
@ -366,13 +397,6 @@
|
||||||
],
|
],
|
||||||
"version": "==1.4.3"
|
"version": "==1.4.3"
|
||||||
},
|
},
|
||||||
"aspy.yaml": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc",
|
|
||||||
"sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"
|
|
||||||
],
|
|
||||||
"version": "==1.3.0"
|
|
||||||
},
|
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||||
|
@ -390,10 +414,10 @@
|
||||||
},
|
},
|
||||||
"cfgv": {
|
"cfgv": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144",
|
"sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53",
|
||||||
"sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289"
|
"sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513"
|
||||||
],
|
],
|
||||||
"version": "==2.0.1"
|
"version": "==3.1.0"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -402,6 +426,12 @@
|
||||||
],
|
],
|
||||||
"version": "==7.0"
|
"version": "==7.0"
|
||||||
},
|
},
|
||||||
|
"distlib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"
|
||||||
|
],
|
||||||
|
"version": "==0.3.0"
|
||||||
|
},
|
||||||
"entrypoints": {
|
"entrypoints": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||||
|
@ -409,6 +439,13 @@
|
||||||
],
|
],
|
||||||
"version": "==0.3"
|
"version": "==0.3"
|
||||||
},
|
},
|
||||||
|
"filelock": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
|
||||||
|
"sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
|
||||||
|
],
|
||||||
|
"version": "==3.0.12"
|
||||||
|
},
|
||||||
"flake8": {
|
"flake8": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
|
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
|
||||||
|
@ -419,115 +456,115 @@
|
||||||
},
|
},
|
||||||
"grpcio": {
|
"grpcio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:066630f6b62bffa291dacbee56994279a6a3682b8a11967e9ccaf3cc770fc11e",
|
"sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6",
|
||||||
"sha256:07e95762ca6b18afbeb3aa2793e827c841152d5e507089b1db0b18304edda105",
|
"sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045",
|
||||||
"sha256:0a0fb2f8e3a13537106bc77e4c63005bc60124a6203034304d9101921afa4e90",
|
"sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1",
|
||||||
"sha256:0c61b74dcfb302613926e785cb3542a0905b9a3a86e9410d8cf5d25e25e10104",
|
"sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00",
|
||||||
"sha256:13383bd70618da03684a8aafbdd9e3d9a6720bf8c07b85d0bc697afed599d8f0",
|
"sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942",
|
||||||
"sha256:1c6e0f6b9d091e3717e9a58d631c8bb4898be3b261c2a01fe46371fdc271052f",
|
"sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8",
|
||||||
"sha256:1cf710c04689daa5cc1e598efba00b028215700dcc1bf66fcb7b4f64f2ea5d5f",
|
"sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a",
|
||||||
"sha256:2da5cee9faf17bb8daf500cd0d28a17ae881ab5500f070a6aace457f4c08cac4",
|
"sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c",
|
||||||
"sha256:2f78ebf340eaf28fa09aba0f836a8b869af1716078dfe8f3b3f6ff785d8f2b0f",
|
"sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6",
|
||||||
"sha256:33a07a1a8e817d733588dbd18e567caad1a6fe0d440c165619866cd490c7911a",
|
"sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a",
|
||||||
"sha256:3d090c66af9c065b7228b07c3416f93173e9839b1d40bb0ce3dd2aa783645026",
|
"sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1",
|
||||||
"sha256:42b903a3596a10e2a3727bae2a76f8aefd324d498424b843cfa9606847faea7b",
|
"sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961",
|
||||||
"sha256:4fffbb58134c4f23e5a8312ac3412db6f5e39e961dc0eb5e3115ce5aa16bf927",
|
"sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7",
|
||||||
"sha256:57be5a6c509a406fe0ffa6f8b86904314c77b5e2791be8123368ad2ebccec874",
|
"sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386",
|
||||||
"sha256:5b0fa09efb33e2af4e8822b4eb8b2cbc201d562e3e185c439be7eaeee2e8b8aa",
|
"sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e",
|
||||||
"sha256:5ef42dfc18f9a63a06aca938770b69470bb322e4c137cf08cf21703d1ef4ae5c",
|
"sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866",
|
||||||
"sha256:6a43d2f2ff8250f200fdf7aa31fa191a997922aa9ea1182453acd705ad83ab72",
|
"sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d",
|
||||||
"sha256:6d8ab28559be98b02f8b3a154b53239df1aa5b0d28ff865ae5be4f30e7ed4d3f",
|
"sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371",
|
||||||
"sha256:6e47866b7dc14ca3a12d40c1d6082e7bea964670f1c5315ea0fb8b0550244d64",
|
"sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6",
|
||||||
"sha256:6edda1b96541187f73aab11800d25f18ee87e53d5f96bb74473873072bf28a0e",
|
"sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb",
|
||||||
"sha256:7109c8738a8a3c98cfb5dda1c45642a8d6d35dc00d257ab7a175099b2b4daecd",
|
"sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3",
|
||||||
"sha256:8d866aafb08657c456a18c4a31c8526ea62de42427c242b58210b9eae6c64559",
|
"sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6",
|
||||||
"sha256:9939727d9ae01690b24a2b159ac9dbca7b7e8e6edd5af6a6eb709243cae7b52b",
|
"sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47",
|
||||||
"sha256:99fd873699df17cb11c542553270ae2b32c169986e475df0d68a8629b8ef4df7",
|
"sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454",
|
||||||
"sha256:b6fda5674f990e15e1bcaacf026428cf50bce36e708ddcbd1de9673b14aab760",
|
"sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e",
|
||||||
"sha256:bdb2f3dcb664f0c39ef1312cd6acf6bc6375252e4420cf8f36fff4cb4fa55c71",
|
"sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8",
|
||||||
"sha256:bfd7d3130683a1a0a50c456273c21ec8a604f2d043b241a55235a78a0090ee06",
|
"sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c",
|
||||||
"sha256:c6c2db348ac73d73afe14e0833b18abbbe920969bf2c5c03c0922719f8020d06",
|
"sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4",
|
||||||
"sha256:cb7a4b41b5e2611f85c3402ac364f1d689f5d7ecbc24a55ef010eedcd6cf460f",
|
"sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7",
|
||||||
"sha256:cd3d3e328f20f7c807a862620c6ee748e8d57ba2a8fc960d48337ed71c6d9d32",
|
"sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6",
|
||||||
"sha256:d1a481777952e4f99b8a6956581f3ee866d7614100d70ae6d7e07327570b85ce",
|
"sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85",
|
||||||
"sha256:d1d49720ed636920bb3d74cedf549382caa9ad55aea89d1de99d817068d896b2",
|
"sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571",
|
||||||
"sha256:d42433f0086cccd192114343473d7dbd4aae9141794f939e2b7b83efc57543db",
|
"sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345",
|
||||||
"sha256:d44c34463a7c481e076f691d8fa25d080c3486978c2c41dca09a8dd75296c2d7",
|
"sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb",
|
||||||
"sha256:d7e5b7af1350e9c8c17a7baf99d575fbd2de69f7f0b0e6ebd47b57506de6493a",
|
"sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49",
|
||||||
"sha256:d9542366a0917b9b48bab1fee481ac01f56bdffc52437b598c09e7840148a6a9",
|
"sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8",
|
||||||
"sha256:df7cdfb40179acc9790a462c049e0b8e109481164dd7ad1a388dd67ff1528759",
|
"sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844",
|
||||||
"sha256:e1a9d9d2e7224d981aea8da79260c7f6932bf31ce1f99b7ccfa5eceeb30dc5d0",
|
"sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54",
|
||||||
"sha256:ed10e5fad105ecb0b12822f924e62d0deb07f46683a0b64416b17fd143daba1d",
|
"sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca",
|
||||||
"sha256:f0ec5371ce2363b03531ed522bfbe691ec940f51f0e111f0500fc0f44518c69d",
|
"sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5",
|
||||||
"sha256:f6580a8a4f5e701289b45fd62a8f6cb5ec41e4d77082424f8b676806dcd22564",
|
"sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3",
|
||||||
"sha256:f7b83e4b2842d44fce3cdc0d54db7a7e0d169a598751bf393601efaa401c83e0",
|
"sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed",
|
||||||
"sha256:ffec45b0db18a555fdfe0c6fa2d0a3fceb751b22b31e8fcd14ceed7bde05481e"
|
"sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b"
|
||||||
],
|
],
|
||||||
"version": "==1.26.0"
|
"version": "==1.27.2"
|
||||||
},
|
},
|
||||||
"grpcio-tools": {
|
"grpcio-tools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0286f704e55e3012fec3910400fe1a4ed11aeb66d3ec4b7f8041845af7fb7206",
|
"sha256:00c5080cfb197ed20ecf0d0ff2d07f1fc9c42c724cad21c40ff2d048de5712b1",
|
||||||
"sha256:033a4e80dc78d9c11860800bd5a66b65ff385be8f669e96b02e795364c860597",
|
"sha256:069826dd02ce1886444cf4519c4fe1b05ac9ef41491f26e97400640531db47f6",
|
||||||
"sha256:0e3b5469912430f19407ebe14cfd1bece1b5a277c4d43e1b65dbff19d9475ccc",
|
"sha256:1266b577abe7c720fd16a83d0a4999a192e87c4a98fc9f97e0b99b106b3e155f",
|
||||||
"sha256:131aa8c3862a555819428856f872ab9e919e351d7cd60c98012e12d2fb6afc45",
|
"sha256:16dc3fad04fe18d50777c56af7b2d9b9984cd1cfc71184646eb431196d1645c6",
|
||||||
"sha256:1783b8fa74f58a643e7780112fc4eb6110789672e852a691fad6af6b94a90c4a",
|
"sha256:1de5a273eaffeb3d126a63345e9e848ea7db740762f700eb8b5d84c5e3e7687d",
|
||||||
"sha256:1e80f74854bd1c7263942e836d69f95ffc66bb45bf14bf3e1ab61113271b5884",
|
"sha256:2ca280af2cae1a014a238057bd3c0a254527569a6a9169a01c07f0590081d530",
|
||||||
"sha256:27ae784acff3d2fa04e3b4dc72f8d60a55d654f90e410adf08f46a4d2d673dd3",
|
"sha256:43a1573400527a23e4174d88604fde7a9d9a69bf9473c21936b7f409858f8ebb",
|
||||||
"sha256:33c6bee5a02408018dc10a5737818d2159f14cbb0613df41cc93ba6cbaeea095",
|
"sha256:4698c6b6a57f73b14d91a542c69ff33a2da8729691b7060a5d7f6383624d045e",
|
||||||
"sha256:376a1840d1f5d25e9c3391557d6b3eeb3de17be697b0e55d8247d0262fcbaacf",
|
"sha256:520b7dafddd0f82cb7e4f6e9c6ba1049aa804d0e207870def9fe7f94d1e14090",
|
||||||
"sha256:3922dffd8160d54dc00c7d32b30776a974cc098086493c668faffac19e752087",
|
"sha256:57f8b9e2c7f55cd45f6dd930d6de61deb42d3eb7f9788137fbc7155cf724132a",
|
||||||
"sha256:4ba7e5afc93b413bbb5f3dd65ba583e078ff5895a5053d825ab793cf7720ae96",
|
"sha256:59fbeb5bb9a7b94eb61642ac2cee1db5233b8094ca76fc56d4e0c6c20b5dd85f",
|
||||||
"sha256:4e9a1276f8699d06518cec8caceb2c423fc7f971765cab7550d39f281795fd81",
|
"sha256:5fd7efc2fd3370bd2c72dc58f31a407a5dff5498befa145da211b2e8c6a52c63",
|
||||||
"sha256:51ac9c4f8a542cd20c6776fde781c84c0acd8faba55ec14f121c6b4eb4245e89",
|
"sha256:6016c07d6566e3109a3c032cf3861902d66501ecc08a5a84c47e43027302f367",
|
||||||
"sha256:5580b86cf49936c9c74f0def44d3582a7a1bb720eba8a14805c3a61efa790c70",
|
"sha256:627c91923df75091d8c4d244af38d5ab7ed8d786d480751d6c2b9267fbb92fe0",
|
||||||
"sha256:58a879208bd84d6819a61c1b0618655574ef9df1d63a0e2f434fdcb5cfa1fb57",
|
"sha256:69c4a63919b9007e845d9f8980becd2f89d808a4a431ca32b9723ee37b521cb1",
|
||||||
"sha256:675918f83fa35bd54f4c29d95d8652c6215d5e95a13b6f14e626cdef6d0fce79",
|
"sha256:77e25c241e33b75612f2aa62985f746c6f6803ec4e452da508bb7f8d90a69db4",
|
||||||
"sha256:68259fd06188951d152665ffe44f9660edd715c102ae4bc4216eca4c4666dadf",
|
"sha256:7a2d5fb558ac153a326e742ebfd7020eb781c43d3ffd920abd42b2e6c6fdfb37",
|
||||||
"sha256:6cea124cbd9081a587e1954b98e9a27c7cca6ae72babc3046ab6b439a5730679",
|
"sha256:7b54b283ec83190680903a9037376dc915e1f03852a2d574ba4d981b7a1fd3d0",
|
||||||
"sha256:6f356a445ba7afc634b1046d9f51d3ae37afbf4fe1a500285aca37677462a7b9",
|
"sha256:845a51305af9fc7f9e2078edaec9a759153195f6cf1fbb12b1fa6f077e56b260",
|
||||||
"sha256:7f7430434bd997584f2136a675559ba0d4afdf7cb71d9bbc429b0cc831e6828c",
|
"sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88",
|
||||||
"sha256:809d60f15a32c21dc221ddb591aff8adfdde4e05095414eb8e015cdfef361615",
|
"sha256:87e8ca2c2d2d3e09b2a2bed5d740d7b3e64028dafb7d6be543b77eec85590736",
|
||||||
"sha256:826c19f26b41e99691e77823ad67f04dc0b69e514212907695e330c6f106415c",
|
"sha256:8e7738a4b93842bca1158cde81a3587c9b7111823e40a1ddf73292ca9d58e08b",
|
||||||
"sha256:96c6f657b93f49243d083840d27a5a686a1fc26044a80ebf8585734d5152d4ee",
|
"sha256:915a695bc112517af48126ee0ecdb6aff05ed33f3eeef28f0d076f1f6b52ef5e",
|
||||||
"sha256:9a2091371298f04ef350f776365945537d0befa95bad5623d80c4207bdff9d3a",
|
"sha256:99961156a36aae4a402d6b14c1e7efde642794b3ddbf32c51db0cb3a199e8b11",
|
||||||
"sha256:9af72b764b41ba939e8e0a7ae9ec8a17d1c46a18797c6342cba6483f29e1790f",
|
"sha256:9ba88c2d99bcaf7b9cb720925e3290d73b2367d238c5779363fd5598b2dc98c7",
|
||||||
"sha256:a209002e3d4787f0e90e29f15cddbe83dc9054238c0da7f539c913002a348cc1",
|
"sha256:a140bf853edb2b5e8692fe94869e3e34077d7599170c113d07a58286c604f4fe",
|
||||||
"sha256:a908d5af2f26673e970c7c03703437bf95d10e88dad3322e7e267467db44a04d",
|
"sha256:a14dc7a36c845991d908a7179502ca47bcba5ae1817c4426ce68cf2c97b20ad9",
|
||||||
"sha256:ab841c69581085b6f9aa54044a13db6ec31183513f7cce0862d29c9b7b4e3c64",
|
"sha256:a3d2aec4b09c8e59fee8b0d1ed668d09e8c48b738f03f5d8401d7eb409111c47",
|
||||||
"sha256:b1bc78efefb8e085c072add2c02326fdecad9b8644b3be11e715ea4c6102ad87",
|
"sha256:a8f892378b0b02526635b806f59141abbb429d19bec56e869e04f396502c9651",
|
||||||
"sha256:b97e74ffe121dfa9ae7ec94393fce4e95e9e0a343827663e989dc4b7c918d1a5",
|
"sha256:aaa5ae26883c3d58d1a4323981f96b941fa09bb8f0f368d97c6225585280cf04",
|
||||||
"sha256:bba8d3b61ec113bb94596599d2568217b22ddfc7baa46c00dec5106cfd4e914b",
|
"sha256:b56caecc16307b088a431a4038c3b3bb7d0e7f9988cbd0e9fa04ac937455ea38",
|
||||||
"sha256:bfe0e33aea60da100b214c72c1746cc0194bb8da910004518c185041cc795543",
|
"sha256:bd7f59ff1252a3db8a143b13ea1c1e93d4b8cf4b852eb48b22ef1e6942f62a84",
|
||||||
"sha256:c15f0718cbc3986e747d5b0734198dce0ac07d188ec5e063b1e9889ac947f86e",
|
"sha256:c1bb8f47d58e9f7c4825abfe01e6b85eda53c8b31d2267ca4cddf3c4d0829b80",
|
||||||
"sha256:c56d0ac769bf1f01dbb6ec6b6492849e70cd35bdeeb660e206a70ab43917ae92",
|
"sha256:d1a5e5fa47ba9557a7d3b31605631805adc66cdba9d95b5d10dfc52cca1fed53",
|
||||||
"sha256:d396fdb7026986e6d3897bb207cc7d5bc536a82a2e50af806a24b3d254c73bc3",
|
"sha256:dcbc06556f3713a9348c4fce02d05d91e678fc320fb2bcf0ddf8e4bb11d17867",
|
||||||
"sha256:d62ab00dea7fa0813fc813a6c848da2eeda5cb71893b892a229d23949de0cecd",
|
"sha256:e17b2e0936b04ced99769e26111e1e86ba81619d1b2691b1364f795e45560953",
|
||||||
"sha256:da75e33e185c8be17a82ec4a97f5c75ec05d57e85f8b285f86e2a22484849e4a",
|
"sha256:e6932518db389ede8bf06b4119bbd3e17f42d4626e72dec2b8955b20ec732cb6",
|
||||||
"sha256:dcbd1fbb540638c9ad9c3a071b392b654f79666a2bc12808080b0e9f674b9a80",
|
"sha256:ea4b3ad696d976d5eac74ec8df9a2c692113e455446ee38d5b3bd87f8e034fa6",
|
||||||
"sha256:e7e90bad5466347a3648358e9f437e72d5f6d6025fe741171a88aca8b9d864df",
|
"sha256:ee50b0cf0d28748ef9f941894eb50fc464bd61b8e96aaf80c5056bea9b80d580",
|
||||||
"sha256:eae371a663ceeef8f930323a120a9d11e13e1c49903a66ddb4ada4830d5bcb7d",
|
"sha256:ef624b6134aef737b3daa4fb7e806cb8c5749efecd0b1fa9ce4f7e060c7a0221",
|
||||||
"sha256:f290cccc972533a288c2ebc55eb3c0fbe0c6a0d0a9775cb34ce6bfb11fe14a11",
|
"sha256:f5450aa904e720f9c6407b59e96a8951ed6a95463f49444b6d2594b067d39588",
|
||||||
"sha256:facb8c588cdd6adc51ae7545f59283565dae8d946c6163e578b70ab6bf161215",
|
"sha256:f8514453411d72cc3cf7d481f2b6057e5b7436736d0cd39ee2b2f72088bbf497",
|
||||||
"sha256:fb043e45f91634776acdfe4b8dfc96b636c53a458799179041ab633e15c3d833"
|
"sha256:fae91f30dc050a8d0b32d20dc700e6092f0bd2138d83e9570fff3f0372c1b27e"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.26.0"
|
"version": "==1.27.2"
|
||||||
},
|
},
|
||||||
"identify": {
|
"identify": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:418f3b2313ac0b531139311a6b426854e9cbdfcfb6175447a5039aa6291d8b30",
|
"sha256:1222b648251bdcb8deb240b294f450fbf704c7984e08baa92507e4ea10b436d5",
|
||||||
"sha256:8ad99ed1f3a965612dcb881435bf58abcfbeb05e230bb8c352b51e8eac103360"
|
"sha256:d824ebe21f38325c771c41b08a95a761db1982f1fc0eee37c6c97df3f1636b96"
|
||||||
],
|
],
|
||||||
"version": "==1.4.10"
|
"version": "==1.4.11"
|
||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
"sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302",
|
||||||
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
|
"sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '3.8'",
|
"markers": "python_version < '3.8'",
|
||||||
"version": "==1.4.0"
|
"version": "==1.5.0"
|
||||||
},
|
},
|
||||||
"importlib-resources": {
|
"importlib-resources": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -554,24 +591,24 @@
|
||||||
},
|
},
|
||||||
"mock": {
|
"mock": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3",
|
"sha256:2a572b715f09dd2f0a583d8aeb5bb67d7ed7a8fd31d193cf1227a99c16a67bc3",
|
||||||
"sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"
|
"sha256:5e48d216809f6f393987ed56920305d8f3c647e6ed35407c1ff2ecb88a9e1151"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.0.5"
|
"version": "==4.0.1"
|
||||||
},
|
},
|
||||||
"more-itertools": {
|
"more-itertools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
|
"sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
|
||||||
"sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
|
"sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
|
||||||
],
|
],
|
||||||
"version": "==8.1.0"
|
"version": "==8.2.0"
|
||||||
},
|
},
|
||||||
"nodeenv": {
|
"nodeenv": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:561057acd4ae3809e665a9aaaf214afff110bbb6a6d5c8a96121aea6878408b3"
|
"sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212"
|
||||||
],
|
],
|
||||||
"version": "==1.3.4"
|
"version": "==1.3.5"
|
||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -589,34 +626,34 @@
|
||||||
},
|
},
|
||||||
"pre-commit": {
|
"pre-commit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850",
|
"sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6",
|
||||||
"sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"
|
"sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.21.0"
|
"version": "==2.1.1"
|
||||||
},
|
},
|
||||||
"protobuf": {
|
"protobuf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37",
|
"sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab",
|
||||||
"sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36",
|
"sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f",
|
||||||
"sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4",
|
"sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a",
|
||||||
"sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d",
|
"sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0",
|
||||||
"sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574",
|
"sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4",
|
||||||
"sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0",
|
"sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2",
|
||||||
"sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5",
|
"sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee",
|
||||||
"sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f",
|
"sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07",
|
||||||
"sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946",
|
"sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151",
|
||||||
"sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104",
|
"sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a",
|
||||||
"sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e",
|
"sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f",
|
||||||
"sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7",
|
"sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7",
|
||||||
"sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a",
|
"sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956",
|
||||||
"sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507",
|
"sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306",
|
||||||
"sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1",
|
"sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961",
|
||||||
"sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593",
|
"sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481",
|
||||||
"sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c",
|
"sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a",
|
||||||
"sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1"
|
"sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80"
|
||||||
],
|
],
|
||||||
"version": "==3.11.2"
|
"version": "==3.11.3"
|
||||||
},
|
},
|
||||||
"py": {
|
"py": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -648,11 +685,11 @@
|
||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1d122e8be54d1a709e56f82e2d85dcba3018313d64647f38a91aec88c239b600",
|
"sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d",
|
||||||
"sha256:c13d1943c63e599b98cf118fcb9703e4d7bde7caa9a432567bcdcae4bf512d20"
|
"sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.3.4"
|
"version": "==5.3.5"
|
||||||
},
|
},
|
||||||
"pyyaml": {
|
"pyyaml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -686,10 +723,10 @@
|
||||||
},
|
},
|
||||||
"virtualenv": {
|
"virtualenv": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3",
|
"sha256:30ea90b21dabd11da5f509710ad3be2ae47d40ccbc717dfdd2efe4367c10f598",
|
||||||
"sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb"
|
"sha256:4a36a96d785428278edd389d9c36d763c5755844beb7509279194647b1ef47f1"
|
||||||
],
|
],
|
||||||
"version": "==16.7.9"
|
"version": "==20.0.7"
|
||||||
},
|
},
|
||||||
"wcwidth": {
|
"wcwidth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -700,10 +737,10 @@
|
||||||
},
|
},
|
||||||
"zipp": {
|
"zipp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
|
"sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2",
|
||||||
"sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67"
|
"sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a"
|
||||||
],
|
],
|
||||||
"version": "==2.0.1"
|
"version": "==3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ from core.api.grpc.configservices_pb2 import (
|
||||||
SetNodeConfigServiceResponse,
|
SetNodeConfigServiceResponse,
|
||||||
)
|
)
|
||||||
from core.api.grpc.core_pb2 import (
|
from core.api.grpc.core_pb2 import (
|
||||||
|
ExecuteScriptRequest,
|
||||||
|
ExecuteScriptResponse,
|
||||||
GetEmaneEventChannelRequest,
|
GetEmaneEventChannelRequest,
|
||||||
GetEmaneEventChannelResponse,
|
GetEmaneEventChannelResponse,
|
||||||
)
|
)
|
||||||
|
@ -146,8 +148,9 @@ def start_streamer(stream: Any, handler: Callable[[core_pb2.Event], None]) -> No
|
||||||
:param handler: function that handles an event
|
:param handler: function that handles an event
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
thread = threading.Thread(target=stream_listener, args=(stream, handler))
|
thread = threading.Thread(
|
||||||
thread.daemon = True
|
target=stream_listener, args=(stream, handler), daemon=True
|
||||||
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
@ -259,6 +262,16 @@ class CoreGrpcClient:
|
||||||
"""
|
"""
|
||||||
return self.stub.GetSessions(core_pb2.GetSessionsRequest())
|
return self.stub.GetSessions(core_pb2.GetSessionsRequest())
|
||||||
|
|
||||||
|
def check_session(self, session_id: int) -> core_pb2.CheckSessionResponse:
|
||||||
|
"""
|
||||||
|
Check if a session exists.
|
||||||
|
|
||||||
|
:param session_id: id of session to check for
|
||||||
|
:return: response with result if session was found
|
||||||
|
"""
|
||||||
|
request = core_pb2.CheckSessionRequest(session_id=session_id)
|
||||||
|
return self.stub.CheckSession(request)
|
||||||
|
|
||||||
def get_session(self, session_id: int) -> core_pb2.GetSessionResponse:
|
def get_session(self, session_id: int) -> core_pb2.GetSessionResponse:
|
||||||
"""
|
"""
|
||||||
Retrieve a session.
|
Retrieve a session.
|
||||||
|
@ -472,9 +485,10 @@ class CoreGrpcClient:
|
||||||
self,
|
self,
|
||||||
session_id: int,
|
session_id: int,
|
||||||
node_id: int,
|
node_id: int,
|
||||||
position: core_pb2.Position,
|
position: core_pb2.Position = None,
|
||||||
icon: str = None,
|
icon: str = None,
|
||||||
source: str = None,
|
source: str = None,
|
||||||
|
geo: core_pb2.Geo = None,
|
||||||
) -> core_pb2.EditNodeResponse:
|
) -> core_pb2.EditNodeResponse:
|
||||||
"""
|
"""
|
||||||
Edit a node, currently only changes position.
|
Edit a node, currently only changes position.
|
||||||
|
@ -484,6 +498,7 @@ class CoreGrpcClient:
|
||||||
:param position: position to set node to
|
:param position: position to set node to
|
||||||
:param icon: path to icon for gui to use for node
|
:param icon: path to icon for gui to use for node
|
||||||
:param source: application source editing node
|
:param source: application source editing node
|
||||||
|
:param geo: lon,lat,alt location for node
|
||||||
:return: response with result of success or failure
|
:return: response with result of success or failure
|
||||||
:raises grpc.RpcError: when session or node doesn't exist
|
:raises grpc.RpcError: when session or node doesn't exist
|
||||||
"""
|
"""
|
||||||
|
@ -493,6 +508,7 @@ class CoreGrpcClient:
|
||||||
position=position,
|
position=position,
|
||||||
icon=icon,
|
icon=icon,
|
||||||
source=source,
|
source=source,
|
||||||
|
geo=geo,
|
||||||
)
|
)
|
||||||
return self.stub.EditNode(request)
|
return self.stub.EditNode(request)
|
||||||
|
|
||||||
|
@ -1147,6 +1163,10 @@ class CoreGrpcClient:
|
||||||
request = GetEmaneEventChannelRequest(session_id=session_id)
|
request = GetEmaneEventChannelRequest(session_id=session_id)
|
||||||
return self.stub.GetEmaneEventChannel(request)
|
return self.stub.GetEmaneEventChannel(request)
|
||||||
|
|
||||||
|
def execute_script(self, script: str) -> ExecuteScriptResponse:
|
||||||
|
request = ExecuteScriptRequest(script=script)
|
||||||
|
return self.stub.ExecuteScript(request)
|
||||||
|
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
"""
|
"""
|
||||||
Open connection to server, must be closed manually.
|
Open connection to server, must be closed manually.
|
||||||
|
|
|
@ -44,7 +44,9 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
|
||||||
|
|
||||||
position = node_proto.position
|
position = node_proto.position
|
||||||
options.set_position(position.x, position.y)
|
options.set_position(position.x, position.y)
|
||||||
options.set_location(position.lat, position.lon, position.alt)
|
if node_proto.HasField("geo"):
|
||||||
|
geo = node_proto.geo
|
||||||
|
options.set_location(geo.lat, geo.lon, geo.alt)
|
||||||
return _type, _id, options
|
return _type, _id, options
|
||||||
|
|
||||||
|
|
||||||
|
@ -377,9 +379,9 @@ def service_configuration(session: Session, config: core_pb2.ServiceConfig) -> N
|
||||||
session.services.set_service(config.node_id, config.service)
|
session.services.set_service(config.node_id, config.service)
|
||||||
service = session.services.get_service(config.node_id, config.service)
|
service = session.services.get_service(config.node_id, config.service)
|
||||||
if config.files:
|
if config.files:
|
||||||
service.files = tuple(config.files)
|
service.configs = tuple(config.files)
|
||||||
if config.directories:
|
if config.directories:
|
||||||
service.directories = tuple(config.directories)
|
service.dirs = tuple(config.directories)
|
||||||
if config.startup:
|
if config.startup:
|
||||||
service.startup = tuple(config.startup)
|
service.startup = tuple(config.startup)
|
||||||
if config.validate:
|
if config.validate:
|
||||||
|
|
|
@ -3,6 +3,7 @@ import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from concurrent import futures
|
from concurrent import futures
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
@ -10,6 +11,7 @@ from typing import Type
|
||||||
import grpc
|
import grpc
|
||||||
from grpc import ServicerContext
|
from grpc import ServicerContext
|
||||||
|
|
||||||
|
from core import utils
|
||||||
from core.api.grpc import (
|
from core.api.grpc import (
|
||||||
common_pb2,
|
common_pb2,
|
||||||
configservices_pb2,
|
configservices_pb2,
|
||||||
|
@ -33,6 +35,7 @@ from core.api.grpc.configservices_pb2 import (
|
||||||
SetNodeConfigServiceResponse,
|
SetNodeConfigServiceResponse,
|
||||||
)
|
)
|
||||||
from core.api.grpc.core_pb2 import (
|
from core.api.grpc.core_pb2 import (
|
||||||
|
ExecuteScriptResponse,
|
||||||
GetEmaneEventChannelRequest,
|
GetEmaneEventChannelRequest,
|
||||||
GetEmaneEventChannelResponse,
|
GetEmaneEventChannelResponse,
|
||||||
)
|
)
|
||||||
|
@ -450,6 +453,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
session.metadata = dict(request.config)
|
session.metadata = dict(request.config)
|
||||||
return core_pb2.SetSessionMetadataResponse(result=True)
|
return core_pb2.SetSessionMetadataResponse(result=True)
|
||||||
|
|
||||||
|
def CheckSession(
|
||||||
|
self, request: core_pb2.GetSessionRequest, context: ServicerContext
|
||||||
|
) -> core_pb2.CheckSessionResponse:
|
||||||
|
"""
|
||||||
|
Checks if a session exists.
|
||||||
|
|
||||||
|
:param request: check session request
|
||||||
|
:param context: context object
|
||||||
|
:return: check session response
|
||||||
|
"""
|
||||||
|
result = request.session_id in self.coreemu.sessions
|
||||||
|
return core_pb2.CheckSessionResponse(result=result)
|
||||||
|
|
||||||
def GetSession(
|
def GetSession(
|
||||||
self, request: core_pb2.GetSessionRequest, context: ServicerContext
|
self, request: core_pb2.GetSessionRequest, context: ServicerContext
|
||||||
) -> core_pb2.GetSessionResponse:
|
) -> core_pb2.GetSessionResponse:
|
||||||
|
@ -685,12 +701,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
node = self.get_node(session, request.node_id, context)
|
node = self.get_node(session, request.node_id, context)
|
||||||
options = NodeOptions()
|
options = NodeOptions()
|
||||||
options.icon = request.icon
|
options.icon = request.icon
|
||||||
|
if request.HasField("position"):
|
||||||
x = request.position.x
|
x = request.position.x
|
||||||
y = request.position.y
|
y = request.position.y
|
||||||
options.set_position(x, y)
|
options.set_position(x, y)
|
||||||
lat = request.position.lat
|
lat, lon, alt = None, None, None
|
||||||
lon = request.position.lon
|
has_geo = request.HasField("geo")
|
||||||
alt = request.position.alt
|
if has_geo:
|
||||||
|
lat = request.geo.lat
|
||||||
|
lon = request.geo.lon
|
||||||
|
alt = request.geo.alt
|
||||||
options.set_location(lat, lon, alt)
|
options.set_location(lat, lon, alt)
|
||||||
result = True
|
result = True
|
||||||
try:
|
try:
|
||||||
|
@ -698,6 +718,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
source = None
|
source = None
|
||||||
if request.source:
|
if request.source:
|
||||||
source = request.source
|
source = request.source
|
||||||
|
if not has_geo:
|
||||||
node_data = node.data(0, source=source)
|
node_data = node.data(0, source=source)
|
||||||
session.broadcast_node(node_data)
|
session.broadcast_node(node_data)
|
||||||
except CoreError:
|
except CoreError:
|
||||||
|
@ -1645,3 +1666,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||||
if session.emane.eventchannel:
|
if session.emane.eventchannel:
|
||||||
group, port, device = session.emane.eventchannel
|
group, port, device = session.emane.eventchannel
|
||||||
return GetEmaneEventChannelResponse(group=group, port=port, device=device)
|
return GetEmaneEventChannelResponse(group=group, port=port, device=device)
|
||||||
|
|
||||||
|
def ExecuteScript(self, request, context):
|
||||||
|
existing_sessions = set(self.coreemu.sessions.keys())
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=utils.execute_file,
|
||||||
|
args=(
|
||||||
|
request.script,
|
||||||
|
{"__file__": request.script, "coreemu": self.coreemu},
|
||||||
|
),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
thread.join()
|
||||||
|
current_sessions = set(self.coreemu.sessions.keys())
|
||||||
|
new_sessions = list(current_sessions.difference(existing_sessions))
|
||||||
|
new_session = -1
|
||||||
|
if new_sessions:
|
||||||
|
new_session = new_sessions[0]
|
||||||
|
return ExecuteScriptResponse(session_id=new_session)
|
||||||
|
|
|
@ -526,11 +526,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"%s handling message:\n%s", threading.currentThread().getName(), message
|
"%s handling message:\n%s", threading.currentThread().getName(), message
|
||||||
)
|
)
|
||||||
|
|
||||||
# provide to sdt, if enabled
|
|
||||||
if self.session and self.session.sdt.is_enabled():
|
|
||||||
self.session.sdt.handle_distributed(message)
|
|
||||||
|
|
||||||
if message.message_type not in self.message_handlers:
|
if message.message_type not in self.message_handlers:
|
||||||
logging.error("no handler for message type: %s", message.type_str())
|
logging.error("no handler for message type: %s", message.type_str())
|
||||||
return
|
return
|
||||||
|
@ -949,11 +944,10 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
file_name,
|
file_name,
|
||||||
{"__file__": file_name, "coreemu": self.coreemu},
|
{"__file__": file_name, "coreemu": self.coreemu},
|
||||||
),
|
),
|
||||||
|
daemon=True,
|
||||||
)
|
)
|
||||||
thread.daemon = True
|
|
||||||
thread.start()
|
thread.start()
|
||||||
# allow time for session creation
|
thread.join()
|
||||||
time.sleep(0.25)
|
|
||||||
|
|
||||||
if message.flags & MessageFlags.STRING.value:
|
if message.flags & MessageFlags.STRING.value:
|
||||||
new_session_ids = set(self.coreemu.sessions.keys())
|
new_session_ids = set(self.coreemu.sessions.keys())
|
||||||
|
@ -1128,7 +1122,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
||||||
self.session.location.refgeo,
|
self.session.location.refgeo,
|
||||||
self.session.location.refscale,
|
self.session.location.refscale,
|
||||||
)
|
)
|
||||||
logging.info("location configured: UTM%s", self.session.location.refutm)
|
|
||||||
|
|
||||||
def handle_config_metadata(self, message_type, config_data):
|
def handle_config_metadata(self, message_type, config_data):
|
||||||
replies = []
|
replies = []
|
||||||
|
@ -2044,7 +2037,6 @@ class CoreUdpHandler(CoreHandler):
|
||||||
logging.debug("session handling message: %s", session.session_id)
|
logging.debug("session handling message: %s", session.session_id)
|
||||||
self.session = session
|
self.session = session
|
||||||
self.handle_message(message)
|
self.handle_message(message)
|
||||||
self.session.sdt.handle_distributed(message)
|
|
||||||
self.broadcast(message)
|
self.broadcast(message)
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
|
@ -2069,7 +2061,6 @@ class CoreUdpHandler(CoreHandler):
|
||||||
if session or message.message_type == MessageTypes.REGISTER.value:
|
if session or message.message_type == MessageTypes.REGISTER.value:
|
||||||
self.session = session
|
self.session = session
|
||||||
self.handle_message(message)
|
self.handle_message(message)
|
||||||
self.session.sdt.handle_distributed(message)
|
|
||||||
self.broadcast(message)
|
self.broadcast(message)
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
|
|
|
@ -31,9 +31,9 @@ def convert_node(node_data):
|
||||||
(NodeTlvs.CANVAS, node_data.canvas),
|
(NodeTlvs.CANVAS, node_data.canvas),
|
||||||
(NodeTlvs.NETWORK_ID, node_data.network_id),
|
(NodeTlvs.NETWORK_ID, node_data.network_id),
|
||||||
(NodeTlvs.SERVICES, node_data.services),
|
(NodeTlvs.SERVICES, node_data.services),
|
||||||
(NodeTlvs.LATITUDE, node_data.latitude),
|
(NodeTlvs.LATITUDE, str(node_data.latitude)),
|
||||||
(NodeTlvs.LONGITUDE, node_data.longitude),
|
(NodeTlvs.LONGITUDE, str(node_data.longitude)),
|
||||||
(NodeTlvs.ALTITUDE, node_data.altitude),
|
(NodeTlvs.ALTITUDE, str(node_data.altitude)),
|
||||||
(NodeTlvs.ICON, node_data.icon),
|
(NodeTlvs.ICON, node_data.icon),
|
||||||
(NodeTlvs.OPAQUE, node_data.opaque),
|
(NodeTlvs.OPAQUE, node_data.opaque),
|
||||||
],
|
],
|
||||||
|
|
|
@ -328,7 +328,6 @@ class EmaneManager(ModelManager):
|
||||||
nems = []
|
nems = []
|
||||||
with self._emane_node_lock:
|
with self._emane_node_lock:
|
||||||
self.buildxml()
|
self.buildxml()
|
||||||
self.initeventservice()
|
|
||||||
self.starteventmonitor()
|
self.starteventmonitor()
|
||||||
|
|
||||||
if self.numnems() > 0:
|
if self.numnems() > 0:
|
||||||
|
@ -683,8 +682,9 @@ class EmaneManager(ModelManager):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
self.doeventloop = True
|
self.doeventloop = True
|
||||||
self.eventmonthread = threading.Thread(target=self.eventmonitorloop)
|
self.eventmonthread = threading.Thread(
|
||||||
self.eventmonthread.daemon = True
|
target=self.eventmonitorloop, daemon=True
|
||||||
|
)
|
||||||
self.eventmonthread.start()
|
self.eventmonthread.start()
|
||||||
|
|
||||||
def stopeventmonitor(self) -> None:
|
def stopeventmonitor(self) -> None:
|
||||||
|
@ -698,8 +698,6 @@ class EmaneManager(ModelManager):
|
||||||
self.initeventservice(shutdown=True)
|
self.initeventservice(shutdown=True)
|
||||||
|
|
||||||
if self.eventmonthread is not None:
|
if self.eventmonthread is not None:
|
||||||
# TODO: fix this
|
|
||||||
self.eventmonthread._Thread__stop()
|
|
||||||
self.eventmonthread.join()
|
self.eventmonthread.join()
|
||||||
self.eventmonthread = None
|
self.eventmonthread = None
|
||||||
|
|
||||||
|
@ -773,7 +771,7 @@ class EmaneManager(ModelManager):
|
||||||
x = int(x)
|
x = int(x)
|
||||||
y = int(y)
|
y = int(y)
|
||||||
z = int(z)
|
z = int(z)
|
||||||
logging.info(
|
logging.debug(
|
||||||
"location event NEM %s (%s, %s, %s) -> (%s, %s, %s)",
|
"location event NEM %s (%s, %s, %s) -> (%s, %s, %s)",
|
||||||
nemid,
|
nemid,
|
||||||
lat,
|
lat,
|
||||||
|
|
|
@ -37,8 +37,8 @@ from core.emulator.emudata import (
|
||||||
from core.emulator.enumerations import EventTypes, ExceptionLevels, LinkTypes, NodeTypes
|
from core.emulator.enumerations import EventTypes, ExceptionLevels, LinkTypes, NodeTypes
|
||||||
from core.emulator.sessionconfig import SessionConfig
|
from core.emulator.sessionconfig import SessionConfig
|
||||||
from core.errors import CoreError
|
from core.errors import CoreError
|
||||||
from core.location.corelocation import CoreLocation
|
|
||||||
from core.location.event import EventLoop
|
from core.location.event import EventLoop
|
||||||
|
from core.location.geo import GeoLocation
|
||||||
from core.location.mobility import BasicRangeModel, MobilityManager
|
from core.location.mobility import BasicRangeModel, MobilityManager
|
||||||
from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase
|
from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase
|
||||||
from core.nodes.docker import DockerNode
|
from core.nodes.docker import DockerNode
|
||||||
|
@ -146,7 +146,7 @@ class Session:
|
||||||
self.distributed = DistributedController(self)
|
self.distributed = DistributedController(self)
|
||||||
|
|
||||||
# initialize session feature helpers
|
# initialize session feature helpers
|
||||||
self.location = CoreLocation()
|
self.location = GeoLocation()
|
||||||
self.mobility = MobilityManager(session=self)
|
self.mobility = MobilityManager(session=self)
|
||||||
self.services = CoreServices(session=self)
|
self.services = CoreServices(session=self)
|
||||||
self.emane = EmaneManager(session=self)
|
self.emane = EmaneManager(session=self)
|
||||||
|
@ -432,6 +432,7 @@ class Session:
|
||||||
if node_two:
|
if node_two:
|
||||||
node_two.lock.release()
|
node_two.lock.release()
|
||||||
|
|
||||||
|
self.sdt.add_link(node_one_id, node_two_id, is_wireless=False)
|
||||||
return node_one_interface, node_two_interface
|
return node_one_interface, node_two_interface
|
||||||
|
|
||||||
def delete_link(
|
def delete_link(
|
||||||
|
@ -540,6 +541,8 @@ class Session:
|
||||||
if node_two:
|
if node_two:
|
||||||
node_two.lock.release()
|
node_two.lock.release()
|
||||||
|
|
||||||
|
self.sdt.delete_link(node_one_id, node_two_id)
|
||||||
|
|
||||||
def update_link(
|
def update_link(
|
||||||
self,
|
self,
|
||||||
node_one_id: int,
|
node_one_id: int,
|
||||||
|
@ -757,6 +760,7 @@ class Session:
|
||||||
self.add_remove_control_interface(node=node, remove=False)
|
self.add_remove_control_interface(node=node, remove=False)
|
||||||
self.services.boot_services(node)
|
self.services.boot_services(node)
|
||||||
|
|
||||||
|
self.sdt.add_node(node)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
def edit_node(self, node_id: int, options: NodeOptions) -> None:
|
def edit_node(self, node_id: int, options: NodeOptions) -> None:
|
||||||
|
@ -765,7 +769,7 @@ class Session:
|
||||||
|
|
||||||
:param node_id: id of node to update
|
:param node_id: id of node to update
|
||||||
:param options: data to update node with
|
:param options: data to update node with
|
||||||
:return: True if node updated, False otherwise
|
:return: nothing
|
||||||
:raises core.CoreError: when node to update does not exist
|
:raises core.CoreError: when node to update does not exist
|
||||||
"""
|
"""
|
||||||
# get node to update
|
# get node to update
|
||||||
|
@ -778,6 +782,9 @@ class Session:
|
||||||
node.canvas = options.canvas
|
node.canvas = options.canvas
|
||||||
node.icon = options.icon
|
node.icon = options.icon
|
||||||
|
|
||||||
|
# provide edits to sdt
|
||||||
|
self.sdt.edit_node(node, options.lon, options.lat, options.alt)
|
||||||
|
|
||||||
def set_node_position(self, node: NodeBase, options: NodeOptions) -> None:
|
def set_node_position(self, node: NodeBase, options: NodeOptions) -> None:
|
||||||
"""
|
"""
|
||||||
Set position for a node, use lat/lon/alt if needed.
|
Set position for a node, use lat/lon/alt if needed.
|
||||||
|
@ -806,9 +813,11 @@ class Session:
|
||||||
|
|
||||||
# broadcast updated location when using lat/lon/alt
|
# broadcast updated location when using lat/lon/alt
|
||||||
if using_lat_lon_alt:
|
if using_lat_lon_alt:
|
||||||
self.broadcast_node_location(node)
|
self.broadcast_node_location(node, lon, lat, alt)
|
||||||
|
|
||||||
def broadcast_node_location(self, node: NodeBase) -> None:
|
def broadcast_node_location(
|
||||||
|
self, node: NodeBase, lon: float, lat: float, alt: float
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Broadcast node location to all listeners.
|
Broadcast node location to all listeners.
|
||||||
|
|
||||||
|
@ -820,6 +829,9 @@ class Session:
|
||||||
id=node.id,
|
id=node.id,
|
||||||
x_position=node.position.x,
|
x_position=node.position.x,
|
||||||
y_position=node.position.y,
|
y_position=node.position.y,
|
||||||
|
latitude=lat,
|
||||||
|
longitude=lon,
|
||||||
|
altitude=alt,
|
||||||
)
|
)
|
||||||
self.broadcast_node(node_data)
|
self.broadcast_node(node_data)
|
||||||
|
|
||||||
|
@ -1402,6 +1414,7 @@ class Session:
|
||||||
if node:
|
if node:
|
||||||
node.shutdown()
|
node.shutdown()
|
||||||
self.check_shutdown()
|
self.check_shutdown()
|
||||||
|
self.sdt.delete_node(_id)
|
||||||
|
|
||||||
return node is not None
|
return node is not None
|
||||||
|
|
||||||
|
@ -1413,6 +1426,7 @@ class Session:
|
||||||
funcs = []
|
funcs = []
|
||||||
while self.nodes:
|
while self.nodes:
|
||||||
_, node = self.nodes.popitem()
|
_, node = self.nodes.popitem()
|
||||||
|
self.sdt.delete_node(node.id)
|
||||||
funcs.append((node.shutdown, [], {}))
|
funcs.append((node.shutdown, [], {}))
|
||||||
utils.threadpool(funcs)
|
utils.threadpool(funcs)
|
||||||
self.node_id_gen.id = 0
|
self.node_id_gen.id = 0
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import math
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import font, ttk
|
||||||
|
|
||||||
from core.gui import appconfig, themes
|
from core.gui import appconfig, themes
|
||||||
from core.gui.coreclient import CoreClient
|
from core.gui.coreclient import CoreClient
|
||||||
|
@ -29,14 +30,28 @@ class Application(tk.Frame):
|
||||||
self.statusbar = None
|
self.statusbar = None
|
||||||
self.validation = None
|
self.validation = None
|
||||||
|
|
||||||
|
# fonts
|
||||||
|
self.fonts_size = None
|
||||||
|
self.icon_text_font = None
|
||||||
|
self.edge_font = None
|
||||||
|
|
||||||
# setup
|
# setup
|
||||||
self.guiconfig = appconfig.read()
|
self.guiconfig = appconfig.read()
|
||||||
|
self.app_scale = self.guiconfig["scale"]
|
||||||
|
self.setup_scaling()
|
||||||
self.style = ttk.Style()
|
self.style = ttk.Style()
|
||||||
self.setup_theme()
|
self.setup_theme()
|
||||||
self.core = CoreClient(self, proxy)
|
self.core = CoreClient(self, proxy)
|
||||||
self.setup_app()
|
self.setup_app()
|
||||||
self.draw()
|
self.draw()
|
||||||
self.core.set_up()
|
self.core.setup()
|
||||||
|
|
||||||
|
def setup_scaling(self):
|
||||||
|
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
|
||||||
|
text_scale = self.app_scale if self.app_scale < 1 else math.sqrt(self.app_scale)
|
||||||
|
themes.scale_fonts(self.fonts_size, self.app_scale)
|
||||||
|
self.icon_text_font = font.Font(family="TkIconFont", size=int(12 * text_scale))
|
||||||
|
self.edge_font = font.Font(family="TkDefaultFont", size=int(8 * text_scale))
|
||||||
|
|
||||||
def setup_theme(self):
|
def setup_theme(self):
|
||||||
themes.load(self.style)
|
themes.load(self.style)
|
||||||
|
@ -56,9 +71,11 @@ class Application(tk.Frame):
|
||||||
def center(self):
|
def center(self):
|
||||||
screen_width = self.master.winfo_screenwidth()
|
screen_width = self.master.winfo_screenwidth()
|
||||||
screen_height = self.master.winfo_screenheight()
|
screen_height = self.master.winfo_screenheight()
|
||||||
x = int((screen_width / 2) - (WIDTH / 2))
|
x = int((screen_width / 2) - (WIDTH * self.app_scale / 2))
|
||||||
y = int((screen_height / 2) - (HEIGHT / 2))
|
y = int((screen_height / 2) - (HEIGHT * self.app_scale / 2))
|
||||||
self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}")
|
self.master.geometry(
|
||||||
|
f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}"
|
||||||
|
)
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
self.master.option_add("*tearOff", tk.FALSE)
|
self.master.option_add("*tearOff", tk.FALSE)
|
||||||
|
|
|
@ -16,6 +16,7 @@ MOBILITY_PATH = HOME_PATH.joinpath("mobility")
|
||||||
XMLS_PATH = HOME_PATH.joinpath("xmls")
|
XMLS_PATH = HOME_PATH.joinpath("xmls")
|
||||||
CONFIG_PATH = HOME_PATH.joinpath("gui.yaml")
|
CONFIG_PATH = HOME_PATH.joinpath("gui.yaml")
|
||||||
LOG_PATH = HOME_PATH.joinpath("gui.log")
|
LOG_PATH = HOME_PATH.joinpath("gui.log")
|
||||||
|
SCRIPT_PATH = HOME_PATH.joinpath("scripts")
|
||||||
|
|
||||||
# local paths
|
# local paths
|
||||||
DATA_PATH = Path(__file__).parent.joinpath("data")
|
DATA_PATH = Path(__file__).parent.joinpath("data")
|
||||||
|
@ -25,17 +26,16 @@ LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute()
|
||||||
LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute()
|
LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute()
|
||||||
|
|
||||||
# configuration data
|
# configuration data
|
||||||
TERMINALS = [
|
TERMINALS = {
|
||||||
"$TERM",
|
"xterm": "xterm -e",
|
||||||
"gnome-terminal --window --",
|
"aterm": "aterm -e",
|
||||||
"lxterminal -e",
|
"eterm": "eterm -e",
|
||||||
"konsole -e",
|
"rxvt": "rxvt -e",
|
||||||
"xterm -e",
|
"konsole": "konsole -e",
|
||||||
"aterm -e",
|
"lxterminal": "lxterminal -e",
|
||||||
"eterm -e",
|
"xfce4-terminal": "xfce4-terminal -x",
|
||||||
"rxvt -e",
|
"gnome-terminal": "gnome-terminal --window --",
|
||||||
"xfce4-terminal -x",
|
}
|
||||||
]
|
|
||||||
EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,6 +50,14 @@ def copy_files(current_path, new_path):
|
||||||
shutil.copy(current_file, new_file)
|
shutil.copy(current_file, new_file)
|
||||||
|
|
||||||
|
|
||||||
|
def find_terminal():
|
||||||
|
for term in sorted(TERMINALS):
|
||||||
|
cmd = TERMINALS[term]
|
||||||
|
if shutil.which(term):
|
||||||
|
return cmd
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_directory():
|
def check_directory():
|
||||||
if HOME_PATH.exists():
|
if HOME_PATH.exists():
|
||||||
return
|
return
|
||||||
|
@ -60,16 +68,14 @@ def check_directory():
|
||||||
ICONS_PATH.mkdir()
|
ICONS_PATH.mkdir()
|
||||||
MOBILITY_PATH.mkdir()
|
MOBILITY_PATH.mkdir()
|
||||||
XMLS_PATH.mkdir()
|
XMLS_PATH.mkdir()
|
||||||
|
SCRIPT_PATH.mkdir()
|
||||||
|
|
||||||
copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
|
copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
|
||||||
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)
|
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)
|
||||||
copy_files(LOCAL_XMLS_PATH, XMLS_PATH)
|
copy_files(LOCAL_XMLS_PATH, XMLS_PATH)
|
||||||
copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH)
|
copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH)
|
||||||
|
|
||||||
if "TERM" in os.environ:
|
terminal = find_terminal()
|
||||||
terminal = TERMINALS[0]
|
|
||||||
else:
|
|
||||||
terminal = TERMINALS[1]
|
|
||||||
if "EDITOR" in os.environ:
|
if "EDITOR" in os.environ:
|
||||||
editor = EDITORS[0]
|
editor = EDITORS[0]
|
||||||
else:
|
else:
|
||||||
|
@ -96,6 +102,7 @@ def check_directory():
|
||||||
"nodes": [],
|
"nodes": [],
|
||||||
"recentfiles": [],
|
"recentfiles": [],
|
||||||
"observers": [{"name": "hello", "cmd": "echo hello"}],
|
"observers": [{"name": "hello", "cmd": "echo hello"}],
|
||||||
|
"scale": 1.0,
|
||||||
}
|
}
|
||||||
save(config)
|
save(config)
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from tkinter import messagebox
|
||||||
from typing import TYPE_CHECKING, Dict, List
|
from typing import TYPE_CHECKING, Dict, List
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
@ -38,17 +39,6 @@ OBSERVERS = {
|
||||||
"IPSec policies": "setkey -DP",
|
"IPSec policies": "setkey -DP",
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_TERMS = {
|
|
||||||
"xterm": "xterm -e",
|
|
||||||
"aterm": "aterm -e",
|
|
||||||
"eterm": "eterm -e",
|
|
||||||
"rxvt": "rxvt -e",
|
|
||||||
"konsole": "konsole -e",
|
|
||||||
"lxterminal": "lxterminal -e",
|
|
||||||
"xfce4-terminal": "xfce4-terminal -x",
|
|
||||||
"gnome-terminal": "gnome-terminal --window--",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CoreServer:
|
class CoreServer:
|
||||||
def __init__(self, name: str, address: str, port: int):
|
def __init__(self, name: str, address: str, port: int):
|
||||||
|
@ -68,7 +58,7 @@ class CoreClient:
|
||||||
"""
|
"""
|
||||||
Create a CoreGrpc instance
|
Create a CoreGrpc instance
|
||||||
"""
|
"""
|
||||||
self.client = client.CoreGrpcClient(proxy=proxy)
|
self._client = client.CoreGrpcClient(proxy=proxy)
|
||||||
self.session_id = None
|
self.session_id = None
|
||||||
self.node_ids = []
|
self.node_ids = []
|
||||||
self.app = app
|
self.app = app
|
||||||
|
@ -112,6 +102,22 @@ class CoreClient:
|
||||||
|
|
||||||
self.modified_service_nodes = set()
|
self.modified_service_nodes = set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
if self.session_id:
|
||||||
|
response = self._client.check_session(self.session_id)
|
||||||
|
if not response.result:
|
||||||
|
throughputs_enabled = self.handling_throughputs is not None
|
||||||
|
self.cancel_throughputs()
|
||||||
|
self.cancel_events()
|
||||||
|
self._client.create_session(self.session_id)
|
||||||
|
self.handling_events = self._client.events(
|
||||||
|
self.session_id, self.handle_events
|
||||||
|
)
|
||||||
|
if throughputs_enabled:
|
||||||
|
self.enable_throughputs()
|
||||||
|
return self._client
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
# helpers
|
# helpers
|
||||||
self.interfaces_manager.reset()
|
self.interfaces_manager.reset()
|
||||||
|
@ -131,12 +137,8 @@ class CoreClient:
|
||||||
mobility_player.handle_close()
|
mobility_player.handle_close()
|
||||||
self.mobility_players.clear()
|
self.mobility_players.clear()
|
||||||
# clear streams
|
# clear streams
|
||||||
if self.handling_throughputs:
|
self.cancel_throughputs()
|
||||||
self.handling_throughputs.cancel()
|
self.cancel_events()
|
||||||
self.handling_throughputs = None
|
|
||||||
if self.handling_events:
|
|
||||||
self.handling_events.cancel()
|
|
||||||
self.handling_events = None
|
|
||||||
|
|
||||||
def set_observer(self, value: str):
|
def set_observer(self, value: str):
|
||||||
self.observer = value
|
self.observer = value
|
||||||
|
@ -227,9 +229,15 @@ class CoreClient:
|
||||||
)
|
)
|
||||||
|
|
||||||
def cancel_throughputs(self):
|
def cancel_throughputs(self):
|
||||||
|
if self.handling_throughputs:
|
||||||
self.handling_throughputs.cancel()
|
self.handling_throughputs.cancel()
|
||||||
self.handling_throughputs = None
|
self.handling_throughputs = None
|
||||||
|
|
||||||
|
def cancel_events(self):
|
||||||
|
if self.handling_events:
|
||||||
|
self.handling_events.cancel()
|
||||||
|
self.handling_events = None
|
||||||
|
|
||||||
def handle_throughputs(self, event: core_pb2.ThroughputsEvent):
|
def handle_throughputs(self, event: core_pb2.ThroughputsEvent):
|
||||||
if event.session_id != self.session_id:
|
if event.session_id != self.session_id:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
|
@ -438,7 +446,7 @@ class CoreClient:
|
||||||
master = parent_frame
|
master = parent_frame
|
||||||
self.app.after(0, show_grpc_error, e, master, self.app)
|
self.app.after(0, show_grpc_error, e, master, self.app)
|
||||||
|
|
||||||
def set_up(self):
|
def setup(self):
|
||||||
"""
|
"""
|
||||||
Query sessions, if there exist any, prompt whether to join one
|
Query sessions, if there exist any, prompt whether to join one
|
||||||
"""
|
"""
|
||||||
|
@ -472,7 +480,7 @@ class CoreClient:
|
||||||
x.node_type: set(x.services) for x in response.defaults
|
x.node_type: set(x.services) for x in response.defaults
|
||||||
}
|
}
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.after(0, show_grpc_error, e, self.app, self.app)
|
show_grpc_error(e, self.app, self.app)
|
||||||
self.app.close()
|
self.app.close()
|
||||||
|
|
||||||
def edit_node(self, core_node: core_pb2.Node):
|
def edit_node(self, core_node: core_pb2.Node):
|
||||||
|
@ -500,7 +508,6 @@ class CoreClient:
|
||||||
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
|
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||||
else:
|
else:
|
||||||
emane_config = None
|
emane_config = None
|
||||||
|
|
||||||
response = core_pb2.StartSessionResponse(result=False)
|
response = core_pb2.StartSessionResponse(result=False)
|
||||||
try:
|
try:
|
||||||
response = self.client.start_session(
|
response = self.client.start_session(
|
||||||
|
@ -521,7 +528,6 @@ class CoreClient:
|
||||||
logging.info(
|
logging.info(
|
||||||
"start session(%s), result: %s", self.session_id, response.result
|
"start session(%s), result: %s", self.session_id, response.result
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.result:
|
if response.result:
|
||||||
self.set_metadata()
|
self.set_metadata()
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
|
@ -573,11 +579,15 @@ class CoreClient:
|
||||||
def launch_terminal(self, node_id: int):
|
def launch_terminal(self, node_id: int):
|
||||||
try:
|
try:
|
||||||
terminal = self.app.guiconfig["preferences"]["terminal"]
|
terminal = self.app.guiconfig["preferences"]["terminal"]
|
||||||
|
if not terminal:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Terminal Error",
|
||||||
|
"No terminal set, please set within the preferences menu",
|
||||||
|
parent=self.app,
|
||||||
|
)
|
||||||
|
return
|
||||||
response = self.client.get_node_terminal(self.session_id, node_id)
|
response = self.client.get_node_terminal(self.session_id, node_id)
|
||||||
output = os.popen(f"echo {terminal}").read()[:-1]
|
cmd = f"{terminal} {response.terminal} &"
|
||||||
if output in DEFAULT_TERMS:
|
|
||||||
terminal = DEFAULT_TERMS[output]
|
|
||||||
cmd = f'{terminal} "{response.terminal}" &'
|
|
||||||
logging.info("launching terminal %s", cmd)
|
logging.info("launching terminal %s", cmd)
|
||||||
os.system(cmd)
|
os.system(cmd)
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
|
@ -620,6 +630,8 @@ class CoreClient:
|
||||||
self,
|
self,
|
||||||
node_id: int,
|
node_id: int,
|
||||||
service_name: str,
|
service_name: str,
|
||||||
|
dirs: List[str],
|
||||||
|
files: List[str],
|
||||||
startups: List[str],
|
startups: List[str],
|
||||||
validations: List[str],
|
validations: List[str],
|
||||||
shutdowns: List[str],
|
shutdowns: List[str],
|
||||||
|
@ -628,14 +640,17 @@ class CoreClient:
|
||||||
self.session_id,
|
self.session_id,
|
||||||
node_id,
|
node_id,
|
||||||
service_name,
|
service_name,
|
||||||
|
directories=dirs,
|
||||||
|
files=files,
|
||||||
startup=startups,
|
startup=startups,
|
||||||
validate=validations,
|
validate=validations,
|
||||||
shutdown=shutdowns,
|
shutdown=shutdowns,
|
||||||
)
|
)
|
||||||
logging.info(
|
logging.info(
|
||||||
"Set %s service for node(%s), Startup: %s, Validation: %s, Shutdown: %s, Result: %s",
|
"Set %s service for node(%s), files: %s, Startup: %s, Validation: %s, Shutdown: %s, Result: %s",
|
||||||
service_name,
|
service_name,
|
||||||
node_id,
|
node_id,
|
||||||
|
files,
|
||||||
startups,
|
startups,
|
||||||
validations,
|
validations,
|
||||||
shutdowns,
|
shutdowns,
|
||||||
|
@ -794,7 +809,7 @@ class CoreClient:
|
||||||
image=image,
|
image=image,
|
||||||
emane=emane,
|
emane=emane,
|
||||||
)
|
)
|
||||||
if NodeUtils.is_custom(model):
|
if NodeUtils.is_custom(node_type, model):
|
||||||
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
|
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
|
||||||
node.services[:] = services
|
node.services[:] = services
|
||||||
logging.info(
|
logging.info(
|
||||||
|
@ -933,6 +948,8 @@ class CoreClient:
|
||||||
config_proto = core_pb2.ServiceConfig(
|
config_proto = core_pb2.ServiceConfig(
|
||||||
node_id=node_id,
|
node_id=node_id,
|
||||||
service=name,
|
service=name,
|
||||||
|
directories=config.dirs,
|
||||||
|
files=config.configs,
|
||||||
startup=config.startup,
|
startup=config.startup,
|
||||||
validate=config.validate,
|
validate=config.validate,
|
||||||
shutdown=config.shutdown,
|
shutdown=config.shutdown,
|
||||||
|
@ -1064,3 +1081,9 @@ class CoreClient:
|
||||||
|
|
||||||
def service_been_modified(self, node_id: int) -> bool:
|
def service_been_modified(self, node_id: int) -> bool:
|
||||||
return node_id in self.modified_service_nodes
|
return node_id in self.modified_service_nodes
|
||||||
|
|
||||||
|
def execute_script(self, script):
|
||||||
|
response = self.client.execute_script(script)
|
||||||
|
logging.info("execute python script %s", response)
|
||||||
|
if response.session_id != -1:
|
||||||
|
self.join_session(response.session_id)
|
||||||
|
|
85
daemon/core/gui/dialogs/executepython.py
Normal file
85
daemon/core/gui/dialogs/executepython.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import logging
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog, ttk
|
||||||
|
|
||||||
|
from core.gui.appconfig import SCRIPT_PATH
|
||||||
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
from core.gui.themes import FRAME_PAD, PADX
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutePythonDialog(Dialog):
|
||||||
|
def __init__(self, master, app):
|
||||||
|
super().__init__(master, app, "Execute Python Script", modal=True)
|
||||||
|
self.app = app
|
||||||
|
self.with_options = tk.IntVar(value=0)
|
||||||
|
self.options = tk.StringVar(value="")
|
||||||
|
self.option_entry = None
|
||||||
|
self.file_entry = None
|
||||||
|
self.draw()
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
i = 0
|
||||||
|
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
||||||
|
frame.columnconfigure(0, weight=1)
|
||||||
|
frame.columnconfigure(1, weight=1)
|
||||||
|
frame.grid(row=i, column=0, sticky="nsew")
|
||||||
|
i = i + 1
|
||||||
|
var = tk.StringVar(value="")
|
||||||
|
self.file_entry = ttk.Entry(frame, textvariable=var)
|
||||||
|
self.file_entry.grid(row=0, column=0, sticky="ew")
|
||||||
|
button = ttk.Button(frame, text="...", command=self.select_file)
|
||||||
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
|
self.top.columnconfigure(0, weight=1)
|
||||||
|
button = ttk.Checkbutton(
|
||||||
|
self.top,
|
||||||
|
text="With Options",
|
||||||
|
variable=self.with_options,
|
||||||
|
command=self.add_options,
|
||||||
|
)
|
||||||
|
button.grid(row=i, column=0, sticky="ew")
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
label = ttk.Label(
|
||||||
|
self.top, text="Any command-line options for running the Python script"
|
||||||
|
)
|
||||||
|
label.grid(row=i, column=0, sticky="ew")
|
||||||
|
i = i + 1
|
||||||
|
self.option_entry = ttk.Entry(
|
||||||
|
self.top, textvariable=self.options, state="disabled"
|
||||||
|
)
|
||||||
|
self.option_entry.grid(row=i, column=0, sticky="ew")
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
||||||
|
frame.columnconfigure(0, weight=1)
|
||||||
|
frame.columnconfigure(1, weight=1)
|
||||||
|
frame.grid(row=i, column=0)
|
||||||
|
button = ttk.Button(frame, text="Execute", command=self.script_execute)
|
||||||
|
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", padx=PADX)
|
||||||
|
|
||||||
|
def add_options(self):
|
||||||
|
if self.with_options.get():
|
||||||
|
self.option_entry.configure(state="normal")
|
||||||
|
else:
|
||||||
|
self.option_entry.configure(state="disabled")
|
||||||
|
|
||||||
|
def select_file(self):
|
||||||
|
file = filedialog.askopenfilename(
|
||||||
|
parent=self.top,
|
||||||
|
initialdir=str(SCRIPT_PATH),
|
||||||
|
title="Open python script",
|
||||||
|
filetypes=((".py Files", "*.py"), ("All Files", "*")),
|
||||||
|
)
|
||||||
|
if file:
|
||||||
|
self.file_entry.delete(0, "end")
|
||||||
|
self.file_entry.insert("end", file)
|
||||||
|
|
||||||
|
def script_execute(self):
|
||||||
|
file = self.file_entry.get()
|
||||||
|
options = self.option_entry.get()
|
||||||
|
logging.info("Execute %s with options %s", file, options)
|
||||||
|
self.app.core.execute_script(file)
|
||||||
|
self.destroy()
|
|
@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from core.gui.dialogs.colorpicker import ColorPickerDialog
|
from core.gui.dialogs.colorpicker import ColorPickerDialog
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
from core.gui.graph import tags
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
@ -19,7 +20,7 @@ class MarkerDialog(Dialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, master: "Application", app: "Application", initcolor: str = "#000000"
|
self, master: "Application", app: "Application", initcolor: str = "#000000"
|
||||||
):
|
):
|
||||||
super().__init__(master, app, "marker tool", modal=False)
|
super().__init__(master, app, "Marker Tool", modal=False)
|
||||||
self.app = app
|
self.app = app
|
||||||
self.color = initcolor
|
self.color = initcolor
|
||||||
self.radius = MARKER_THICKNESS[0]
|
self.radius = MARKER_THICKNESS[0]
|
||||||
|
@ -56,8 +57,7 @@ class MarkerDialog(Dialog):
|
||||||
|
|
||||||
def clear_marker(self):
|
def clear_marker(self):
|
||||||
canvas = self.app.canvas
|
canvas = self.app.canvas
|
||||||
for i in canvas.find_withtag("marker"):
|
canvas.delete(tags.MARKER)
|
||||||
canvas.delete(i)
|
|
||||||
|
|
||||||
def change_color(self, event: tk.Event):
|
def change_color(self, event: tk.Event):
|
||||||
color_picker = ColorPickerDialog(self, self.app, self.color)
|
color_picker = ColorPickerDialog(self, self.app, self.color)
|
||||||
|
|
|
@ -100,17 +100,17 @@ class MobilityPlayerDialog(Dialog):
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
frame.columnconfigure(i, weight=1)
|
frame.columnconfigure(i, weight=1)
|
||||||
|
|
||||||
image = Images.get(ImageEnum.START, width=ICON_SIZE)
|
image = Images.get(ImageEnum.START, width=int(ICON_SIZE * self.app.app_scale))
|
||||||
self.play_button = ttk.Button(frame, image=image, command=self.click_play)
|
self.play_button = ttk.Button(frame, image=image, command=self.click_play)
|
||||||
self.play_button.image = image
|
self.play_button.image = image
|
||||||
self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||||
|
|
||||||
image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE)
|
image = Images.get(ImageEnum.PAUSE, width=int(ICON_SIZE * self.app.app_scale))
|
||||||
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
|
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
|
||||||
self.pause_button.image = image
|
self.pause_button.image = image
|
||||||
self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||||
|
|
||||||
image = Images.get(ImageEnum.STOP, width=ICON_SIZE)
|
image = Images.get(ImageEnum.STOP, width=int(ICON_SIZE * self.app.app_scale))
|
||||||
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
|
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
|
||||||
self.stop_button.image = image
|
self.stop_button.image = image
|
||||||
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from tkinter import ttk
|
from tkinter import messagebox, ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import netaddr
|
||||||
|
|
||||||
from core.gui import nodeutils
|
from core.gui import nodeutils
|
||||||
from core.gui.appconfig import ICONS_PATH
|
from core.gui.appconfig import ICONS_PATH
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
|
@ -18,6 +20,56 @@ if TYPE_CHECKING:
|
||||||
from core.gui.graph.node import CanvasNode
|
from core.gui.graph.node import CanvasNode
|
||||||
|
|
||||||
|
|
||||||
|
def check_ip6(parent, name: str, value: str) -> bool:
|
||||||
|
if not value:
|
||||||
|
return True
|
||||||
|
title = f"IP6 Error for {name}"
|
||||||
|
values = value.split("/")
|
||||||
|
if len(values) != 2:
|
||||||
|
messagebox.showerror(
|
||||||
|
title, "Must be in the format address/prefix", parent=parent
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
addr, mask = values
|
||||||
|
if not netaddr.valid_ipv6(addr):
|
||||||
|
messagebox.showerror(title, "Invalid IP6 address", parent=parent)
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mask = int(mask)
|
||||||
|
if not (0 <= mask <= 128):
|
||||||
|
messagebox.showerror(title, "Mask must be between 0-128", parent=parent)
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
messagebox.showerror(title, "Invalid Mask", parent=parent)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_ip4(parent, name: str, value: str) -> bool:
|
||||||
|
if not value:
|
||||||
|
return True
|
||||||
|
title = f"IP4 Error for {name}"
|
||||||
|
values = value.split("/")
|
||||||
|
if len(values) != 2:
|
||||||
|
messagebox.showerror(
|
||||||
|
title, "Must be in the format address/prefix", parent=parent
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
addr, mask = values
|
||||||
|
if not netaddr.valid_ipv4(addr):
|
||||||
|
messagebox.showerror(title, "Invalid IP4 address", parent=parent)
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mask = int(mask)
|
||||||
|
if not (0 <= mask <= 32):
|
||||||
|
messagebox.showerror(title, "Mask must be between 0-32", parent=parent)
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
messagebox.showerror(title, "Invalid mask", parent=parent)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry):
|
def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry):
|
||||||
logging.info("mac auto clicked")
|
logging.info("mac auto clicked")
|
||||||
if is_auto.get():
|
if is_auto.get():
|
||||||
|
@ -188,12 +240,17 @@ class NodeConfigDialog(Dialog):
|
||||||
|
|
||||||
label = ttk.Label(tab, text="MAC")
|
label = ttk.Label(tab, text="MAC")
|
||||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||||
is_auto = tk.BooleanVar(value=True)
|
auto_set = not interface.mac
|
||||||
|
if auto_set:
|
||||||
|
state = tk.DISABLED
|
||||||
|
else:
|
||||||
|
state = tk.NORMAL
|
||||||
|
is_auto = tk.BooleanVar(value=auto_set)
|
||||||
checkbutton = ttk.Checkbutton(tab, text="Auto?", variable=is_auto)
|
checkbutton = ttk.Checkbutton(tab, text="Auto?", variable=is_auto)
|
||||||
checkbutton.var = is_auto
|
checkbutton.var = is_auto
|
||||||
checkbutton.grid(row=row, column=1, padx=PADX)
|
checkbutton.grid(row=row, column=1, padx=PADX)
|
||||||
mac = tk.StringVar(value=interface.mac)
|
mac = tk.StringVar(value=interface.mac)
|
||||||
entry = ttk.Entry(tab, textvariable=mac, state=tk.DISABLED)
|
entry = ttk.Entry(tab, textvariable=mac, state=state)
|
||||||
entry.grid(row=row, column=2, sticky="ew")
|
entry.grid(row=row, column=2, sticky="ew")
|
||||||
func = partial(mac_auto, is_auto, entry)
|
func = partial(mac_auto, is_auto, entry)
|
||||||
checkbutton.config(command=func)
|
checkbutton.config(command=func)
|
||||||
|
@ -201,17 +258,21 @@ class NodeConfigDialog(Dialog):
|
||||||
|
|
||||||
label = ttk.Label(tab, text="IPv4")
|
label = ttk.Label(tab, text="IPv4")
|
||||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||||
ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}")
|
ip4_net = ""
|
||||||
|
if interface.ip4:
|
||||||
|
ip4_net = f"{interface.ip4}/{interface.ip4mask}"
|
||||||
|
ip4 = tk.StringVar(value=ip4_net)
|
||||||
entry = ttk.Entry(tab, textvariable=ip4)
|
entry = ttk.Entry(tab, textvariable=ip4)
|
||||||
entry.bind("<FocusOut>", self.app.validation.ip_focus_out)
|
|
||||||
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
label = ttk.Label(tab, text="IPv6")
|
label = ttk.Label(tab, text="IPv6")
|
||||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||||
ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}")
|
ip6_net = ""
|
||||||
|
if interface.ip6:
|
||||||
|
ip6_net = f"{interface.ip6}/{interface.ip6mask}"
|
||||||
|
ip6 = tk.StringVar(value=ip6_net)
|
||||||
entry = ttk.Entry(tab, textvariable=ip6)
|
entry = ttk.Entry(tab, textvariable=ip6)
|
||||||
entry.bind("<FocusOut>", self.app.validation.ip_focus_out)
|
|
||||||
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
||||||
|
|
||||||
self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6)
|
self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6)
|
||||||
|
@ -240,6 +301,8 @@ class NodeConfigDialog(Dialog):
|
||||||
self.image_file = file_path
|
self.image_file = file_path
|
||||||
|
|
||||||
def config_apply(self):
|
def config_apply(self):
|
||||||
|
error = False
|
||||||
|
|
||||||
# update core node
|
# update core node
|
||||||
self.node.name = self.name.get()
|
self.node.name = self.name.get()
|
||||||
if NodeUtils.is_image_node(self.node.type):
|
if NodeUtils.is_image_node(self.node.type):
|
||||||
|
@ -255,7 +318,52 @@ class NodeConfigDialog(Dialog):
|
||||||
# update canvas node
|
# update canvas node
|
||||||
self.canvas_node.image = self.image
|
self.canvas_node.image = self.image
|
||||||
|
|
||||||
|
# update node interface data
|
||||||
|
for interface in self.canvas_node.interfaces:
|
||||||
|
data = self.interfaces[interface.id]
|
||||||
|
|
||||||
|
# validate ip4
|
||||||
|
ip4_net = data.ip4.get()
|
||||||
|
if not check_ip4(self, interface.name, ip4_net):
|
||||||
|
error = True
|
||||||
|
data.ip4.set(f"{interface.ip4}/{interface.ip4mask}")
|
||||||
|
break
|
||||||
|
if ip4_net:
|
||||||
|
ip4, ip4mask = ip4_net.split("/")
|
||||||
|
ip4mask = int(ip4mask)
|
||||||
|
else:
|
||||||
|
ip4, ip4mask = "", 0
|
||||||
|
interface.ip4 = ip4
|
||||||
|
interface.ip4mask = ip4mask
|
||||||
|
|
||||||
|
# validate ip6
|
||||||
|
ip6_net = data.ip6.get()
|
||||||
|
if not check_ip6(self, interface.name, ip6_net):
|
||||||
|
error = True
|
||||||
|
data.ip6.set(f"{interface.ip6}/{interface.ip6mask}")
|
||||||
|
break
|
||||||
|
if ip6_net:
|
||||||
|
ip6, ip6mask = ip6_net.split("/")
|
||||||
|
ip6mask = int(ip6mask)
|
||||||
|
else:
|
||||||
|
ip6, ip6mask = "", 0
|
||||||
|
interface.ip6 = ip6
|
||||||
|
interface.ip6mask = ip6mask
|
||||||
|
|
||||||
|
mac = data.mac.get()
|
||||||
|
if mac and not netaddr.valid_mac(mac):
|
||||||
|
title = f"MAC Error for {interface.name}"
|
||||||
|
messagebox.showerror(title, "Invalid MAC Address")
|
||||||
|
error = True
|
||||||
|
data.mac.set(interface.mac)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
mac = netaddr.EUI(mac)
|
||||||
|
mac.dialect = netaddr.mac_unix_expanded
|
||||||
|
interface.mac = str(mac)
|
||||||
|
|
||||||
# redraw
|
# redraw
|
||||||
|
if not error:
|
||||||
self.canvas_node.redraw()
|
self.canvas_node.redraw()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
|
|
@ -128,7 +128,9 @@ class NodeConfigServiceDialog(Dialog):
|
||||||
dialog.show()
|
dialog.show()
|
||||||
else:
|
else:
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Node service configuration", "Select a service to configure"
|
"Config Service Configuration",
|
||||||
|
"Select a service to configure",
|
||||||
|
parent=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
def click_save(self):
|
def click_save(self):
|
||||||
|
|
|
@ -38,7 +38,7 @@ class NodeServiceDialog(Dialog):
|
||||||
if len(services) == 0:
|
if len(services) == 0:
|
||||||
# not custom node type and node's services haven't been modified before
|
# not custom node type and node's services haven't been modified before
|
||||||
if not NodeUtils.is_custom(
|
if not NodeUtils.is_custom(
|
||||||
canvas_node.core_node.model
|
canvas_node.core_node.type, canvas_node.core_node.model
|
||||||
) and not self.app.core.service_been_modified(self.node_id):
|
) and not self.app.core.service_been_modified(self.node_id):
|
||||||
services = set(self.app.core.default_services[model])
|
services = set(self.app.core.default_services[model])
|
||||||
# services of default type nodes were modified to be empty
|
# services of default type nodes were modified to be empty
|
||||||
|
@ -148,11 +148,12 @@ class NodeServiceDialog(Dialog):
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
else:
|
else:
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Node service configuration", "Select a service to configure"
|
"Service Configuration", "Select a service to configure", parent=self
|
||||||
)
|
)
|
||||||
|
|
||||||
def click_save(self):
|
def click_save(self):
|
||||||
# if node is custom type or current services are not the default services then set core node services and add node to modified services node set
|
# if node is custom type or current services are not the default services then
|
||||||
|
# set core node services and add node to modified services node set
|
||||||
if (
|
if (
|
||||||
self.canvas_node.core_node.model not in self.app.core.default_services
|
self.canvas_node.core_node.model not in self.app.core.default_services
|
||||||
or self.current_services
|
or self.current_services
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from core.gui import appconfig
|
from core.gui import appconfig
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts
|
||||||
|
from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
|
||||||
|
SCALE_INTERVAL = 0.01
|
||||||
|
|
||||||
|
|
||||||
class PreferencesDialog(Dialog):
|
class PreferencesDialog(Dialog):
|
||||||
def __init__(self, master: "Application", app: "Application"):
|
def __init__(self, master: "Application", app: "Application"):
|
||||||
super().__init__(master, app, "Preferences", modal=True)
|
super().__init__(master, app, "Preferences", modal=True)
|
||||||
|
self.gui_scale = tk.DoubleVar(value=self.app.app_scale)
|
||||||
preferences = self.app.guiconfig["preferences"]
|
preferences = self.app.guiconfig["preferences"]
|
||||||
self.editor = tk.StringVar(value=preferences["editor"])
|
self.editor = tk.StringVar(value=preferences["editor"])
|
||||||
self.theme = tk.StringVar(value=preferences["theme"])
|
self.theme = tk.StringVar(value=preferences["theme"])
|
||||||
|
@ -51,12 +56,8 @@ class PreferencesDialog(Dialog):
|
||||||
|
|
||||||
label = ttk.Label(frame, text="Terminal")
|
label = ttk.Label(frame, text="Terminal")
|
||||||
label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w")
|
label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||||
combobox = ttk.Combobox(
|
terminals = sorted(appconfig.TERMINALS.values())
|
||||||
frame,
|
combobox = ttk.Combobox(frame, textvariable=self.terminal, values=terminals)
|
||||||
textvariable=self.terminal,
|
|
||||||
values=appconfig.TERMINALS,
|
|
||||||
state="readonly",
|
|
||||||
)
|
|
||||||
combobox.grid(row=2, column=1, sticky="ew")
|
combobox.grid(row=2, column=1, sticky="ew")
|
||||||
|
|
||||||
label = ttk.Label(frame, text="3D GUI")
|
label = ttk.Label(frame, text="3D GUI")
|
||||||
|
@ -64,6 +65,33 @@ class PreferencesDialog(Dialog):
|
||||||
entry = ttk.Entry(frame, textvariable=self.gui3d)
|
entry = ttk.Entry(frame, textvariable=self.gui3d)
|
||||||
entry.grid(row=3, column=1, sticky="ew")
|
entry.grid(row=3, column=1, sticky="ew")
|
||||||
|
|
||||||
|
label = ttk.Label(frame, text="Scaling")
|
||||||
|
label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||||
|
|
||||||
|
scale_frame = ttk.Frame(frame)
|
||||||
|
scale_frame.grid(row=4, column=1, sticky="ew")
|
||||||
|
scale_frame.columnconfigure(0, weight=1)
|
||||||
|
scale = ttk.Scale(
|
||||||
|
scale_frame,
|
||||||
|
from_=SMALLEST_SCALE,
|
||||||
|
to=LARGEST_SCALE,
|
||||||
|
value=1,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
variable=self.gui_scale,
|
||||||
|
)
|
||||||
|
scale.grid(row=0, column=0, sticky="ew")
|
||||||
|
entry = ttk.Entry(
|
||||||
|
scale_frame,
|
||||||
|
textvariable=self.gui_scale,
|
||||||
|
width=4,
|
||||||
|
validate="key",
|
||||||
|
validatecommand=(self.app.validation.app_scale, "%P"),
|
||||||
|
)
|
||||||
|
entry.grid(row=0, column=1)
|
||||||
|
|
||||||
|
scrollbar = ttk.Scrollbar(scale_frame, command=self.adjust_scale)
|
||||||
|
scrollbar.grid(row=0, column=2)
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self):
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
|
@ -87,5 +115,41 @@ class PreferencesDialog(Dialog):
|
||||||
preferences["editor"] = self.editor.get()
|
preferences["editor"] = self.editor.get()
|
||||||
preferences["gui3d"] = self.gui3d.get()
|
preferences["gui3d"] = self.gui3d.get()
|
||||||
preferences["theme"] = self.theme.get()
|
preferences["theme"] = self.theme.get()
|
||||||
|
self.gui_scale.set(round(self.gui_scale.get(), 2))
|
||||||
|
app_scale = self.gui_scale.get()
|
||||||
|
self.app.guiconfig["scale"] = app_scale
|
||||||
|
|
||||||
self.app.save_config()
|
self.app.save_config()
|
||||||
|
self.scale_adjust()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
def scale_adjust(self):
|
||||||
|
app_scale = self.gui_scale.get()
|
||||||
|
self.app.app_scale = app_scale
|
||||||
|
self.app.master.tk.call("tk", "scaling", app_scale)
|
||||||
|
|
||||||
|
# scale fonts
|
||||||
|
scale_fonts(self.app.fonts_size, app_scale)
|
||||||
|
text_scale = app_scale if app_scale < 1 else math.sqrt(app_scale)
|
||||||
|
self.app.icon_text_font.config(size=int(12 * text_scale))
|
||||||
|
self.app.edge_font.config(size=int(8 * text_scale))
|
||||||
|
|
||||||
|
# scale application window
|
||||||
|
self.app.center()
|
||||||
|
|
||||||
|
# scale toolbar and canvas items
|
||||||
|
self.app.toolbar.scale()
|
||||||
|
self.app.canvas.scale_graph()
|
||||||
|
|
||||||
|
def adjust_scale(self, arg1: str, arg2: str, arg3: str):
|
||||||
|
scale_value = self.gui_scale.get()
|
||||||
|
if arg2 == "-1":
|
||||||
|
if scale_value <= LARGEST_SCALE - SCALE_INTERVAL:
|
||||||
|
self.gui_scale.set(round(scale_value + SCALE_INTERVAL, 2))
|
||||||
|
else:
|
||||||
|
self.gui_scale.set(round(LARGEST_SCALE, 2))
|
||||||
|
elif arg2 == "1":
|
||||||
|
if scale_value >= SMALLEST_SCALE + SCALE_INTERVAL:
|
||||||
|
self.gui_scale.set(round(scale_value - SCALE_INTERVAL, 2))
|
||||||
|
else:
|
||||||
|
self.gui_scale.set(round(SMALLEST_SCALE, 2))
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
"""
|
import logging
|
||||||
Service configuration dialog
|
import os
|
||||||
"""
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import filedialog, ttk
|
||||||
from typing import TYPE_CHECKING, Any, List
|
from typing import TYPE_CHECKING, Any, List
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
@ -48,12 +47,18 @@ class ServiceConfigDialog(Dialog):
|
||||||
self.validation_mode = None
|
self.validation_mode = None
|
||||||
self.validation_time = None
|
self.validation_time = None
|
||||||
self.validation_period = None
|
self.validation_period = None
|
||||||
self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16)
|
self.directory_entry = None
|
||||||
self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16)
|
self.default_directories = []
|
||||||
|
self.temp_directories = []
|
||||||
|
self.documentnew_img = Images.get(
|
||||||
|
ImageEnum.DOCUMENTNEW, int(16 * app.app_scale)
|
||||||
|
)
|
||||||
|
self.editdelete_img = Images.get(ImageEnum.EDITDELETE, int(16 * app.app_scale))
|
||||||
|
|
||||||
self.notebook = None
|
self.notebook = None
|
||||||
self.metadata_entry = None
|
self.metadata_entry = None
|
||||||
self.filename_combobox = None
|
self.filename_combobox = None
|
||||||
|
self.dir_list = None
|
||||||
self.startup_commands_listbox = None
|
self.startup_commands_listbox = None
|
||||||
self.shutdown_commands_listbox = None
|
self.shutdown_commands_listbox = None
|
||||||
self.validate_commands_listbox = None
|
self.validate_commands_listbox = None
|
||||||
|
@ -62,6 +67,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
self.service_file_data = None
|
self.service_file_data = None
|
||||||
self.validation_period_entry = None
|
self.validation_period_entry = None
|
||||||
self.original_service_files = {}
|
self.original_service_files = {}
|
||||||
|
self.default_config = None
|
||||||
self.temp_service_files = {}
|
self.temp_service_files = {}
|
||||||
self.modified_files = set()
|
self.modified_files = set()
|
||||||
|
|
||||||
|
@ -71,7 +77,7 @@ class ServiceConfigDialog(Dialog):
|
||||||
if not self.has_error:
|
if not self.has_error:
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def load(self) -> bool:
|
def load(self):
|
||||||
try:
|
try:
|
||||||
self.app.core.create_nodes_and_links()
|
self.app.core.create_nodes_and_links()
|
||||||
default_config = self.app.core.get_node_service(
|
default_config = self.app.core.get_node_service(
|
||||||
|
@ -80,15 +86,14 @@ class ServiceConfigDialog(Dialog):
|
||||||
self.default_startup = default_config.startup[:]
|
self.default_startup = default_config.startup[:]
|
||||||
self.default_validate = default_config.validate[:]
|
self.default_validate = default_config.validate[:]
|
||||||
self.default_shutdown = default_config.shutdown[:]
|
self.default_shutdown = default_config.shutdown[:]
|
||||||
custom_configs = self.service_configs
|
self.default_directories = default_config.dirs[:]
|
||||||
if (
|
custom_service_config = self.service_configs.get(self.node_id, {}).get(
|
||||||
self.node_id in custom_configs
|
self.service_name, None
|
||||||
and self.service_name in custom_configs[self.node_id]
|
)
|
||||||
):
|
self.default_config = default_config
|
||||||
service_config = custom_configs[self.node_id][self.service_name]
|
service_config = (
|
||||||
else:
|
custom_service_config if custom_service_config else default_config
|
||||||
service_config = default_config
|
)
|
||||||
|
|
||||||
self.dependencies = service_config.dependencies[:]
|
self.dependencies = service_config.dependencies[:]
|
||||||
self.executables = service_config.executables[:]
|
self.executables = service_config.executables[:]
|
||||||
self.metadata = service_config.meta
|
self.metadata = service_config.meta
|
||||||
|
@ -98,19 +103,18 @@ class ServiceConfigDialog(Dialog):
|
||||||
self.shutdown_commands = service_config.shutdown[:]
|
self.shutdown_commands = service_config.shutdown[:]
|
||||||
self.validation_mode = service_config.validation_mode
|
self.validation_mode = service_config.validation_mode
|
||||||
self.validation_time = service_config.validation_timer
|
self.validation_time = service_config.validation_timer
|
||||||
|
self.temp_directories = service_config.dirs[:]
|
||||||
self.original_service_files = {
|
self.original_service_files = {
|
||||||
x: self.app.core.get_node_service_file(
|
x: self.app.core.get_node_service_file(
|
||||||
self.node_id, self.service_name, x
|
self.node_id, self.service_name, x
|
||||||
)
|
)
|
||||||
for x in self.filenames
|
for x in default_config.configs
|
||||||
}
|
}
|
||||||
self.temp_service_files = dict(self.original_service_files)
|
self.temp_service_files = dict(self.original_service_files)
|
||||||
file_configs = self.file_configs
|
file_config = self.file_configs.get(self.node_id, {}).get(
|
||||||
if (
|
self.service_name, {}
|
||||||
self.node_id in file_configs
|
)
|
||||||
and self.service_name in file_configs[self.node_id]
|
for file, data in file_config.items():
|
||||||
):
|
|
||||||
for file, data in file_configs[self.node_id][self.service_name].items():
|
|
||||||
self.temp_service_files[file] = data
|
self.temp_service_files[file] = data
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.has_error = True
|
self.has_error = True
|
||||||
|
@ -155,18 +159,18 @@ class ServiceConfigDialog(Dialog):
|
||||||
frame.columnconfigure(1, weight=1)
|
frame.columnconfigure(1, weight=1)
|
||||||
label = ttk.Label(frame, text="File Name")
|
label = ttk.Label(frame, text="File Name")
|
||||||
label.grid(row=0, column=0, padx=PADX, sticky="w")
|
label.grid(row=0, column=0, padx=PADX, sticky="w")
|
||||||
self.filename_combobox = ttk.Combobox(
|
self.filename_combobox = ttk.Combobox(frame, values=self.filenames)
|
||||||
frame, values=self.filenames, state="readonly"
|
|
||||||
)
|
|
||||||
self.filename_combobox.bind(
|
self.filename_combobox.bind(
|
||||||
"<<ComboboxSelected>>", self.display_service_file_data
|
"<<ComboboxSelected>>", self.display_service_file_data
|
||||||
)
|
)
|
||||||
self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX)
|
self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||||
button = ttk.Button(frame, image=self.documentnew_img, state="disabled")
|
button = ttk.Button(
|
||||||
button.bind("<Button-1>", self.add_filename)
|
frame, image=self.documentnew_img, command=self.add_filename
|
||||||
|
)
|
||||||
button.grid(row=0, column=2, padx=PADX)
|
button.grid(row=0, column=2, padx=PADX)
|
||||||
button = ttk.Button(frame, image=self.editdelete_img, state="disabled")
|
button = ttk.Button(
|
||||||
button.bind("<Button-1>", self.delete_filename)
|
frame, image=self.editdelete_img, command=self.delete_filename
|
||||||
|
)
|
||||||
button.grid(row=0, column=3)
|
button.grid(row=0, column=3)
|
||||||
|
|
||||||
frame = ttk.Frame(tab)
|
frame = ttk.Frame(tab)
|
||||||
|
@ -229,7 +233,30 @@ class ServiceConfigDialog(Dialog):
|
||||||
tab,
|
tab,
|
||||||
text="Directories required by this service that are unique for each node.",
|
text="Directories required by this service that are unique for each node.",
|
||||||
)
|
)
|
||||||
label.grid()
|
label.grid(row=0, column=0, sticky="ew")
|
||||||
|
frame = ttk.Frame(tab, padding=FRAME_PAD)
|
||||||
|
frame.columnconfigure(0, weight=1)
|
||||||
|
frame.columnconfigure(1, weight=1)
|
||||||
|
frame.grid(row=1, column=0, sticky="nsew")
|
||||||
|
var = tk.StringVar(value="")
|
||||||
|
self.directory_entry = ttk.Entry(frame, textvariable=var)
|
||||||
|
self.directory_entry.grid(row=0, column=0, sticky="ew")
|
||||||
|
button = ttk.Button(frame, text="...", command=self.find_directory_button)
|
||||||
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
self.dir_list = ListboxScroll(tab)
|
||||||
|
self.dir_list.grid(row=2, column=0, sticky="nsew")
|
||||||
|
self.dir_list.listbox.bind("<<ListboxSelect>>", self.directory_select)
|
||||||
|
for d in self.temp_directories:
|
||||||
|
self.dir_list.listbox.insert("end", d)
|
||||||
|
|
||||||
|
frame = ttk.Frame(tab)
|
||||||
|
frame.grid(row=3, column=0, sticky="nsew")
|
||||||
|
frame.columnconfigure(0, weight=1)
|
||||||
|
frame.columnconfigure(1, weight=1)
|
||||||
|
button = ttk.Button(frame, text="Add", command=self.add_directory)
|
||||||
|
button.grid(row=0, column=0, sticky="ew")
|
||||||
|
button = ttk.Button(frame, text="Remove", command=self.remove_directory)
|
||||||
|
button.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
def draw_tab_startstop(self):
|
def draw_tab_startstop(self):
|
||||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||||
|
@ -358,26 +385,30 @@ class ServiceConfigDialog(Dialog):
|
||||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||||
button.grid(row=0, column=3, sticky="ew")
|
button.grid(row=0, column=3, sticky="ew")
|
||||||
|
|
||||||
def add_filename(self, event: tk.Event):
|
def add_filename(self):
|
||||||
# not worry about it for now
|
filename = self.filename_combobox.get()
|
||||||
return
|
if filename not in self.filename_combobox["values"]:
|
||||||
frame_contains_button = event.widget.master
|
self.filename_combobox["values"] += (filename,)
|
||||||
combobox = frame_contains_button.grid_slaves(row=0, column=1)[0]
|
self.filename_combobox.set(filename)
|
||||||
filename = combobox.get()
|
self.temp_service_files[filename] = self.service_file_data.text.get(
|
||||||
if filename not in combobox["values"]:
|
1.0, "end"
|
||||||
combobox["values"] += (filename,)
|
)
|
||||||
|
else:
|
||||||
|
logging.debug("file already existed")
|
||||||
|
|
||||||
def delete_filename(self, event: tk.Event):
|
def delete_filename(self):
|
||||||
# not worry about it for now
|
cbb = self.filename_combobox
|
||||||
return
|
filename = cbb.get()
|
||||||
frame_comntains_button = event.widget.master
|
if filename in cbb["values"]:
|
||||||
combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0]
|
cbb["values"] = tuple([x for x in cbb["values"] if x != filename])
|
||||||
filename = combobox.get()
|
cbb.set("")
|
||||||
if filename in combobox["values"]:
|
self.service_file_data.text.delete(1.0, "end")
|
||||||
combobox["values"] = tuple([x for x in combobox["values"] if x != filename])
|
self.temp_service_files.pop(filename, None)
|
||||||
combobox.set("")
|
if filename in self.modified_files:
|
||||||
|
self.modified_files.remove(filename)
|
||||||
|
|
||||||
def add_command(self, event: tk.Event):
|
@classmethod
|
||||||
|
def add_command(cls, event: tk.Event):
|
||||||
frame_contains_button = event.widget.master
|
frame_contains_button = event.widget.master
|
||||||
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
|
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()
|
command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get()
|
||||||
|
@ -388,7 +419,8 @@ class ServiceConfigDialog(Dialog):
|
||||||
return
|
return
|
||||||
listbox.insert(tk.END, command_to_add)
|
listbox.insert(tk.END, command_to_add)
|
||||||
|
|
||||||
def update_entry(self, event: tk.Event):
|
@classmethod
|
||||||
|
def update_entry(cls, event: tk.Event):
|
||||||
listbox = event.widget
|
listbox = event.widget
|
||||||
current_selection = listbox.curselection()
|
current_selection = listbox.curselection()
|
||||||
if len(current_selection) > 0:
|
if len(current_selection) > 0:
|
||||||
|
@ -399,7 +431,8 @@ class ServiceConfigDialog(Dialog):
|
||||||
entry.delete(0, "end")
|
entry.delete(0, "end")
|
||||||
entry.insert(0, cmd)
|
entry.insert(0, cmd)
|
||||||
|
|
||||||
def delete_command(self, event: tk.Event):
|
@classmethod
|
||||||
|
def delete_command(cls, event: tk.Event):
|
||||||
button = event.widget
|
button = event.widget
|
||||||
frame_contains_button = button.master
|
frame_contains_button = button.master
|
||||||
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
|
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
|
||||||
|
@ -410,30 +443,36 @@ class ServiceConfigDialog(Dialog):
|
||||||
entry.delete(0, tk.END)
|
entry.delete(0, tk.END)
|
||||||
|
|
||||||
def click_apply(self):
|
def click_apply(self):
|
||||||
current_listbox = self.master.current.listbox
|
if (
|
||||||
if not self.is_custom_service_config() and not self.is_custom_service_file():
|
not self.is_custom_command()
|
||||||
if self.node_id in self.service_configs:
|
and not self.is_custom_service_file()
|
||||||
self.service_configs[self.node_id].pop(self.service_name, None)
|
and not self.has_new_files()
|
||||||
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
|
and not self.is_custom_directory()
|
||||||
|
):
|
||||||
|
self.service_configs.get(self.node_id, {}).pop(self.service_name, None)
|
||||||
|
self.current_service_color("")
|
||||||
self.destroy()
|
self.destroy()
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.is_custom_service_config():
|
if (
|
||||||
startup_commands = self.startup_commands_listbox.get(0, "end")
|
self.is_custom_command()
|
||||||
shutdown_commands = self.shutdown_commands_listbox.get(0, "end")
|
or self.has_new_files()
|
||||||
validate_commands = self.validate_commands_listbox.get(0, "end")
|
or self.is_custom_directory()
|
||||||
|
):
|
||||||
|
startup, validate, shutdown = self.get_commands()
|
||||||
config = self.core.set_node_service(
|
config = self.core.set_node_service(
|
||||||
self.node_id,
|
self.node_id,
|
||||||
self.service_name,
|
self.service_name,
|
||||||
startups=startup_commands,
|
dirs=self.temp_directories,
|
||||||
validations=validate_commands,
|
files=list(self.filename_combobox["values"]),
|
||||||
shutdowns=shutdown_commands,
|
startups=startup,
|
||||||
|
validations=validate,
|
||||||
|
shutdowns=shutdown,
|
||||||
)
|
)
|
||||||
if self.node_id not in self.service_configs:
|
if self.node_id not in self.service_configs:
|
||||||
self.service_configs[self.node_id] = {}
|
self.service_configs[self.node_id] = {}
|
||||||
self.service_configs[self.node_id][self.service_name] = config
|
self.service_configs[self.node_id][self.service_name] = config
|
||||||
|
|
||||||
for file in self.modified_files:
|
for file in self.modified_files:
|
||||||
if self.node_id not in self.file_configs:
|
if self.node_id not in self.file_configs:
|
||||||
self.file_configs[self.node_id] = {}
|
self.file_configs[self.node_id] = {}
|
||||||
|
@ -442,53 +481,67 @@ class ServiceConfigDialog(Dialog):
|
||||||
self.file_configs[self.node_id][self.service_name][
|
self.file_configs[self.node_id][self.service_name][
|
||||||
file
|
file
|
||||||
] = self.temp_service_files[file]
|
] = self.temp_service_files[file]
|
||||||
|
|
||||||
self.app.core.set_node_service_file(
|
self.app.core.set_node_service_file(
|
||||||
self.node_id, self.service_name, file, self.temp_service_files[file]
|
self.node_id, self.service_name, file, self.temp_service_files[file]
|
||||||
)
|
)
|
||||||
all_current = current_listbox.get(0, tk.END)
|
self.current_service_color("green")
|
||||||
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
|
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
show_grpc_error(e, self.top, self.app)
|
show_grpc_error(e, self.top, self.app)
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def display_service_file_data(self, event: tk.Event):
|
def display_service_file_data(self, event: tk.Event):
|
||||||
combobox = event.widget
|
filename = self.filename_combobox.get()
|
||||||
filename = combobox.get()
|
|
||||||
self.service_file_data.text.delete(1.0, "end")
|
self.service_file_data.text.delete(1.0, "end")
|
||||||
self.service_file_data.text.insert("end", self.temp_service_files[filename])
|
self.service_file_data.text.insert("end", self.temp_service_files[filename])
|
||||||
|
|
||||||
def update_temp_service_file_data(self, event: tk.Event):
|
def update_temp_service_file_data(self, event: tk.Event):
|
||||||
scrolledtext = event.widget
|
|
||||||
filename = self.filename_combobox.get()
|
filename = self.filename_combobox.get()
|
||||||
self.temp_service_files[filename] = scrolledtext.get(1.0, "end")
|
self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end")
|
||||||
if self.temp_service_files[filename] != self.original_service_files[filename]:
|
if self.temp_service_files[filename] != self.original_service_files.get(
|
||||||
|
filename, ""
|
||||||
|
):
|
||||||
self.modified_files.add(filename)
|
self.modified_files.add(filename)
|
||||||
else:
|
else:
|
||||||
self.modified_files.discard(filename)
|
self.modified_files.discard(filename)
|
||||||
|
|
||||||
def is_custom_service_config(self):
|
def is_custom_command(self):
|
||||||
startup_commands = self.startup_commands_listbox.get(0, "end")
|
startup, validate, shutdown = self.get_commands()
|
||||||
shutdown_commands = self.shutdown_commands_listbox.get(0, "end")
|
|
||||||
validate_commands = self.validate_commands_listbox.get(0, "end")
|
|
||||||
return (
|
return (
|
||||||
set(self.default_startup) != set(startup_commands)
|
set(self.default_startup) != set(startup)
|
||||||
or set(self.default_validate) != set(validate_commands)
|
or set(self.default_validate) != set(validate)
|
||||||
or set(self.default_shutdown) != set(shutdown_commands)
|
or set(self.default_shutdown) != set(shutdown)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def has_new_files(self):
|
||||||
|
return set(self.filenames) != set(self.filename_combobox["values"])
|
||||||
|
|
||||||
def is_custom_service_file(self):
|
def is_custom_service_file(self):
|
||||||
return len(self.modified_files) > 0
|
return len(self.modified_files) > 0
|
||||||
|
|
||||||
|
def is_custom_directory(self):
|
||||||
|
return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end"))
|
||||||
|
|
||||||
def click_defaults(self):
|
def click_defaults(self):
|
||||||
if self.node_id in self.service_configs:
|
"""
|
||||||
self.service_configs[self.node_id].pop(self.service_name, None)
|
clears out any custom configuration permanently
|
||||||
if self.node_id in self.file_configs:
|
"""
|
||||||
self.file_configs[self.node_id].pop(self.service_name, None)
|
# clear coreclient data
|
||||||
|
self.service_configs.get(self.node_id, {}).pop(self.service_name, None)
|
||||||
|
self.file_configs.get(self.node_id, {}).pop(self.service_name, None)
|
||||||
self.temp_service_files = dict(self.original_service_files)
|
self.temp_service_files = dict(self.original_service_files)
|
||||||
filename = self.filename_combobox.get()
|
self.modified_files.clear()
|
||||||
|
|
||||||
|
# reset files tab
|
||||||
|
files = list(self.default_config.configs[:])
|
||||||
|
self.filenames = files
|
||||||
|
self.filename_combobox.config(values=files)
|
||||||
self.service_file_data.text.delete(1.0, "end")
|
self.service_file_data.text.delete(1.0, "end")
|
||||||
|
if len(files) > 0:
|
||||||
|
filename = files[0]
|
||||||
|
self.filename_combobox.set(filename)
|
||||||
self.service_file_data.text.insert("end", self.temp_service_files[filename])
|
self.service_file_data.text.insert("end", self.temp_service_files[filename])
|
||||||
|
|
||||||
|
# reset commands
|
||||||
self.startup_commands_listbox.delete(0, tk.END)
|
self.startup_commands_listbox.delete(0, tk.END)
|
||||||
self.validate_commands_listbox.delete(0, tk.END)
|
self.validate_commands_listbox.delete(0, tk.END)
|
||||||
self.shutdown_commands_listbox.delete(0, tk.END)
|
self.shutdown_commands_listbox.delete(0, tk.END)
|
||||||
|
@ -499,13 +552,68 @@ class ServiceConfigDialog(Dialog):
|
||||||
for cmd in self.default_shutdown:
|
for cmd in self.default_shutdown:
|
||||||
self.shutdown_commands_listbox.insert(tk.END, cmd)
|
self.shutdown_commands_listbox.insert(tk.END, cmd)
|
||||||
|
|
||||||
|
# reset directories
|
||||||
|
self.directory_entry.delete(0, "end")
|
||||||
|
self.dir_list.listbox.delete(0, "end")
|
||||||
|
self.temp_directories = list(self.default_directories)
|
||||||
|
for d in self.default_directories:
|
||||||
|
self.dir_list.listbox.insert("end", d)
|
||||||
|
|
||||||
|
self.current_service_color("")
|
||||||
|
|
||||||
def click_copy(self):
|
def click_copy(self):
|
||||||
dialog = CopyServiceConfigDialog(self, self.app, self.node_id)
|
dialog = CopyServiceConfigDialog(self, self.app, self.node_id)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def append_commands(
|
def append_commands(
|
||||||
self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
cls, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
||||||
):
|
):
|
||||||
for cmd in to_add:
|
for cmd in to_add:
|
||||||
commands.append(cmd)
|
commands.append(cmd)
|
||||||
listbox.insert(tk.END, cmd)
|
listbox.insert(tk.END, cmd)
|
||||||
|
|
||||||
|
def get_commands(self):
|
||||||
|
startup = self.startup_commands_listbox.get(0, "end")
|
||||||
|
shutdown = self.shutdown_commands_listbox.get(0, "end")
|
||||||
|
validate = self.validate_commands_listbox.get(0, "end")
|
||||||
|
return startup, validate, shutdown
|
||||||
|
|
||||||
|
def find_directory_button(self):
|
||||||
|
d = filedialog.askdirectory(initialdir="/")
|
||||||
|
self.directory_entry.delete(0, "end")
|
||||||
|
self.directory_entry.insert("end", d)
|
||||||
|
|
||||||
|
def add_directory(self):
|
||||||
|
d = self.directory_entry.get()
|
||||||
|
if os.path.isdir(d):
|
||||||
|
if d not in self.temp_directories:
|
||||||
|
self.dir_list.listbox.insert("end", d)
|
||||||
|
self.temp_directories.append(d)
|
||||||
|
|
||||||
|
def remove_directory(self):
|
||||||
|
d = self.directory_entry.get()
|
||||||
|
dirs = self.dir_list.listbox.get(0, "end")
|
||||||
|
if d and d in self.temp_directories:
|
||||||
|
self.temp_directories.remove(d)
|
||||||
|
try:
|
||||||
|
i = dirs.index(d)
|
||||||
|
self.dir_list.listbox.delete(i)
|
||||||
|
except ValueError:
|
||||||
|
logging.debug("directory is not in the list")
|
||||||
|
self.directory_entry.delete(0, "end")
|
||||||
|
|
||||||
|
def directory_select(self, event):
|
||||||
|
i = self.dir_list.listbox.curselection()
|
||||||
|
if i:
|
||||||
|
d = self.dir_list.listbox.get(i)
|
||||||
|
self.directory_entry.delete(0, "end")
|
||||||
|
self.directory_entry.insert("end", d)
|
||||||
|
|
||||||
|
def current_service_color(self, color=""):
|
||||||
|
"""
|
||||||
|
change the current service label color
|
||||||
|
"""
|
||||||
|
listbox = self.master.current.listbox
|
||||||
|
services = listbox.get(0, tk.END)
|
||||||
|
listbox.itemconfig(services.index(self.service_name), bg=color)
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
"""
|
|
||||||
wlan configuration
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -16,6 +12,9 @@ if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
from core.gui.graph.node import CanvasNode
|
from core.gui.graph.node import CanvasNode
|
||||||
|
|
||||||
|
RANGE_COLOR = "#009933"
|
||||||
|
RANGE_WIDTH = 3
|
||||||
|
|
||||||
|
|
||||||
class WlanConfigDialog(Dialog):
|
class WlanConfigDialog(Dialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -27,15 +26,29 @@ class WlanConfigDialog(Dialog):
|
||||||
self.canvas_node = canvas_node
|
self.canvas_node = canvas_node
|
||||||
self.node = canvas_node.core_node
|
self.node = canvas_node.core_node
|
||||||
self.config_frame = None
|
self.config_frame = None
|
||||||
|
self.range_entry = None
|
||||||
self.has_error = False
|
self.has_error = False
|
||||||
|
self.canvas = app.canvas
|
||||||
|
self.ranges = {}
|
||||||
|
self.positive_int = self.app.master.register(self.validate_and_update)
|
||||||
try:
|
try:
|
||||||
self.config = self.app.core.get_wlan_config(self.node.id)
|
self.config = self.app.core.get_wlan_config(self.node.id)
|
||||||
|
self.init_draw_range()
|
||||||
self.draw()
|
self.draw()
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
show_grpc_error(e, self.app, self.app)
|
show_grpc_error(e, self.app, self.app)
|
||||||
self.has_error = True
|
self.has_error = True
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
def init_draw_range(self):
|
||||||
|
if self.canvas_node.id in self.canvas.wireless_network:
|
||||||
|
for cid in self.canvas.wireless_network[self.canvas_node.id]:
|
||||||
|
x, y = self.canvas.coords(cid)
|
||||||
|
range_id = self.canvas.create_oval(
|
||||||
|
x, y, x, y, width=RANGE_WIDTH, outline=RANGE_COLOR, tags="range"
|
||||||
|
)
|
||||||
|
self.ranges[cid] = range_id
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(0, weight=1)
|
||||||
|
@ -43,6 +56,7 @@ class WlanConfigDialog(Dialog):
|
||||||
self.config_frame.draw_config()
|
self.config_frame.draw_config()
|
||||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||||
self.draw_apply_buttons()
|
self.draw_apply_buttons()
|
||||||
|
self.top.bind("<Destroy>", self.remove_ranges)
|
||||||
|
|
||||||
def draw_apply_buttons(self):
|
def draw_apply_buttons(self):
|
||||||
"""
|
"""
|
||||||
|
@ -53,6 +67,11 @@ class WlanConfigDialog(Dialog):
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
frame.columnconfigure(i, weight=1)
|
frame.columnconfigure(i, weight=1)
|
||||||
|
|
||||||
|
self.range_entry = self.config_frame.winfo_children()[0].frame.winfo_children()[
|
||||||
|
-1
|
||||||
|
]
|
||||||
|
self.range_entry.config(validatecommand=(self.positive_int, "%P"))
|
||||||
|
|
||||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||||
|
|
||||||
|
@ -68,4 +87,35 @@ class WlanConfigDialog(Dialog):
|
||||||
if self.app.core.is_runtime():
|
if self.app.core.is_runtime():
|
||||||
session_id = self.app.core.session_id
|
session_id = self.app.core.session_id
|
||||||
self.app.core.client.set_wlan_config(session_id, self.node.id, config)
|
self.app.core.client.set_wlan_config(session_id, self.node.id, config)
|
||||||
|
self.remove_ranges()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
def remove_ranges(self, event=None):
|
||||||
|
for cid in self.canvas.find_withtag("range"):
|
||||||
|
self.canvas.delete(cid)
|
||||||
|
self.ranges.clear()
|
||||||
|
|
||||||
|
def validate_and_update(self, s: str) -> bool:
|
||||||
|
"""
|
||||||
|
custom validation to also redraw the mdr ranges when the range value changes
|
||||||
|
"""
|
||||||
|
if len(s) == 0:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
int_value = int(s)
|
||||||
|
if int_value >= 0:
|
||||||
|
net_range = int_value * self.canvas.ratio
|
||||||
|
if self.canvas_node.id in self.canvas.wireless_network:
|
||||||
|
for cid in self.canvas.wireless_network[self.canvas_node.id]:
|
||||||
|
x, y = self.canvas.coords(cid)
|
||||||
|
self.canvas.coords(
|
||||||
|
self.ranges[cid],
|
||||||
|
x - net_range,
|
||||||
|
y - net_range,
|
||||||
|
x + net_range,
|
||||||
|
y + net_range,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
|
@ -1,38 +1,58 @@
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import grpc
|
||||||
|
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.images import ImageEnum, Images
|
from core.gui.images import ImageEnum, Images
|
||||||
|
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||||
from core.gui.widgets import CodeText
|
from core.gui.widgets import CodeText
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import grpc
|
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
|
||||||
|
|
||||||
class ErrorDialog(Dialog):
|
class ErrorDialog(Dialog):
|
||||||
def __init__(self, master, app: "Application", title: str, details: str):
|
def __init__(self, master, app: "Application", title: str, details: str) -> None:
|
||||||
super().__init__(master, app, title, modal=True)
|
super().__init__(master, app, "CORE Exception", modal=True)
|
||||||
self.error_message = None
|
self.title = title
|
||||||
self.details = details
|
self.details = details
|
||||||
|
self.error_message = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.top.columnconfigure(0, weight=1)
|
self.top.columnconfigure(0, weight=1)
|
||||||
self.top.rowconfigure(0, weight=1)
|
self.top.rowconfigure(1, weight=1)
|
||||||
|
|
||||||
|
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
||||||
|
frame.grid(pady=PADY, sticky="ew")
|
||||||
|
frame.columnconfigure(1, weight=1)
|
||||||
image = Images.get(ImageEnum.ERROR, 36)
|
image = Images.get(ImageEnum.ERROR, 36)
|
||||||
label = ttk.Label(self.top, image=image)
|
label = ttk.Label(frame, image=image)
|
||||||
label.image = image
|
label.image = image
|
||||||
label.grid(row=0, column=0)
|
label.grid(row=0, column=0, padx=PADX)
|
||||||
|
label = ttk.Label(frame, text=self.title)
|
||||||
|
label.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
self.error_message = CodeText(self.top)
|
self.error_message = CodeText(self.top)
|
||||||
self.error_message.text.insert("1.0", self.details)
|
self.error_message.text.insert("1.0", self.details)
|
||||||
self.error_message.text.config(state="disabled")
|
self.error_message.text.config(state="disabled")
|
||||||
self.error_message.grid(row=1, column=0, sticky="nsew")
|
self.error_message.grid(sticky="nsew", pady=PADY)
|
||||||
|
|
||||||
|
button = ttk.Button(self.top, text="Close", command=lambda: self.destroy())
|
||||||
|
button.grid(sticky="ew")
|
||||||
|
|
||||||
|
|
||||||
def show_grpc_error(e: "grpc.RpcError", master, app: "Application"):
|
def show_grpc_error(e: grpc.RpcError, master, app: "Application"):
|
||||||
title = [x.capitalize() for x in e.code().name.lower().split("_")]
|
title = [x.capitalize() for x in e.code().name.lower().split("_")]
|
||||||
title = " ".join(title)
|
title = " ".join(title)
|
||||||
title = f"GRPC {title}"
|
title = f"GRPC {title}"
|
||||||
dialog = ErrorDialog(master, app, title, e.details())
|
dialog = ErrorDialog(master, app, title, e.details())
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
|
|
||||||
|
def show_grpc_response_exceptions(class_name, exceptions, master, app: "Application"):
|
||||||
|
title = f"Exceptions from {class_name}"
|
||||||
|
detail = "\n".join([str(x) for x in exceptions])
|
||||||
|
dialog = ErrorDialog(master, app, title, detail)
|
||||||
|
dialog.show()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter.font import Font
|
|
||||||
from typing import TYPE_CHECKING, Any, Tuple
|
from typing import TYPE_CHECKING, Any, Tuple
|
||||||
|
|
||||||
from core.gui import themes
|
from core.gui import themes
|
||||||
|
@ -14,6 +13,8 @@ if TYPE_CHECKING:
|
||||||
TEXT_DISTANCE = 0.30
|
TEXT_DISTANCE = 0.30
|
||||||
EDGE_WIDTH = 3
|
EDGE_WIDTH = 3
|
||||||
EDGE_COLOR = "#ff0000"
|
EDGE_COLOR = "#ff0000"
|
||||||
|
WIRELESS_WIDTH = 1.5
|
||||||
|
WIRELESS_COLOR = "#009933"
|
||||||
|
|
||||||
|
|
||||||
class CanvasWirelessEdge:
|
class CanvasWirelessEdge:
|
||||||
|
@ -31,7 +32,10 @@ class CanvasWirelessEdge:
|
||||||
self.dst = dst
|
self.dst = dst
|
||||||
self.canvas = canvas
|
self.canvas = canvas
|
||||||
self.id = self.canvas.create_line(
|
self.id = self.canvas.create_line(
|
||||||
*position, tags=tags.WIRELESS_EDGE, width=1.5, fill="#009933"
|
*position,
|
||||||
|
tags=tags.WIRELESS_EDGE,
|
||||||
|
width=WIRELESS_WIDTH * self.canvas.app.app_scale,
|
||||||
|
fill=WIRELESS_COLOR,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
|
@ -61,13 +65,18 @@ class CanvasEdge:
|
||||||
self.dst_interface = None
|
self.dst_interface = None
|
||||||
self.canvas = canvas
|
self.canvas = canvas
|
||||||
self.id = self.canvas.create_line(
|
self.id = self.canvas.create_line(
|
||||||
x1, y1, x2, y2, tags=tags.EDGE, width=EDGE_WIDTH, fill=EDGE_COLOR
|
x1,
|
||||||
|
y1,
|
||||||
|
x2,
|
||||||
|
y2,
|
||||||
|
tags=tags.EDGE,
|
||||||
|
width=EDGE_WIDTH * self.canvas.app.app_scale,
|
||||||
|
fill=EDGE_COLOR,
|
||||||
)
|
)
|
||||||
self.text_src = None
|
self.text_src = None
|
||||||
self.text_dst = None
|
self.text_dst = None
|
||||||
self.text_middle = None
|
self.text_middle = None
|
||||||
self.token = None
|
self.token = None
|
||||||
self.font = Font(size=8)
|
|
||||||
self.link = None
|
self.link = None
|
||||||
self.asymmetric_link = None
|
self.asymmetric_link = None
|
||||||
self.throughput = None
|
self.throughput = None
|
||||||
|
@ -98,26 +107,32 @@ class CanvasEdge:
|
||||||
y = (y1 + y2) / 2
|
y = (y1 + y2) / 2
|
||||||
return x, y
|
return x, y
|
||||||
|
|
||||||
def draw_labels(self):
|
def create_labels(self):
|
||||||
x1, y1, x2, y2 = self.get_coordinates()
|
|
||||||
label_one = None
|
label_one = None
|
||||||
if self.link.HasField("interface_one"):
|
if self.link.HasField("interface_one"):
|
||||||
label_one = (
|
label_one = self.create_label(self.link.interface_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
|
label_two = None
|
||||||
if self.link.HasField("interface_two"):
|
if self.link.HasField("interface_two"):
|
||||||
label_two = (
|
label_two = self.create_label(self.link.interface_two)
|
||||||
f"{self.link.interface_two.ip4}/{self.link.interface_two.ip4mask}\n"
|
return label_one, label_two
|
||||||
f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n"
|
|
||||||
)
|
def create_label(self, interface):
|
||||||
|
label = ""
|
||||||
|
if interface.ip4:
|
||||||
|
label = f"{interface.ip4}/{interface.ip4mask}"
|
||||||
|
if interface.ip6:
|
||||||
|
label = f"{label}\n{interface.ip6}/{interface.ip6mask}"
|
||||||
|
return label
|
||||||
|
|
||||||
|
def draw_labels(self):
|
||||||
|
x1, y1, x2, y2 = self.get_coordinates()
|
||||||
|
label_one, label_two = self.create_labels()
|
||||||
self.text_src = self.canvas.create_text(
|
self.text_src = self.canvas.create_text(
|
||||||
x1,
|
x1,
|
||||||
y1,
|
y1,
|
||||||
text=label_one,
|
text=label_one,
|
||||||
justify=tk.CENTER,
|
justify=tk.CENTER,
|
||||||
font=self.font,
|
font=self.canvas.app.edge_font,
|
||||||
tags=tags.LINK_INFO,
|
tags=tags.LINK_INFO,
|
||||||
)
|
)
|
||||||
self.text_dst = self.canvas.create_text(
|
self.text_dst = self.canvas.create_text(
|
||||||
|
@ -125,10 +140,15 @@ class CanvasEdge:
|
||||||
y2,
|
y2,
|
||||||
text=label_two,
|
text=label_two,
|
||||||
justify=tk.CENTER,
|
justify=tk.CENTER,
|
||||||
font=self.font,
|
font=self.canvas.app.edge_font,
|
||||||
tags=tags.LINK_INFO,
|
tags=tags.LINK_INFO,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def redraw(self):
|
||||||
|
label_one, label_two = self.create_labels()
|
||||||
|
self.canvas.itemconfig(self.text_src, text=label_one)
|
||||||
|
self.canvas.itemconfig(self.text_dst, text=label_two)
|
||||||
|
|
||||||
def update_labels(self):
|
def update_labels(self):
|
||||||
"""
|
"""
|
||||||
Move edge labels based on current position.
|
Move edge labels based on current position.
|
||||||
|
@ -146,7 +166,7 @@ class CanvasEdge:
|
||||||
if self.text_middle is None:
|
if self.text_middle is None:
|
||||||
x, y = self.get_midpoint()
|
x, y = self.get_midpoint()
|
||||||
self.text_middle = self.canvas.create_text(
|
self.text_middle = self.canvas.create_text(
|
||||||
x, y, tags=tags.THROUGHPUT, font=self.font, text=value
|
x, y, tags=tags.THROUGHPUT, font=self.canvas.app.edge_font, text=value
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.canvas.itemconfig(self.text_middle, text=value)
|
self.canvas.itemconfig(self.text_middle, text=value)
|
||||||
|
@ -177,6 +197,17 @@ class CanvasEdge:
|
||||||
dst_node_type = dst_node.core_node.type
|
dst_node_type = dst_node.core_node.type
|
||||||
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
|
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
|
||||||
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
|
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
|
||||||
|
|
||||||
|
# update the wlan/EMANE network
|
||||||
|
wlan_network = self.canvas.wireless_network
|
||||||
|
if is_src_wireless and not is_dst_wireless:
|
||||||
|
if self.src not in wlan_network:
|
||||||
|
wlan_network[self.src] = set()
|
||||||
|
wlan_network[self.src].add(self.dst)
|
||||||
|
elif not is_src_wireless and is_dst_wireless:
|
||||||
|
if self.dst not in wlan_network:
|
||||||
|
wlan_network[self.dst] = set()
|
||||||
|
wlan_network[self.dst].add(self.src)
|
||||||
return is_src_wireless or is_dst_wireless
|
return is_src_wireless or is_dst_wireless
|
||||||
|
|
||||||
def check_wireless(self):
|
def check_wireless(self):
|
||||||
|
|
|
@ -7,12 +7,12 @@ from PIL import Image, ImageTk
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
from core.gui.dialogs.shapemod import ShapeDialog
|
from core.gui.dialogs.shapemod import ShapeDialog
|
||||||
from core.gui.graph import tags
|
from core.gui.graph import tags
|
||||||
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
|
from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge, CanvasWirelessEdge
|
||||||
from core.gui.graph.enums import GraphMode, ScaleOption
|
from core.gui.graph.enums import GraphMode, ScaleOption
|
||||||
from core.gui.graph.node import CanvasNode
|
from core.gui.graph.node import CanvasNode
|
||||||
from core.gui.graph.shape import Shape
|
from core.gui.graph.shape import Shape
|
||||||
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
|
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
|
||||||
from core.gui.images import ImageEnum, Images
|
from core.gui.images import ImageEnum, Images, TypeToImage
|
||||||
from core.gui.nodeutils import EdgeUtils, NodeUtils
|
from core.gui.nodeutils import EdgeUtils, NodeUtils
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -42,6 +42,10 @@ class CanvasGraph(tk.Canvas):
|
||||||
self.edges = {}
|
self.edges = {}
|
||||||
self.shapes = {}
|
self.shapes = {}
|
||||||
self.wireless_edges = {}
|
self.wireless_edges = {}
|
||||||
|
|
||||||
|
# map wireless/EMANE node to the set of MDRs connected to that node
|
||||||
|
self.wireless_network = {}
|
||||||
|
|
||||||
self.drawing_edge = None
|
self.drawing_edge = None
|
||||||
self.grid = None
|
self.grid = None
|
||||||
self.shape_drawing = False
|
self.shape_drawing = False
|
||||||
|
@ -113,6 +117,7 @@ class CanvasGraph(tk.Canvas):
|
||||||
self.edges.clear()
|
self.edges.clear()
|
||||||
self.shapes.clear()
|
self.shapes.clear()
|
||||||
self.wireless_edges.clear()
|
self.wireless_edges.clear()
|
||||||
|
self.wireless_network.clear()
|
||||||
self.drawing_edge = None
|
self.drawing_edge = None
|
||||||
self.draw_session(session)
|
self.draw_session(session)
|
||||||
|
|
||||||
|
@ -220,10 +225,14 @@ class CanvasGraph(tk.Canvas):
|
||||||
# peer to peer node is not drawn on the GUI
|
# peer to peer node is not drawn on the GUI
|
||||||
if NodeUtils.is_ignore_node(core_node.type):
|
if NodeUtils.is_ignore_node(core_node.type):
|
||||||
continue
|
continue
|
||||||
image = NodeUtils.node_image(core_node, self.app.guiconfig)
|
image = NodeUtils.node_image(
|
||||||
|
core_node, self.app.guiconfig, self.app.app_scale
|
||||||
|
)
|
||||||
# if the gui can't find node's image, default to the "edit-node" image
|
# if the gui can't find node's image, default to the "edit-node" image
|
||||||
if not image:
|
if not image:
|
||||||
image = Images.get(ImageEnum.EDITNODE, ICON_SIZE)
|
image = Images.get(
|
||||||
|
ImageEnum.EDITNODE, int(ICON_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
x = core_node.position.x
|
x = core_node.position.x
|
||||||
y = core_node.position.y
|
y = core_node.position.y
|
||||||
node = CanvasNode(self.master, x, y, core_node, image)
|
node = CanvasNode(self.master, x, y, core_node, image)
|
||||||
|
@ -525,7 +534,7 @@ class CanvasGraph(tk.Canvas):
|
||||||
y + r,
|
y + r,
|
||||||
fill=self.app.toolbar.marker_tool.color,
|
fill=self.app.toolbar.marker_tool.color,
|
||||||
outline="",
|
outline="",
|
||||||
tags="marker",
|
tags=tags.MARKER,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if selected is None:
|
if selected is None:
|
||||||
|
@ -647,8 +656,11 @@ class CanvasGraph(tk.Canvas):
|
||||||
delete selected nodes and any data that relates to it
|
delete selected nodes and any data that relates to it
|
||||||
"""
|
"""
|
||||||
logging.debug("press delete key")
|
logging.debug("press delete key")
|
||||||
|
if not self.app.core.is_runtime():
|
||||||
nodes = self.delete_selection_objects()
|
nodes = self.delete_selection_objects()
|
||||||
self.core.delete_graph_nodes(nodes)
|
self.core.delete_graph_nodes(nodes)
|
||||||
|
else:
|
||||||
|
logging.info("node deletion is disabled during runtime state")
|
||||||
|
|
||||||
def double_click(self, event: tk.Event):
|
def double_click(self, event: tk.Event):
|
||||||
selected = self.get_selected(event)
|
selected = self.get_selected(event)
|
||||||
|
@ -663,6 +675,14 @@ class CanvasGraph(tk.Canvas):
|
||||||
core_node = self.core.create_node(
|
core_node = self.core.create_node(
|
||||||
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
|
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
self.node_draw.image = Images.get(
|
||||||
|
self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
self.node_draw.image = Images.get_custom(
|
||||||
|
self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
node = CanvasNode(self.master, x, y, core_node, self.node_draw.image)
|
node = CanvasNode(self.master, x, y, core_node, self.node_draw.image)
|
||||||
self.core.canvas_nodes[core_node.id] = node
|
self.core.canvas_nodes[core_node.id] = node
|
||||||
self.nodes[node.id] = node
|
self.nodes[node.id] = node
|
||||||
|
@ -833,11 +853,17 @@ class CanvasGraph(tk.Canvas):
|
||||||
self.core.create_link(edge, source, dest)
|
self.core.create_link(edge, source, dest)
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
|
if self.app.core.is_runtime():
|
||||||
|
logging.info("copy is disabled during runtime state")
|
||||||
|
return
|
||||||
if self.selection:
|
if self.selection:
|
||||||
logging.debug("to copy %s nodes", len(self.selection))
|
logging.debug("to copy %s nodes", len(self.selection))
|
||||||
self.to_copy = self.selection.keys()
|
self.to_copy = self.selection.keys()
|
||||||
|
|
||||||
def paste(self):
|
def paste(self):
|
||||||
|
if self.app.core.is_runtime():
|
||||||
|
logging.info("paste is disabled during runtime state")
|
||||||
|
return
|
||||||
# maps original node canvas id to copy node canvas id
|
# maps original node canvas id to copy node canvas id
|
||||||
copy_map = {}
|
copy_map = {}
|
||||||
# the edges that will be copy over
|
# the edges that will be copy over
|
||||||
|
@ -911,3 +937,28 @@ class CanvasGraph(tk.Canvas):
|
||||||
width=self.itemcget(edge.id, "width"),
|
width=self.itemcget(edge.id, "width"),
|
||||||
fill=self.itemcget(edge.id, "fill"),
|
fill=self.itemcget(edge.id, "fill"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def scale_graph(self):
|
||||||
|
for nid, canvas_node in self.nodes.items():
|
||||||
|
img = None
|
||||||
|
if NodeUtils.is_custom(
|
||||||
|
canvas_node.core_node.type, canvas_node.core_node.model
|
||||||
|
):
|
||||||
|
for custom_node in self.app.guiconfig["nodes"]:
|
||||||
|
if custom_node["name"] == canvas_node.core_node.model:
|
||||||
|
img = Images.get_custom(
|
||||||
|
custom_node["image"], int(ICON_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
image_enum = TypeToImage.get(
|
||||||
|
canvas_node.core_node.type, canvas_node.core_node.model
|
||||||
|
)
|
||||||
|
img = Images.get(image_enum, int(ICON_SIZE * self.app.app_scale))
|
||||||
|
|
||||||
|
self.itemconfig(nid, image=img)
|
||||||
|
canvas_node.image = img
|
||||||
|
canvas_node.scale_text()
|
||||||
|
canvas_node.scale_antennas()
|
||||||
|
|
||||||
|
for edge_id in self.find_withtag(tags.EDGE):
|
||||||
|
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale))
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import font
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
@ -17,7 +16,8 @@ from core.gui.dialogs.wlanconfig import WlanConfigDialog
|
||||||
from core.gui.errors import show_grpc_error
|
from core.gui.errors import show_grpc_error
|
||||||
from core.gui.graph import tags
|
from core.gui.graph import tags
|
||||||
from core.gui.graph.tooltip import CanvasTooltip
|
from core.gui.graph.tooltip import CanvasTooltip
|
||||||
from core.gui.nodeutils import NodeUtils
|
from core.gui.images import ImageEnum, Images
|
||||||
|
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
@ -42,21 +42,21 @@ class CanvasNode:
|
||||||
self.id = self.canvas.create_image(
|
self.id = self.canvas.create_image(
|
||||||
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
|
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()
|
label_y = self._get_label_y()
|
||||||
self.text_id = self.canvas.create_text(
|
self.text_id = self.canvas.create_text(
|
||||||
x,
|
x,
|
||||||
label_y,
|
label_y,
|
||||||
text=self.core_node.name,
|
text=self.core_node.name,
|
||||||
tags=tags.NODE_NAME,
|
tags=tags.NODE_NAME,
|
||||||
font=text_font,
|
font=self.app.icon_text_font,
|
||||||
fill="#0000CD",
|
fill="#0000CD",
|
||||||
)
|
)
|
||||||
self.tooltip = CanvasTooltip(self.canvas)
|
self.tooltip = CanvasTooltip(self.canvas)
|
||||||
self.edges = set()
|
self.edges = set()
|
||||||
self.interfaces = []
|
self.interfaces = []
|
||||||
self.wireless_edges = set()
|
self.wireless_edges = set()
|
||||||
self.antennae = []
|
self.antennas = []
|
||||||
|
self.antenna_images = {}
|
||||||
self.setup_bindings()
|
self.setup_bindings()
|
||||||
|
|
||||||
def setup_bindings(self):
|
def setup_bindings(self):
|
||||||
|
@ -72,42 +72,54 @@ class CanvasNode:
|
||||||
|
|
||||||
def add_antenna(self):
|
def add_antenna(self):
|
||||||
x, y = self.canvas.coords(self.id)
|
x, y = self.canvas.coords(self.id)
|
||||||
offset = len(self.antennae) * 8
|
offset = len(self.antennas) * 8 * self.app.app_scale
|
||||||
|
img = Images.get(ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale))
|
||||||
antenna_id = self.canvas.create_image(
|
antenna_id = self.canvas.create_image(
|
||||||
x - 16 + offset,
|
x - 16 + offset,
|
||||||
y - 23,
|
y - int(23 * self.app.app_scale),
|
||||||
anchor=tk.CENTER,
|
anchor=tk.CENTER,
|
||||||
image=NodeUtils.ANTENNA_ICON,
|
image=img,
|
||||||
tags=tags.ANTENNA,
|
tags=tags.ANTENNA,
|
||||||
)
|
)
|
||||||
self.antennae.append(antenna_id)
|
self.antennas.append(antenna_id)
|
||||||
|
self.antenna_images[antenna_id] = img
|
||||||
|
|
||||||
def delete_antenna(self):
|
def delete_antenna(self):
|
||||||
"""
|
"""
|
||||||
delete one antenna
|
delete one antenna
|
||||||
"""
|
"""
|
||||||
logging.debug("Delete an antenna on %s", self.core_node.name)
|
logging.debug("Delete an antenna on %s", self.core_node.name)
|
||||||
if self.antennae:
|
if self.antennas:
|
||||||
antenna_id = self.antennae.pop()
|
antenna_id = self.antennas.pop()
|
||||||
self.canvas.delete(antenna_id)
|
self.canvas.delete(antenna_id)
|
||||||
|
self.antenna_images.pop(antenna_id, None)
|
||||||
|
|
||||||
def delete_antennas(self):
|
def delete_antennas(self):
|
||||||
"""
|
"""
|
||||||
delete all antennas
|
delete all antennas
|
||||||
"""
|
"""
|
||||||
logging.debug("Remove all antennas for %s", self.core_node.name)
|
logging.debug("Remove all antennas for %s", self.core_node.name)
|
||||||
for antenna_id in self.antennae:
|
for antenna_id in self.antennas:
|
||||||
self.canvas.delete(antenna_id)
|
self.canvas.delete(antenna_id)
|
||||||
self.antennae.clear()
|
self.antennas.clear()
|
||||||
|
self.antenna_images.clear()
|
||||||
|
|
||||||
def redraw(self):
|
def redraw(self):
|
||||||
self.canvas.itemconfig(self.id, image=self.image)
|
self.canvas.itemconfig(self.id, image=self.image)
|
||||||
self.canvas.itemconfig(self.text_id, text=self.core_node.name)
|
self.canvas.itemconfig(self.text_id, text=self.core_node.name)
|
||||||
|
for edge in self.edges:
|
||||||
|
edge.redraw()
|
||||||
|
|
||||||
def _get_label_y(self):
|
def _get_label_y(self):
|
||||||
image_box = self.canvas.bbox(self.id)
|
image_box = self.canvas.bbox(self.id)
|
||||||
return image_box[3] + NODE_TEXT_OFFSET
|
return image_box[3] + NODE_TEXT_OFFSET
|
||||||
|
|
||||||
|
def scale_text(self):
|
||||||
|
text_bound = self.canvas.bbox(self.text_id)
|
||||||
|
prev_y = (text_bound[3] + text_bound[1]) / 2
|
||||||
|
new_y = self._get_label_y()
|
||||||
|
self.canvas.move(self.text_id, 0, new_y - prev_y)
|
||||||
|
|
||||||
def move(self, x: int, y: int):
|
def move(self, x: int, y: int):
|
||||||
x, y = self.canvas.get_scaled_coords(x, y)
|
x, y = self.canvas.get_scaled_coords(x, y)
|
||||||
current_x, current_y = self.canvas.coords(self.id)
|
current_x, current_y = self.canvas.coords(self.id)
|
||||||
|
@ -131,7 +143,7 @@ class CanvasNode:
|
||||||
self.canvas.move_selection(self.id, x_offset, y_offset)
|
self.canvas.move_selection(self.id, x_offset, y_offset)
|
||||||
|
|
||||||
# move antennae
|
# move antennae
|
||||||
for antenna_id in self.antennae:
|
for antenna_id in self.antennas:
|
||||||
self.canvas.move(antenna_id, x_offset, y_offset)
|
self.canvas.move(antenna_id, x_offset, y_offset)
|
||||||
|
|
||||||
# move edges
|
# move edges
|
||||||
|
@ -295,3 +307,17 @@ class CanvasNode:
|
||||||
if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr":
|
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.create_edge(self, self.canvas.nodes[canvas_nid])
|
||||||
self.canvas.clear_selection()
|
self.canvas.clear_selection()
|
||||||
|
|
||||||
|
def scale_antennas(self):
|
||||||
|
for i in range(len(self.antennas)):
|
||||||
|
antenna_id = self.antennas[i]
|
||||||
|
image = Images.get(
|
||||||
|
ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
|
self.canvas.itemconfig(antenna_id, image=image)
|
||||||
|
self.antenna_images[antenna_id] = image
|
||||||
|
node_x, node_y = self.canvas.coords(self.id)
|
||||||
|
x, y = self.canvas.coords(antenna_id)
|
||||||
|
dx = node_x - 16 + (i * 8 * self.app.app_scale) - x
|
||||||
|
dy = node_y - int(23 * self.app.app_scale) - y
|
||||||
|
self.canvas.move(antenna_id, dx, dy)
|
||||||
|
|
|
@ -10,6 +10,7 @@ NODE = "node"
|
||||||
WALLPAPER = "wallpaper"
|
WALLPAPER = "wallpaper"
|
||||||
SELECTION = "selectednodes"
|
SELECTION = "selectednodes"
|
||||||
THROUGHPUT = "throughput"
|
THROUGHPUT = "throughput"
|
||||||
|
MARKER = "marker"
|
||||||
ABOVE_WALLPAPER_TAGS = [
|
ABOVE_WALLPAPER_TAGS = [
|
||||||
GRIDLINE,
|
GRIDLINE,
|
||||||
SHAPE,
|
SHAPE,
|
||||||
|
@ -33,4 +34,5 @@ COMPONENT_TAGS = [
|
||||||
SELECTION,
|
SELECTION,
|
||||||
SHAPE,
|
SHAPE,
|
||||||
SHAPE_TEXT,
|
SHAPE_TEXT,
|
||||||
|
MARKER,
|
||||||
]
|
]
|
||||||
|
|
|
@ -3,6 +3,7 @@ from tkinter import messagebox
|
||||||
|
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
from core.api.grpc import core_pb2
|
||||||
from core.gui.appconfig import LOCAL_ICONS_PATH
|
from core.gui.appconfig import LOCAL_ICONS_PATH
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,3 +91,25 @@ class ImageEnum(Enum):
|
||||||
SHUTDOWN = "shutdown"
|
SHUTDOWN = "shutdown"
|
||||||
CANCEL = "cancel"
|
CANCEL = "cancel"
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
|
|
||||||
|
|
||||||
|
class TypeToImage:
|
||||||
|
type_to_image = {
|
||||||
|
(core_pb2.NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
|
||||||
|
(core_pb2.NodeType.DEFAULT, "PC"): ImageEnum.PC,
|
||||||
|
(core_pb2.NodeType.DEFAULT, "host"): ImageEnum.HOST,
|
||||||
|
(core_pb2.NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
|
||||||
|
(core_pb2.NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
|
||||||
|
(core_pb2.NodeType.HUB, ""): ImageEnum.HUB,
|
||||||
|
(core_pb2.NodeType.SWITCH, ""): ImageEnum.SWITCH,
|
||||||
|
(core_pb2.NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
|
||||||
|
(core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE,
|
||||||
|
(core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45,
|
||||||
|
(core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
|
||||||
|
(core_pb2.NodeType.DOCKER, ""): ImageEnum.DOCKER,
|
||||||
|
(core_pb2.NodeType.LXC, ""): ImageEnum.LXC,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, node_type, model):
|
||||||
|
return cls.type_to_image.get((node_type, model), None)
|
||||||
|
|
|
@ -90,6 +90,7 @@ class MenuAction:
|
||||||
if file_path:
|
if file_path:
|
||||||
self.add_recent_file_to_gui_config(file_path)
|
self.add_recent_file_to_gui_config(file_path)
|
||||||
self.app.core.save_xml(file_path)
|
self.app.core.save_xml(file_path)
|
||||||
|
self.app.core.xml_file = file_path
|
||||||
|
|
||||||
def file_open_xml(self, event: tk.Event = None):
|
def file_open_xml(self, event: tk.Event = None):
|
||||||
init_dir = self.app.core.xml_dir
|
init_dir = self.app.core.xml_dir
|
||||||
|
@ -192,3 +193,8 @@ class MenuAction:
|
||||||
logging.error("unexpected number of recent files")
|
logging.error("unexpected number of recent files")
|
||||||
self.app.save_config()
|
self.app.save_config()
|
||||||
self.app.menubar.update_recent_files()
|
self.app.menubar.update_recent_files()
|
||||||
|
|
||||||
|
def new_session(self):
|
||||||
|
self.prompt_save_running_session()
|
||||||
|
self.app.core.create_new_session()
|
||||||
|
self.app.core.xml_file = None
|
||||||
|
|
|
@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import core.gui.menuaction as action
|
import core.gui.menuaction as action
|
||||||
from core.gui.coreclient import OBSERVERS
|
from core.gui.coreclient import OBSERVERS
|
||||||
|
from core.gui.dialogs.executepython import ExecutePythonDialog
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
@ -25,6 +26,7 @@ class Menubar(tk.Menu):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.menuaction = action.MenuAction(app, master)
|
self.menuaction = action.MenuAction(app, master)
|
||||||
self.recent_menu = None
|
self.recent_menu = None
|
||||||
|
self.edit_menu = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
|
@ -48,7 +50,7 @@ class Menubar(tk.Menu):
|
||||||
menu.add_command(
|
menu.add_command(
|
||||||
label="New Session",
|
label="New Session",
|
||||||
accelerator="Ctrl+N",
|
accelerator="Ctrl+N",
|
||||||
command=self.app.core.create_new_session,
|
command=self.menuaction.new_session,
|
||||||
)
|
)
|
||||||
self.app.bind_all("<Control-n>", lambda e: self.app.core.create_new_session())
|
self.app.bind_all("<Control-n>", lambda e: self.app.core.create_new_session())
|
||||||
menu.add_command(
|
menu.add_command(
|
||||||
|
@ -56,6 +58,7 @@ class Menubar(tk.Menu):
|
||||||
)
|
)
|
||||||
self.app.bind_all("<Control-o>", self.menuaction.file_open_xml)
|
self.app.bind_all("<Control-o>", self.menuaction.file_open_xml)
|
||||||
menu.add_command(label="Save", accelerator="Ctrl+S", command=self.save)
|
menu.add_command(label="Save", accelerator="Ctrl+S", command=self.save)
|
||||||
|
menu.add_command(label="Save As", command=self.menuaction.file_save_as_xml)
|
||||||
menu.add_command(label="Reload", underline=0, state=tk.DISABLED)
|
menu.add_command(label="Reload", underline=0, state=tk.DISABLED)
|
||||||
self.app.bind_all("<Control-s>", self.save)
|
self.app.bind_all("<Control-s>", self.save)
|
||||||
|
|
||||||
|
@ -67,7 +70,7 @@ class Menubar(tk.Menu):
|
||||||
menu.add_cascade(label="Recent files", menu=self.recent_menu)
|
menu.add_cascade(label="Recent files", menu=self.recent_menu)
|
||||||
menu.add_separator()
|
menu.add_separator()
|
||||||
menu.add_command(label="Export Python script...", state=tk.DISABLED)
|
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...", command=self.execute_python)
|
||||||
menu.add_command(
|
menu.add_command(
|
||||||
label="Execute Python script with options...", state=tk.DISABLED
|
label="Execute Python script with options...", state=tk.DISABLED
|
||||||
)
|
)
|
||||||
|
@ -110,6 +113,7 @@ class Menubar(tk.Menu):
|
||||||
|
|
||||||
self.app.master.bind_all("<Control-c>", self.menuaction.copy)
|
self.app.master.bind_all("<Control-c>", self.menuaction.copy)
|
||||||
self.app.master.bind_all("<Control-v>", self.menuaction.paste)
|
self.app.master.bind_all("<Control-v>", self.menuaction.paste)
|
||||||
|
self.edit_menu = menu
|
||||||
|
|
||||||
def draw_canvas_menu(self):
|
def draw_canvas_menu(self):
|
||||||
"""
|
"""
|
||||||
|
@ -439,3 +443,19 @@ class Menubar(tk.Menu):
|
||||||
self.app.core.save_xml(xml_file)
|
self.app.core.save_xml(xml_file)
|
||||||
else:
|
else:
|
||||||
self.menuaction.file_save_as_xml()
|
self.menuaction.file_save_as_xml()
|
||||||
|
|
||||||
|
def execute_python(self):
|
||||||
|
dialog = ExecutePythonDialog(self.app, self.app)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
def change_menubar_item_state(self, is_runtime: bool):
|
||||||
|
for i in range(self.edit_menu.index("end")):
|
||||||
|
try:
|
||||||
|
label_name = self.edit_menu.entrycget(i, "label")
|
||||||
|
if label_name in ["Copy", "Paste"]:
|
||||||
|
if is_runtime:
|
||||||
|
self.edit_menu.entryconfig(i, state="disabled")
|
||||||
|
else:
|
||||||
|
self.edit_menu.entryconfig(i, state="normal")
|
||||||
|
except tk.TclError:
|
||||||
|
logging.debug("Ignore separators")
|
||||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
from core.api.grpc.core_pb2 import NodeType
|
from core.api.grpc.core_pb2 import NodeType
|
||||||
from core.gui.images import ImageEnum, Images
|
from core.gui.images import ImageEnum, Images, TypeToImage
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
|
@ -96,32 +96,35 @@ class NodeUtils:
|
||||||
node_type: NodeType,
|
node_type: NodeType,
|
||||||
model: str,
|
model: str,
|
||||||
gui_config: Dict[str, List[Dict[str, str]]],
|
gui_config: Dict[str, List[Dict[str, str]]],
|
||||||
|
scale=1.0,
|
||||||
) -> "ImageTk.PhotoImage":
|
) -> "ImageTk.PhotoImage":
|
||||||
if model == "":
|
|
||||||
model = None
|
image_enum = TypeToImage.get(node_type, model)
|
||||||
try:
|
if image_enum:
|
||||||
image = cls.NODE_ICONS[(node_type, model)]
|
return Images.get(image_enum, int(ICON_SIZE * scale))
|
||||||
return image
|
else:
|
||||||
except KeyError:
|
|
||||||
image_stem = cls.get_image_file(gui_config, model)
|
image_stem = cls.get_image_file(gui_config, model)
|
||||||
if image_stem:
|
if image_stem:
|
||||||
return Images.get_with_image_file(image_stem, ICON_SIZE)
|
return Images.get_with_image_file(image_stem, int(ICON_SIZE * scale))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def node_image(
|
def node_image(
|
||||||
cls, core_node: "core_pb2.Node", gui_config: Dict[str, List[Dict[str, str]]]
|
cls,
|
||||||
|
core_node: "core_pb2.Node",
|
||||||
|
gui_config: Dict[str, List[Dict[str, str]]],
|
||||||
|
scale=1.0,
|
||||||
) -> "ImageTk.PhotoImage":
|
) -> "ImageTk.PhotoImage":
|
||||||
image = cls.node_icon(core_node.type, core_node.model, gui_config)
|
image = cls.node_icon(core_node.type, core_node.model, gui_config, scale)
|
||||||
if core_node.icon:
|
if core_node.icon:
|
||||||
try:
|
try:
|
||||||
image = Images.create(core_node.icon, ICON_SIZE)
|
image = Images.create(core_node.icon, int(ICON_SIZE * scale))
|
||||||
except OSError:
|
except OSError:
|
||||||
logging.error("invalid icon: %s", core_node.icon)
|
logging.error("invalid icon: %s", core_node.icon)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_custom(cls, model: str) -> bool:
|
def is_custom(cls, node_type: NodeType, model: str) -> bool:
|
||||||
return model not in cls.NODE_MODELS
|
return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_custom_node_services(
|
def get_custom_node_services(
|
||||||
|
|
|
@ -29,7 +29,7 @@ class StatusBar(ttk.Frame):
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
self.columnconfigure(0, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
self.columnconfigure(1, weight=7)
|
self.columnconfigure(1, weight=5)
|
||||||
self.columnconfigure(2, weight=1)
|
self.columnconfigure(2, weight=1)
|
||||||
self.columnconfigure(3, weight=1)
|
self.columnconfigure(3, weight=1)
|
||||||
self.columnconfigure(4, weight=1)
|
self.columnconfigure(4, weight=1)
|
||||||
|
|
|
@ -2,6 +2,8 @@ import logging
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from core.gui.errors import show_grpc_response_exceptions
|
||||||
|
|
||||||
|
|
||||||
class BackgroundTask:
|
class BackgroundTask:
|
||||||
def __init__(self, master: Any, task: Callable, callback: Callable = None, args=()):
|
def __init__(self, master: Any, task: Callable, callback: Callable = None, args=()):
|
||||||
|
@ -19,6 +21,19 @@ class BackgroundTask:
|
||||||
def run(self):
|
def run(self):
|
||||||
result = self.task(*self.args)
|
result = self.task(*self.args)
|
||||||
logging.info("task completed")
|
logging.info("task completed")
|
||||||
|
# if start session fails, a response with Result: False and a list of exceptions is returned
|
||||||
|
if not getattr(result, "result", True):
|
||||||
|
if len(getattr(result, "exceptions", [])) > 0:
|
||||||
|
self.master.after(
|
||||||
|
0,
|
||||||
|
show_grpc_response_exceptions,
|
||||||
|
*(
|
||||||
|
result.__class__.__name__,
|
||||||
|
result.exceptions,
|
||||||
|
self.master,
|
||||||
|
self.master,
|
||||||
|
)
|
||||||
|
)
|
||||||
if self.callback:
|
if self.callback:
|
||||||
if result is None:
|
if result is None:
|
||||||
args = ()
|
args = ()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import font, ttk
|
||||||
|
|
||||||
THEME_DARK = "black"
|
THEME_DARK = "black"
|
||||||
PADX = (0, 5)
|
PADX = (0, 5)
|
||||||
|
@ -176,25 +176,35 @@ def style_listbox(widget: tk.Widget):
|
||||||
|
|
||||||
def theme_change(event: tk.Event):
|
def theme_change(event: tk.Event):
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal"))
|
style.configure(Styles.picker_button, font="TkSmallCaptionFont")
|
||||||
style.configure(
|
style.configure(
|
||||||
Styles.green_alert,
|
Styles.green_alert,
|
||||||
background="green",
|
background="green",
|
||||||
padding=0,
|
padding=0,
|
||||||
relief=tk.NONE,
|
relief=tk.NONE,
|
||||||
font=("TkDefaultFont", 8, "normal"),
|
font="TkDefaultFont",
|
||||||
)
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
Styles.yellow_alert,
|
Styles.yellow_alert,
|
||||||
background="yellow",
|
background="yellow",
|
||||||
padding=0,
|
padding=0,
|
||||||
relief=tk.NONE,
|
relief=tk.NONE,
|
||||||
font=("TkDefaultFont", 8, "normal"),
|
font="TkDefaultFont",
|
||||||
)
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
Styles.red_alert,
|
Styles.red_alert,
|
||||||
background="red",
|
background="red",
|
||||||
padding=0,
|
padding=0,
|
||||||
relief=tk.NONE,
|
relief=tk.NONE,
|
||||||
font=("TkDefaultFont", 8, "normal"),
|
font="TkDefaultFont",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def scale_fonts(fonts_size, scale):
|
||||||
|
for name in font.names():
|
||||||
|
f = font.nametofont(name)
|
||||||
|
if name in fonts_size:
|
||||||
|
if name == "TkSmallCaptionFont":
|
||||||
|
f.config(size=int(fonts_size[name] * scale * 8 / 9))
|
||||||
|
else:
|
||||||
|
f.config(size=int(fonts_size[name] * scale))
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
from enum import Enum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from tkinter.font import Font
|
|
||||||
from typing import TYPE_CHECKING, Callable
|
from typing import TYPE_CHECKING, Callable
|
||||||
|
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
|
@ -25,6 +25,12 @@ TOOLBAR_SIZE = 32
|
||||||
PICKER_SIZE = 24
|
PICKER_SIZE = 24
|
||||||
|
|
||||||
|
|
||||||
|
class NodeTypeEnum(Enum):
|
||||||
|
NODE = 0
|
||||||
|
NETWORK = 1
|
||||||
|
OTHER = 2
|
||||||
|
|
||||||
|
|
||||||
def icon(image_enum, width=TOOLBAR_SIZE):
|
def icon(image_enum, width=TOOLBAR_SIZE):
|
||||||
return Images.get(image_enum, width)
|
return Images.get(image_enum, width)
|
||||||
|
|
||||||
|
@ -43,10 +49,8 @@ class Toolbar(ttk.Frame):
|
||||||
self.master = app.master
|
self.master = app.master
|
||||||
self.time = None
|
self.time = None
|
||||||
|
|
||||||
# picker data
|
|
||||||
self.picker_font = Font(size=8)
|
|
||||||
|
|
||||||
# design buttons
|
# design buttons
|
||||||
|
self.play_button = None
|
||||||
self.select_button = None
|
self.select_button = None
|
||||||
self.link_button = None
|
self.link_button = None
|
||||||
self.node_button = None
|
self.node_button = None
|
||||||
|
@ -71,9 +75,18 @@ class Toolbar(ttk.Frame):
|
||||||
# dialog
|
# dialog
|
||||||
self.marker_tool = None
|
self.marker_tool = None
|
||||||
|
|
||||||
|
# these variables help keep track of what images being drawn so that scaling is possible
|
||||||
|
# since ImageTk.PhotoImage does not have resize method
|
||||||
|
self.node_enum = None
|
||||||
|
self.network_enum = None
|
||||||
|
self.annotation_enum = None
|
||||||
|
|
||||||
# draw components
|
# draw components
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
|
def get_icon(self, image_enum, width=TOOLBAR_SIZE):
|
||||||
|
return Images.get(image_enum, int(width * self.app.app_scale))
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
self.columnconfigure(0, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
self.rowconfigure(0, weight=1)
|
self.rowconfigure(0, weight=1)
|
||||||
|
@ -85,20 +98,23 @@ class Toolbar(ttk.Frame):
|
||||||
self.design_frame = ttk.Frame(self)
|
self.design_frame = ttk.Frame(self)
|
||||||
self.design_frame.grid(row=0, column=0, sticky="nsew")
|
self.design_frame.grid(row=0, column=0, sticky="nsew")
|
||||||
self.design_frame.columnconfigure(0, weight=1)
|
self.design_frame.columnconfigure(0, weight=1)
|
||||||
self.create_button(
|
self.play_button = self.create_button(
|
||||||
self.design_frame,
|
self.design_frame,
|
||||||
icon(ImageEnum.START),
|
self.get_icon(ImageEnum.START),
|
||||||
self.click_start,
|
self.click_start,
|
||||||
"start the session",
|
"start the session",
|
||||||
)
|
)
|
||||||
self.select_button = self.create_button(
|
self.select_button = self.create_button(
|
||||||
self.design_frame,
|
self.design_frame,
|
||||||
icon(ImageEnum.SELECT),
|
self.get_icon(ImageEnum.SELECT),
|
||||||
self.click_selection,
|
self.click_selection,
|
||||||
"selection tool",
|
"selection tool",
|
||||||
)
|
)
|
||||||
self.link_button = self.create_button(
|
self.link_button = self.create_button(
|
||||||
self.design_frame, icon(ImageEnum.LINK), self.click_link, "link tool"
|
self.design_frame,
|
||||||
|
self.get_icon(ImageEnum.LINK),
|
||||||
|
self.click_link,
|
||||||
|
"link tool",
|
||||||
)
|
)
|
||||||
self.create_node_button()
|
self.create_node_button()
|
||||||
self.create_network_button()
|
self.create_network_button()
|
||||||
|
@ -130,18 +146,21 @@ class Toolbar(ttk.Frame):
|
||||||
|
|
||||||
self.stop_button = self.create_button(
|
self.stop_button = self.create_button(
|
||||||
self.runtime_frame,
|
self.runtime_frame,
|
||||||
icon(ImageEnum.STOP),
|
self.get_icon(ImageEnum.STOP),
|
||||||
self.click_stop,
|
self.click_stop,
|
||||||
"stop the session",
|
"stop the session",
|
||||||
)
|
)
|
||||||
self.runtime_select_button = self.create_button(
|
self.runtime_select_button = self.create_button(
|
||||||
self.runtime_frame,
|
self.runtime_frame,
|
||||||
icon(ImageEnum.SELECT),
|
self.get_icon(ImageEnum.SELECT),
|
||||||
self.click_runtime_selection,
|
self.click_runtime_selection,
|
||||||
"selection tool",
|
"selection tool",
|
||||||
)
|
)
|
||||||
self.plot_button = self.create_button(
|
self.plot_button = self.create_button(
|
||||||
self.runtime_frame, icon(ImageEnum.PLOT), self.click_plot_button, "plot"
|
self.runtime_frame,
|
||||||
|
self.get_icon(ImageEnum.PLOT),
|
||||||
|
self.click_plot_button,
|
||||||
|
"plot",
|
||||||
)
|
)
|
||||||
self.runtime_marker_button = self.create_button(
|
self.runtime_marker_button = self.create_button(
|
||||||
self.runtime_frame,
|
self.runtime_frame,
|
||||||
|
@ -164,23 +183,38 @@ class Toolbar(ttk.Frame):
|
||||||
self.node_picker = ttk.Frame(self.master)
|
self.node_picker = ttk.Frame(self.master)
|
||||||
# draw default nodes
|
# draw default nodes
|
||||||
for node_draw in NodeUtils.NODES:
|
for node_draw in NodeUtils.NODES:
|
||||||
toolbar_image = icon(node_draw.image_enum)
|
toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE)
|
||||||
image = icon(node_draw.image_enum, PICKER_SIZE)
|
image = self.get_icon(node_draw.image_enum, PICKER_SIZE)
|
||||||
func = partial(
|
func = partial(
|
||||||
self.update_button, self.node_button, toolbar_image, node_draw
|
self.update_button,
|
||||||
|
self.node_button,
|
||||||
|
toolbar_image,
|
||||||
|
node_draw,
|
||||||
|
NodeTypeEnum.NODE,
|
||||||
|
node_draw.image_enum,
|
||||||
)
|
)
|
||||||
self.create_picker_button(image, func, self.node_picker, node_draw.label)
|
self.create_picker_button(image, func, self.node_picker, node_draw.label)
|
||||||
# draw custom nodes
|
# draw custom nodes
|
||||||
for name in sorted(self.app.core.custom_nodes):
|
for name in sorted(self.app.core.custom_nodes):
|
||||||
node_draw = self.app.core.custom_nodes[name]
|
node_draw = self.app.core.custom_nodes[name]
|
||||||
toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE)
|
toolbar_image = Images.get_custom(
|
||||||
image = Images.get_custom(node_draw.image_file, PICKER_SIZE)
|
node_draw.image_file, int(TOOLBAR_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
|
image = Images.get_custom(
|
||||||
|
node_draw.image_file, int(PICKER_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
func = partial(
|
func = partial(
|
||||||
self.update_button, self.node_button, toolbar_image, node_draw
|
self.update_button,
|
||||||
|
self.node_button,
|
||||||
|
toolbar_image,
|
||||||
|
node_draw,
|
||||||
|
NodeTypeEnum,
|
||||||
|
node_draw.image_file,
|
||||||
)
|
)
|
||||||
self.create_picker_button(image, func, self.node_picker, name)
|
self.create_picker_button(image, func, self.node_picker, name)
|
||||||
# draw edit node
|
# draw edit node
|
||||||
image = icon(ImageEnum.EDITNODE, PICKER_SIZE)
|
# image = icon(ImageEnum.EDITNODE, PICKER_SIZE)
|
||||||
|
image = self.get_icon(ImageEnum.EDITNODE, PICKER_SIZE)
|
||||||
self.create_picker_button(
|
self.create_picker_button(
|
||||||
image, self.click_edit_node, self.node_picker, "Custom"
|
image, self.click_edit_node, self.node_picker, "Custom"
|
||||||
)
|
)
|
||||||
|
@ -246,6 +280,7 @@ class Toolbar(ttk.Frame):
|
||||||
server.
|
server.
|
||||||
"""
|
"""
|
||||||
self.app.canvas.hide_context()
|
self.app.canvas.hide_context()
|
||||||
|
self.app.menubar.change_menubar_item_state(is_runtime=True)
|
||||||
self.app.statusbar.progress_bar.start(5)
|
self.app.statusbar.progress_bar.start(5)
|
||||||
self.app.canvas.mode = GraphMode.SELECT
|
self.app.canvas.mode = GraphMode.SELECT
|
||||||
self.time = time.perf_counter()
|
self.time = time.perf_counter()
|
||||||
|
@ -281,13 +316,24 @@ class Toolbar(ttk.Frame):
|
||||||
dialog = CustomNodesDialog(self.app, self.app)
|
dialog = CustomNodesDialog(self.app, self.app)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
def update_button(self, button: ttk.Button, image: "ImageTk", node_draw: NodeDraw):
|
def update_button(
|
||||||
|
self,
|
||||||
|
button: ttk.Button,
|
||||||
|
image: "ImageTk",
|
||||||
|
node_draw: NodeDraw,
|
||||||
|
type_enum,
|
||||||
|
image_enum,
|
||||||
|
):
|
||||||
logging.debug("update button(%s): %s", button, node_draw)
|
logging.debug("update button(%s): %s", button, node_draw)
|
||||||
self.hide_pickers()
|
self.hide_pickers()
|
||||||
button.configure(image=image)
|
button.configure(image=image)
|
||||||
button.image = image
|
button.image = image
|
||||||
self.app.canvas.mode = GraphMode.NODE
|
self.app.canvas.mode = GraphMode.NODE
|
||||||
self.app.canvas.node_draw = node_draw
|
self.app.canvas.node_draw = node_draw
|
||||||
|
if type_enum == NodeTypeEnum.NODE:
|
||||||
|
self.node_enum = image_enum
|
||||||
|
elif type_enum == NodeTypeEnum.NETWORK:
|
||||||
|
self.network_enum = image_enum
|
||||||
|
|
||||||
def hide_pickers(self):
|
def hide_pickers(self):
|
||||||
logging.debug("hiding pickers")
|
logging.debug("hiding pickers")
|
||||||
|
@ -305,13 +351,14 @@ class Toolbar(ttk.Frame):
|
||||||
"""
|
"""
|
||||||
Create network layer button
|
Create network layer button
|
||||||
"""
|
"""
|
||||||
image = icon(ImageEnum.ROUTER)
|
image = self.get_icon(ImageEnum.ROUTER, TOOLBAR_SIZE)
|
||||||
self.node_button = ttk.Button(
|
self.node_button = ttk.Button(
|
||||||
self.design_frame, image=image, command=self.draw_node_picker
|
self.design_frame, image=image, command=self.draw_node_picker
|
||||||
)
|
)
|
||||||
self.node_button.image = image
|
self.node_button.image = image
|
||||||
self.node_button.grid(sticky="ew")
|
self.node_button.grid(sticky="ew")
|
||||||
Tooltip(self.node_button, "Network-layer virtual nodes")
|
Tooltip(self.node_button, "Network-layer virtual nodes")
|
||||||
|
self.node_enum = ImageEnum.ROUTER
|
||||||
|
|
||||||
def draw_network_picker(self):
|
def draw_network_picker(self):
|
||||||
"""
|
"""
|
||||||
|
@ -320,12 +367,17 @@ class Toolbar(ttk.Frame):
|
||||||
self.hide_pickers()
|
self.hide_pickers()
|
||||||
self.network_picker = ttk.Frame(self.master)
|
self.network_picker = ttk.Frame(self.master)
|
||||||
for node_draw in NodeUtils.NETWORK_NODES:
|
for node_draw in NodeUtils.NETWORK_NODES:
|
||||||
toolbar_image = icon(node_draw.image_enum)
|
toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE)
|
||||||
image = icon(node_draw.image_enum, PICKER_SIZE)
|
image = self.get_icon(node_draw.image_enum, PICKER_SIZE)
|
||||||
self.create_picker_button(
|
self.create_picker_button(
|
||||||
image,
|
image,
|
||||||
partial(
|
partial(
|
||||||
self.update_button, self.network_button, toolbar_image, node_draw
|
self.update_button,
|
||||||
|
self.network_button,
|
||||||
|
toolbar_image,
|
||||||
|
node_draw,
|
||||||
|
NodeTypeEnum.NETWORK,
|
||||||
|
node_draw.image_enum,
|
||||||
),
|
),
|
||||||
self.network_picker,
|
self.network_picker,
|
||||||
node_draw.label,
|
node_draw.label,
|
||||||
|
@ -340,13 +392,14 @@ class Toolbar(ttk.Frame):
|
||||||
Create link-layer node button and the options that represent different
|
Create link-layer node button and the options that represent different
|
||||||
link-layer node types.
|
link-layer node types.
|
||||||
"""
|
"""
|
||||||
image = icon(ImageEnum.HUB)
|
image = self.get_icon(ImageEnum.HUB, TOOLBAR_SIZE)
|
||||||
self.network_button = ttk.Button(
|
self.network_button = ttk.Button(
|
||||||
self.design_frame, image=image, command=self.draw_network_picker
|
self.design_frame, image=image, command=self.draw_network_picker
|
||||||
)
|
)
|
||||||
self.network_button.image = image
|
self.network_button.image = image
|
||||||
self.network_button.grid(sticky="ew")
|
self.network_button.grid(sticky="ew")
|
||||||
Tooltip(self.network_button, "link-layer nodes")
|
Tooltip(self.network_button, "link-layer nodes")
|
||||||
|
self.network_enum = ImageEnum.HUB
|
||||||
|
|
||||||
def draw_annotation_picker(self):
|
def draw_annotation_picker(self):
|
||||||
"""
|
"""
|
||||||
|
@ -361,11 +414,11 @@ class Toolbar(ttk.Frame):
|
||||||
(ImageEnum.TEXT, ShapeType.TEXT),
|
(ImageEnum.TEXT, ShapeType.TEXT),
|
||||||
]
|
]
|
||||||
for image_enum, shape_type in nodes:
|
for image_enum, shape_type in nodes:
|
||||||
toolbar_image = icon(image_enum)
|
toolbar_image = self.get_icon(image_enum, TOOLBAR_SIZE)
|
||||||
image = icon(image_enum, PICKER_SIZE)
|
image = self.get_icon(image_enum, PICKER_SIZE)
|
||||||
self.create_picker_button(
|
self.create_picker_button(
|
||||||
image,
|
image,
|
||||||
partial(self.update_annotation, toolbar_image, shape_type),
|
partial(self.update_annotation, toolbar_image, shape_type, image_enum),
|
||||||
self.annotation_picker,
|
self.annotation_picker,
|
||||||
shape_type.value,
|
shape_type.value,
|
||||||
)
|
)
|
||||||
|
@ -378,13 +431,14 @@ class Toolbar(ttk.Frame):
|
||||||
"""
|
"""
|
||||||
Create marker button and options that represent different marker types
|
Create marker button and options that represent different marker types
|
||||||
"""
|
"""
|
||||||
image = icon(ImageEnum.MARKER)
|
image = self.get_icon(ImageEnum.MARKER, TOOLBAR_SIZE)
|
||||||
self.annotation_button = ttk.Button(
|
self.annotation_button = ttk.Button(
|
||||||
self.design_frame, image=image, command=self.draw_annotation_picker
|
self.design_frame, image=image, command=self.draw_annotation_picker
|
||||||
)
|
)
|
||||||
self.annotation_button.image = image
|
self.annotation_button.image = image
|
||||||
self.annotation_button.grid(sticky="ew")
|
self.annotation_button.grid(sticky="ew")
|
||||||
Tooltip(self.annotation_button, "background annotation tools")
|
Tooltip(self.annotation_button, "background annotation tools")
|
||||||
|
self.annotation_enum = ImageEnum.MARKER
|
||||||
|
|
||||||
def create_observe_button(self):
|
def create_observe_button(self):
|
||||||
menu_button = ttk.Menubutton(
|
menu_button = ttk.Menubutton(
|
||||||
|
@ -416,6 +470,7 @@ class Toolbar(ttk.Frame):
|
||||||
"""
|
"""
|
||||||
logging.info("Click stop button")
|
logging.info("Click stop button")
|
||||||
self.app.canvas.hide_context()
|
self.app.canvas.hide_context()
|
||||||
|
self.app.menubar.change_menubar_item_state(is_runtime=False)
|
||||||
self.app.statusbar.progress_bar.start(5)
|
self.app.statusbar.progress_bar.start(5)
|
||||||
self.time = time.perf_counter()
|
self.time = time.perf_counter()
|
||||||
task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback)
|
task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback)
|
||||||
|
@ -429,13 +484,16 @@ class Toolbar(ttk.Frame):
|
||||||
self.app.statusbar.set_status(message)
|
self.app.statusbar.set_status(message)
|
||||||
self.app.canvas.stopped_session()
|
self.app.canvas.stopped_session()
|
||||||
|
|
||||||
def update_annotation(self, image: "ImageTk.PhotoImage", shape_type: ShapeType):
|
def update_annotation(
|
||||||
|
self, image: "ImageTk.PhotoImage", shape_type: ShapeType, image_enum
|
||||||
|
):
|
||||||
logging.debug("clicked annotation: ")
|
logging.debug("clicked annotation: ")
|
||||||
self.hide_pickers()
|
self.hide_pickers()
|
||||||
self.annotation_button.configure(image=image)
|
self.annotation_button.configure(image=image)
|
||||||
self.annotation_button.image = image
|
self.annotation_button.image = image
|
||||||
self.app.canvas.mode = GraphMode.ANNOTATION
|
self.app.canvas.mode = GraphMode.ANNOTATION
|
||||||
self.app.canvas.annotation_type = shape_type
|
self.app.canvas.annotation_type = shape_type
|
||||||
|
self.annotation_enum = image_enum
|
||||||
if is_marker(shape_type):
|
if is_marker(shape_type):
|
||||||
if self.marker_tool:
|
if self.marker_tool:
|
||||||
self.marker_tool.destroy()
|
self.marker_tool.destroy()
|
||||||
|
@ -460,3 +518,24 @@ class Toolbar(ttk.Frame):
|
||||||
|
|
||||||
def click_two_node_button(self):
|
def click_two_node_button(self):
|
||||||
logging.debug("Click TWONODE button")
|
logging.debug("Click TWONODE button")
|
||||||
|
|
||||||
|
# def scale_button(cls, button, image_enum, scale):
|
||||||
|
def scale_button(self, button, image_enum):
|
||||||
|
image = icon(image_enum, int(TOOLBAR_SIZE * self.app.app_scale))
|
||||||
|
button.config(image=image)
|
||||||
|
button.image = image
|
||||||
|
|
||||||
|
def scale(self):
|
||||||
|
self.scale_button(self.play_button, ImageEnum.START)
|
||||||
|
self.scale_button(self.select_button, ImageEnum.SELECT)
|
||||||
|
self.scale_button(self.link_button, ImageEnum.LINK)
|
||||||
|
self.scale_button(self.node_button, self.node_enum)
|
||||||
|
self.scale_button(self.network_button, self.network_enum)
|
||||||
|
self.scale_button(self.annotation_button, self.annotation_enum)
|
||||||
|
|
||||||
|
self.scale_button(self.runtime_select_button, ImageEnum.SELECT)
|
||||||
|
self.scale_button(self.stop_button, ImageEnum.STOP)
|
||||||
|
self.scale_button(self.plot_button, ImageEnum.PLOT)
|
||||||
|
self.scale_button(self.runtime_marker_button, ImageEnum.MARKER)
|
||||||
|
self.scale_button(self.node_command_button, ImageEnum.TWONODE)
|
||||||
|
self.scale_button(self.run_command_button, ImageEnum.RUN)
|
||||||
|
|
|
@ -11,12 +11,16 @@ from netaddr import IPNetwork
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
|
||||||
|
SMALLEST_SCALE = 0.5
|
||||||
|
LARGEST_SCALE = 5.0
|
||||||
|
|
||||||
|
|
||||||
class InputValidation:
|
class InputValidation:
|
||||||
def __init__(self, app: "Application"):
|
def __init__(self, app: "Application"):
|
||||||
self.master = app.master
|
self.master = app.master
|
||||||
self.positive_int = None
|
self.positive_int = None
|
||||||
self.positive_float = None
|
self.positive_float = None
|
||||||
|
self.app_scale = None
|
||||||
self.name = None
|
self.name = None
|
||||||
self.ip4 = None
|
self.ip4 = None
|
||||||
self.rgb = None
|
self.rgb = None
|
||||||
|
@ -26,6 +30,7 @@ class InputValidation:
|
||||||
def register(self):
|
def register(self):
|
||||||
self.positive_int = self.master.register(self.check_positive_int)
|
self.positive_int = self.master.register(self.check_positive_int)
|
||||||
self.positive_float = self.master.register(self.check_positive_float)
|
self.positive_float = self.master.register(self.check_positive_float)
|
||||||
|
self.app_scale = self.master.register(self.check_scale_value)
|
||||||
self.name = self.master.register(self.check_node_name)
|
self.name = self.master.register(self.check_node_name)
|
||||||
self.ip4 = self.master.register(self.check_ip4)
|
self.ip4 = self.master.register(self.check_ip4)
|
||||||
self.rgb = self.master.register(self.check_rbg)
|
self.rgb = self.master.register(self.check_rbg)
|
||||||
|
@ -105,6 +110,18 @@ class InputValidation:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_scale_value(cls, s: str) -> bool:
|
||||||
|
if not s:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
float_value = float(s)
|
||||||
|
if SMALLEST_SCALE <= float_value <= LARGEST_SCALE or float_value == 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_ip4(cls, s: str) -> bool:
|
def check_ip4(cls, s: str) -> bool:
|
||||||
if not s:
|
if not s:
|
||||||
|
|
|
@ -1,279 +0,0 @@
|
||||||
"""
|
|
||||||
location.py: definition of CoreLocation class that is a member of the
|
|
||||||
Session object. Provides conversions between Cartesian and geographic coordinate
|
|
||||||
systems. Depends on utm contributed module, from
|
|
||||||
https://pypi.python.org/pypi/utm (version 0.3.0).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
|
|
||||||
from core.emulator.enumerations import RegisterTlvs
|
|
||||||
from core.location import utm
|
|
||||||
|
|
||||||
|
|
||||||
class CoreLocation:
|
|
||||||
"""
|
|
||||||
Member of session class for handling global location data. This keeps
|
|
||||||
track of a latitude/longitude/altitude reference point and scale in
|
|
||||||
order to convert between X,Y and geo coordinates.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = "location"
|
|
||||||
config_type = RegisterTlvs.UTILITY.value
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""
|
|
||||||
Creates a MobilityManager instance.
|
|
||||||
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
# ConfigurableManager.__init__(self)
|
|
||||||
self.reset()
|
|
||||||
self.zonemap = {}
|
|
||||||
self.refxyz = (0.0, 0.0, 0.0)
|
|
||||||
self.refscale = 1.0
|
|
||||||
self.zoneshifts = {}
|
|
||||||
self.refgeo = (0.0, 0.0, 0.0)
|
|
||||||
for n, l in utm.ZONE_LETTERS:
|
|
||||||
self.zonemap[l] = n
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
"""
|
|
||||||
Reset to initial state.
|
|
||||||
"""
|
|
||||||
# (x, y, z) coordinates of the point given by self.refgeo
|
|
||||||
self.refxyz = (0.0, 0.0, 0.0)
|
|
||||||
# decimal latitude, longitude, and altitude at the point (x, y, z)
|
|
||||||
self.setrefgeo(0.0, 0.0, 0.0)
|
|
||||||
# 100 pixels equals this many meters
|
|
||||||
self.refscale = 1.0
|
|
||||||
# cached distance to refpt in other zones
|
|
||||||
self.zoneshifts = {}
|
|
||||||
|
|
||||||
def px2m(self, val: float) -> float:
|
|
||||||
"""
|
|
||||||
Convert the specified value in pixels to meters using the
|
|
||||||
configured scale. The scale is given as s, where
|
|
||||||
100 pixels = s meters.
|
|
||||||
|
|
||||||
:param val: value to use in converting to meters
|
|
||||||
:return: value converted to meters
|
|
||||||
"""
|
|
||||||
return (val / 100.0) * self.refscale
|
|
||||||
|
|
||||||
def m2px(self, val: float) -> float:
|
|
||||||
"""
|
|
||||||
Convert the specified value in meters to pixels using the
|
|
||||||
configured scale. The scale is given as s, where
|
|
||||||
100 pixels = s meters.
|
|
||||||
|
|
||||||
:param val: value to convert to pixels
|
|
||||||
:return: value converted to pixels
|
|
||||||
"""
|
|
||||||
if self.refscale == 0.0:
|
|
||||||
return 0.0
|
|
||||||
return 100.0 * (val / self.refscale)
|
|
||||||
|
|
||||||
def setrefgeo(self, lat: float, lon: float, alt: float) -> None:
|
|
||||||
"""
|
|
||||||
Record the geographical reference point decimal (lat, lon, alt)
|
|
||||||
and convert and store its UTM equivalent for later use.
|
|
||||||
|
|
||||||
:param lat: latitude
|
|
||||||
:param lon: longitude
|
|
||||||
:param alt: altitude
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
self.refgeo = (lat, lon, alt)
|
|
||||||
# easting, northing, zone
|
|
||||||
e, n, zonen, zonel = utm.from_latlon(lat, lon)
|
|
||||||
self.refutm = ((zonen, zonel), e, n, alt)
|
|
||||||
|
|
||||||
def getgeo(self, x: float, y: float, z: float) -> Tuple[float, float, float]:
|
|
||||||
"""
|
|
||||||
Given (x, y, z) Cartesian coordinates, convert them to latitude,
|
|
||||||
longitude, and altitude based on the configured reference point
|
|
||||||
and scale.
|
|
||||||
|
|
||||||
:param x: x value
|
|
||||||
:param y: y value
|
|
||||||
:param z: z value
|
|
||||||
:return: lat, lon, alt values for provided coordinates
|
|
||||||
"""
|
|
||||||
# shift (x,y,z) over to reference point (x,y,z)
|
|
||||||
x -= self.refxyz[0]
|
|
||||||
y = -(y - self.refxyz[1])
|
|
||||||
if z is None:
|
|
||||||
z = self.refxyz[2]
|
|
||||||
else:
|
|
||||||
z -= self.refxyz[2]
|
|
||||||
# use UTM coordinates since unit is meters
|
|
||||||
zone = self.refutm[0]
|
|
||||||
if zone == "":
|
|
||||||
raise ValueError("reference point not configured")
|
|
||||||
e = self.refutm[1] + self.px2m(x)
|
|
||||||
n = self.refutm[2] + self.px2m(y)
|
|
||||||
alt = self.refutm[3] + self.px2m(z)
|
|
||||||
(e, n, zone) = self.getutmzoneshift(e, n)
|
|
||||||
try:
|
|
||||||
lat, lon = utm.to_latlon(e, n, zone[0], zone[1])
|
|
||||||
except utm.OutOfRangeError:
|
|
||||||
logging.exception(
|
|
||||||
"UTM out of range error for n=%s zone=%s xyz=(%s,%s,%s)",
|
|
||||||
n,
|
|
||||||
zone,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
z,
|
|
||||||
)
|
|
||||||
lat, lon = self.refgeo[:2]
|
|
||||||
return lat, lon, alt
|
|
||||||
|
|
||||||
def getxyz(self, lat: float, lon: float, alt: float) -> Tuple[float, float, float]:
|
|
||||||
"""
|
|
||||||
Given latitude, longitude, and altitude location data, convert them
|
|
||||||
to (x, y, z) Cartesian coordinates based on the configured
|
|
||||||
reference point and scale. Lat/lon is converted to UTM meter
|
|
||||||
coordinates, UTM zones are accounted for, and the scale turns
|
|
||||||
meters to pixels.
|
|
||||||
|
|
||||||
:param lat: latitude
|
|
||||||
:param lon: longitude
|
|
||||||
:param alt: altitude
|
|
||||||
:return: converted x, y, z coordinates
|
|
||||||
"""
|
|
||||||
# convert lat/lon to UTM coordinates in meters
|
|
||||||
e, n, zonen, zonel = utm.from_latlon(lat, lon)
|
|
||||||
_rlat, _rlon, ralt = self.refgeo
|
|
||||||
xshift = self.geteastingshift(zonen, zonel)
|
|
||||||
if xshift is None:
|
|
||||||
xm = e - self.refutm[1]
|
|
||||||
else:
|
|
||||||
xm = e + xshift
|
|
||||||
yshift = self.getnorthingshift(zonen, zonel)
|
|
||||||
if yshift is None:
|
|
||||||
ym = n - self.refutm[2]
|
|
||||||
else:
|
|
||||||
ym = n + yshift
|
|
||||||
zm = alt - ralt
|
|
||||||
|
|
||||||
# shift (x,y,z) over to reference point (x,y,z)
|
|
||||||
x = self.m2px(xm) + self.refxyz[0]
|
|
||||||
y = -(self.m2px(ym) + self.refxyz[1])
|
|
||||||
z = self.m2px(zm) + self.refxyz[2]
|
|
||||||
return x, y, z
|
|
||||||
|
|
||||||
def geteastingshift(self, zonen: float, zonel: float) -> Optional[float]:
|
|
||||||
"""
|
|
||||||
If the lat, lon coordinates being converted are located in a
|
|
||||||
different UTM zone than the canvas reference point, the UTM meters
|
|
||||||
may need to be shifted.
|
|
||||||
This picks a reference point in the same longitudinal band
|
|
||||||
(UTM zone number) as the provided zone, to calculate the shift in
|
|
||||||
meters for the x coordinate.
|
|
||||||
|
|
||||||
:param zonen: zonen
|
|
||||||
:param zonel: zone1
|
|
||||||
:return: the x shift value
|
|
||||||
"""
|
|
||||||
rzonen = int(self.refutm[0][0])
|
|
||||||
# same zone number, no x shift required
|
|
||||||
if zonen == rzonen:
|
|
||||||
return None
|
|
||||||
z = (zonen, zonel)
|
|
||||||
# x shift already calculated, cached
|
|
||||||
if z in self.zoneshifts and self.zoneshifts[z][0] is not None:
|
|
||||||
return self.zoneshifts[z][0]
|
|
||||||
|
|
||||||
rlat, rlon, _ralt = self.refgeo
|
|
||||||
# ea. zone is 6deg band
|
|
||||||
lon2 = rlon + 6 * (zonen - rzonen)
|
|
||||||
# ignore northing
|
|
||||||
e2, _n2, _zonen2, _zonel2 = utm.from_latlon(rlat, lon2)
|
|
||||||
# NOTE: great circle distance used here, not reference ellipsoid!
|
|
||||||
xshift = utm.haversine(rlon, rlat, lon2, rlat) - e2
|
|
||||||
# cache the return value
|
|
||||||
yshift = None
|
|
||||||
if z in self.zoneshifts:
|
|
||||||
yshift = self.zoneshifts[z][1]
|
|
||||||
self.zoneshifts[z] = (xshift, yshift)
|
|
||||||
return xshift
|
|
||||||
|
|
||||||
def getnorthingshift(self, zonen: float, zonel: float) -> Optional[float]:
|
|
||||||
"""
|
|
||||||
If the lat, lon coordinates being converted are located in a
|
|
||||||
different UTM zone than the canvas reference point, the UTM meters
|
|
||||||
may need to be shifted.
|
|
||||||
This picks a reference point in the same latitude band (UTM zone letter)
|
|
||||||
as the provided zone, to calculate the shift in meters for the
|
|
||||||
y coordinate.
|
|
||||||
|
|
||||||
:param zonen: zonen
|
|
||||||
:param zonel: zone1
|
|
||||||
:return: calculated y shift
|
|
||||||
"""
|
|
||||||
rzonel = self.refutm[0][1]
|
|
||||||
# same zone letter, no y shift required
|
|
||||||
if zonel == rzonel:
|
|
||||||
return None
|
|
||||||
z = (zonen, zonel)
|
|
||||||
# y shift already calculated, cached
|
|
||||||
if z in self.zoneshifts and self.zoneshifts[z][1] is not None:
|
|
||||||
return self.zoneshifts[z][1]
|
|
||||||
|
|
||||||
rlat, rlon, _ralt = self.refgeo
|
|
||||||
# zonemap is used to calculate degrees difference between zone letters
|
|
||||||
latshift = self.zonemap[zonel] - self.zonemap[rzonel]
|
|
||||||
# ea. latitude band is 8deg high
|
|
||||||
lat2 = rlat + latshift
|
|
||||||
_e2, n2, _zonen2, _zonel2 = utm.from_latlon(lat2, rlon)
|
|
||||||
# NOTE: great circle distance used here, not reference ellipsoid
|
|
||||||
yshift = -(utm.haversine(rlon, rlat, rlon, lat2) + n2)
|
|
||||||
# cache the return value
|
|
||||||
xshift = None
|
|
||||||
if z in self.zoneshifts:
|
|
||||||
xshift = self.zoneshifts[z][0]
|
|
||||||
self.zoneshifts[z] = (xshift, yshift)
|
|
||||||
return yshift
|
|
||||||
|
|
||||||
def getutmzoneshift(
|
|
||||||
self, e: float, n: float
|
|
||||||
) -> Tuple[float, float, Tuple[float, str]]:
|
|
||||||
"""
|
|
||||||
Given UTM easting and northing values, check if they fall outside
|
|
||||||
the reference point's zone boundary. Return the UTM coordinates in a
|
|
||||||
different zone and the new zone if they do. Zone lettering is only
|
|
||||||
changed when the reference point is in the opposite hemisphere.
|
|
||||||
|
|
||||||
:param e: easting value
|
|
||||||
:param n: northing value
|
|
||||||
:return: modified easting, northing, and zone values
|
|
||||||
"""
|
|
||||||
zone = self.refutm[0]
|
|
||||||
rlat, rlon, _ralt = self.refgeo
|
|
||||||
if e > 834000 or e < 166000:
|
|
||||||
num_zones = (int(e) - 166000) / (utm.R / 10)
|
|
||||||
# estimate number of zones to shift, E (positive) or W (negative)
|
|
||||||
rlon2 = self.refgeo[1] + (num_zones * 6)
|
|
||||||
_e2, _n2, zonen2, zonel2 = utm.from_latlon(rlat, rlon2)
|
|
||||||
xshift = utm.haversine(rlon, rlat, rlon2, rlat)
|
|
||||||
# after >3 zones away from refpt, the above estimate won't work
|
|
||||||
# (the above estimate could be improved)
|
|
||||||
if not 100000 <= (e - xshift) < 1000000:
|
|
||||||
# move one more zone away
|
|
||||||
num_zones = (abs(num_zones) + 1) * (abs(num_zones) / num_zones)
|
|
||||||
rlon2 = self.refgeo[1] + (num_zones * 6)
|
|
||||||
_e2, _n2, zonen2, zonel2 = utm.from_latlon(rlat, rlon2)
|
|
||||||
xshift = utm.haversine(rlon, rlat, rlon2, rlat)
|
|
||||||
e = e - xshift
|
|
||||||
zone = (zonen2, zonel2)
|
|
||||||
if n < 0:
|
|
||||||
# refpt in northern hemisphere and we crossed south of equator
|
|
||||||
n += 10000000
|
|
||||||
zone = (zone[0], "M")
|
|
||||||
elif n > 10000000:
|
|
||||||
# refpt in southern hemisphere and we crossed north of equator
|
|
||||||
n -= 10000000
|
|
||||||
zone = (zone[0], "N")
|
|
||||||
return e, n, zone
|
|
124
daemon/core/location/geo.py
Normal file
124
daemon/core/location/geo.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
"""
|
||||||
|
Provides conversions from x,y,z to lon,lat,alt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import pyproj
|
||||||
|
|
||||||
|
from core.emulator.enumerations import RegisterTlvs
|
||||||
|
|
||||||
|
SCALE_FACTOR = 100.0
|
||||||
|
CRS_WGS84 = 4326
|
||||||
|
CRS_PROJ = 3857
|
||||||
|
|
||||||
|
|
||||||
|
class GeoLocation:
|
||||||
|
"""
|
||||||
|
Provides logic to convert x,y,z coordinates to lon,lat,alt using
|
||||||
|
defined projections.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "location"
|
||||||
|
config_type = RegisterTlvs.UTILITY.value
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""
|
||||||
|
Creates a GeoLocation instance.
|
||||||
|
"""
|
||||||
|
self.to_pixels = pyproj.Transformer.from_crs(
|
||||||
|
CRS_WGS84, CRS_PROJ, always_xy=True
|
||||||
|
)
|
||||||
|
self.to_geo = pyproj.Transformer.from_crs(CRS_PROJ, CRS_WGS84, always_xy=True)
|
||||||
|
self.refproj = (0.0, 0.0)
|
||||||
|
self.refgeo = (0.0, 0.0, 0.0)
|
||||||
|
self.refxyz = (0.0, 0.0, 0.0)
|
||||||
|
self.refscale = 1.0
|
||||||
|
|
||||||
|
def setrefgeo(self, lat: float, lon: float, alt: float) -> None:
|
||||||
|
"""
|
||||||
|
Set the geospatial reference point.
|
||||||
|
|
||||||
|
:param lat: latitude reference
|
||||||
|
:param lon: longitude reference
|
||||||
|
:param alt: altitude reference
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
self.refgeo = (lat, lon, alt)
|
||||||
|
px, py = self.to_pixels.transform(lon, lat)
|
||||||
|
self.refproj = (px, py, alt)
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""
|
||||||
|
Reset reference data to default values.
|
||||||
|
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
self.refxyz = (0.0, 0.0, 0.0)
|
||||||
|
self.refgeo = (0.0, 0.0, 0.0)
|
||||||
|
self.refscale = 1.0
|
||||||
|
self.refproj = self.to_pixels.transform(self.refgeo[0], self.refgeo[1])
|
||||||
|
|
||||||
|
def pixels2meters(self, value: float) -> float:
|
||||||
|
"""
|
||||||
|
Provides conversion from pixels to meters.
|
||||||
|
|
||||||
|
:param value: pixels value
|
||||||
|
:return: pixels value in meters
|
||||||
|
"""
|
||||||
|
return (value / SCALE_FACTOR) * self.refscale
|
||||||
|
|
||||||
|
def meters2pixels(self, value: float) -> float:
|
||||||
|
"""
|
||||||
|
Provides conversion from meters to pixels.
|
||||||
|
|
||||||
|
:param value: meters value
|
||||||
|
:return: meters value in pixels
|
||||||
|
"""
|
||||||
|
if self.refscale == 0.0:
|
||||||
|
return 0.0
|
||||||
|
return SCALE_FACTOR * (value / self.refscale)
|
||||||
|
|
||||||
|
def getxyz(self, lat: float, lon: float, alt: float) -> Tuple[float, float, float]:
|
||||||
|
"""
|
||||||
|
Convert provided lon,lat,alt to x,y,z.
|
||||||
|
|
||||||
|
:param lat: latitude value
|
||||||
|
:param lon: longitude value
|
||||||
|
:param alt: altitude value
|
||||||
|
:return: x,y,z representation of provided values
|
||||||
|
"""
|
||||||
|
logging.debug("input lon,lat,alt(%s, %s, %s)", lon, lat, alt)
|
||||||
|
px, py = self.to_pixels.transform(lon, lat)
|
||||||
|
px -= self.refproj[0]
|
||||||
|
py -= self.refproj[1]
|
||||||
|
pz = alt - self.refproj[2]
|
||||||
|
x = self.meters2pixels(px) + self.refxyz[0]
|
||||||
|
y = -(self.meters2pixels(py) + self.refxyz[1])
|
||||||
|
z = self.meters2pixels(pz) + self.refxyz[2]
|
||||||
|
logging.debug("result x,y,z(%s, %s, %s)", x, y, z)
|
||||||
|
return x, y, z
|
||||||
|
|
||||||
|
def getgeo(self, x: float, y: float, z: float) -> Tuple[float, float, float]:
|
||||||
|
"""
|
||||||
|
Convert provided x,y,z to lon,lat,alt.
|
||||||
|
|
||||||
|
:param x: x value
|
||||||
|
:param y: y value
|
||||||
|
:param z: z value
|
||||||
|
:return: lat,lon,alt representation of provided values
|
||||||
|
"""
|
||||||
|
logging.debug("input x,y(%s, %s)", x, y)
|
||||||
|
x -= self.refxyz[0]
|
||||||
|
y = -(y - self.refxyz[1])
|
||||||
|
if z is None:
|
||||||
|
z = self.refxyz[2]
|
||||||
|
else:
|
||||||
|
z -= self.refxyz[2]
|
||||||
|
px = self.refproj[0] + self.pixels2meters(x)
|
||||||
|
py = self.refproj[1] + self.pixels2meters(y)
|
||||||
|
lon, lat = self.to_geo.transform(px, py)
|
||||||
|
alt = self.refgeo[2] + self.pixels2meters(z)
|
||||||
|
logging.debug("result lon,lat,alt(%s, %s, %s)", lon, lat, alt)
|
||||||
|
return lat, lon, alt
|
|
@ -1,259 +0,0 @@
|
||||||
"""
|
|
||||||
utm
|
|
||||||
===
|
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/Turbo87/utm.png
|
|
||||||
|
|
||||||
Bidirectional UTM-WGS84 converter for python
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
import utm
|
|
||||||
|
|
||||||
Convert a (latitude, longitude) tuple into an UTM coordinate::
|
|
||||||
|
|
||||||
utm.from_latlon(51.2, 7.5)
|
|
||||||
>>> (395201.3103811303, 5673135.241182375, 32, 'U')
|
|
||||||
|
|
||||||
Convert an UTM coordinate into a (latitude, longitude) tuple::
|
|
||||||
|
|
||||||
utm.to_latlon(340000, 5710000, 32, 'U')
|
|
||||||
>>> (51.51852098408468, 6.693872395145327)
|
|
||||||
|
|
||||||
Speed
|
|
||||||
-----
|
|
||||||
|
|
||||||
The library has been compared to the more generic pyproj library by running the
|
|
||||||
unit test suite through pyproj instead of utm. These are the results:
|
|
||||||
|
|
||||||
* with pyproj (without projection cache): 4.0 - 4.5 sec
|
|
||||||
* with pyproj (with projection cache): 0.9 - 1.0 sec
|
|
||||||
* with utm: 0.4 - 0.5 sec
|
|
||||||
|
|
||||||
Authors
|
|
||||||
-------
|
|
||||||
|
|
||||||
* Tobias Bieniek <Tobias.Bieniek@gmx.de>
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
Copyright (C) 2012 Tobias Bieniek <Tobias.Bieniek@gmx.de>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
__all__ = ['to_latlon', 'from_latlon']
|
|
||||||
|
|
||||||
|
|
||||||
class OutOfRangeError(ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
K0 = 0.9996
|
|
||||||
|
|
||||||
E = 0.00669438
|
|
||||||
E2 = E * E
|
|
||||||
E3 = E2 * E
|
|
||||||
E_P2 = E / (1.0 - E)
|
|
||||||
|
|
||||||
SQRT_E = math.sqrt(1 - E)
|
|
||||||
_E = (1 - SQRT_E) / (1 + SQRT_E)
|
|
||||||
_E3 = _E * _E * _E
|
|
||||||
_E4 = _E3 * _E
|
|
||||||
|
|
||||||
M1 = (1 - E / 4 - 3 * E2 / 64 - 5 * E3 / 256)
|
|
||||||
M2 = (3 * E / 8 + 3 * E2 / 32 + 45 * E3 / 1024)
|
|
||||||
M3 = (15 * E2 / 256 + 45 * E3 / 1024)
|
|
||||||
M4 = (35 * E3 / 3072)
|
|
||||||
|
|
||||||
P2 = (3 * _E / 2 - 27 * _E3 / 32)
|
|
||||||
P3 = (21 * _E3 / 16 - 55 * _E4 / 32)
|
|
||||||
P4 = (151 * _E3 / 96)
|
|
||||||
|
|
||||||
R = 6378137
|
|
||||||
|
|
||||||
ZONE_LETTERS = [
|
|
||||||
(84, None), (72, 'X'), (64, 'W'), (56, 'V'), (48, 'U'), (40, 'T'),
|
|
||||||
(32, 'S'), (24, 'R'), (16, 'Q'), (8, 'P'), (0, 'N'), (-8, 'M'), (-16, 'L'),
|
|
||||||
(-24, 'K'), (-32, 'J'), (-40, 'H'), (-48, 'G'), (-56, 'F'), (-64, 'E'),
|
|
||||||
(-72, 'D'), (-80, 'C')
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def to_latlon(easting, northing, zone_number, zone_letter):
|
|
||||||
zone_letter = zone_letter.upper()
|
|
||||||
|
|
||||||
if not 100000 <= easting < 1000000:
|
|
||||||
raise OutOfRangeError('easting out of range (must be between 100.000 m and 999.999 m)')
|
|
||||||
if not 0 <= northing <= 10000000:
|
|
||||||
raise OutOfRangeError('northing out of range (must be between 0 m and 10.000.000 m)')
|
|
||||||
if not 1 <= zone_number <= 60:
|
|
||||||
raise OutOfRangeError('zone number out of range (must be between 1 and 60)')
|
|
||||||
if not 'C' <= zone_letter <= 'X' or zone_letter in ['I', 'O']:
|
|
||||||
raise OutOfRangeError('zone letter out of range (must be between C and X)')
|
|
||||||
|
|
||||||
x = easting - 500000
|
|
||||||
y = northing
|
|
||||||
|
|
||||||
if zone_letter < 'N':
|
|
||||||
y -= 10000000
|
|
||||||
|
|
||||||
m = y / K0
|
|
||||||
mu = m / (R * M1)
|
|
||||||
|
|
||||||
p_rad = (mu + P2 * math.sin(2 * mu) + P3 * math.sin(4 * mu) + P4 * math.sin(6 * mu))
|
|
||||||
|
|
||||||
p_sin = math.sin(p_rad)
|
|
||||||
p_sin2 = p_sin * p_sin
|
|
||||||
|
|
||||||
p_cos = math.cos(p_rad)
|
|
||||||
|
|
||||||
p_tan = p_sin / p_cos
|
|
||||||
p_tan2 = p_tan * p_tan
|
|
||||||
p_tan4 = p_tan2 * p_tan2
|
|
||||||
|
|
||||||
ep_sin = 1 - E * p_sin2
|
|
||||||
ep_sin_sqrt = math.sqrt(1 - E * p_sin2)
|
|
||||||
|
|
||||||
n = R / ep_sin_sqrt
|
|
||||||
r = (1 - E) / ep_sin
|
|
||||||
|
|
||||||
c = _E * p_cos ** 2
|
|
||||||
c2 = c * c
|
|
||||||
|
|
||||||
d = x / (n * K0)
|
|
||||||
d2 = d * d
|
|
||||||
d3 = d2 * d
|
|
||||||
d4 = d3 * d
|
|
||||||
d5 = d4 * d
|
|
||||||
d6 = d5 * d
|
|
||||||
|
|
||||||
latitude = (p_rad - (p_tan / r) *
|
|
||||||
(d2 / 2 -
|
|
||||||
d4 / 24 * (5 + 3 * p_tan2 + 10 * c - 4 * c2 - 9 * E_P2)) +
|
|
||||||
d6 / 720 * (61 + 90 * p_tan2 + 298 * c + 45 * p_tan4 - 252 * E_P2 - 3 * c2))
|
|
||||||
|
|
||||||
longitude = (d -
|
|
||||||
d3 / 6 * (1 + 2 * p_tan2 + c) +
|
|
||||||
d5 / 120 * (5 - 2 * c + 28 * p_tan2 - 3 * c2 + 8 * E_P2 + 24 * p_tan4)) / p_cos
|
|
||||||
|
|
||||||
return (math.degrees(latitude),
|
|
||||||
math.degrees(longitude) + zone_number_to_central_longitude(zone_number))
|
|
||||||
|
|
||||||
|
|
||||||
def from_latlon(latitude, longitude):
|
|
||||||
if not -80.0 <= latitude <= 84.0:
|
|
||||||
raise OutOfRangeError('latitude out of range (must be between 80 deg S and 84 deg N)')
|
|
||||||
if not -180.0 <= longitude <= 180.0:
|
|
||||||
raise OutOfRangeError('northing out of range (must be between 180 deg W and 180 deg E)')
|
|
||||||
|
|
||||||
lat_rad = math.radians(latitude)
|
|
||||||
lat_sin = math.sin(lat_rad)
|
|
||||||
lat_cos = math.cos(lat_rad)
|
|
||||||
|
|
||||||
lat_tan = lat_sin / lat_cos
|
|
||||||
lat_tan2 = lat_tan * lat_tan
|
|
||||||
lat_tan4 = lat_tan2 * lat_tan2
|
|
||||||
|
|
||||||
lon_rad = math.radians(longitude)
|
|
||||||
|
|
||||||
zone_number = latlon_to_zone_number(latitude, longitude)
|
|
||||||
central_lon = zone_number_to_central_longitude(zone_number)
|
|
||||||
central_lon_rad = math.radians(central_lon)
|
|
||||||
|
|
||||||
zone_letter = latitude_to_zone_letter(latitude)
|
|
||||||
|
|
||||||
n = R / math.sqrt(1 - E * lat_sin ** 2)
|
|
||||||
c = E_P2 * lat_cos ** 2
|
|
||||||
|
|
||||||
a = lat_cos * (lon_rad - central_lon_rad)
|
|
||||||
a2 = a * a
|
|
||||||
a3 = a2 * a
|
|
||||||
a4 = a3 * a
|
|
||||||
a5 = a4 * a
|
|
||||||
a6 = a5 * a
|
|
||||||
|
|
||||||
m = R * (M1 * lat_rad -
|
|
||||||
M2 * math.sin(2 * lat_rad) +
|
|
||||||
M3 * math.sin(4 * lat_rad) -
|
|
||||||
M4 * math.sin(6 * lat_rad))
|
|
||||||
|
|
||||||
easting = K0 * n * (a +
|
|
||||||
a3 / 6 * (1 - lat_tan2 + c) +
|
|
||||||
a5 / 120 * (5 - 18 * lat_tan2 + lat_tan4 + 72 * c - 58 * E_P2)) + 500000
|
|
||||||
|
|
||||||
northing = K0 * (m + n * lat_tan * (a2 / 2 +
|
|
||||||
a4 / 24 * (5 - lat_tan2 + 9 * c + 4 * c ** 2) +
|
|
||||||
a6 / 720 * (61 - 58 * lat_tan2 + lat_tan4 + 600 * c - 330 * E_P2)))
|
|
||||||
|
|
||||||
if latitude < 0:
|
|
||||||
northing += 10000000
|
|
||||||
|
|
||||||
return easting, northing, zone_number, zone_letter
|
|
||||||
|
|
||||||
|
|
||||||
def latitude_to_zone_letter(latitude):
|
|
||||||
for lat_min, zone_letter in ZONE_LETTERS:
|
|
||||||
if latitude >= lat_min:
|
|
||||||
return zone_letter
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def latlon_to_zone_number(latitude, longitude):
|
|
||||||
if 56 <= latitude <= 64 and 3 <= longitude <= 12:
|
|
||||||
return 32
|
|
||||||
|
|
||||||
if 72 <= latitude <= 84 and longitude >= 0:
|
|
||||||
if longitude <= 9:
|
|
||||||
return 31
|
|
||||||
elif longitude <= 21:
|
|
||||||
return 33
|
|
||||||
elif longitude <= 33:
|
|
||||||
return 35
|
|
||||||
elif longitude <= 42:
|
|
||||||
return 37
|
|
||||||
|
|
||||||
return int((longitude + 180) / 6) + 1
|
|
||||||
|
|
||||||
|
|
||||||
def zone_number_to_central_longitude(zone_number):
|
|
||||||
return (zone_number - 1) * 6 - 180 + 3
|
|
||||||
|
|
||||||
|
|
||||||
def haversine(lon1, lat1, lon2, lat2):
|
|
||||||
"""
|
|
||||||
Calculate the great circle distance between two points
|
|
||||||
on the earth (specified in decimal degrees)
|
|
||||||
"""
|
|
||||||
# convert decimal degrees to radians
|
|
||||||
lon1, lat1, lon2, lat2 = map(math.radians, [lon1, lat1, lon2, lat2])
|
|
||||||
# haversine formula
|
|
||||||
dlon = lon2 - lon1
|
|
||||||
dlat = lat2 - lat1
|
|
||||||
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
|
||||||
c = 2 * math.asin(math.sqrt(a))
|
|
||||||
m = 6367000 * c
|
|
||||||
return m
|
|
|
@ -70,8 +70,7 @@ class EbtablesQueue:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.doupdateloop = True
|
self.doupdateloop = True
|
||||||
self.updatethread = threading.Thread(target=self.updateloop)
|
self.updatethread = threading.Thread(target=self.updateloop, daemon=True)
|
||||||
self.updatethread.daemon = True
|
|
||||||
self.updatethread.start()
|
self.updatethread.start()
|
||||||
|
|
||||||
def stopupdateloop(self, wlan: "CoreNetwork") -> None:
|
def stopupdateloop(self, wlan: "CoreNetwork") -> None:
|
||||||
|
@ -1068,6 +1067,7 @@ class WlanNode(CoreNetwork):
|
||||||
"""
|
"""
|
||||||
super().startup()
|
super().startup()
|
||||||
self.net_client.disable_mac_learning(self.brname)
|
self.net_client.disable_mac_learning(self.brname)
|
||||||
|
ebq.ebchange(self)
|
||||||
|
|
||||||
def attach(self, netif: CoreInterface) -> None:
|
def attach(self, netif: CoreInterface) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -4,22 +4,15 @@ sdt.py: Scripted Display Tool (SDT3D) helper
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
from typing import TYPE_CHECKING, Any, Optional
|
import threading
|
||||||
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from core import constants
|
from core import constants
|
||||||
from core.api.tlv.coreapi import CoreLinkMessage, CoreMessage, CoreNodeMessage
|
|
||||||
from core.constants import CORE_DATA_DIR
|
from core.constants import CORE_DATA_DIR
|
||||||
from core.emane.nodes import EmaneNet
|
from core.emane.nodes import EmaneNet
|
||||||
from core.emulator.data import LinkData, NodeData
|
from core.emulator.data import LinkData, NodeData
|
||||||
from core.emulator.enumerations import (
|
from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
|
||||||
EventTypes,
|
|
||||||
LinkTlvs,
|
|
||||||
LinkTypes,
|
|
||||||
MessageFlags,
|
|
||||||
NodeTlvs,
|
|
||||||
NodeTypes,
|
|
||||||
)
|
|
||||||
from core.errors import CoreError
|
from core.errors import CoreError
|
||||||
from core.nodes.base import CoreNetworkBase, NodeBase
|
from core.nodes.base import CoreNetworkBase, NodeBase
|
||||||
from core.nodes.network import WlanNode
|
from core.nodes.network import WlanNode
|
||||||
|
@ -28,19 +21,11 @@ if TYPE_CHECKING:
|
||||||
from core.emulator.session import Session
|
from core.emulator.session import Session
|
||||||
|
|
||||||
|
|
||||||
# TODO: A named tuple may be more appropriate, than abusing a class dict like this
|
def link_data_params(link_data: LinkData) -> Tuple[int, int, bool]:
|
||||||
class Bunch:
|
node_one = link_data.node1_id
|
||||||
"""
|
node_two = link_data.node2_id
|
||||||
Helper class for recording a collection of attributes.
|
is_wireless = link_data.link_type == LinkTypes.WIRELESS.value
|
||||||
"""
|
return node_one, node_two, is_wireless
|
||||||
|
|
||||||
def __init__(self, **kwargs: Any) -> None:
|
|
||||||
"""
|
|
||||||
Create a Bunch instance.
|
|
||||||
|
|
||||||
:param kwargs: keyword arguments
|
|
||||||
"""
|
|
||||||
self.__dict__.update(kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Sdt:
|
class Sdt:
|
||||||
|
@ -74,60 +59,16 @@ class Sdt:
|
||||||
:param session: session this manager is tied to
|
:param session: session this manager is tied to
|
||||||
"""
|
"""
|
||||||
self.session = session
|
self.session = session
|
||||||
|
self.lock = threading.Lock()
|
||||||
self.sock = None
|
self.sock = None
|
||||||
self.connected = False
|
self.connected = False
|
||||||
self.showerror = True
|
self.showerror = True
|
||||||
self.url = self.DEFAULT_SDT_URL
|
self.url = self.DEFAULT_SDT_URL
|
||||||
# node information for remote nodes not in session._objs
|
self.address = None
|
||||||
# local nodes also appear here since their obj may not exist yet
|
self.protocol = None
|
||||||
self.remotes = {}
|
|
||||||
|
|
||||||
# add handler for node updates
|
|
||||||
self.session.node_handlers.append(self.handle_node_update)
|
self.session.node_handlers.append(self.handle_node_update)
|
||||||
|
|
||||||
# add handler for link updates
|
|
||||||
self.session.link_handlers.append(self.handle_link_update)
|
self.session.link_handlers.append(self.handle_link_update)
|
||||||
|
|
||||||
def handle_node_update(self, node_data: NodeData) -> None:
|
|
||||||
"""
|
|
||||||
Handler for node updates, specifically for updating their location.
|
|
||||||
|
|
||||||
:param node_data: node data being updated
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
x = node_data.x_position
|
|
||||||
y = node_data.y_position
|
|
||||||
lat = node_data.latitude
|
|
||||||
lon = node_data.longitude
|
|
||||||
alt = node_data.altitude
|
|
||||||
|
|
||||||
if all([lat, lon, alt]):
|
|
||||||
self.updatenodegeo(
|
|
||||||
node_data.id,
|
|
||||||
node_data.latitude,
|
|
||||||
node_data.longitude,
|
|
||||||
node_data.altitude,
|
|
||||||
)
|
|
||||||
|
|
||||||
if node_data.message_type == 0:
|
|
||||||
# TODO: z is not currently supported by node messages
|
|
||||||
self.updatenode(node_data.id, 0, x, y, 0)
|
|
||||||
|
|
||||||
def handle_link_update(self, link_data: LinkData) -> None:
|
|
||||||
"""
|
|
||||||
Handler for link updates, checking for wireless link/unlink messages.
|
|
||||||
|
|
||||||
:param link_data: link data being updated
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if link_data.link_type == LinkTypes.WIRELESS.value:
|
|
||||||
self.updatelink(
|
|
||||||
link_data.node1_id,
|
|
||||||
link_data.node2_id,
|
|
||||||
link_data.message_type,
|
|
||||||
wireless=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_enabled(self) -> bool:
|
def is_enabled(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Check for "enablesdt" session option. Return False by default if
|
Check for "enablesdt" session option. Return False by default if
|
||||||
|
@ -144,9 +85,7 @@ class Sdt:
|
||||||
|
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
url = self.session.options.get_config("stdurl")
|
url = self.session.options.get_config("stdurl", default=self.DEFAULT_SDT_URL)
|
||||||
if not url:
|
|
||||||
url = self.DEFAULT_SDT_URL
|
|
||||||
self.url = urlparse(url)
|
self.url = urlparse(url)
|
||||||
self.address = (self.url.hostname, self.url.port)
|
self.address = (self.url.hostname, self.url.port)
|
||||||
self.protocol = self.url.scheme
|
self.protocol = self.url.scheme
|
||||||
|
@ -185,7 +124,6 @@ class Sdt:
|
||||||
# refresh all objects in SDT3D when connecting after session start
|
# refresh all objects in SDT3D when connecting after session start
|
||||||
if not flags & MessageFlags.ADD.value and not self.sendobjs():
|
if not flags & MessageFlags.ADD.value and not self.sendobjs():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def initialize(self) -> bool:
|
def initialize(self) -> bool:
|
||||||
|
@ -241,8 +179,10 @@ class Sdt:
|
||||||
"""
|
"""
|
||||||
if self.sock is None:
|
if self.sock is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cmd = f"{cmdstr}\n".encode()
|
cmd = f"{cmdstr}\n".encode()
|
||||||
|
logging.debug("sdt cmd: %s", cmd)
|
||||||
self.sock.sendall(cmd)
|
self.sock.sendall(cmd)
|
||||||
return True
|
return True
|
||||||
except IOError:
|
except IOError:
|
||||||
|
@ -251,91 +191,6 @@ class Sdt:
|
||||||
self.connected = False
|
self.connected = False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def updatenode(
|
|
||||||
self,
|
|
||||||
nodenum: int,
|
|
||||||
flags: int,
|
|
||||||
x: Optional[float],
|
|
||||||
y: Optional[float],
|
|
||||||
z: Optional[float],
|
|
||||||
name: str = None,
|
|
||||||
node_type: str = None,
|
|
||||||
icon: str = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Node is updated from a Node Message or mobility script.
|
|
||||||
|
|
||||||
:param nodenum: node id to update
|
|
||||||
:param flags: update flags
|
|
||||||
:param x: x position
|
|
||||||
:param y: y position
|
|
||||||
:param z: z position
|
|
||||||
:param name: node name
|
|
||||||
:param node_type: node type
|
|
||||||
:param icon: node icon
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if not self.connect():
|
|
||||||
return
|
|
||||||
if flags & MessageFlags.DELETE.value:
|
|
||||||
self.cmd(f"delete node,{nodenum}")
|
|
||||||
return
|
|
||||||
if x is None or y is None:
|
|
||||||
return
|
|
||||||
lat, lon, alt = self.session.location.getgeo(x, y, z)
|
|
||||||
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
|
|
||||||
if flags & MessageFlags.ADD.value:
|
|
||||||
if icon is not None:
|
|
||||||
node_type = name
|
|
||||||
icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR)
|
|
||||||
icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR)
|
|
||||||
self.cmd(f"sprite {node_type} image {icon}")
|
|
||||||
self.cmd(f'node {nodenum} type {node_type} label on,"{name}" {pos}')
|
|
||||||
else:
|
|
||||||
self.cmd(f"node {nodenum} {pos}")
|
|
||||||
|
|
||||||
def updatenodegeo(self, nodenum: int, lat: float, lon: float, alt: float) -> None:
|
|
||||||
"""
|
|
||||||
Node is updated upon receiving an EMANE Location Event.
|
|
||||||
|
|
||||||
:param nodenum: node id to update geospatial for
|
|
||||||
:param lat: latitude
|
|
||||||
:param lon: longitude
|
|
||||||
:param alt: altitude
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO: received Node Message with lat/long/alt.
|
|
||||||
if not self.connect():
|
|
||||||
return
|
|
||||||
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
|
|
||||||
self.cmd(f"node {nodenum} {pos}")
|
|
||||||
|
|
||||||
def updatelink(
|
|
||||||
self, node1num: int, node2num: int, flags: int, wireless: bool = False
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Link is updated from a Link Message or by a wireless model.
|
|
||||||
|
|
||||||
:param node1num: node one id
|
|
||||||
:param node2num: node two id
|
|
||||||
:param flags: link flags
|
|
||||||
:param wireless: flag to check if wireless or not
|
|
||||||
:return: nothing
|
|
||||||
"""
|
|
||||||
if node1num is None or node2num is None:
|
|
||||||
return
|
|
||||||
if not self.connect():
|
|
||||||
return
|
|
||||||
if flags & MessageFlags.DELETE.value:
|
|
||||||
self.cmd(f"delete link,{node1num},{node2num}")
|
|
||||||
elif flags & MessageFlags.ADD.value:
|
|
||||||
if wireless:
|
|
||||||
attr = " line green,2"
|
|
||||||
else:
|
|
||||||
attr = " line red,2"
|
|
||||||
self.cmd(f"link {node1num},{node2num}{attr}")
|
|
||||||
|
|
||||||
def sendobjs(self) -> None:
|
def sendobjs(self) -> None:
|
||||||
"""
|
"""
|
||||||
Session has already started, and the SDT3D GUI later connects.
|
Session has already started, and the SDT3D GUI later connects.
|
||||||
|
@ -352,171 +207,177 @@ class Sdt:
|
||||||
nets.append(node)
|
nets.append(node)
|
||||||
if not isinstance(node, NodeBase):
|
if not isinstance(node, NodeBase):
|
||||||
continue
|
continue
|
||||||
(x, y, z) = node.getposition()
|
self.add_node(node)
|
||||||
if x is None or y is None:
|
|
||||||
continue
|
|
||||||
self.updatenode(
|
|
||||||
node.id,
|
|
||||||
MessageFlags.ADD.value,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
z,
|
|
||||||
node.name,
|
|
||||||
node.type,
|
|
||||||
node.icon,
|
|
||||||
)
|
|
||||||
for nodenum in sorted(self.remotes.keys()):
|
|
||||||
r = self.remotes[nodenum]
|
|
||||||
x, y, z = r.pos
|
|
||||||
self.updatenode(
|
|
||||||
nodenum, MessageFlags.ADD.value, x, y, z, r.name, r.type, r.icon
|
|
||||||
)
|
|
||||||
|
|
||||||
for net in nets:
|
for net in nets:
|
||||||
all_links = net.all_link_data(flags=MessageFlags.ADD.value)
|
all_links = net.all_link_data(flags=MessageFlags.ADD.value)
|
||||||
for link_data in all_links:
|
for link_data in all_links:
|
||||||
is_wireless = isinstance(net, (WlanNode, EmaneNet))
|
is_wireless = isinstance(net, (WlanNode, EmaneNet))
|
||||||
wireless_link = link_data.message_type == LinkTypes.WIRELESS.value
|
|
||||||
if is_wireless and link_data.node1_id == net.id:
|
if is_wireless and link_data.node1_id == net.id:
|
||||||
continue
|
continue
|
||||||
|
params = link_data_params(link_data)
|
||||||
|
self.add_link(*params)
|
||||||
|
|
||||||
self.updatelink(
|
def get_node_position(self, node: NodeBase) -> Optional[str]:
|
||||||
link_data.node1_id,
|
|
||||||
link_data.node2_id,
|
|
||||||
MessageFlags.ADD.value,
|
|
||||||
wireless_link,
|
|
||||||
)
|
|
||||||
|
|
||||||
for n1num in sorted(self.remotes.keys()):
|
|
||||||
r = self.remotes[n1num]
|
|
||||||
for n2num, wireless_link in r.links:
|
|
||||||
self.updatelink(n1num, n2num, MessageFlags.ADD.value, wireless_link)
|
|
||||||
|
|
||||||
def handle_distributed(self, message: CoreMessage) -> None:
|
|
||||||
"""
|
"""
|
||||||
Broker handler for processing CORE API messages as they are
|
Convenience to generate an SDT position string, given a node.
|
||||||
received. This is used to snoop the Node messages and update
|
|
||||||
node positions.
|
|
||||||
|
|
||||||
:param message: message to handle
|
:param node:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
x, y, z = node.position.get()
|
||||||
|
if x is None or y is None:
|
||||||
|
return None
|
||||||
|
lat, lon, alt = self.session.location.getgeo(x, y, z)
|
||||||
|
return f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
|
||||||
|
|
||||||
|
def add_node(self, node: NodeBase) -> None:
|
||||||
|
"""
|
||||||
|
Handle adding a node in SDT.
|
||||||
|
|
||||||
|
:param node: node to add
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
if isinstance(message, CoreLinkMessage):
|
logging.debug("sdt add node: %s - %s", node.id, node.name)
|
||||||
self.handlelinkmsg(message)
|
if not self.connect():
|
||||||
elif isinstance(message, CoreNodeMessage):
|
return
|
||||||
self.handlenodemsg(message)
|
pos = self.get_node_position(node)
|
||||||
|
if not pos:
|
||||||
|
return
|
||||||
|
node_type = node.type
|
||||||
|
if node_type is None:
|
||||||
|
node_type = type(node).type
|
||||||
|
icon = node.icon
|
||||||
|
if icon:
|
||||||
|
node_type = node.name
|
||||||
|
icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR)
|
||||||
|
icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR)
|
||||||
|
self.cmd(f"sprite {node_type} image {icon}")
|
||||||
|
self.cmd(f'node {node.id} type {node_type} label on,"{node.name}" {pos}')
|
||||||
|
|
||||||
def handlenodemsg(self, msg: CoreNodeMessage) -> None:
|
def edit_node(self, node: NodeBase, lon: float, lat: float, alt: float) -> None:
|
||||||
"""
|
"""
|
||||||
Process a Node Message to add/delete or move a node on
|
Handle updating a node in SDT.
|
||||||
the SDT display. Node properties are found in a session or
|
|
||||||
self.remotes for remote nodes (or those not yet instantiated).
|
|
||||||
|
|
||||||
:param msg: node message to handle
|
:param node: node to update
|
||||||
|
:param lon: node longitude
|
||||||
|
:param lat: node latitude
|
||||||
|
:param alt: node altitude
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
# for distributed sessions to work properly, the SDT option should be
|
logging.debug("sdt update node: %s - %s", node.id, node.name)
|
||||||
# enabled prior to starting the session
|
if not self.connect():
|
||||||
if not self.is_enabled():
|
|
||||||
return
|
return
|
||||||
# node.(_id, type, icon, name) are used.
|
|
||||||
nodenum = msg.get_tlv(NodeTlvs.NUMBER.value)
|
|
||||||
if not nodenum:
|
|
||||||
return
|
|
||||||
x = msg.get_tlv(NodeTlvs.X_POSITION.value)
|
|
||||||
y = msg.get_tlv(NodeTlvs.Y_POSITION.value)
|
|
||||||
z = None
|
|
||||||
name = msg.get_tlv(NodeTlvs.NAME.value)
|
|
||||||
|
|
||||||
nodetype = msg.get_tlv(NodeTlvs.TYPE.value)
|
if all([lat is not None, lon is not None, alt is not None]):
|
||||||
model = msg.get_tlv(NodeTlvs.MODEL.value)
|
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
|
||||||
icon = msg.get_tlv(NodeTlvs.ICON.value)
|
self.cmd(f"node {node.id} {pos}")
|
||||||
|
|
||||||
net = False
|
|
||||||
if nodetype == NodeTypes.DEFAULT.value or nodetype == NodeTypes.PHYSICAL.value:
|
|
||||||
if model is None:
|
|
||||||
model = "router"
|
|
||||||
nodetype = model
|
|
||||||
elif nodetype is not None:
|
|
||||||
nodetype = NodeTypes(nodetype)
|
|
||||||
nodetype = self.session.get_node_class(nodetype).type
|
|
||||||
net = True
|
|
||||||
else:
|
else:
|
||||||
nodetype = None
|
pos = self.get_node_position(node)
|
||||||
|
if not pos:
|
||||||
|
return
|
||||||
|
self.cmd(f"node {node.id} {pos}")
|
||||||
|
|
||||||
|
def delete_node(self, node_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Handle deleting a node in SDT.
|
||||||
|
|
||||||
|
:param node_id: node id to delete
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
logging.debug("sdt delete node: %s", node_id)
|
||||||
|
if not self.connect():
|
||||||
|
return
|
||||||
|
self.cmd(f"delete node,{node_id}")
|
||||||
|
|
||||||
|
def handle_node_update(self, node_data: NodeData) -> None:
|
||||||
|
"""
|
||||||
|
Handler for node updates, specifically for updating their location.
|
||||||
|
|
||||||
|
:param node_data: node data being updated
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
logging.debug("sdt handle node update: %s - %s", node_data.id, node_data.name)
|
||||||
|
if not self.connect():
|
||||||
|
return
|
||||||
|
|
||||||
|
# delete node
|
||||||
|
if node_data.message_type == MessageFlags.DELETE.value:
|
||||||
|
self.cmd(f"delete node,{node_data.id}")
|
||||||
|
else:
|
||||||
|
x = node_data.x_position
|
||||||
|
y = node_data.y_position
|
||||||
|
lat = node_data.latitude
|
||||||
|
lon = node_data.longitude
|
||||||
|
alt = node_data.altitude
|
||||||
|
if all([lat is not None, lon is not None, alt is not None]):
|
||||||
|
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
|
||||||
|
self.cmd(f"node {node_data.id} {pos}")
|
||||||
|
elif node_data.message_type == 0:
|
||||||
|
lat, lon, alt = self.session.location.getgeo(x, y, 0)
|
||||||
|
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
|
||||||
|
self.cmd(f"node {node_data.id} {pos}")
|
||||||
|
|
||||||
|
def wireless_net_check(self, node_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Determines if a node is either a wireless node type.
|
||||||
|
|
||||||
|
:param node_id: node id to check
|
||||||
|
:return: True is a wireless node type, False otherwise
|
||||||
|
"""
|
||||||
|
result = False
|
||||||
try:
|
try:
|
||||||
node = self.session.get_node(nodenum)
|
node = self.session.get_node(node_id)
|
||||||
|
result = isinstance(node, (WlanNode, EmaneNet))
|
||||||
except CoreError:
|
except CoreError:
|
||||||
node = None
|
pass
|
||||||
if node:
|
return result
|
||||||
self.updatenode(
|
|
||||||
node.id, msg.flags, x, y, z, node.name, node.type, node.icon
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if nodenum in self.remotes:
|
|
||||||
remote = self.remotes[nodenum]
|
|
||||||
if name is None:
|
|
||||||
name = remote.name
|
|
||||||
if nodetype is None:
|
|
||||||
nodetype = remote.type
|
|
||||||
if icon is None:
|
|
||||||
icon = remote.icon
|
|
||||||
else:
|
|
||||||
remote = Bunch(
|
|
||||||
_id=nodenum,
|
|
||||||
type=nodetype,
|
|
||||||
icon=icon,
|
|
||||||
name=name,
|
|
||||||
net=net,
|
|
||||||
links=set(),
|
|
||||||
)
|
|
||||||
self.remotes[nodenum] = remote
|
|
||||||
remote.pos = (x, y, z)
|
|
||||||
self.updatenode(nodenum, msg.flags, x, y, z, name, nodetype, icon)
|
|
||||||
|
|
||||||
def handlelinkmsg(self, msg: CoreLinkMessage) -> None:
|
def add_link(self, node_one: int, node_two: int, is_wireless: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Process a Link Message to add/remove links on the SDT display.
|
Handle adding a link in SDT.
|
||||||
Links are recorded in the remotes[nodenum1].links set for updating
|
|
||||||
the SDT display at a later time.
|
|
||||||
|
|
||||||
:param msg: link message to handle
|
:param node_one: node one id
|
||||||
|
:param node_two: node two id
|
||||||
|
:param is_wireless: True if link is wireless, False otherwise
|
||||||
:return: nothing
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
if not self.is_enabled():
|
logging.debug("sdt add link: %s, %s, %s", node_one, node_two, is_wireless)
|
||||||
|
if not self.connect():
|
||||||
return
|
return
|
||||||
nodenum1 = msg.get_tlv(LinkTlvs.N1_NUMBER.value)
|
if self.wireless_net_check(node_one) or self.wireless_net_check(node_two):
|
||||||
nodenum2 = msg.get_tlv(LinkTlvs.N2_NUMBER.value)
|
|
||||||
link_msg_type = msg.get_tlv(LinkTlvs.TYPE.value)
|
|
||||||
# this filters out links to WLAN and EMANE nodes which are not drawn
|
|
||||||
if self.wlancheck(nodenum1):
|
|
||||||
return
|
return
|
||||||
wl = link_msg_type == LinkTypes.WIRELESS.value
|
if is_wireless:
|
||||||
if nodenum1 in self.remotes:
|
attr = "green,2"
|
||||||
r = self.remotes[nodenum1]
|
|
||||||
if msg.flags & MessageFlags.DELETE.value:
|
|
||||||
if (nodenum2, wl) in r.links:
|
|
||||||
r.links.remove((nodenum2, wl))
|
|
||||||
else:
|
else:
|
||||||
r.links.add((nodenum2, wl))
|
attr = "red,2"
|
||||||
self.updatelink(nodenum1, nodenum2, msg.flags, wireless=wl)
|
self.cmd(f"link {node_one},{node_two} line {attr}")
|
||||||
|
|
||||||
def wlancheck(self, nodenum: int) -> bool:
|
def delete_link(self, node_one: int, node_two: int) -> None:
|
||||||
"""
|
"""
|
||||||
Helper returns True if a node number corresponds to a WLAN or EMANE node.
|
Handle deleting a node in SDT.
|
||||||
|
|
||||||
:param nodenum: node id to check
|
:param node_one: node one id
|
||||||
:return: True if node is wlan or emane, False otherwise
|
:param node_two: node two id
|
||||||
|
:return: nothing
|
||||||
"""
|
"""
|
||||||
if nodenum in self.remotes:
|
logging.debug("sdt delete link: %s, %s", node_one, node_two)
|
||||||
node_type = self.remotes[nodenum].type
|
if not self.connect():
|
||||||
if node_type in ("wlan", "emane"):
|
return
|
||||||
return True
|
if self.wireless_net_check(node_one) or self.wireless_net_check(node_two):
|
||||||
else:
|
return
|
||||||
try:
|
self.cmd(f"delete link,{node_one},{node_two}")
|
||||||
n = self.session.get_node(nodenum)
|
|
||||||
except CoreError:
|
def handle_link_update(self, link_data: LinkData) -> None:
|
||||||
return False
|
"""
|
||||||
if isinstance(n, (WlanNode, EmaneNet)):
|
Handle link broadcast messages and push changes to SDT.
|
||||||
return True
|
|
||||||
return False
|
:param link_data: link data to handle
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
if link_data.message_type == MessageFlags.ADD.value:
|
||||||
|
params = link_data_params(link_data)
|
||||||
|
self.add_link(*params)
|
||||||
|
elif link_data.message_type == MessageFlags.DELETE.value:
|
||||||
|
params = link_data_params(link_data)
|
||||||
|
self.delete_link(*params[:2])
|
||||||
|
|
|
@ -13,6 +13,7 @@ import logging.config
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import shlex
|
import shlex
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from subprocess import PIPE, STDOUT, Popen
|
from subprocess import PIPE, STDOUT, Popen
|
||||||
from typing import (
|
from typing import (
|
||||||
|
@ -151,16 +152,9 @@ def which(command: str, required: bool) -> str:
|
||||||
:return: command location or None
|
:return: command location or None
|
||||||
:raises ValueError: when not found and required
|
:raises ValueError: when not found and required
|
||||||
"""
|
"""
|
||||||
found_path = None
|
found_path = shutil.which(command)
|
||||||
for path in os.environ["PATH"].split(os.pathsep):
|
|
||||||
command_path = os.path.join(path, command)
|
|
||||||
if os.path.isfile(command_path) and os.access(command_path, os.X_OK):
|
|
||||||
found_path = command_path
|
|
||||||
break
|
|
||||||
|
|
||||||
if found_path is None and required:
|
if found_path is None and required:
|
||||||
raise ValueError(f"failed to find required executable({command}) in path")
|
raise ValueError(f"failed to find required executable({command}) in path")
|
||||||
|
|
||||||
return found_path
|
return found_path
|
||||||
|
|
||||||
|
|
||||||
|
@ -444,7 +438,7 @@ def random_mac() -> str:
|
||||||
value = random.randint(0, 0xFFFFFF)
|
value = random.randint(0, 0xFFFFFF)
|
||||||
value |= 0x00163E << 24
|
value |= 0x00163E << 24
|
||||||
mac = netaddr.EUI(value)
|
mac = netaddr.EUI(value)
|
||||||
mac.dialect = netaddr.mac_unix
|
mac.dialect = netaddr.mac_unix_expanded
|
||||||
return str(mac)
|
return str(mac)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ from core.emulator.data import LinkData
|
||||||
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
|
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
|
||||||
from core.emulator.enumerations import NodeTypes
|
from core.emulator.enumerations import NodeTypes
|
||||||
from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase
|
from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase
|
||||||
|
from core.nodes.docker import DockerNode
|
||||||
|
from core.nodes.lxd import LxcNode
|
||||||
from core.nodes.network import CtrlNet
|
from core.nodes.network import CtrlNet
|
||||||
from core.services.coreservices import CoreService
|
from core.services.coreservices import CoreService
|
||||||
|
|
||||||
|
@ -213,8 +215,21 @@ class DeviceElement(NodeElement):
|
||||||
def __init__(self, session: "Session", node: NodeBase) -> None:
|
def __init__(self, session: "Session", node: NodeBase) -> None:
|
||||||
super().__init__(session, node, "device")
|
super().__init__(session, node, "device")
|
||||||
add_attribute(self.element, "type", node.type)
|
add_attribute(self.element, "type", node.type)
|
||||||
|
self.add_class()
|
||||||
self.add_services()
|
self.add_services()
|
||||||
|
|
||||||
|
def add_class(self) -> None:
|
||||||
|
clazz = ""
|
||||||
|
image = ""
|
||||||
|
if isinstance(self.node, DockerNode):
|
||||||
|
clazz = "docker"
|
||||||
|
image = self.node.image
|
||||||
|
elif isinstance(self.node, LxcNode):
|
||||||
|
clazz = "lxc"
|
||||||
|
image = self.node.image
|
||||||
|
add_attribute(self.element, "class", clazz)
|
||||||
|
add_attribute(self.element, "image", image)
|
||||||
|
|
||||||
def add_services(self) -> None:
|
def add_services(self) -> None:
|
||||||
service_elements = etree.Element("services")
|
service_elements = etree.Element("services")
|
||||||
for service in self.node.services:
|
for service in self.node.services:
|
||||||
|
@ -796,9 +811,17 @@ class CoreXmlReader:
|
||||||
name = device_element.get("name")
|
name = device_element.get("name")
|
||||||
model = device_element.get("type")
|
model = device_element.get("type")
|
||||||
icon = device_element.get("icon")
|
icon = device_element.get("icon")
|
||||||
options = NodeOptions(name, model)
|
clazz = device_element.get("class")
|
||||||
|
image = device_element.get("image")
|
||||||
|
options = NodeOptions(name, model, image)
|
||||||
options.icon = icon
|
options.icon = icon
|
||||||
|
|
||||||
|
node_type = NodeTypes.DEFAULT
|
||||||
|
if clazz == "docker":
|
||||||
|
node_type = NodeTypes.DOCKER
|
||||||
|
elif clazz == "lxc":
|
||||||
|
node_type = NodeTypes.LXC
|
||||||
|
|
||||||
service_elements = device_element.find("services")
|
service_elements = device_element.find("services")
|
||||||
if service_elements is not None:
|
if service_elements is not None:
|
||||||
options.services = [x.get("name") for x in service_elements.iterchildren()]
|
options.services = [x.get("name") for x in service_elements.iterchildren()]
|
||||||
|
@ -823,7 +846,7 @@ class CoreXmlReader:
|
||||||
options.set_location(lat, lon, alt)
|
options.set_location(lat, lon, alt)
|
||||||
|
|
||||||
logging.info("reading node id(%s) model(%s) name(%s)", node_id, model, name)
|
logging.info("reading node id(%s) model(%s) name(%s)", node_id, model, name)
|
||||||
self.session.add_node(_id=node_id, options=options)
|
self.session.add_node(_type=node_type, _id=node_id, options=options)
|
||||||
|
|
||||||
def read_network(self, network_element: etree.Element) -> None:
|
def read_network(self, network_element: etree.Element) -> None:
|
||||||
node_id = get_int(network_element, "id")
|
node_id = get_int(network_element, "id")
|
||||||
|
|
|
@ -22,6 +22,8 @@ service CoreApi {
|
||||||
}
|
}
|
||||||
rpc GetSession (GetSessionRequest) returns (GetSessionResponse) {
|
rpc GetSession (GetSessionRequest) returns (GetSessionResponse) {
|
||||||
}
|
}
|
||||||
|
rpc CheckSession (CheckSessionRequest) returns (CheckSessionResponse) {
|
||||||
|
}
|
||||||
rpc GetSessionOptions (GetSessionOptionsRequest) returns (GetSessionOptionsResponse) {
|
rpc GetSessionOptions (GetSessionOptionsRequest) returns (GetSessionOptionsResponse) {
|
||||||
}
|
}
|
||||||
rpc SetSessionOptions (SetSessionOptionsRequest) returns (SetSessionOptionsResponse) {
|
rpc SetSessionOptions (SetSessionOptionsRequest) returns (SetSessionOptionsResponse) {
|
||||||
|
@ -154,6 +156,8 @@ service CoreApi {
|
||||||
}
|
}
|
||||||
rpc EmaneLink (EmaneLinkRequest) returns (EmaneLinkResponse) {
|
rpc EmaneLink (EmaneLinkRequest) returns (EmaneLinkResponse) {
|
||||||
}
|
}
|
||||||
|
rpc ExecuteScript (ExecuteScriptRequest) returns (ExecuteScriptResponse) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rpc request/response messages
|
// rpc request/response messages
|
||||||
|
@ -210,6 +214,14 @@ message GetSessionsResponse {
|
||||||
repeated SessionSummary sessions = 1;
|
repeated SessionSummary sessions = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CheckSessionRequest {
|
||||||
|
int32 session_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CheckSessionResponse {
|
||||||
|
bool result = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message GetSessionRequest {
|
message GetSessionRequest {
|
||||||
int32 session_id = 1;
|
int32 session_id = 1;
|
||||||
}
|
}
|
||||||
|
@ -406,6 +418,7 @@ message EditNodeRequest {
|
||||||
Position position = 3;
|
Position position = 3;
|
||||||
string icon = 4;
|
string icon = 4;
|
||||||
string source = 5;
|
string source = 5;
|
||||||
|
Geo geo = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EditNodeResponse {
|
message EditNodeResponse {
|
||||||
|
@ -759,6 +772,14 @@ message EmaneLinkResponse {
|
||||||
bool result = 1;
|
bool result = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ExecuteScriptRequest {
|
||||||
|
string script = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExecuteScriptResponse {
|
||||||
|
int32 session_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// data structures for messages below
|
// data structures for messages below
|
||||||
message WlanConfig {
|
message WlanConfig {
|
||||||
int32 node_id = 1;
|
int32 node_id = 1;
|
||||||
|
@ -967,6 +988,7 @@ message Node {
|
||||||
string image = 10;
|
string image = 10;
|
||||||
string server = 11;
|
string server = 11;
|
||||||
repeated string config_services = 12;
|
repeated string config_services = 12;
|
||||||
|
Geo geo = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Link {
|
message Link {
|
||||||
|
@ -1019,7 +1041,10 @@ message Position {
|
||||||
float x = 1;
|
float x = 1;
|
||||||
float y = 2;
|
float y = 2;
|
||||||
float z = 3;
|
float z = 3;
|
||||||
float lat = 4;
|
}
|
||||||
float lon = 5;
|
|
||||||
float alt = 6;
|
message Geo {
|
||||||
|
float lat = 1;
|
||||||
|
float lon = 2;
|
||||||
|
float alt = 3;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
bcrypt==3.1.7
|
bcrypt==3.1.7
|
||||||
cffi==1.13.2
|
cffi==1.14.0
|
||||||
cryptography==2.8
|
cryptography==2.8
|
||||||
fabric==2.5.0
|
fabric==2.5.0
|
||||||
grpcio==1.26.0
|
grpcio==1.27.2
|
||||||
invoke==1.4.0
|
invoke==1.4.1
|
||||||
lxml==4.4.2
|
lxml==4.5.0
|
||||||
Mako==1.1.1
|
Mako==1.1.1
|
||||||
MarkupSafe==1.1.1
|
MarkupSafe==1.1.1
|
||||||
netaddr==0.7.19
|
netaddr==0.7.19
|
||||||
paramiko==2.7.1
|
paramiko==2.7.1
|
||||||
Pillow==7.0.0
|
Pillow==7.0.0
|
||||||
protobuf==3.11.2
|
protobuf==3.11.3
|
||||||
pycparser==2.19
|
pycparser==2.19
|
||||||
PyNaCl==1.3.0
|
PyNaCl==1.3.0
|
||||||
|
pyproj==2.5.0
|
||||||
PyYAML==5.3
|
PyYAML==5.3
|
||||||
six==1.14.0
|
six==1.14.0
|
||||||
|
|
|
@ -19,7 +19,7 @@ PATH="/sbin:/bin:/usr/sbin:/usr/bin"
|
||||||
export PATH
|
export PATH
|
||||||
|
|
||||||
if [ "z$1" = "z-d" ]; then
|
if [ "z$1" = "z-d" ]; then
|
||||||
pypids=`pidof python python2`
|
pypids=`pidof python3 python`
|
||||||
for p in $pypids; do
|
for p in $pypids; do
|
||||||
grep -q core-daemon /proc/$p/cmdline
|
grep -q core-daemon /proc/$p/cmdline
|
||||||
if [ $? = 0 ]; then
|
if [ $? = 0 ]; then
|
||||||
|
|
|
@ -42,6 +42,7 @@ setup(
|
||||||
"mako",
|
"mako",
|
||||||
"pillow",
|
"pillow",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
|
"pyproj",
|
||||||
"pyyaml",
|
"pyyaml",
|
||||||
],
|
],
|
||||||
tests_require=[
|
tests_require=[
|
||||||
|
|
|
@ -19,84 +19,43 @@ Current development focuses on the Python modules and daemon. Here is a brief de
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
Overview for setting up the pipenv environment, building core, installing the GUI and netns, then running
|
To setup CORE for develop we will leverage to automated install script.
|
||||||
the core-daemon for development based on Ubuntu 18.04.
|
|
||||||
|
|
||||||
### Install Dependencies
|
|
||||||
|
|
||||||
```shell
|
## Clone CORE Repo
|
||||||
sudo apt install -y automake pkg-config gcc libev-dev ebtables gawk \
|
|
||||||
python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install OSPF MDR
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cd ~/Documents
|
|
||||||
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 \
|
|
||||||
--localstatedir=/var/run/quagga
|
|
||||||
make
|
|
||||||
sudo make install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clone CORE Repo
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd ~/Documents
|
cd ~/Documents
|
||||||
git clone https://github.com/coreemu/core.git
|
git clone https://github.com/coreemu/core.git
|
||||||
cd core
|
cd core
|
||||||
|
git checkout develop
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build CORE
|
## Install the Development Environment
|
||||||
|
|
||||||
|
This command will automatically install system dependencies, clone and build OSPF-MDR,
|
||||||
|
build CORE, setup the CORE pipenv environment, and install pre-commit hooks.
|
||||||
|
|
||||||
|
This script is currently compatible with Ubuntu and CentOS, tested on Ubuntu 18.04 and
|
||||||
|
CentOS 7.6. The script also currently defaults to using python3.6, but a different
|
||||||
|
version of python can be targeted if python3.6 is not available on your system.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./bootstrap.sh
|
# default dev install using python3.6
|
||||||
./configure
|
./install.sh -d
|
||||||
make -j8
|
|
||||||
|
# providing a newer python version for ubuntu
|
||||||
|
./install.sh -d -v 3.7
|
||||||
|
|
||||||
|
# providing a newer python version for centos
|
||||||
|
./install.sh -d -v 37
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install netns and GUI
|
### pre-commit
|
||||||
|
|
||||||
Install legacy GUI if desired and mandatory netns executables.
|
pre-commit hooks help automate running tools to check modified code. Every time a commit is made
|
||||||
|
python utilities will be ran to check validity of code, potentially failing and backing out the commit.
|
||||||
```shell
|
These changes are currently mandated as part of the current CI, so add the changes and commit again.
|
||||||
# install GUI
|
|
||||||
cd $REPO/gui
|
|
||||||
sudo make install
|
|
||||||
|
|
||||||
# install netns scripts
|
|
||||||
cd $REPO/netns
|
|
||||||
sudo make install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setup Python Environment
|
|
||||||
|
|
||||||
To leverage the dev environment you need python 3.6+.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# change to daemon directory
|
|
||||||
cd $REPO/daemon
|
|
||||||
|
|
||||||
# install pipenv
|
|
||||||
sudo python3 -m pip install pipenv
|
|
||||||
|
|
||||||
# setup a virtual environment and install all required development dependencies
|
|
||||||
python3 -m pipenv install --dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setup pre-commit
|
|
||||||
|
|
||||||
Install pre-commit hooks to help automate running tool checks against code. Once installed every time a commit is made
|
|
||||||
python utilities will be ran to check validity of code, potentially failing and backing out the commit. This allows
|
|
||||||
one to review changes being made by tools ro the fix the issue noted. Then add the changes and commit again.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
python3 -m pipenv run pre-commit install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding EMANE to Pipenv
|
### Adding EMANE to Pipenv
|
||||||
|
|
||||||
|
@ -121,9 +80,9 @@ make -j8
|
||||||
python3 -m pipenv pip install $EMANEREPO/src/python
|
python3 -m pipenv pip install $EMANEREPO/src/python
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running CORE
|
## Running CORE
|
||||||
|
|
||||||
This will run the core-daemon server using the configuration files within the repo.
|
Commands below can be used to run the core-daemon, the new core gui, and tests.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# runs for daemon
|
# runs for daemon
|
||||||
|
|
167
docs/install.md
167
docs/install.md
|
@ -3,30 +3,30 @@
|
||||||
* Table of Contents
|
* Table of Contents
|
||||||
{:toc}
|
{:toc}
|
||||||
|
|
||||||
# Overview
|
## Overview
|
||||||
|
|
||||||
This section will describe how to install CORE from source or from a pre-built package.
|
This section will describe how to install CORE from source or from a pre-built package.
|
||||||
|
|
||||||
# Required Hardware
|
## Required Hardware
|
||||||
|
|
||||||
Any computer capable of running Linux should be able to run CORE. Since the physical machine will be hosting numerous
|
Any computer capable of running Linux should be able to run CORE. Since the physical machine will be hosting numerous
|
||||||
virtual machines, as a general rule you should select a machine having as much RAM and CPU resources as possible.
|
virtual machines, as a general rule you should select a machine having as much RAM and CPU resources as possible.
|
||||||
|
|
||||||
# Operating System
|
## Operating System
|
||||||
|
|
||||||
CORE requires a Linux operating system because it uses virtualization provided by the kernel. It does not run on
|
CORE requires a Linux operating system because it uses virtualization provided by the kernel. It does not run on
|
||||||
Windows or Mac OS X operating systems (unless it is running within a virtual machine guest.) The virtualization
|
Windows or Mac OS X operating systems (unless it is running within a virtual machine guest.) The virtualization
|
||||||
technology that CORE currently uses is Linux network namespaces.
|
technology that CORE currently uses is Linux network namespaces.
|
||||||
|
|
||||||
Ubuntu and Fedora/CentOS Linux are the recommended distributions for running CORE. However, these distributions are
|
Ubuntu and CentOS Linux are the recommended distributions for running CORE. However, these distributions are
|
||||||
not strictly required. CORE will likely work on other flavors of Linux as well, assuming dependencies are met.
|
not strictly required. CORE will likely work on other flavors of Linux as well, assuming dependencies are met.
|
||||||
|
|
||||||
**NOTE: CORE Services determine what run on each node. You may require other software packages depending on the
|
**NOTE: CORE Services determine what run on each node. You may require other software packages depending on the
|
||||||
services you wish to use. For example, the HTTP service will require the apache2 package.**
|
services you wish to use. For example, the HTTP service will require the apache2 package.**
|
||||||
|
|
||||||
# Installed Files
|
## Installed Files
|
||||||
|
|
||||||
CORE files are installed to the following directories, when the installation prefix is **/usr**.
|
CORE files are installed to the following directories by default, when the installation prefix is **/usr**.
|
||||||
|
|
||||||
Install Path | Description
|
Install Path | Description
|
||||||
-------------|------------
|
-------------|------------
|
||||||
|
@ -43,27 +43,35 @@ Install Path | Description
|
||||||
/etc/init.d/core-daemon|SysV startup script for daemon
|
/etc/init.d/core-daemon|SysV startup script for daemon
|
||||||
/usr/lib/systemd/system/core-daemon.service|Systemd startup script for daemon
|
/usr/lib/systemd/system/core-daemon.service|Systemd startup script for daemon
|
||||||
|
|
||||||
# Pre-Req Installing Python
|
## Pre-Req Installing Python
|
||||||
|
|
||||||
You may already have these installed, and can ignore this step if so, but if
|
Python 3.6 is the minimum required python version. Newer versions can be used if available.
|
||||||
needed you can run the following to install python and pip.
|
These steps are needed, since the system packages can not provide all the
|
||||||
|
dependencies needed by CORE.
|
||||||
|
|
||||||
|
### Ubuntu
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo apt install python3.6
|
sudo apt install python3.6
|
||||||
sudo apt install python3-pip
|
sudo apt install python3-pip
|
||||||
```
|
```
|
||||||
|
|
||||||
# Pre-Req Python Requirements
|
### CentOS
|
||||||
|
|
||||||
The newly added gRPC API which depends on python library grpcio is not commonly found within system repos.
|
|
||||||
To account for this it would be recommended to install the python dependencies using the **requirements.txt** found in
|
|
||||||
the latest [CORE Release](https://github.com/coreemu/core/releases).
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo pip3 install -r requirements.txt
|
sudo yum install python36
|
||||||
|
sudo yum install python3-pip
|
||||||
```
|
```
|
||||||
|
|
||||||
# Pre-Req Installing OSPF MDR
|
### Dependencies
|
||||||
|
|
||||||
|
Install the current python dependencies.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo python3 -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pre-Req Installing OSPF MDR
|
||||||
|
|
||||||
Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing
|
Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing
|
||||||
tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by
|
tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by
|
||||||
|
@ -73,23 +81,21 @@ default when the blue router node type is used.
|
||||||
suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type
|
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.
|
(and the MDR service) requires this variant of Quagga.
|
||||||
|
|
||||||
## Ubuntu <= 16.04 and Fedora/CentOS
|
### Ubuntu
|
||||||
|
|
||||||
There is a built package which can be used.
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
wget https://github.com/USNavalResearchLaboratory/ospf-mdr/releases/download/v0.99.21mr2.2/quagga-mr_0.99.21mr2.2_amd64.deb
|
sudo apt install libtool gawk libreadline-dev
|
||||||
sudo dpkg -i quagga-mr_0.99.21mr2.2_amd64.deb
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ubuntu >= 18.04
|
### CentOS
|
||||||
|
|
||||||
Requires building from source, from the latest nightly snapshot.
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# packages needed beyond what's normally required to build core on ubuntu
|
sudo yum install libtool gawk readline-devel
|
||||||
sudo apt install libtool libreadline-dev autoconf gawk
|
```
|
||||||
|
|
||||||
|
### Build and Install
|
||||||
|
|
||||||
|
```shell
|
||||||
git clone https://github.com/USNavalResearchLaboratory/ospf-mdr
|
git clone https://github.com/USNavalResearchLaboratory/ospf-mdr
|
||||||
cd ospf-mdr
|
cd ospf-mdr
|
||||||
./bootstrap.sh
|
./bootstrap.sh
|
||||||
|
@ -112,14 +118,14 @@ error while loading shared libraries libzebra.so.0
|
||||||
|
|
||||||
this is usually a sign that you have to run ```sudo ldconfig```` to refresh the cache file.
|
this is usually a sign that you have to run ```sudo ldconfig```` to refresh the cache file.
|
||||||
|
|
||||||
# Installing from Packages
|
## Installing from Packages
|
||||||
|
|
||||||
The easiest way to install CORE is using the pre-built packages. The package managers on Ubuntu or Fedora/CentOS
|
The easiest way to install CORE is using the pre-built packages. The package managers on Ubuntu or CentOS
|
||||||
will help in automatically installing most dependencies, except for the python ones described previously.
|
will help in automatically installing most dependencies, except for the python ones described previously.
|
||||||
|
|
||||||
You can obtain the CORE packages from [CORE Releases](https://github.com/coreemu/core/releases).
|
You can obtain the CORE packages from [CORE Releases](https://github.com/coreemu/core/releases).
|
||||||
|
|
||||||
## Ubuntu
|
### Ubuntu
|
||||||
|
|
||||||
Ubuntu package defaults to using systemd for running as a service.
|
Ubuntu package defaults to using systemd for running as a service.
|
||||||
|
|
||||||
|
@ -127,16 +133,7 @@ Ubuntu package defaults to using systemd for running as a service.
|
||||||
sudo apt install ./core_$VERSION_amd64.deb
|
sudo apt install ./core_$VERSION_amd64.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the CORE GUI as a normal user:
|
### CentOS
|
||||||
|
|
||||||
```shell
|
|
||||||
core-gui
|
|
||||||
```
|
|
||||||
|
|
||||||
After running the *core-gui* command, a GUI should appear with a canvas for drawing topologies.
|
|
||||||
Messages will print out on the console about connecting to the CORE daemon.
|
|
||||||
|
|
||||||
## Fedora/CentOS
|
|
||||||
|
|
||||||
**NOTE: tkimg is not required for the core-gui, but if you get an error message about it you can install the package
|
**NOTE: tkimg is not required for the core-gui, but if you get an error message about it you can install the package
|
||||||
on CentOS <= 6, or build from source otherwise**
|
on CentOS <= 6, or build from source otherwise**
|
||||||
|
@ -153,10 +150,6 @@ SELINUX=disabled
|
||||||
|
|
||||||
# add the following to the kernel line in /etc/grub.conf
|
# add the following to the kernel line in /etc/grub.conf
|
||||||
selinux=0
|
selinux=0
|
||||||
|
|
||||||
# Fedora 15 and newer, disable sandboxd
|
|
||||||
# reboot in order for this change to take effect
|
|
||||||
chkconfig sandbox off
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Turn off firewalls:
|
Turn off firewalls:
|
||||||
|
@ -176,63 +169,46 @@ iptables -F
|
||||||
ip6tables -F
|
ip6tables -F
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the CORE daemon.
|
## Installing from Source
|
||||||
|
|
||||||
|
Steps for building from cloned source code. Python 3.6 is the minimum required version
|
||||||
|
a newer version can be used below if available.
|
||||||
|
|
||||||
|
### Distro Requirements
|
||||||
|
|
||||||
|
System packages required to build from source.
|
||||||
|
|
||||||
|
#### Ubuntu
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# systemd
|
sudo apt install git automake pkg-config gcc libev-dev ebtables iproute2 \
|
||||||
sudo systemctl daemon-reload
|
python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool autoconf
|
||||||
sudo systemctl start core-daemon
|
|
||||||
|
|
||||||
# sysv
|
|
||||||
sudo service core-daemon start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the CORE GUI as a normal user:
|
#### CentOS
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
core-gui
|
sudo yum install git automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \
|
||||||
|
python36 python36-devel python3-pip python3-tkinter tk ethtool autoconf
|
||||||
```
|
```
|
||||||
|
|
||||||
After running the *core-gui* command, a GUI should appear with a canvas for drawing topologies. Messages will print out on the console about connecting to the CORE daemon.
|
### Clone Repository
|
||||||
|
|
||||||
# Building and Installing from Source
|
Clone the CORE repository for building from source.
|
||||||
|
|
||||||
This option is listed here for developers and advanced users who are comfortable patching and building source code.
|
|
||||||
Please consider using the binary packages instead for a simplified install experience.
|
|
||||||
|
|
||||||
## Download and Extract Source Code
|
|
||||||
|
|
||||||
You can obtain the CORE source from the [CORE GitHub](https://github.com/coreemu/core) page.
|
|
||||||
|
|
||||||
## Install grpcio-tools
|
|
||||||
|
|
||||||
Python module grpcio-tools is currently needed to generate code from the CORE protobuf file during the build.
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo pip3 install grpcio-tools
|
git clone https://github.com/coreemu/core.git
|
||||||
```
|
```
|
||||||
|
|
||||||
## Distro Requirements
|
### Install grpcio-tools
|
||||||
|
|
||||||
### Ubuntu 18.04 Requirements
|
Python module grpcio-tools is currently needed to generate gRPC protobuf code.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo apt install automake pkg-config gcc iproute2 libev-dev ebtables python3.6 python3.6-dev python3-pip tk libtk-img ethtool python3-tk
|
sudo python3 -m pip install grpcio-tools
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ubuntu 16.04 Requirements
|
### Build and Install
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo apt-get install automake ebtables python3-dev libev-dev python3-setuptools libtk-img ethtool
|
|
||||||
```
|
|
||||||
|
|
||||||
### CentOS 7 with Gnome Desktop Requirements
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo yum -y install automake gcc python36 python36-devel libev-devel tk ethtool iptables-ebtables iproute python3-pip python3-tkinter
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build and Install
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./bootstrap.sh
|
./bootstrap.sh
|
||||||
|
@ -241,7 +217,7 @@ make
|
||||||
sudo make install
|
sudo make install
|
||||||
```
|
```
|
||||||
|
|
||||||
# Building Documentation
|
## Building Documentation
|
||||||
|
|
||||||
Building documentation requires python-sphinx not noted above.
|
Building documentation requires python-sphinx not noted above.
|
||||||
|
|
||||||
|
@ -254,7 +230,7 @@ sudo yum install python3-sphinx
|
||||||
make doc
|
make doc
|
||||||
```
|
```
|
||||||
|
|
||||||
# Building Packages
|
## Building Packages
|
||||||
Build package commands, DESTDIR is used to make install into and then for packaging by fpm.
|
Build package commands, DESTDIR is used to make install into and then for packaging by fpm.
|
||||||
|
|
||||||
**NOTE: clean the DESTDIR if re-using the same directory**
|
**NOTE: clean the DESTDIR if re-using the same directory**
|
||||||
|
@ -270,3 +246,26 @@ make fpm DESTDIR=/tmp/core-build
|
||||||
```
|
```
|
||||||
|
|
||||||
This will produce and RPM and Deb package for the currently configured python version.
|
This will produce and RPM and Deb package for the currently configured python version.
|
||||||
|
|
||||||
|
## Running CORE
|
||||||
|
|
||||||
|
Start the CORE daemon.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# systemd
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl start core-daemon
|
||||||
|
|
||||||
|
# sysv
|
||||||
|
sudo service core-daemon start
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the GUI
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# default gui
|
||||||
|
core-gui
|
||||||
|
|
||||||
|
# new beta gui
|
||||||
|
coretk-gui
|
||||||
|
```
|
||||||
|
|
44
install.sh
44
install.sh
|
@ -3,14 +3,13 @@
|
||||||
# exit on error
|
# exit on error
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
ubuntu_py=3.6
|
||||||
|
centos_py=36
|
||||||
|
|
||||||
function install_python_depencencies() {
|
function install_python_depencencies() {
|
||||||
sudo python3 -m pip install -r daemon/requirements.txt
|
sudo python3 -m pip install -r daemon/requirements.txt
|
||||||
}
|
}
|
||||||
|
|
||||||
function install_python_dev_dependencies() {
|
|
||||||
sudo python3 -m pip install pipenv grpcio-tools
|
|
||||||
}
|
|
||||||
|
|
||||||
function install_ospf_mdr() {
|
function install_ospf_mdr() {
|
||||||
rm -rf /tmp/ospf-mdr
|
rm -rf /tmp/ospf-mdr
|
||||||
git clone https://github.com/USNavalResearchLaboratory/ospf-mdr /tmp/ospf-mdr
|
git clone https://github.com/USNavalResearchLaboratory/ospf-mdr /tmp/ospf-mdr
|
||||||
|
@ -42,8 +41,6 @@ function install_dev_core() {
|
||||||
sudo make install
|
sudo make install
|
||||||
cd -
|
cd -
|
||||||
cd daemon
|
cd daemon
|
||||||
pipenv install --dev
|
|
||||||
cd -
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# detect os/ver for install type
|
# detect os/ver for install type
|
||||||
|
@ -54,13 +51,18 @@ if [[ -f /etc/os-release ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# parse arguments
|
# parse arguments
|
||||||
while getopts ":d" opt; do
|
while getopts "dv:" opt; do
|
||||||
case ${opt} in
|
case ${opt} in
|
||||||
d)
|
d)
|
||||||
dev=1
|
dev=1
|
||||||
;;
|
;;
|
||||||
|
v)
|
||||||
|
ubuntu_py=${OPTARG}
|
||||||
|
centos_py=${OPTARG}
|
||||||
|
;;
|
||||||
\?)
|
\?)
|
||||||
echo "Invalid Option: $OPTARG" 1>&2
|
echo "script usage: $(basename $0) [-d] [-v python version]" >&2
|
||||||
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
@ -70,8 +72,12 @@ shift $((OPTIND - 1))
|
||||||
case ${os} in
|
case ${os} in
|
||||||
"ubuntu")
|
"ubuntu")
|
||||||
echo "Installing CORE for Ubuntu"
|
echo "Installing CORE for Ubuntu"
|
||||||
sudo apt install -y automake pkg-config gcc libev-dev ebtables gawk iproute2 \
|
echo "installing core system dependencies"
|
||||||
python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf
|
sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \
|
||||||
|
python${ubuntu_py} python${ubuntu_py}-dev python3-pip python3-tk tk libtk-img ethtool autoconf
|
||||||
|
python3 -m pip install grpcio-tools
|
||||||
|
echo "installing ospf-mdr system dependencies"
|
||||||
|
sudo apt install -y libtool gawk libreadline-dev
|
||||||
install_ospf_mdr
|
install_ospf_mdr
|
||||||
if [[ -z ${dev} ]]; then
|
if [[ -z ${dev} ]]; then
|
||||||
echo "normal install"
|
echo "normal install"
|
||||||
|
@ -80,23 +86,35 @@ case ${os} in
|
||||||
install_core
|
install_core
|
||||||
else
|
else
|
||||||
echo "dev install"
|
echo "dev install"
|
||||||
install_python_dev_dependencies
|
python3 -m pip install pipenv
|
||||||
build_core
|
build_core
|
||||||
install_dev_core
|
install_dev_core
|
||||||
|
python3 -m pipenv sync --dev
|
||||||
|
python3 -m pipenv run pre-commit install
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
"centos")
|
"centos")
|
||||||
|
echo "Installing CORE for CentOS"
|
||||||
|
echo "installing core system dependencies"
|
||||||
sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \
|
sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \
|
||||||
python36 python36-devel python3-pip python3-tkinter tk ethtool libtool readline-devel autoconf gawk
|
python${centos_py} python${centos_py}-devel python3-pip python3-tkinter tk ethtool autoconf
|
||||||
|
sudo python3 -m pip install grpcio-tools
|
||||||
|
echo "installing ospf-mdr system dependencies"
|
||||||
|
sudo yum install -y libtool gawk readline-devel
|
||||||
install_ospf_mdr
|
install_ospf_mdr
|
||||||
if [[ -z ${dev} ]]; then
|
if [[ -z ${dev} ]]; then
|
||||||
|
echo "normal install"
|
||||||
install_python_depencencies
|
install_python_depencencies
|
||||||
build_core --prefix=/usr
|
build_core --prefix=/usr
|
||||||
install_core
|
install_core
|
||||||
else
|
else
|
||||||
install_python_dev_dependencies
|
echo "dev install"
|
||||||
|
sudo python3 -m pip install pipenv
|
||||||
build_core --prefix=/usr
|
build_core --prefix=/usr
|
||||||
install_dev_core
|
install_dev_core
|
||||||
|
sudo python3 -m pipenv sync --dev
|
||||||
|
python3 -m pipenv sync --dev
|
||||||
|
python3 -m pipenv run pre-commit install
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|
Loading…
Add table
Reference in a new issue