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.
|
||||
|
||||
# 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
|
||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||
|
|
581
daemon/Pipfile.lock
generated
581
daemon/Pipfile.lock
generated
|
@ -39,41 +39,36 @@
|
|||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42",
|
||||
"sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04",
|
||||
"sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5",
|
||||
"sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54",
|
||||
"sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba",
|
||||
"sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57",
|
||||
"sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396",
|
||||
"sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12",
|
||||
"sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97",
|
||||
"sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43",
|
||||
"sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db",
|
||||
"sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3",
|
||||
"sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b",
|
||||
"sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579",
|
||||
"sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346",
|
||||
"sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159",
|
||||
"sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652",
|
||||
"sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e",
|
||||
"sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a",
|
||||
"sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506",
|
||||
"sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f",
|
||||
"sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d",
|
||||
"sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c",
|
||||
"sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20",
|
||||
"sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858",
|
||||
"sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc",
|
||||
"sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a",
|
||||
"sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3",
|
||||
"sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e",
|
||||
"sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410",
|
||||
"sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25",
|
||||
"sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b",
|
||||
"sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"
|
||||
"sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
|
||||
"sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
|
||||
"sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
|
||||
"sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
|
||||
"sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
|
||||
"sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
|
||||
"sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
|
||||
"sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
|
||||
"sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
|
||||
"sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
|
||||
"sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
|
||||
"sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
|
||||
"sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
|
||||
"sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
|
||||
"sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
|
||||
"sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
|
||||
"sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
|
||||
"sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
|
||||
"sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
|
||||
"sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
|
||||
"sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
|
||||
"sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
|
||||
"sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
|
||||
"sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
|
||||
"sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
|
||||
"sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
|
||||
"sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
|
||||
"sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
|
||||
],
|
||||
"version": "==1.13.2"
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"core": {
|
||||
"editable": true,
|
||||
|
@ -114,90 +109,91 @@
|
|||
},
|
||||
"grpcio": {
|
||||
"hashes": [
|
||||
"sha256:066630f6b62bffa291dacbee56994279a6a3682b8a11967e9ccaf3cc770fc11e",
|
||||
"sha256:07e95762ca6b18afbeb3aa2793e827c841152d5e507089b1db0b18304edda105",
|
||||
"sha256:0a0fb2f8e3a13537106bc77e4c63005bc60124a6203034304d9101921afa4e90",
|
||||
"sha256:0c61b74dcfb302613926e785cb3542a0905b9a3a86e9410d8cf5d25e25e10104",
|
||||
"sha256:13383bd70618da03684a8aafbdd9e3d9a6720bf8c07b85d0bc697afed599d8f0",
|
||||
"sha256:1c6e0f6b9d091e3717e9a58d631c8bb4898be3b261c2a01fe46371fdc271052f",
|
||||
"sha256:1cf710c04689daa5cc1e598efba00b028215700dcc1bf66fcb7b4f64f2ea5d5f",
|
||||
"sha256:2da5cee9faf17bb8daf500cd0d28a17ae881ab5500f070a6aace457f4c08cac4",
|
||||
"sha256:2f78ebf340eaf28fa09aba0f836a8b869af1716078dfe8f3b3f6ff785d8f2b0f",
|
||||
"sha256:33a07a1a8e817d733588dbd18e567caad1a6fe0d440c165619866cd490c7911a",
|
||||
"sha256:3d090c66af9c065b7228b07c3416f93173e9839b1d40bb0ce3dd2aa783645026",
|
||||
"sha256:42b903a3596a10e2a3727bae2a76f8aefd324d498424b843cfa9606847faea7b",
|
||||
"sha256:4fffbb58134c4f23e5a8312ac3412db6f5e39e961dc0eb5e3115ce5aa16bf927",
|
||||
"sha256:57be5a6c509a406fe0ffa6f8b86904314c77b5e2791be8123368ad2ebccec874",
|
||||
"sha256:5b0fa09efb33e2af4e8822b4eb8b2cbc201d562e3e185c439be7eaeee2e8b8aa",
|
||||
"sha256:5ef42dfc18f9a63a06aca938770b69470bb322e4c137cf08cf21703d1ef4ae5c",
|
||||
"sha256:6a43d2f2ff8250f200fdf7aa31fa191a997922aa9ea1182453acd705ad83ab72",
|
||||
"sha256:6d8ab28559be98b02f8b3a154b53239df1aa5b0d28ff865ae5be4f30e7ed4d3f",
|
||||
"sha256:6e47866b7dc14ca3a12d40c1d6082e7bea964670f1c5315ea0fb8b0550244d64",
|
||||
"sha256:6edda1b96541187f73aab11800d25f18ee87e53d5f96bb74473873072bf28a0e",
|
||||
"sha256:7109c8738a8a3c98cfb5dda1c45642a8d6d35dc00d257ab7a175099b2b4daecd",
|
||||
"sha256:8d866aafb08657c456a18c4a31c8526ea62de42427c242b58210b9eae6c64559",
|
||||
"sha256:9939727d9ae01690b24a2b159ac9dbca7b7e8e6edd5af6a6eb709243cae7b52b",
|
||||
"sha256:99fd873699df17cb11c542553270ae2b32c169986e475df0d68a8629b8ef4df7",
|
||||
"sha256:b6fda5674f990e15e1bcaacf026428cf50bce36e708ddcbd1de9673b14aab760",
|
||||
"sha256:bdb2f3dcb664f0c39ef1312cd6acf6bc6375252e4420cf8f36fff4cb4fa55c71",
|
||||
"sha256:bfd7d3130683a1a0a50c456273c21ec8a604f2d043b241a55235a78a0090ee06",
|
||||
"sha256:c6c2db348ac73d73afe14e0833b18abbbe920969bf2c5c03c0922719f8020d06",
|
||||
"sha256:cb7a4b41b5e2611f85c3402ac364f1d689f5d7ecbc24a55ef010eedcd6cf460f",
|
||||
"sha256:cd3d3e328f20f7c807a862620c6ee748e8d57ba2a8fc960d48337ed71c6d9d32",
|
||||
"sha256:d1a481777952e4f99b8a6956581f3ee866d7614100d70ae6d7e07327570b85ce",
|
||||
"sha256:d1d49720ed636920bb3d74cedf549382caa9ad55aea89d1de99d817068d896b2",
|
||||
"sha256:d42433f0086cccd192114343473d7dbd4aae9141794f939e2b7b83efc57543db",
|
||||
"sha256:d44c34463a7c481e076f691d8fa25d080c3486978c2c41dca09a8dd75296c2d7",
|
||||
"sha256:d7e5b7af1350e9c8c17a7baf99d575fbd2de69f7f0b0e6ebd47b57506de6493a",
|
||||
"sha256:d9542366a0917b9b48bab1fee481ac01f56bdffc52437b598c09e7840148a6a9",
|
||||
"sha256:df7cdfb40179acc9790a462c049e0b8e109481164dd7ad1a388dd67ff1528759",
|
||||
"sha256:e1a9d9d2e7224d981aea8da79260c7f6932bf31ce1f99b7ccfa5eceeb30dc5d0",
|
||||
"sha256:ed10e5fad105ecb0b12822f924e62d0deb07f46683a0b64416b17fd143daba1d",
|
||||
"sha256:f0ec5371ce2363b03531ed522bfbe691ec940f51f0e111f0500fc0f44518c69d",
|
||||
"sha256:f6580a8a4f5e701289b45fd62a8f6cb5ec41e4d77082424f8b676806dcd22564",
|
||||
"sha256:f7b83e4b2842d44fce3cdc0d54db7a7e0d169a598751bf393601efaa401c83e0",
|
||||
"sha256:ffec45b0db18a555fdfe0c6fa2d0a3fceb751b22b31e8fcd14ceed7bde05481e"
|
||||
"sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6",
|
||||
"sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045",
|
||||
"sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1",
|
||||
"sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00",
|
||||
"sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942",
|
||||
"sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8",
|
||||
"sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a",
|
||||
"sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c",
|
||||
"sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6",
|
||||
"sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a",
|
||||
"sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1",
|
||||
"sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961",
|
||||
"sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7",
|
||||
"sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386",
|
||||
"sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e",
|
||||
"sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866",
|
||||
"sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d",
|
||||
"sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371",
|
||||
"sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6",
|
||||
"sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb",
|
||||
"sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3",
|
||||
"sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6",
|
||||
"sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47",
|
||||
"sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454",
|
||||
"sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e",
|
||||
"sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8",
|
||||
"sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c",
|
||||
"sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4",
|
||||
"sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7",
|
||||
"sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6",
|
||||
"sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85",
|
||||
"sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571",
|
||||
"sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345",
|
||||
"sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb",
|
||||
"sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49",
|
||||
"sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8",
|
||||
"sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844",
|
||||
"sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54",
|
||||
"sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca",
|
||||
"sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5",
|
||||
"sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3",
|
||||
"sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed",
|
||||
"sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b"
|
||||
],
|
||||
"version": "==1.26.0"
|
||||
"version": "==1.27.2"
|
||||
},
|
||||
"invoke": {
|
||||
"hashes": [
|
||||
"sha256:4668a4a594a47f2da2f0672ec2f7b1566f809cebf10bcd95ce2de9ecf39b95d1",
|
||||
"sha256:ae7b4513638bde9afcda0825e9535599637a3f65bd819a27098356027bb17c8a",
|
||||
"sha256:e04faba8ea7cdf6f5c912be42dcafd5c1074b7f2f306998992c4bfb40a9a690b"
|
||||
"sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132",
|
||||
"sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134",
|
||||
"sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d"
|
||||
],
|
||||
"version": "==1.4.0"
|
||||
"version": "==1.4.1"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:00ac0d64949fef6b3693813fe636a2d56d97a5a49b5bbb86e4cc4cc50ebc9ea2",
|
||||
"sha256:0571e607558665ed42e450d7bf0e2941d542c18e117b1ebbf0ba72f287ad841c",
|
||||
"sha256:0e3f04a7615fdac0be5e18b2406529521d6dbdb0167d2a690ee328bef7807487",
|
||||
"sha256:13cf89be53348d1c17b453867da68704802966c433b2bb4fa1f970daadd2ef70",
|
||||
"sha256:217262fcf6a4c2e1c7cb1efa08bd9ebc432502abc6c255c4abab611e8be0d14d",
|
||||
"sha256:223e544828f1955daaf4cefbb4853bc416b2ec3fd56d4f4204a8b17007c21250",
|
||||
"sha256:277cb61fede2f95b9c61912fefb3d43fbd5f18bf18a14fae4911b67984486f5d",
|
||||
"sha256:3213f753e8ae86c396e0e066866e64c6b04618e85c723b32ecb0909885211f74",
|
||||
"sha256:4690984a4dee1033da0af6df0b7a6bde83f74e1c0c870623797cec77964de34d",
|
||||
"sha256:4fcc472ef87f45c429d3b923b925704aa581f875d65bac80f8ab0c3296a63f78",
|
||||
"sha256:61409bd745a265a742f2693e4600e4dbd45cc1daebe1d5fad6fcb22912d44145",
|
||||
"sha256:678f1963f755c5d9f5f6968dded7b245dd1ece8cf53c1aa9d80e6734a8c7f41d",
|
||||
"sha256:6c6d03549d4e2734133badb9ab1c05d9f0ef4bcd31d83e5d2b4747c85cfa21da",
|
||||
"sha256:6e74d5f4d6ecd6942375c52ffcd35f4318a61a02328f6f1bd79fcb4ffedf969e",
|
||||
"sha256:7b4fc7b1ecc987ca7aaf3f4f0e71bbfbd81aaabf87002558f5bc95da3a865bcd",
|
||||
"sha256:7ed386a40e172ddf44c061ad74881d8622f791d9af0b6f5be20023029129bc85",
|
||||
"sha256:8f54f0924d12c47a382c600c880770b5ebfc96c9fd94cf6f6bdc21caf6163ea7",
|
||||
"sha256:ad9b81351fdc236bda538efa6879315448411a81186c836d4b80d6ca8217cdb9",
|
||||
"sha256:bbd00e21ea17f7bcc58dccd13869d68441b32899e89cf6cfa90d624a9198ce85",
|
||||
"sha256:c3c289762cc09735e2a8f8a49571d0e8b4f57ea831ea11558247b5bdea0ac4db",
|
||||
"sha256:cf4650942de5e5685ad308e22bcafbccfe37c54aa7c0e30cd620c2ee5c93d336",
|
||||
"sha256:cfcbc33c9c59c93776aa41ab02e55c288a042211708b72fdb518221cc803abc8",
|
||||
"sha256:e301055deadfedbd80cf94f2f65ff23126b232b0d1fea28f332ce58137bcdb18",
|
||||
"sha256:ebbfe24df7f7b5c6c7620702496b6419f6a9aa2fd7f005eb731cc80d7b4692b9",
|
||||
"sha256:eff69ddbf3ad86375c344339371168640951c302450c5d3e9936e98d6459db06",
|
||||
"sha256:f6ed60a62c5f1c44e789d2cf14009423cb1646b44a43e40a9cf6a21f077678a1"
|
||||
"sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd",
|
||||
"sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c",
|
||||
"sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081",
|
||||
"sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f",
|
||||
"sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261",
|
||||
"sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a",
|
||||
"sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9",
|
||||
"sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a",
|
||||
"sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb",
|
||||
"sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60",
|
||||
"sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128",
|
||||
"sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a",
|
||||
"sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717",
|
||||
"sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89",
|
||||
"sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72",
|
||||
"sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8",
|
||||
"sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3",
|
||||
"sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7",
|
||||
"sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8",
|
||||
"sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77",
|
||||
"sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1",
|
||||
"sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15",
|
||||
"sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679",
|
||||
"sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012",
|
||||
"sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6",
|
||||
"sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc",
|
||||
"sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca"
|
||||
],
|
||||
"version": "==4.4.2"
|
||||
"version": "==4.5.0"
|
||||
},
|
||||
"mako": {
|
||||
"hashes": [
|
||||
|
@ -211,13 +207,16 @@
|
|||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
|
||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
|
||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
|
||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||
|
@ -234,7 +233,9 @@
|
|||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
|
||||
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
|
@ -281,26 +282,26 @@
|
|||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37",
|
||||
"sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36",
|
||||
"sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4",
|
||||
"sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d",
|
||||
"sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574",
|
||||
"sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0",
|
||||
"sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5",
|
||||
"sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f",
|
||||
"sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946",
|
||||
"sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104",
|
||||
"sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e",
|
||||
"sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7",
|
||||
"sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a",
|
||||
"sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507",
|
||||
"sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1",
|
||||
"sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593",
|
||||
"sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c",
|
||||
"sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1"
|
||||
"sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab",
|
||||
"sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f",
|
||||
"sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a",
|
||||
"sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0",
|
||||
"sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4",
|
||||
"sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2",
|
||||
"sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee",
|
||||
"sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07",
|
||||
"sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151",
|
||||
"sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a",
|
||||
"sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f",
|
||||
"sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7",
|
||||
"sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956",
|
||||
"sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306",
|
||||
"sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961",
|
||||
"sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481",
|
||||
"sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a",
|
||||
"sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80"
|
||||
],
|
||||
"version": "==3.11.2"
|
||||
"version": "==3.11.3"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
|
@ -334,6 +335,36 @@
|
|||
],
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
|
||||
|
@ -366,13 +397,6 @@
|
|||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"aspy.yaml": {
|
||||
"hashes": [
|
||||
"sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc",
|
||||
"sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
|
@ -390,10 +414,10 @@
|
|||
},
|
||||
"cfgv": {
|
||||
"hashes": [
|
||||
"sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144",
|
||||
"sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289"
|
||||
"sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53",
|
||||
"sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513"
|
||||
],
|
||||
"version": "==2.0.1"
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
|
@ -402,6 +426,12 @@
|
|||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"distlib": {
|
||||
"hashes": [
|
||||
"sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"
|
||||
],
|
||||
"version": "==0.3.0"
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||
|
@ -409,6 +439,13 @@
|
|||
],
|
||||
"version": "==0.3"
|
||||
},
|
||||
"filelock": {
|
||||
"hashes": [
|
||||
"sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
|
||||
"sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
|
||||
],
|
||||
"version": "==3.0.12"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
|
||||
|
@ -419,115 +456,115 @@
|
|||
},
|
||||
"grpcio": {
|
||||
"hashes": [
|
||||
"sha256:066630f6b62bffa291dacbee56994279a6a3682b8a11967e9ccaf3cc770fc11e",
|
||||
"sha256:07e95762ca6b18afbeb3aa2793e827c841152d5e507089b1db0b18304edda105",
|
||||
"sha256:0a0fb2f8e3a13537106bc77e4c63005bc60124a6203034304d9101921afa4e90",
|
||||
"sha256:0c61b74dcfb302613926e785cb3542a0905b9a3a86e9410d8cf5d25e25e10104",
|
||||
"sha256:13383bd70618da03684a8aafbdd9e3d9a6720bf8c07b85d0bc697afed599d8f0",
|
||||
"sha256:1c6e0f6b9d091e3717e9a58d631c8bb4898be3b261c2a01fe46371fdc271052f",
|
||||
"sha256:1cf710c04689daa5cc1e598efba00b028215700dcc1bf66fcb7b4f64f2ea5d5f",
|
||||
"sha256:2da5cee9faf17bb8daf500cd0d28a17ae881ab5500f070a6aace457f4c08cac4",
|
||||
"sha256:2f78ebf340eaf28fa09aba0f836a8b869af1716078dfe8f3b3f6ff785d8f2b0f",
|
||||
"sha256:33a07a1a8e817d733588dbd18e567caad1a6fe0d440c165619866cd490c7911a",
|
||||
"sha256:3d090c66af9c065b7228b07c3416f93173e9839b1d40bb0ce3dd2aa783645026",
|
||||
"sha256:42b903a3596a10e2a3727bae2a76f8aefd324d498424b843cfa9606847faea7b",
|
||||
"sha256:4fffbb58134c4f23e5a8312ac3412db6f5e39e961dc0eb5e3115ce5aa16bf927",
|
||||
"sha256:57be5a6c509a406fe0ffa6f8b86904314c77b5e2791be8123368ad2ebccec874",
|
||||
"sha256:5b0fa09efb33e2af4e8822b4eb8b2cbc201d562e3e185c439be7eaeee2e8b8aa",
|
||||
"sha256:5ef42dfc18f9a63a06aca938770b69470bb322e4c137cf08cf21703d1ef4ae5c",
|
||||
"sha256:6a43d2f2ff8250f200fdf7aa31fa191a997922aa9ea1182453acd705ad83ab72",
|
||||
"sha256:6d8ab28559be98b02f8b3a154b53239df1aa5b0d28ff865ae5be4f30e7ed4d3f",
|
||||
"sha256:6e47866b7dc14ca3a12d40c1d6082e7bea964670f1c5315ea0fb8b0550244d64",
|
||||
"sha256:6edda1b96541187f73aab11800d25f18ee87e53d5f96bb74473873072bf28a0e",
|
||||
"sha256:7109c8738a8a3c98cfb5dda1c45642a8d6d35dc00d257ab7a175099b2b4daecd",
|
||||
"sha256:8d866aafb08657c456a18c4a31c8526ea62de42427c242b58210b9eae6c64559",
|
||||
"sha256:9939727d9ae01690b24a2b159ac9dbca7b7e8e6edd5af6a6eb709243cae7b52b",
|
||||
"sha256:99fd873699df17cb11c542553270ae2b32c169986e475df0d68a8629b8ef4df7",
|
||||
"sha256:b6fda5674f990e15e1bcaacf026428cf50bce36e708ddcbd1de9673b14aab760",
|
||||
"sha256:bdb2f3dcb664f0c39ef1312cd6acf6bc6375252e4420cf8f36fff4cb4fa55c71",
|
||||
"sha256:bfd7d3130683a1a0a50c456273c21ec8a604f2d043b241a55235a78a0090ee06",
|
||||
"sha256:c6c2db348ac73d73afe14e0833b18abbbe920969bf2c5c03c0922719f8020d06",
|
||||
"sha256:cb7a4b41b5e2611f85c3402ac364f1d689f5d7ecbc24a55ef010eedcd6cf460f",
|
||||
"sha256:cd3d3e328f20f7c807a862620c6ee748e8d57ba2a8fc960d48337ed71c6d9d32",
|
||||
"sha256:d1a481777952e4f99b8a6956581f3ee866d7614100d70ae6d7e07327570b85ce",
|
||||
"sha256:d1d49720ed636920bb3d74cedf549382caa9ad55aea89d1de99d817068d896b2",
|
||||
"sha256:d42433f0086cccd192114343473d7dbd4aae9141794f939e2b7b83efc57543db",
|
||||
"sha256:d44c34463a7c481e076f691d8fa25d080c3486978c2c41dca09a8dd75296c2d7",
|
||||
"sha256:d7e5b7af1350e9c8c17a7baf99d575fbd2de69f7f0b0e6ebd47b57506de6493a",
|
||||
"sha256:d9542366a0917b9b48bab1fee481ac01f56bdffc52437b598c09e7840148a6a9",
|
||||
"sha256:df7cdfb40179acc9790a462c049e0b8e109481164dd7ad1a388dd67ff1528759",
|
||||
"sha256:e1a9d9d2e7224d981aea8da79260c7f6932bf31ce1f99b7ccfa5eceeb30dc5d0",
|
||||
"sha256:ed10e5fad105ecb0b12822f924e62d0deb07f46683a0b64416b17fd143daba1d",
|
||||
"sha256:f0ec5371ce2363b03531ed522bfbe691ec940f51f0e111f0500fc0f44518c69d",
|
||||
"sha256:f6580a8a4f5e701289b45fd62a8f6cb5ec41e4d77082424f8b676806dcd22564",
|
||||
"sha256:f7b83e4b2842d44fce3cdc0d54db7a7e0d169a598751bf393601efaa401c83e0",
|
||||
"sha256:ffec45b0db18a555fdfe0c6fa2d0a3fceb751b22b31e8fcd14ceed7bde05481e"
|
||||
"sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6",
|
||||
"sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045",
|
||||
"sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1",
|
||||
"sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00",
|
||||
"sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942",
|
||||
"sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8",
|
||||
"sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a",
|
||||
"sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c",
|
||||
"sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6",
|
||||
"sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a",
|
||||
"sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1",
|
||||
"sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961",
|
||||
"sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7",
|
||||
"sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386",
|
||||
"sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e",
|
||||
"sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866",
|
||||
"sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d",
|
||||
"sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371",
|
||||
"sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6",
|
||||
"sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb",
|
||||
"sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3",
|
||||
"sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6",
|
||||
"sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47",
|
||||
"sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454",
|
||||
"sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e",
|
||||
"sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8",
|
||||
"sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c",
|
||||
"sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4",
|
||||
"sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7",
|
||||
"sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6",
|
||||
"sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85",
|
||||
"sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571",
|
||||
"sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345",
|
||||
"sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb",
|
||||
"sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49",
|
||||
"sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8",
|
||||
"sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844",
|
||||
"sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54",
|
||||
"sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca",
|
||||
"sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5",
|
||||
"sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3",
|
||||
"sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed",
|
||||
"sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b"
|
||||
],
|
||||
"version": "==1.26.0"
|
||||
"version": "==1.27.2"
|
||||
},
|
||||
"grpcio-tools": {
|
||||
"hashes": [
|
||||
"sha256:0286f704e55e3012fec3910400fe1a4ed11aeb66d3ec4b7f8041845af7fb7206",
|
||||
"sha256:033a4e80dc78d9c11860800bd5a66b65ff385be8f669e96b02e795364c860597",
|
||||
"sha256:0e3b5469912430f19407ebe14cfd1bece1b5a277c4d43e1b65dbff19d9475ccc",
|
||||
"sha256:131aa8c3862a555819428856f872ab9e919e351d7cd60c98012e12d2fb6afc45",
|
||||
"sha256:1783b8fa74f58a643e7780112fc4eb6110789672e852a691fad6af6b94a90c4a",
|
||||
"sha256:1e80f74854bd1c7263942e836d69f95ffc66bb45bf14bf3e1ab61113271b5884",
|
||||
"sha256:27ae784acff3d2fa04e3b4dc72f8d60a55d654f90e410adf08f46a4d2d673dd3",
|
||||
"sha256:33c6bee5a02408018dc10a5737818d2159f14cbb0613df41cc93ba6cbaeea095",
|
||||
"sha256:376a1840d1f5d25e9c3391557d6b3eeb3de17be697b0e55d8247d0262fcbaacf",
|
||||
"sha256:3922dffd8160d54dc00c7d32b30776a974cc098086493c668faffac19e752087",
|
||||
"sha256:4ba7e5afc93b413bbb5f3dd65ba583e078ff5895a5053d825ab793cf7720ae96",
|
||||
"sha256:4e9a1276f8699d06518cec8caceb2c423fc7f971765cab7550d39f281795fd81",
|
||||
"sha256:51ac9c4f8a542cd20c6776fde781c84c0acd8faba55ec14f121c6b4eb4245e89",
|
||||
"sha256:5580b86cf49936c9c74f0def44d3582a7a1bb720eba8a14805c3a61efa790c70",
|
||||
"sha256:58a879208bd84d6819a61c1b0618655574ef9df1d63a0e2f434fdcb5cfa1fb57",
|
||||
"sha256:675918f83fa35bd54f4c29d95d8652c6215d5e95a13b6f14e626cdef6d0fce79",
|
||||
"sha256:68259fd06188951d152665ffe44f9660edd715c102ae4bc4216eca4c4666dadf",
|
||||
"sha256:6cea124cbd9081a587e1954b98e9a27c7cca6ae72babc3046ab6b439a5730679",
|
||||
"sha256:6f356a445ba7afc634b1046d9f51d3ae37afbf4fe1a500285aca37677462a7b9",
|
||||
"sha256:7f7430434bd997584f2136a675559ba0d4afdf7cb71d9bbc429b0cc831e6828c",
|
||||
"sha256:809d60f15a32c21dc221ddb591aff8adfdde4e05095414eb8e015cdfef361615",
|
||||
"sha256:826c19f26b41e99691e77823ad67f04dc0b69e514212907695e330c6f106415c",
|
||||
"sha256:96c6f657b93f49243d083840d27a5a686a1fc26044a80ebf8585734d5152d4ee",
|
||||
"sha256:9a2091371298f04ef350f776365945537d0befa95bad5623d80c4207bdff9d3a",
|
||||
"sha256:9af72b764b41ba939e8e0a7ae9ec8a17d1c46a18797c6342cba6483f29e1790f",
|
||||
"sha256:a209002e3d4787f0e90e29f15cddbe83dc9054238c0da7f539c913002a348cc1",
|
||||
"sha256:a908d5af2f26673e970c7c03703437bf95d10e88dad3322e7e267467db44a04d",
|
||||
"sha256:ab841c69581085b6f9aa54044a13db6ec31183513f7cce0862d29c9b7b4e3c64",
|
||||
"sha256:b1bc78efefb8e085c072add2c02326fdecad9b8644b3be11e715ea4c6102ad87",
|
||||
"sha256:b97e74ffe121dfa9ae7ec94393fce4e95e9e0a343827663e989dc4b7c918d1a5",
|
||||
"sha256:bba8d3b61ec113bb94596599d2568217b22ddfc7baa46c00dec5106cfd4e914b",
|
||||
"sha256:bfe0e33aea60da100b214c72c1746cc0194bb8da910004518c185041cc795543",
|
||||
"sha256:c15f0718cbc3986e747d5b0734198dce0ac07d188ec5e063b1e9889ac947f86e",
|
||||
"sha256:c56d0ac769bf1f01dbb6ec6b6492849e70cd35bdeeb660e206a70ab43917ae92",
|
||||
"sha256:d396fdb7026986e6d3897bb207cc7d5bc536a82a2e50af806a24b3d254c73bc3",
|
||||
"sha256:d62ab00dea7fa0813fc813a6c848da2eeda5cb71893b892a229d23949de0cecd",
|
||||
"sha256:da75e33e185c8be17a82ec4a97f5c75ec05d57e85f8b285f86e2a22484849e4a",
|
||||
"sha256:dcbd1fbb540638c9ad9c3a071b392b654f79666a2bc12808080b0e9f674b9a80",
|
||||
"sha256:e7e90bad5466347a3648358e9f437e72d5f6d6025fe741171a88aca8b9d864df",
|
||||
"sha256:eae371a663ceeef8f930323a120a9d11e13e1c49903a66ddb4ada4830d5bcb7d",
|
||||
"sha256:f290cccc972533a288c2ebc55eb3c0fbe0c6a0d0a9775cb34ce6bfb11fe14a11",
|
||||
"sha256:facb8c588cdd6adc51ae7545f59283565dae8d946c6163e578b70ab6bf161215",
|
||||
"sha256:fb043e45f91634776acdfe4b8dfc96b636c53a458799179041ab633e15c3d833"
|
||||
"sha256:00c5080cfb197ed20ecf0d0ff2d07f1fc9c42c724cad21c40ff2d048de5712b1",
|
||||
"sha256:069826dd02ce1886444cf4519c4fe1b05ac9ef41491f26e97400640531db47f6",
|
||||
"sha256:1266b577abe7c720fd16a83d0a4999a192e87c4a98fc9f97e0b99b106b3e155f",
|
||||
"sha256:16dc3fad04fe18d50777c56af7b2d9b9984cd1cfc71184646eb431196d1645c6",
|
||||
"sha256:1de5a273eaffeb3d126a63345e9e848ea7db740762f700eb8b5d84c5e3e7687d",
|
||||
"sha256:2ca280af2cae1a014a238057bd3c0a254527569a6a9169a01c07f0590081d530",
|
||||
"sha256:43a1573400527a23e4174d88604fde7a9d9a69bf9473c21936b7f409858f8ebb",
|
||||
"sha256:4698c6b6a57f73b14d91a542c69ff33a2da8729691b7060a5d7f6383624d045e",
|
||||
"sha256:520b7dafddd0f82cb7e4f6e9c6ba1049aa804d0e207870def9fe7f94d1e14090",
|
||||
"sha256:57f8b9e2c7f55cd45f6dd930d6de61deb42d3eb7f9788137fbc7155cf724132a",
|
||||
"sha256:59fbeb5bb9a7b94eb61642ac2cee1db5233b8094ca76fc56d4e0c6c20b5dd85f",
|
||||
"sha256:5fd7efc2fd3370bd2c72dc58f31a407a5dff5498befa145da211b2e8c6a52c63",
|
||||
"sha256:6016c07d6566e3109a3c032cf3861902d66501ecc08a5a84c47e43027302f367",
|
||||
"sha256:627c91923df75091d8c4d244af38d5ab7ed8d786d480751d6c2b9267fbb92fe0",
|
||||
"sha256:69c4a63919b9007e845d9f8980becd2f89d808a4a431ca32b9723ee37b521cb1",
|
||||
"sha256:77e25c241e33b75612f2aa62985f746c6f6803ec4e452da508bb7f8d90a69db4",
|
||||
"sha256:7a2d5fb558ac153a326e742ebfd7020eb781c43d3ffd920abd42b2e6c6fdfb37",
|
||||
"sha256:7b54b283ec83190680903a9037376dc915e1f03852a2d574ba4d981b7a1fd3d0",
|
||||
"sha256:845a51305af9fc7f9e2078edaec9a759153195f6cf1fbb12b1fa6f077e56b260",
|
||||
"sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88",
|
||||
"sha256:87e8ca2c2d2d3e09b2a2bed5d740d7b3e64028dafb7d6be543b77eec85590736",
|
||||
"sha256:8e7738a4b93842bca1158cde81a3587c9b7111823e40a1ddf73292ca9d58e08b",
|
||||
"sha256:915a695bc112517af48126ee0ecdb6aff05ed33f3eeef28f0d076f1f6b52ef5e",
|
||||
"sha256:99961156a36aae4a402d6b14c1e7efde642794b3ddbf32c51db0cb3a199e8b11",
|
||||
"sha256:9ba88c2d99bcaf7b9cb720925e3290d73b2367d238c5779363fd5598b2dc98c7",
|
||||
"sha256:a140bf853edb2b5e8692fe94869e3e34077d7599170c113d07a58286c604f4fe",
|
||||
"sha256:a14dc7a36c845991d908a7179502ca47bcba5ae1817c4426ce68cf2c97b20ad9",
|
||||
"sha256:a3d2aec4b09c8e59fee8b0d1ed668d09e8c48b738f03f5d8401d7eb409111c47",
|
||||
"sha256:a8f892378b0b02526635b806f59141abbb429d19bec56e869e04f396502c9651",
|
||||
"sha256:aaa5ae26883c3d58d1a4323981f96b941fa09bb8f0f368d97c6225585280cf04",
|
||||
"sha256:b56caecc16307b088a431a4038c3b3bb7d0e7f9988cbd0e9fa04ac937455ea38",
|
||||
"sha256:bd7f59ff1252a3db8a143b13ea1c1e93d4b8cf4b852eb48b22ef1e6942f62a84",
|
||||
"sha256:c1bb8f47d58e9f7c4825abfe01e6b85eda53c8b31d2267ca4cddf3c4d0829b80",
|
||||
"sha256:d1a5e5fa47ba9557a7d3b31605631805adc66cdba9d95b5d10dfc52cca1fed53",
|
||||
"sha256:dcbc06556f3713a9348c4fce02d05d91e678fc320fb2bcf0ddf8e4bb11d17867",
|
||||
"sha256:e17b2e0936b04ced99769e26111e1e86ba81619d1b2691b1364f795e45560953",
|
||||
"sha256:e6932518db389ede8bf06b4119bbd3e17f42d4626e72dec2b8955b20ec732cb6",
|
||||
"sha256:ea4b3ad696d976d5eac74ec8df9a2c692113e455446ee38d5b3bd87f8e034fa6",
|
||||
"sha256:ee50b0cf0d28748ef9f941894eb50fc464bd61b8e96aaf80c5056bea9b80d580",
|
||||
"sha256:ef624b6134aef737b3daa4fb7e806cb8c5749efecd0b1fa9ce4f7e060c7a0221",
|
||||
"sha256:f5450aa904e720f9c6407b59e96a8951ed6a95463f49444b6d2594b067d39588",
|
||||
"sha256:f8514453411d72cc3cf7d481f2b6057e5b7436736d0cd39ee2b2f72088bbf497",
|
||||
"sha256:fae91f30dc050a8d0b32d20dc700e6092f0bd2138d83e9570fff3f0372c1b27e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.26.0"
|
||||
"version": "==1.27.2"
|
||||
},
|
||||
"identify": {
|
||||
"hashes": [
|
||||
"sha256:418f3b2313ac0b531139311a6b426854e9cbdfcfb6175447a5039aa6291d8b30",
|
||||
"sha256:8ad99ed1f3a965612dcb881435bf58abcfbeb05e230bb8c352b51e8eac103360"
|
||||
"sha256:1222b648251bdcb8deb240b294f450fbf704c7984e08baa92507e4ea10b436d5",
|
||||
"sha256:d824ebe21f38325c771c41b08a95a761db1982f1fc0eee37c6c97df3f1636b96"
|
||||
],
|
||||
"version": "==1.4.10"
|
||||
"version": "==1.4.11"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
||||
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
|
||||
"sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302",
|
||||
"sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.4.0"
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"importlib-resources": {
|
||||
"hashes": [
|
||||
|
@ -554,24 +591,24 @@
|
|||
},
|
||||
"mock": {
|
||||
"hashes": [
|
||||
"sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3",
|
||||
"sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"
|
||||
"sha256:2a572b715f09dd2f0a583d8aeb5bb67d7ed7a8fd31d193cf1227a99c16a67bc3",
|
||||
"sha256:5e48d216809f6f393987ed56920305d8f3c647e6ed35407c1ff2ecb88a9e1151"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.5"
|
||||
"version": "==4.0.1"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
|
||||
"sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
|
||||
"sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
|
||||
"sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
|
||||
],
|
||||
"version": "==8.1.0"
|
||||
"version": "==8.2.0"
|
||||
},
|
||||
"nodeenv": {
|
||||
"hashes": [
|
||||
"sha256:561057acd4ae3809e665a9aaaf214afff110bbb6a6d5c8a96121aea6878408b3"
|
||||
"sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212"
|
||||
],
|
||||
"version": "==1.3.4"
|
||||
"version": "==1.3.5"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
|
@ -589,34 +626,34 @@
|
|||
},
|
||||
"pre-commit": {
|
||||
"hashes": [
|
||||
"sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850",
|
||||
"sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"
|
||||
"sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6",
|
||||
"sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.21.0"
|
||||
"version": "==2.1.1"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37",
|
||||
"sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36",
|
||||
"sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4",
|
||||
"sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d",
|
||||
"sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574",
|
||||
"sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0",
|
||||
"sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5",
|
||||
"sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f",
|
||||
"sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946",
|
||||
"sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104",
|
||||
"sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e",
|
||||
"sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7",
|
||||
"sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a",
|
||||
"sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507",
|
||||
"sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1",
|
||||
"sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593",
|
||||
"sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c",
|
||||
"sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1"
|
||||
"sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab",
|
||||
"sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f",
|
||||
"sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a",
|
||||
"sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0",
|
||||
"sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4",
|
||||
"sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2",
|
||||
"sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee",
|
||||
"sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07",
|
||||
"sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151",
|
||||
"sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a",
|
||||
"sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f",
|
||||
"sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7",
|
||||
"sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956",
|
||||
"sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306",
|
||||
"sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961",
|
||||
"sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481",
|
||||
"sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a",
|
||||
"sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80"
|
||||
],
|
||||
"version": "==3.11.2"
|
||||
"version": "==3.11.3"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
|
@ -648,11 +685,11 @@
|
|||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:1d122e8be54d1a709e56f82e2d85dcba3018313d64647f38a91aec88c239b600",
|
||||
"sha256:c13d1943c63e599b98cf118fcb9703e4d7bde7caa9a432567bcdcae4bf512d20"
|
||||
"sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d",
|
||||
"sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.3.4"
|
||||
"version": "==5.3.5"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
|
@ -686,10 +723,10 @@
|
|||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3",
|
||||
"sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb"
|
||||
"sha256:30ea90b21dabd11da5f509710ad3be2ae47d40ccbc717dfdd2efe4367c10f598",
|
||||
"sha256:4a36a96d785428278edd389d9c36d763c5755844beb7509279194647b1ef47f1"
|
||||
],
|
||||
"version": "==16.7.9"
|
||||
"version": "==20.0.7"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
|
@ -700,10 +737,10 @@
|
|||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
|
||||
"sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67"
|
||||
"sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2",
|
||||
"sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a"
|
||||
],
|
||||
"version": "==2.0.1"
|
||||
"version": "==3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ from core.api.grpc.configservices_pb2 import (
|
|||
SetNodeConfigServiceResponse,
|
||||
)
|
||||
from core.api.grpc.core_pb2 import (
|
||||
ExecuteScriptRequest,
|
||||
ExecuteScriptResponse,
|
||||
GetEmaneEventChannelRequest,
|
||||
GetEmaneEventChannelResponse,
|
||||
)
|
||||
|
@ -146,8 +148,9 @@ def start_streamer(stream: Any, handler: Callable[[core_pb2.Event], None]) -> No
|
|||
:param handler: function that handles an event
|
||||
:return: nothing
|
||||
"""
|
||||
thread = threading.Thread(target=stream_listener, args=(stream, handler))
|
||||
thread.daemon = True
|
||||
thread = threading.Thread(
|
||||
target=stream_listener, args=(stream, handler), daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
|
||||
|
@ -259,6 +262,16 @@ class CoreGrpcClient:
|
|||
"""
|
||||
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:
|
||||
"""
|
||||
Retrieve a session.
|
||||
|
@ -472,9 +485,10 @@ class CoreGrpcClient:
|
|||
self,
|
||||
session_id: int,
|
||||
node_id: int,
|
||||
position: core_pb2.Position,
|
||||
position: core_pb2.Position = None,
|
||||
icon: str = None,
|
||||
source: str = None,
|
||||
geo: core_pb2.Geo = None,
|
||||
) -> core_pb2.EditNodeResponse:
|
||||
"""
|
||||
Edit a node, currently only changes position.
|
||||
|
@ -484,6 +498,7 @@ class CoreGrpcClient:
|
|||
:param position: position to set node to
|
||||
:param icon: path to icon for gui to use for node
|
||||
:param source: application source editing node
|
||||
:param geo: lon,lat,alt location for node
|
||||
:return: response with result of success or failure
|
||||
:raises grpc.RpcError: when session or node doesn't exist
|
||||
"""
|
||||
|
@ -493,6 +508,7 @@ class CoreGrpcClient:
|
|||
position=position,
|
||||
icon=icon,
|
||||
source=source,
|
||||
geo=geo,
|
||||
)
|
||||
return self.stub.EditNode(request)
|
||||
|
||||
|
@ -1147,6 +1163,10 @@ class CoreGrpcClient:
|
|||
request = GetEmaneEventChannelRequest(session_id=session_id)
|
||||
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:
|
||||
"""
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
|
@ -377,9 +379,9 @@ def service_configuration(session: Session, config: core_pb2.ServiceConfig) -> N
|
|||
session.services.set_service(config.node_id, config.service)
|
||||
service = session.services.get_service(config.node_id, config.service)
|
||||
if config.files:
|
||||
service.files = tuple(config.files)
|
||||
service.configs = tuple(config.files)
|
||||
if config.directories:
|
||||
service.directories = tuple(config.directories)
|
||||
service.dirs = tuple(config.directories)
|
||||
if config.startup:
|
||||
service.startup = tuple(config.startup)
|
||||
if config.validate:
|
||||
|
|
|
@ -3,6 +3,7 @@ import logging
|
|||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from concurrent import futures
|
||||
from typing import Type
|
||||
|
@ -10,6 +11,7 @@ from typing import Type
|
|||
import grpc
|
||||
from grpc import ServicerContext
|
||||
|
||||
from core import utils
|
||||
from core.api.grpc import (
|
||||
common_pb2,
|
||||
configservices_pb2,
|
||||
|
@ -33,6 +35,7 @@ from core.api.grpc.configservices_pb2 import (
|
|||
SetNodeConfigServiceResponse,
|
||||
)
|
||||
from core.api.grpc.core_pb2 import (
|
||||
ExecuteScriptResponse,
|
||||
GetEmaneEventChannelRequest,
|
||||
GetEmaneEventChannelResponse,
|
||||
)
|
||||
|
@ -450,6 +453,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
session.metadata = dict(request.config)
|
||||
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(
|
||||
self, request: core_pb2.GetSessionRequest, context: ServicerContext
|
||||
) -> core_pb2.GetSessionResponse:
|
||||
|
@ -685,21 +701,26 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
node = self.get_node(session, request.node_id, context)
|
||||
options = NodeOptions()
|
||||
options.icon = request.icon
|
||||
x = request.position.x
|
||||
y = request.position.y
|
||||
options.set_position(x, y)
|
||||
lat = request.position.lat
|
||||
lon = request.position.lon
|
||||
alt = request.position.alt
|
||||
options.set_location(lat, lon, alt)
|
||||
if request.HasField("position"):
|
||||
x = request.position.x
|
||||
y = request.position.y
|
||||
options.set_position(x, y)
|
||||
lat, lon, alt = None, None, None
|
||||
has_geo = request.HasField("geo")
|
||||
if has_geo:
|
||||
lat = request.geo.lat
|
||||
lon = request.geo.lon
|
||||
alt = request.geo.alt
|
||||
options.set_location(lat, lon, alt)
|
||||
result = True
|
||||
try:
|
||||
session.edit_node(node.id, options)
|
||||
source = None
|
||||
if request.source:
|
||||
source = request.source
|
||||
node_data = node.data(0, source=source)
|
||||
session.broadcast_node(node_data)
|
||||
if not has_geo:
|
||||
node_data = node.data(0, source=source)
|
||||
session.broadcast_node(node_data)
|
||||
except CoreError:
|
||||
result = False
|
||||
return core_pb2.EditNodeResponse(result=result)
|
||||
|
@ -1645,3 +1666,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
if session.emane.eventchannel:
|
||||
group, port, device = session.emane.eventchannel
|
||||
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(
|
||||
"%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:
|
||||
logging.error("no handler for message type: %s", message.type_str())
|
||||
return
|
||||
|
@ -949,11 +944,10 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
|||
file_name,
|
||||
{"__file__": file_name, "coreemu": self.coreemu},
|
||||
),
|
||||
daemon=True,
|
||||
)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
# allow time for session creation
|
||||
time.sleep(0.25)
|
||||
thread.join()
|
||||
|
||||
if message.flags & MessageFlags.STRING.value:
|
||||
new_session_ids = set(self.coreemu.sessions.keys())
|
||||
|
@ -1128,7 +1122,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
|||
self.session.location.refgeo,
|
||||
self.session.location.refscale,
|
||||
)
|
||||
logging.info("location configured: UTM%s", self.session.location.refutm)
|
||||
|
||||
def handle_config_metadata(self, message_type, config_data):
|
||||
replies = []
|
||||
|
@ -2044,7 +2037,6 @@ class CoreUdpHandler(CoreHandler):
|
|||
logging.debug("session handling message: %s", session.session_id)
|
||||
self.session = session
|
||||
self.handle_message(message)
|
||||
self.session.sdt.handle_distributed(message)
|
||||
self.broadcast(message)
|
||||
else:
|
||||
logging.error(
|
||||
|
@ -2069,7 +2061,6 @@ class CoreUdpHandler(CoreHandler):
|
|||
if session or message.message_type == MessageTypes.REGISTER.value:
|
||||
self.session = session
|
||||
self.handle_message(message)
|
||||
self.session.sdt.handle_distributed(message)
|
||||
self.broadcast(message)
|
||||
else:
|
||||
logging.error(
|
||||
|
|
|
@ -31,9 +31,9 @@ def convert_node(node_data):
|
|||
(NodeTlvs.CANVAS, node_data.canvas),
|
||||
(NodeTlvs.NETWORK_ID, node_data.network_id),
|
||||
(NodeTlvs.SERVICES, node_data.services),
|
||||
(NodeTlvs.LATITUDE, node_data.latitude),
|
||||
(NodeTlvs.LONGITUDE, node_data.longitude),
|
||||
(NodeTlvs.ALTITUDE, node_data.altitude),
|
||||
(NodeTlvs.LATITUDE, str(node_data.latitude)),
|
||||
(NodeTlvs.LONGITUDE, str(node_data.longitude)),
|
||||
(NodeTlvs.ALTITUDE, str(node_data.altitude)),
|
||||
(NodeTlvs.ICON, node_data.icon),
|
||||
(NodeTlvs.OPAQUE, node_data.opaque),
|
||||
],
|
||||
|
|
|
@ -328,7 +328,6 @@ class EmaneManager(ModelManager):
|
|||
nems = []
|
||||
with self._emane_node_lock:
|
||||
self.buildxml()
|
||||
self.initeventservice()
|
||||
self.starteventmonitor()
|
||||
|
||||
if self.numnems() > 0:
|
||||
|
@ -683,8 +682,9 @@ class EmaneManager(ModelManager):
|
|||
)
|
||||
return
|
||||
self.doeventloop = True
|
||||
self.eventmonthread = threading.Thread(target=self.eventmonitorloop)
|
||||
self.eventmonthread.daemon = True
|
||||
self.eventmonthread = threading.Thread(
|
||||
target=self.eventmonitorloop, daemon=True
|
||||
)
|
||||
self.eventmonthread.start()
|
||||
|
||||
def stopeventmonitor(self) -> None:
|
||||
|
@ -698,8 +698,6 @@ class EmaneManager(ModelManager):
|
|||
self.initeventservice(shutdown=True)
|
||||
|
||||
if self.eventmonthread is not None:
|
||||
# TODO: fix this
|
||||
self.eventmonthread._Thread__stop()
|
||||
self.eventmonthread.join()
|
||||
self.eventmonthread = None
|
||||
|
||||
|
@ -773,7 +771,7 @@ class EmaneManager(ModelManager):
|
|||
x = int(x)
|
||||
y = int(y)
|
||||
z = int(z)
|
||||
logging.info(
|
||||
logging.debug(
|
||||
"location event NEM %s (%s, %s, %s) -> (%s, %s, %s)",
|
||||
nemid,
|
||||
lat,
|
||||
|
|
|
@ -37,8 +37,8 @@ from core.emulator.emudata import (
|
|||
from core.emulator.enumerations import EventTypes, ExceptionLevels, LinkTypes, NodeTypes
|
||||
from core.emulator.sessionconfig import SessionConfig
|
||||
from core.errors import CoreError
|
||||
from core.location.corelocation import CoreLocation
|
||||
from core.location.event import EventLoop
|
||||
from core.location.geo import GeoLocation
|
||||
from core.location.mobility import BasicRangeModel, MobilityManager
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase
|
||||
from core.nodes.docker import DockerNode
|
||||
|
@ -146,7 +146,7 @@ class Session:
|
|||
self.distributed = DistributedController(self)
|
||||
|
||||
# initialize session feature helpers
|
||||
self.location = CoreLocation()
|
||||
self.location = GeoLocation()
|
||||
self.mobility = MobilityManager(session=self)
|
||||
self.services = CoreServices(session=self)
|
||||
self.emane = EmaneManager(session=self)
|
||||
|
@ -432,6 +432,7 @@ class Session:
|
|||
if node_two:
|
||||
node_two.lock.release()
|
||||
|
||||
self.sdt.add_link(node_one_id, node_two_id, is_wireless=False)
|
||||
return node_one_interface, node_two_interface
|
||||
|
||||
def delete_link(
|
||||
|
@ -540,6 +541,8 @@ class Session:
|
|||
if node_two:
|
||||
node_two.lock.release()
|
||||
|
||||
self.sdt.delete_link(node_one_id, node_two_id)
|
||||
|
||||
def update_link(
|
||||
self,
|
||||
node_one_id: int,
|
||||
|
@ -757,6 +760,7 @@ class Session:
|
|||
self.add_remove_control_interface(node=node, remove=False)
|
||||
self.services.boot_services(node)
|
||||
|
||||
self.sdt.add_node(node)
|
||||
return node
|
||||
|
||||
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 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
|
||||
"""
|
||||
# get node to update
|
||||
|
@ -778,6 +782,9 @@ class Session:
|
|||
node.canvas = options.canvas
|
||||
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:
|
||||
"""
|
||||
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
|
||||
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.
|
||||
|
||||
|
@ -820,6 +829,9 @@ class Session:
|
|||
id=node.id,
|
||||
x_position=node.position.x,
|
||||
y_position=node.position.y,
|
||||
latitude=lat,
|
||||
longitude=lon,
|
||||
altitude=alt,
|
||||
)
|
||||
self.broadcast_node(node_data)
|
||||
|
||||
|
@ -1402,6 +1414,7 @@ class Session:
|
|||
if node:
|
||||
node.shutdown()
|
||||
self.check_shutdown()
|
||||
self.sdt.delete_node(_id)
|
||||
|
||||
return node is not None
|
||||
|
||||
|
@ -1413,6 +1426,7 @@ class Session:
|
|||
funcs = []
|
||||
while self.nodes:
|
||||
_, node = self.nodes.popitem()
|
||||
self.sdt.delete_node(node.id)
|
||||
funcs.append((node.shutdown, [], {}))
|
||||
utils.threadpool(funcs)
|
||||
self.node_id_gen.id = 0
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import math
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import font, ttk
|
||||
|
||||
from core.gui import appconfig, themes
|
||||
from core.gui.coreclient import CoreClient
|
||||
|
@ -29,14 +30,28 @@ class Application(tk.Frame):
|
|||
self.statusbar = None
|
||||
self.validation = None
|
||||
|
||||
# fonts
|
||||
self.fonts_size = None
|
||||
self.icon_text_font = None
|
||||
self.edge_font = None
|
||||
|
||||
# setup
|
||||
self.guiconfig = appconfig.read()
|
||||
self.app_scale = self.guiconfig["scale"]
|
||||
self.setup_scaling()
|
||||
self.style = ttk.Style()
|
||||
self.setup_theme()
|
||||
self.core = CoreClient(self, proxy)
|
||||
self.setup_app()
|
||||
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):
|
||||
themes.load(self.style)
|
||||
|
@ -56,9 +71,11 @@ class Application(tk.Frame):
|
|||
def center(self):
|
||||
screen_width = self.master.winfo_screenwidth()
|
||||
screen_height = self.master.winfo_screenheight()
|
||||
x = int((screen_width / 2) - (WIDTH / 2))
|
||||
y = int((screen_height / 2) - (HEIGHT / 2))
|
||||
self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}")
|
||||
x = int((screen_width / 2) - (WIDTH * self.app_scale / 2))
|
||||
y = int((screen_height / 2) - (HEIGHT * self.app_scale / 2))
|
||||
self.master.geometry(
|
||||
f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}"
|
||||
)
|
||||
|
||||
def draw(self):
|
||||
self.master.option_add("*tearOff", tk.FALSE)
|
||||
|
|
|
@ -16,6 +16,7 @@ MOBILITY_PATH = HOME_PATH.joinpath("mobility")
|
|||
XMLS_PATH = HOME_PATH.joinpath("xmls")
|
||||
CONFIG_PATH = HOME_PATH.joinpath("gui.yaml")
|
||||
LOG_PATH = HOME_PATH.joinpath("gui.log")
|
||||
SCRIPT_PATH = HOME_PATH.joinpath("scripts")
|
||||
|
||||
# local paths
|
||||
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()
|
||||
|
||||
# configuration data
|
||||
TERMINALS = [
|
||||
"$TERM",
|
||||
"gnome-terminal --window --",
|
||||
"lxterminal -e",
|
||||
"konsole -e",
|
||||
"xterm -e",
|
||||
"aterm -e",
|
||||
"eterm -e",
|
||||
"rxvt -e",
|
||||
"xfce4-terminal -x",
|
||||
]
|
||||
TERMINALS = {
|
||||
"xterm": "xterm -e",
|
||||
"aterm": "aterm -e",
|
||||
"eterm": "eterm -e",
|
||||
"rxvt": "rxvt -e",
|
||||
"konsole": "konsole -e",
|
||||
"lxterminal": "lxterminal -e",
|
||||
"xfce4-terminal": "xfce4-terminal -x",
|
||||
"gnome-terminal": "gnome-terminal --window --",
|
||||
}
|
||||
EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||
|
||||
|
||||
|
@ -50,6 +50,14 @@ def copy_files(current_path, new_path):
|
|||
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():
|
||||
if HOME_PATH.exists():
|
||||
return
|
||||
|
@ -60,16 +68,14 @@ def check_directory():
|
|||
ICONS_PATH.mkdir()
|
||||
MOBILITY_PATH.mkdir()
|
||||
XMLS_PATH.mkdir()
|
||||
SCRIPT_PATH.mkdir()
|
||||
|
||||
copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
|
||||
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)
|
||||
copy_files(LOCAL_XMLS_PATH, XMLS_PATH)
|
||||
copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH)
|
||||
|
||||
if "TERM" in os.environ:
|
||||
terminal = TERMINALS[0]
|
||||
else:
|
||||
terminal = TERMINALS[1]
|
||||
terminal = find_terminal()
|
||||
if "EDITOR" in os.environ:
|
||||
editor = EDITORS[0]
|
||||
else:
|
||||
|
@ -96,6 +102,7 @@ def check_directory():
|
|||
"nodes": [],
|
||||
"recentfiles": [],
|
||||
"observers": [{"name": "hello", "cmd": "echo hello"}],
|
||||
"scale": 1.0,
|
||||
}
|
||||
save(config)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import json
|
|||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tkinter import messagebox
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
import grpc
|
||||
|
@ -38,17 +39,6 @@ OBSERVERS = {
|
|||
"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:
|
||||
def __init__(self, name: str, address: str, port: int):
|
||||
|
@ -68,7 +58,7 @@ class CoreClient:
|
|||
"""
|
||||
Create a CoreGrpc instance
|
||||
"""
|
||||
self.client = client.CoreGrpcClient(proxy=proxy)
|
||||
self._client = client.CoreGrpcClient(proxy=proxy)
|
||||
self.session_id = None
|
||||
self.node_ids = []
|
||||
self.app = app
|
||||
|
@ -112,6 +102,22 @@ class CoreClient:
|
|||
|
||||
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):
|
||||
# helpers
|
||||
self.interfaces_manager.reset()
|
||||
|
@ -131,12 +137,8 @@ class CoreClient:
|
|||
mobility_player.handle_close()
|
||||
self.mobility_players.clear()
|
||||
# clear streams
|
||||
if self.handling_throughputs:
|
||||
self.handling_throughputs.cancel()
|
||||
self.handling_throughputs = None
|
||||
if self.handling_events:
|
||||
self.handling_events.cancel()
|
||||
self.handling_events = None
|
||||
self.cancel_throughputs()
|
||||
self.cancel_events()
|
||||
|
||||
def set_observer(self, value: str):
|
||||
self.observer = value
|
||||
|
@ -227,8 +229,14 @@ class CoreClient:
|
|||
)
|
||||
|
||||
def cancel_throughputs(self):
|
||||
self.handling_throughputs.cancel()
|
||||
self.handling_throughputs = None
|
||||
if self.handling_throughputs:
|
||||
self.handling_throughputs.cancel()
|
||||
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):
|
||||
if event.session_id != self.session_id:
|
||||
|
@ -438,7 +446,7 @@ class CoreClient:
|
|||
master = parent_frame
|
||||
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
|
||||
"""
|
||||
|
@ -472,7 +480,7 @@ class CoreClient:
|
|||
x.node_type: set(x.services) for x in response.defaults
|
||||
}
|
||||
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()
|
||||
|
||||
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}
|
||||
else:
|
||||
emane_config = None
|
||||
|
||||
response = core_pb2.StartSessionResponse(result=False)
|
||||
try:
|
||||
response = self.client.start_session(
|
||||
|
@ -521,7 +528,6 @@ class CoreClient:
|
|||
logging.info(
|
||||
"start session(%s), result: %s", self.session_id, response.result
|
||||
)
|
||||
|
||||
if response.result:
|
||||
self.set_metadata()
|
||||
except grpc.RpcError as e:
|
||||
|
@ -573,11 +579,15 @@ class CoreClient:
|
|||
def launch_terminal(self, node_id: int):
|
||||
try:
|
||||
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)
|
||||
output = os.popen(f"echo {terminal}").read()[:-1]
|
||||
if output in DEFAULT_TERMS:
|
||||
terminal = DEFAULT_TERMS[output]
|
||||
cmd = f'{terminal} "{response.terminal}" &'
|
||||
cmd = f"{terminal} {response.terminal} &"
|
||||
logging.info("launching terminal %s", cmd)
|
||||
os.system(cmd)
|
||||
except grpc.RpcError as e:
|
||||
|
@ -620,6 +630,8 @@ class CoreClient:
|
|||
self,
|
||||
node_id: int,
|
||||
service_name: str,
|
||||
dirs: List[str],
|
||||
files: List[str],
|
||||
startups: List[str],
|
||||
validations: List[str],
|
||||
shutdowns: List[str],
|
||||
|
@ -628,14 +640,17 @@ class CoreClient:
|
|||
self.session_id,
|
||||
node_id,
|
||||
service_name,
|
||||
directories=dirs,
|
||||
files=files,
|
||||
startup=startups,
|
||||
validate=validations,
|
||||
shutdown=shutdowns,
|
||||
)
|
||||
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,
|
||||
node_id,
|
||||
files,
|
||||
startups,
|
||||
validations,
|
||||
shutdowns,
|
||||
|
@ -794,7 +809,7 @@ class CoreClient:
|
|||
image=image,
|
||||
emane=emane,
|
||||
)
|
||||
if NodeUtils.is_custom(model):
|
||||
if NodeUtils.is_custom(node_type, model):
|
||||
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
|
||||
node.services[:] = services
|
||||
logging.info(
|
||||
|
@ -933,6 +948,8 @@ class CoreClient:
|
|||
config_proto = core_pb2.ServiceConfig(
|
||||
node_id=node_id,
|
||||
service=name,
|
||||
directories=config.dirs,
|
||||
files=config.configs,
|
||||
startup=config.startup,
|
||||
validate=config.validate,
|
||||
shutdown=config.shutdown,
|
||||
|
@ -1064,3 +1081,9 @@ class CoreClient:
|
|||
|
||||
def service_been_modified(self, node_id: int) -> bool:
|
||||
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.dialog import Dialog
|
||||
from core.gui.graph import tags
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -19,7 +20,7 @@ class MarkerDialog(Dialog):
|
|||
def __init__(
|
||||
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.color = initcolor
|
||||
self.radius = MARKER_THICKNESS[0]
|
||||
|
@ -56,8 +57,7 @@ class MarkerDialog(Dialog):
|
|||
|
||||
def clear_marker(self):
|
||||
canvas = self.app.canvas
|
||||
for i in canvas.find_withtag("marker"):
|
||||
canvas.delete(i)
|
||||
canvas.delete(tags.MARKER)
|
||||
|
||||
def change_color(self, event: tk.Event):
|
||||
color_picker = ColorPickerDialog(self, self.app, self.color)
|
||||
|
|
|
@ -100,17 +100,17 @@ class MobilityPlayerDialog(Dialog):
|
|||
for i in range(3):
|
||||
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.image = image
|
||||
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.image = image
|
||||
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.image = image
|
||||
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from functools import partial
|
||||
from tkinter import ttk
|
||||
from tkinter import messagebox, ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import netaddr
|
||||
|
||||
from core.gui import nodeutils
|
||||
from core.gui.appconfig import ICONS_PATH
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
|
@ -18,6 +20,56 @@ if TYPE_CHECKING:
|
|||
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):
|
||||
logging.info("mac auto clicked")
|
||||
if is_auto.get():
|
||||
|
@ -188,12 +240,17 @@ class NodeConfigDialog(Dialog):
|
|||
|
||||
label = ttk.Label(tab, text="MAC")
|
||||
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.var = is_auto
|
||||
checkbutton.grid(row=row, column=1, padx=PADX)
|
||||
mac = tk.StringVar(value=interface.mac)
|
||||
entry = ttk.Entry(tab, textvariable=mac, state=tk.DISABLED)
|
||||
entry = ttk.Entry(tab, textvariable=mac, state=state)
|
||||
entry.grid(row=row, column=2, sticky="ew")
|
||||
func = partial(mac_auto, is_auto, entry)
|
||||
checkbutton.config(command=func)
|
||||
|
@ -201,17 +258,21 @@ class NodeConfigDialog(Dialog):
|
|||
|
||||
label = ttk.Label(tab, text="IPv4")
|
||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||
ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}")
|
||||
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.bind("<FocusOut>", self.app.validation.ip_focus_out)
|
||||
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
||||
row += 1
|
||||
|
||||
label = ttk.Label(tab, text="IPv6")
|
||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||
ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}")
|
||||
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.bind("<FocusOut>", self.app.validation.ip_focus_out)
|
||||
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
||||
|
||||
self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6)
|
||||
|
@ -240,6 +301,8 @@ class NodeConfigDialog(Dialog):
|
|||
self.image_file = file_path
|
||||
|
||||
def config_apply(self):
|
||||
error = False
|
||||
|
||||
# update core node
|
||||
self.node.name = self.name.get()
|
||||
if NodeUtils.is_image_node(self.node.type):
|
||||
|
@ -255,9 +318,54 @@ class NodeConfigDialog(Dialog):
|
|||
# update canvas node
|
||||
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
|
||||
self.canvas_node.redraw()
|
||||
self.destroy()
|
||||
if not error:
|
||||
self.canvas_node.redraw()
|
||||
self.destroy()
|
||||
|
||||
def interface_select(self, event: tk.Event):
|
||||
listbox = event.widget
|
||||
|
|
|
@ -128,7 +128,9 @@ class NodeConfigServiceDialog(Dialog):
|
|||
dialog.show()
|
||||
else:
|
||||
messagebox.showinfo(
|
||||
"Node service configuration", "Select a service to configure"
|
||||
"Config Service Configuration",
|
||||
"Select a service to configure",
|
||||
parent=self,
|
||||
)
|
||||
|
||||
def click_save(self):
|
||||
|
|
|
@ -38,7 +38,7 @@ class NodeServiceDialog(Dialog):
|
|||
if len(services) == 0:
|
||||
# not custom node type and node's services haven't been modified before
|
||||
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):
|
||||
services = set(self.app.core.default_services[model])
|
||||
# services of default type nodes were modified to be empty
|
||||
|
@ -148,11 +148,12 @@ class NodeServiceDialog(Dialog):
|
|||
dialog.destroy()
|
||||
else:
|
||||
messagebox.showinfo(
|
||||
"Node service configuration", "Select a service to configure"
|
||||
"Service Configuration", "Select a service to configure", parent=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 (
|
||||
self.canvas_node.core_node.model not in self.app.core.default_services
|
||||
or self.current_services
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import logging
|
||||
import math
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.gui import appconfig
|
||||
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:
|
||||
from core.gui.app import Application
|
||||
|
||||
SCALE_INTERVAL = 0.01
|
||||
|
||||
|
||||
class PreferencesDialog(Dialog):
|
||||
def __init__(self, master: "Application", app: "Application"):
|
||||
super().__init__(master, app, "Preferences", modal=True)
|
||||
self.gui_scale = tk.DoubleVar(value=self.app.app_scale)
|
||||
preferences = self.app.guiconfig["preferences"]
|
||||
self.editor = tk.StringVar(value=preferences["editor"])
|
||||
self.theme = tk.StringVar(value=preferences["theme"])
|
||||
|
@ -51,12 +56,8 @@ class PreferencesDialog(Dialog):
|
|||
|
||||
label = ttk.Label(frame, text="Terminal")
|
||||
label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||
combobox = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.terminal,
|
||||
values=appconfig.TERMINALS,
|
||||
state="readonly",
|
||||
)
|
||||
terminals = sorted(appconfig.TERMINALS.values())
|
||||
combobox = ttk.Combobox(frame, textvariable=self.terminal, values=terminals)
|
||||
combobox.grid(row=2, column=1, sticky="ew")
|
||||
|
||||
label = ttk.Label(frame, text="3D GUI")
|
||||
|
@ -64,6 +65,33 @@ class PreferencesDialog(Dialog):
|
|||
entry = ttk.Entry(frame, textvariable=self.gui3d)
|
||||
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):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
|
@ -87,5 +115,41 @@ class PreferencesDialog(Dialog):
|
|||
preferences["editor"] = self.editor.get()
|
||||
preferences["gui3d"] = self.gui3d.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.scale_adjust()
|
||||
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 @@
|
|||
"""
|
||||
Service configuration dialog
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import filedialog, ttk
|
||||
from typing import TYPE_CHECKING, Any, List
|
||||
|
||||
import grpc
|
||||
|
@ -48,12 +47,18 @@ class ServiceConfigDialog(Dialog):
|
|||
self.validation_mode = None
|
||||
self.validation_time = None
|
||||
self.validation_period = None
|
||||
self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16)
|
||||
self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16)
|
||||
self.directory_entry = None
|
||||
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.metadata_entry = None
|
||||
self.filename_combobox = None
|
||||
self.dir_list = None
|
||||
self.startup_commands_listbox = None
|
||||
self.shutdown_commands_listbox = None
|
||||
self.validate_commands_listbox = None
|
||||
|
@ -62,6 +67,7 @@ class ServiceConfigDialog(Dialog):
|
|||
self.service_file_data = None
|
||||
self.validation_period_entry = None
|
||||
self.original_service_files = {}
|
||||
self.default_config = None
|
||||
self.temp_service_files = {}
|
||||
self.modified_files = set()
|
||||
|
||||
|
@ -71,7 +77,7 @@ class ServiceConfigDialog(Dialog):
|
|||
if not self.has_error:
|
||||
self.draw()
|
||||
|
||||
def load(self) -> bool:
|
||||
def load(self):
|
||||
try:
|
||||
self.app.core.create_nodes_and_links()
|
||||
default_config = self.app.core.get_node_service(
|
||||
|
@ -80,15 +86,14 @@ class ServiceConfigDialog(Dialog):
|
|||
self.default_startup = default_config.startup[:]
|
||||
self.default_validate = default_config.validate[:]
|
||||
self.default_shutdown = default_config.shutdown[:]
|
||||
custom_configs = self.service_configs
|
||||
if (
|
||||
self.node_id in custom_configs
|
||||
and self.service_name in custom_configs[self.node_id]
|
||||
):
|
||||
service_config = custom_configs[self.node_id][self.service_name]
|
||||
else:
|
||||
service_config = default_config
|
||||
|
||||
self.default_directories = default_config.dirs[:]
|
||||
custom_service_config = self.service_configs.get(self.node_id, {}).get(
|
||||
self.service_name, None
|
||||
)
|
||||
self.default_config = default_config
|
||||
service_config = (
|
||||
custom_service_config if custom_service_config else default_config
|
||||
)
|
||||
self.dependencies = service_config.dependencies[:]
|
||||
self.executables = service_config.executables[:]
|
||||
self.metadata = service_config.meta
|
||||
|
@ -98,20 +103,19 @@ class ServiceConfigDialog(Dialog):
|
|||
self.shutdown_commands = service_config.shutdown[:]
|
||||
self.validation_mode = service_config.validation_mode
|
||||
self.validation_time = service_config.validation_timer
|
||||
self.temp_directories = service_config.dirs[:]
|
||||
self.original_service_files = {
|
||||
x: self.app.core.get_node_service_file(
|
||||
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)
|
||||
file_configs = self.file_configs
|
||||
if (
|
||||
self.node_id in file_configs
|
||||
and self.service_name in file_configs[self.node_id]
|
||||
):
|
||||
for file, data in file_configs[self.node_id][self.service_name].items():
|
||||
self.temp_service_files[file] = data
|
||||
file_config = self.file_configs.get(self.node_id, {}).get(
|
||||
self.service_name, {}
|
||||
)
|
||||
for file, data in file_config.items():
|
||||
self.temp_service_files[file] = data
|
||||
except grpc.RpcError as e:
|
||||
self.has_error = True
|
||||
show_grpc_error(e, self.master, self.app)
|
||||
|
@ -155,18 +159,18 @@ class ServiceConfigDialog(Dialog):
|
|||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="File Name")
|
||||
label.grid(row=0, column=0, padx=PADX, sticky="w")
|
||||
self.filename_combobox = ttk.Combobox(
|
||||
frame, values=self.filenames, state="readonly"
|
||||
)
|
||||
self.filename_combobox = ttk.Combobox(frame, values=self.filenames)
|
||||
self.filename_combobox.bind(
|
||||
"<<ComboboxSelected>>", self.display_service_file_data
|
||||
)
|
||||
self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, image=self.documentnew_img, state="disabled")
|
||||
button.bind("<Button-1>", self.add_filename)
|
||||
button = ttk.Button(
|
||||
frame, image=self.documentnew_img, command=self.add_filename
|
||||
)
|
||||
button.grid(row=0, column=2, padx=PADX)
|
||||
button = ttk.Button(frame, image=self.editdelete_img, state="disabled")
|
||||
button.bind("<Button-1>", self.delete_filename)
|
||||
button = ttk.Button(
|
||||
frame, image=self.editdelete_img, command=self.delete_filename
|
||||
)
|
||||
button.grid(row=0, column=3)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
|
@ -229,7 +233,30 @@ class ServiceConfigDialog(Dialog):
|
|||
tab,
|
||||
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):
|
||||
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.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
def add_filename(self, event: tk.Event):
|
||||
# not worry about it for now
|
||||
return
|
||||
frame_contains_button = event.widget.master
|
||||
combobox = frame_contains_button.grid_slaves(row=0, column=1)[0]
|
||||
filename = combobox.get()
|
||||
if filename not in combobox["values"]:
|
||||
combobox["values"] += (filename,)
|
||||
def add_filename(self):
|
||||
filename = self.filename_combobox.get()
|
||||
if filename not in self.filename_combobox["values"]:
|
||||
self.filename_combobox["values"] += (filename,)
|
||||
self.filename_combobox.set(filename)
|
||||
self.temp_service_files[filename] = self.service_file_data.text.get(
|
||||
1.0, "end"
|
||||
)
|
||||
else:
|
||||
logging.debug("file already existed")
|
||||
|
||||
def delete_filename(self, event: tk.Event):
|
||||
# not worry about it for now
|
||||
return
|
||||
frame_comntains_button = event.widget.master
|
||||
combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0]
|
||||
filename = combobox.get()
|
||||
if filename in combobox["values"]:
|
||||
combobox["values"] = tuple([x for x in combobox["values"] if x != filename])
|
||||
combobox.set("")
|
||||
def delete_filename(self):
|
||||
cbb = self.filename_combobox
|
||||
filename = cbb.get()
|
||||
if filename in cbb["values"]:
|
||||
cbb["values"] = tuple([x for x in cbb["values"] if x != filename])
|
||||
cbb.set("")
|
||||
self.service_file_data.text.delete(1.0, "end")
|
||||
self.temp_service_files.pop(filename, None)
|
||||
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
|
||||
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()
|
||||
|
@ -388,7 +419,8 @@ class ServiceConfigDialog(Dialog):
|
|||
return
|
||||
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
|
||||
current_selection = listbox.curselection()
|
||||
if len(current_selection) > 0:
|
||||
|
@ -399,7 +431,8 @@ class ServiceConfigDialog(Dialog):
|
|||
entry.delete(0, "end")
|
||||
entry.insert(0, cmd)
|
||||
|
||||
def delete_command(self, event: tk.Event):
|
||||
@classmethod
|
||||
def delete_command(cls, event: tk.Event):
|
||||
button = event.widget
|
||||
frame_contains_button = button.master
|
||||
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)
|
||||
|
||||
def click_apply(self):
|
||||
current_listbox = self.master.current.listbox
|
||||
if not self.is_custom_service_config() and not self.is_custom_service_file():
|
||||
if self.node_id in self.service_configs:
|
||||
self.service_configs[self.node_id].pop(self.service_name, None)
|
||||
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
|
||||
if (
|
||||
not self.is_custom_command()
|
||||
and not self.is_custom_service_file()
|
||||
and not self.has_new_files()
|
||||
and not self.is_custom_directory()
|
||||
):
|
||||
self.service_configs.get(self.node_id, {}).pop(self.service_name, None)
|
||||
self.current_service_color("")
|
||||
self.destroy()
|
||||
return
|
||||
|
||||
try:
|
||||
if self.is_custom_service_config():
|
||||
startup_commands = self.startup_commands_listbox.get(0, "end")
|
||||
shutdown_commands = self.shutdown_commands_listbox.get(0, "end")
|
||||
validate_commands = self.validate_commands_listbox.get(0, "end")
|
||||
if (
|
||||
self.is_custom_command()
|
||||
or self.has_new_files()
|
||||
or self.is_custom_directory()
|
||||
):
|
||||
startup, validate, shutdown = self.get_commands()
|
||||
config = self.core.set_node_service(
|
||||
self.node_id,
|
||||
self.service_name,
|
||||
startups=startup_commands,
|
||||
validations=validate_commands,
|
||||
shutdowns=shutdown_commands,
|
||||
dirs=self.temp_directories,
|
||||
files=list(self.filename_combobox["values"]),
|
||||
startups=startup,
|
||||
validations=validate,
|
||||
shutdowns=shutdown,
|
||||
)
|
||||
if self.node_id not in self.service_configs:
|
||||
self.service_configs[self.node_id] = {}
|
||||
self.service_configs[self.node_id][self.service_name] = config
|
||||
|
||||
for file in self.modified_files:
|
||||
if self.node_id not in self.file_configs:
|
||||
self.file_configs[self.node_id] = {}
|
||||
|
@ -442,53 +481,67 @@ class ServiceConfigDialog(Dialog):
|
|||
self.file_configs[self.node_id][self.service_name][
|
||||
file
|
||||
] = self.temp_service_files[file]
|
||||
|
||||
self.app.core.set_node_service_file(
|
||||
self.node_id, self.service_name, file, self.temp_service_files[file]
|
||||
)
|
||||
all_current = current_listbox.get(0, tk.END)
|
||||
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
|
||||
self.current_service_color("green")
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e, self.top, self.app)
|
||||
self.destroy()
|
||||
|
||||
def display_service_file_data(self, event: tk.Event):
|
||||
combobox = event.widget
|
||||
filename = combobox.get()
|
||||
filename = self.filename_combobox.get()
|
||||
self.service_file_data.text.delete(1.0, "end")
|
||||
self.service_file_data.text.insert("end", self.temp_service_files[filename])
|
||||
|
||||
def update_temp_service_file_data(self, event: tk.Event):
|
||||
scrolledtext = event.widget
|
||||
filename = self.filename_combobox.get()
|
||||
self.temp_service_files[filename] = scrolledtext.get(1.0, "end")
|
||||
if self.temp_service_files[filename] != self.original_service_files[filename]:
|
||||
self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end")
|
||||
if self.temp_service_files[filename] != self.original_service_files.get(
|
||||
filename, ""
|
||||
):
|
||||
self.modified_files.add(filename)
|
||||
else:
|
||||
self.modified_files.discard(filename)
|
||||
|
||||
def is_custom_service_config(self):
|
||||
startup_commands = self.startup_commands_listbox.get(0, "end")
|
||||
shutdown_commands = self.shutdown_commands_listbox.get(0, "end")
|
||||
validate_commands = self.validate_commands_listbox.get(0, "end")
|
||||
def is_custom_command(self):
|
||||
startup, validate, shutdown = self.get_commands()
|
||||
return (
|
||||
set(self.default_startup) != set(startup_commands)
|
||||
or set(self.default_validate) != set(validate_commands)
|
||||
or set(self.default_shutdown) != set(shutdown_commands)
|
||||
set(self.default_startup) != set(startup)
|
||||
or set(self.default_validate) != set(validate)
|
||||
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):
|
||||
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):
|
||||
if self.node_id in self.service_configs:
|
||||
self.service_configs[self.node_id].pop(self.service_name, None)
|
||||
if self.node_id in self.file_configs:
|
||||
self.file_configs[self.node_id].pop(self.service_name, None)
|
||||
"""
|
||||
clears out any custom configuration permanently
|
||||
"""
|
||||
# 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)
|
||||
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.insert("end", self.temp_service_files[filename])
|
||||
if len(files) > 0:
|
||||
filename = files[0]
|
||||
self.filename_combobox.set(filename)
|
||||
self.service_file_data.text.insert("end", self.temp_service_files[filename])
|
||||
|
||||
# reset commands
|
||||
self.startup_commands_listbox.delete(0, tk.END)
|
||||
self.validate_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:
|
||||
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):
|
||||
dialog = CopyServiceConfigDialog(self, self.app, self.node_id)
|
||||
dialog.show()
|
||||
|
||||
@classmethod
|
||||
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:
|
||||
commands.append(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 typing import TYPE_CHECKING
|
||||
|
||||
|
@ -16,6 +12,9 @@ if TYPE_CHECKING:
|
|||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
RANGE_COLOR = "#009933"
|
||||
RANGE_WIDTH = 3
|
||||
|
||||
|
||||
class WlanConfigDialog(Dialog):
|
||||
def __init__(
|
||||
|
@ -27,15 +26,29 @@ class WlanConfigDialog(Dialog):
|
|||
self.canvas_node = canvas_node
|
||||
self.node = canvas_node.core_node
|
||||
self.config_frame = None
|
||||
self.range_entry = None
|
||||
self.has_error = False
|
||||
self.canvas = app.canvas
|
||||
self.ranges = {}
|
||||
self.positive_int = self.app.master.register(self.validate_and_update)
|
||||
try:
|
||||
self.config = self.app.core.get_wlan_config(self.node.id)
|
||||
self.init_draw_range()
|
||||
self.draw()
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e, self.app, self.app)
|
||||
self.has_error = True
|
||||
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):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
@ -43,6 +56,7 @@ class WlanConfigDialog(Dialog):
|
|||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_apply_buttons()
|
||||
self.top.bind("<Destroy>", self.remove_ranges)
|
||||
|
||||
def draw_apply_buttons(self):
|
||||
"""
|
||||
|
@ -53,6 +67,11 @@ class WlanConfigDialog(Dialog):
|
|||
for i in range(2):
|
||||
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.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
|
||||
|
@ -68,4 +87,35 @@ class WlanConfigDialog(Dialog):
|
|||
if self.app.core.is_runtime():
|
||||
session_id = self.app.core.session_id
|
||||
self.app.core.client.set_wlan_config(session_id, self.node.id, config)
|
||||
self.remove_ranges()
|
||||
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 typing import TYPE_CHECKING
|
||||
|
||||
import grpc
|
||||
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import grpc
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class ErrorDialog(Dialog):
|
||||
def __init__(self, master, app: "Application", title: str, details: str):
|
||||
super().__init__(master, app, title, modal=True)
|
||||
self.error_message = None
|
||||
def __init__(self, master, app: "Application", title: str, details: str) -> None:
|
||||
super().__init__(master, app, "CORE Exception", modal=True)
|
||||
self.title = title
|
||||
self.details = details
|
||||
self.error_message = None
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
def draw(self) -> None:
|
||||
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)
|
||||
label = ttk.Label(self.top, image=image)
|
||||
label = ttk.Label(frame, 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.text.insert("1.0", self.details)
|
||||
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 = " ".join(title)
|
||||
title = f"GRPC {title}"
|
||||
dialog = ErrorDialog(master, app, title, e.details())
|
||||
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 tkinter as tk
|
||||
from tkinter.font import Font
|
||||
from typing import TYPE_CHECKING, Any, Tuple
|
||||
|
||||
from core.gui import themes
|
||||
|
@ -14,6 +13,8 @@ if TYPE_CHECKING:
|
|||
TEXT_DISTANCE = 0.30
|
||||
EDGE_WIDTH = 3
|
||||
EDGE_COLOR = "#ff0000"
|
||||
WIRELESS_WIDTH = 1.5
|
||||
WIRELESS_COLOR = "#009933"
|
||||
|
||||
|
||||
class CanvasWirelessEdge:
|
||||
|
@ -31,7 +32,10 @@ class CanvasWirelessEdge:
|
|||
self.dst = dst
|
||||
self.canvas = canvas
|
||||
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):
|
||||
|
@ -61,13 +65,18 @@ class CanvasEdge:
|
|||
self.dst_interface = None
|
||||
self.canvas = canvas
|
||||
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_dst = None
|
||||
self.text_middle = None
|
||||
self.token = None
|
||||
self.font = Font(size=8)
|
||||
self.link = None
|
||||
self.asymmetric_link = None
|
||||
self.throughput = None
|
||||
|
@ -98,26 +107,32 @@ class CanvasEdge:
|
|||
y = (y1 + y2) / 2
|
||||
return x, y
|
||||
|
||||
def draw_labels(self):
|
||||
x1, y1, x2, y2 = self.get_coordinates()
|
||||
def create_labels(self):
|
||||
label_one = None
|
||||
if self.link.HasField("interface_one"):
|
||||
label_one = (
|
||||
f"{self.link.interface_one.ip4}/{self.link.interface_one.ip4mask}\n"
|
||||
f"{self.link.interface_one.ip6}/{self.link.interface_one.ip6mask}\n"
|
||||
)
|
||||
label_one = self.create_label(self.link.interface_one)
|
||||
label_two = None
|
||||
if self.link.HasField("interface_two"):
|
||||
label_two = (
|
||||
f"{self.link.interface_two.ip4}/{self.link.interface_two.ip4mask}\n"
|
||||
f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n"
|
||||
)
|
||||
label_two = self.create_label(self.link.interface_two)
|
||||
return label_one, label_two
|
||||
|
||||
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(
|
||||
x1,
|
||||
y1,
|
||||
text=label_one,
|
||||
justify=tk.CENTER,
|
||||
font=self.font,
|
||||
font=self.canvas.app.edge_font,
|
||||
tags=tags.LINK_INFO,
|
||||
)
|
||||
self.text_dst = self.canvas.create_text(
|
||||
|
@ -125,10 +140,15 @@ class CanvasEdge:
|
|||
y2,
|
||||
text=label_two,
|
||||
justify=tk.CENTER,
|
||||
font=self.font,
|
||||
font=self.canvas.app.edge_font,
|
||||
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):
|
||||
"""
|
||||
Move edge labels based on current position.
|
||||
|
@ -146,7 +166,7 @@ class CanvasEdge:
|
|||
if self.text_middle is None:
|
||||
x, y = self.get_midpoint()
|
||||
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:
|
||||
self.canvas.itemconfig(self.text_middle, text=value)
|
||||
|
@ -177,6 +197,17 @@ class CanvasEdge:
|
|||
dst_node_type = dst_node.core_node.type
|
||||
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
|
||||
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
|
||||
|
||||
# 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
|
||||
|
||||
def check_wireless(self):
|
||||
|
|
|
@ -7,12 +7,12 @@ from PIL import Image, ImageTk
|
|||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.shapemod import ShapeDialog
|
||||
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.node import CanvasNode
|
||||
from core.gui.graph.shape import Shape
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -42,6 +42,10 @@ class CanvasGraph(tk.Canvas):
|
|||
self.edges = {}
|
||||
self.shapes = {}
|
||||
self.wireless_edges = {}
|
||||
|
||||
# map wireless/EMANE node to the set of MDRs connected to that node
|
||||
self.wireless_network = {}
|
||||
|
||||
self.drawing_edge = None
|
||||
self.grid = None
|
||||
self.shape_drawing = False
|
||||
|
@ -113,6 +117,7 @@ class CanvasGraph(tk.Canvas):
|
|||
self.edges.clear()
|
||||
self.shapes.clear()
|
||||
self.wireless_edges.clear()
|
||||
self.wireless_network.clear()
|
||||
self.drawing_edge = None
|
||||
self.draw_session(session)
|
||||
|
||||
|
@ -220,10 +225,14 @@ class CanvasGraph(tk.Canvas):
|
|||
# peer to peer node is not drawn on the GUI
|
||||
if NodeUtils.is_ignore_node(core_node.type):
|
||||
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 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
|
||||
y = core_node.position.y
|
||||
node = CanvasNode(self.master, x, y, core_node, image)
|
||||
|
@ -525,7 +534,7 @@ class CanvasGraph(tk.Canvas):
|
|||
y + r,
|
||||
fill=self.app.toolbar.marker_tool.color,
|
||||
outline="",
|
||||
tags="marker",
|
||||
tags=tags.MARKER,
|
||||
)
|
||||
return
|
||||
if selected is None:
|
||||
|
@ -647,8 +656,11 @@ class CanvasGraph(tk.Canvas):
|
|||
delete selected nodes and any data that relates to it
|
||||
"""
|
||||
logging.debug("press delete key")
|
||||
nodes = self.delete_selection_objects()
|
||||
self.core.delete_graph_nodes(nodes)
|
||||
if not self.app.core.is_runtime():
|
||||
nodes = self.delete_selection_objects()
|
||||
self.core.delete_graph_nodes(nodes)
|
||||
else:
|
||||
logging.info("node deletion is disabled during runtime state")
|
||||
|
||||
def double_click(self, event: tk.Event):
|
||||
selected = self.get_selected(event)
|
||||
|
@ -663,6 +675,14 @@ class CanvasGraph(tk.Canvas):
|
|||
core_node = self.core.create_node(
|
||||
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)
|
||||
self.core.canvas_nodes[core_node.id] = node
|
||||
self.nodes[node.id] = node
|
||||
|
@ -833,11 +853,17 @@ class CanvasGraph(tk.Canvas):
|
|||
self.core.create_link(edge, source, dest)
|
||||
|
||||
def copy(self):
|
||||
if self.app.core.is_runtime():
|
||||
logging.info("copy is disabled during runtime state")
|
||||
return
|
||||
if self.selection:
|
||||
logging.debug("to copy %s nodes", len(self.selection))
|
||||
self.to_copy = self.selection.keys()
|
||||
|
||||
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
|
||||
copy_map = {}
|
||||
# the edges that will be copy over
|
||||
|
@ -911,3 +937,28 @@ class CanvasGraph(tk.Canvas):
|
|||
width=self.itemcget(edge.id, "width"),
|
||||
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 tkinter as tk
|
||||
from tkinter import font
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import grpc
|
||||
|
@ -17,7 +16,8 @@ from core.gui.dialogs.wlanconfig import WlanConfigDialog
|
|||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.graph import tags
|
||||
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:
|
||||
from core.gui.app import Application
|
||||
|
@ -42,21 +42,21 @@ class CanvasNode:
|
|||
self.id = self.canvas.create_image(
|
||||
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
|
||||
)
|
||||
text_font = font.Font(family="TkIconFont", size=12)
|
||||
label_y = self._get_label_y()
|
||||
self.text_id = self.canvas.create_text(
|
||||
x,
|
||||
label_y,
|
||||
text=self.core_node.name,
|
||||
tags=tags.NODE_NAME,
|
||||
font=text_font,
|
||||
font=self.app.icon_text_font,
|
||||
fill="#0000CD",
|
||||
)
|
||||
self.tooltip = CanvasTooltip(self.canvas)
|
||||
self.edges = set()
|
||||
self.interfaces = []
|
||||
self.wireless_edges = set()
|
||||
self.antennae = []
|
||||
self.antennas = []
|
||||
self.antenna_images = {}
|
||||
self.setup_bindings()
|
||||
|
||||
def setup_bindings(self):
|
||||
|
@ -72,42 +72,54 @@ class CanvasNode:
|
|||
|
||||
def add_antenna(self):
|
||||
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(
|
||||
x - 16 + offset,
|
||||
y - 23,
|
||||
y - int(23 * self.app.app_scale),
|
||||
anchor=tk.CENTER,
|
||||
image=NodeUtils.ANTENNA_ICON,
|
||||
image=img,
|
||||
tags=tags.ANTENNA,
|
||||
)
|
||||
self.antennae.append(antenna_id)
|
||||
self.antennas.append(antenna_id)
|
||||
self.antenna_images[antenna_id] = img
|
||||
|
||||
def delete_antenna(self):
|
||||
"""
|
||||
delete one antenna
|
||||
"""
|
||||
logging.debug("Delete an antenna on %s", self.core_node.name)
|
||||
if self.antennae:
|
||||
antenna_id = self.antennae.pop()
|
||||
if self.antennas:
|
||||
antenna_id = self.antennas.pop()
|
||||
self.canvas.delete(antenna_id)
|
||||
self.antenna_images.pop(antenna_id, None)
|
||||
|
||||
def delete_antennas(self):
|
||||
"""
|
||||
delete all antennas
|
||||
"""
|
||||
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.antennae.clear()
|
||||
self.antennas.clear()
|
||||
self.antenna_images.clear()
|
||||
|
||||
def redraw(self):
|
||||
self.canvas.itemconfig(self.id, image=self.image)
|
||||
self.canvas.itemconfig(self.text_id, text=self.core_node.name)
|
||||
for edge in self.edges:
|
||||
edge.redraw()
|
||||
|
||||
def _get_label_y(self):
|
||||
image_box = self.canvas.bbox(self.id)
|
||||
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):
|
||||
x, y = self.canvas.get_scaled_coords(x, y)
|
||||
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)
|
||||
|
||||
# move antennae
|
||||
for antenna_id in self.antennae:
|
||||
for antenna_id in self.antennas:
|
||||
self.canvas.move(antenna_id, x_offset, y_offset)
|
||||
|
||||
# move edges
|
||||
|
@ -295,3 +307,17 @@ class CanvasNode:
|
|||
if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr":
|
||||
self.canvas.create_edge(self, self.canvas.nodes[canvas_nid])
|
||||
self.canvas.clear_selection()
|
||||
|
||||
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"
|
||||
SELECTION = "selectednodes"
|
||||
THROUGHPUT = "throughput"
|
||||
MARKER = "marker"
|
||||
ABOVE_WALLPAPER_TAGS = [
|
||||
GRIDLINE,
|
||||
SHAPE,
|
||||
|
@ -33,4 +34,5 @@ COMPONENT_TAGS = [
|
|||
SELECTION,
|
||||
SHAPE,
|
||||
SHAPE_TEXT,
|
||||
MARKER,
|
||||
]
|
||||
|
|
|
@ -3,6 +3,7 @@ from tkinter import messagebox
|
|||
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.appconfig import LOCAL_ICONS_PATH
|
||||
|
||||
|
||||
|
@ -90,3 +91,25 @@ class ImageEnum(Enum):
|
|||
SHUTDOWN = "shutdown"
|
||||
CANCEL = "cancel"
|
||||
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:
|
||||
self.add_recent_file_to_gui_config(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):
|
||||
init_dir = self.app.core.xml_dir
|
||||
|
@ -192,3 +193,8 @@ class MenuAction:
|
|||
logging.error("unexpected number of recent files")
|
||||
self.app.save_config()
|
||||
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
|
||||
from core.gui.coreclient import OBSERVERS
|
||||
from core.gui.dialogs.executepython import ExecutePythonDialog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -25,6 +26,7 @@ class Menubar(tk.Menu):
|
|||
self.app = app
|
||||
self.menuaction = action.MenuAction(app, master)
|
||||
self.recent_menu = None
|
||||
self.edit_menu = None
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
|
@ -48,7 +50,7 @@ class Menubar(tk.Menu):
|
|||
menu.add_command(
|
||||
label="New Session",
|
||||
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())
|
||||
menu.add_command(
|
||||
|
@ -56,6 +58,7 @@ class Menubar(tk.Menu):
|
|||
)
|
||||
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 As", command=self.menuaction.file_save_as_xml)
|
||||
menu.add_command(label="Reload", underline=0, state=tk.DISABLED)
|
||||
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_separator()
|
||||
menu.add_command(label="Export Python script...", state=tk.DISABLED)
|
||||
menu.add_command(label="Execute XML or Python script...", state=tk.DISABLED)
|
||||
menu.add_command(label="Execute Python script...", command=self.execute_python)
|
||||
menu.add_command(
|
||||
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-v>", self.menuaction.paste)
|
||||
self.edit_menu = menu
|
||||
|
||||
def draw_canvas_menu(self):
|
||||
"""
|
||||
|
@ -439,3 +443,19 @@ class Menubar(tk.Menu):
|
|||
self.app.core.save_xml(xml_file)
|
||||
else:
|
||||
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 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:
|
||||
from core.api.grpc import core_pb2
|
||||
|
@ -96,32 +96,35 @@ class NodeUtils:
|
|||
node_type: NodeType,
|
||||
model: str,
|
||||
gui_config: Dict[str, List[Dict[str, str]]],
|
||||
scale=1.0,
|
||||
) -> "ImageTk.PhotoImage":
|
||||
if model == "":
|
||||
model = None
|
||||
try:
|
||||
image = cls.NODE_ICONS[(node_type, model)]
|
||||
return image
|
||||
except KeyError:
|
||||
|
||||
image_enum = TypeToImage.get(node_type, model)
|
||||
if image_enum:
|
||||
return Images.get(image_enum, int(ICON_SIZE * scale))
|
||||
else:
|
||||
image_stem = cls.get_image_file(gui_config, model)
|
||||
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
|
||||
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":
|
||||
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:
|
||||
try:
|
||||
image = Images.create(core_node.icon, ICON_SIZE)
|
||||
image = Images.create(core_node.icon, int(ICON_SIZE * scale))
|
||||
except OSError:
|
||||
logging.error("invalid icon: %s", core_node.icon)
|
||||
return image
|
||||
|
||||
@classmethod
|
||||
def is_custom(cls, model: str) -> bool:
|
||||
return model not in cls.NODE_MODELS
|
||||
def is_custom(cls, node_type: NodeType, model: str) -> bool:
|
||||
return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS
|
||||
|
||||
@classmethod
|
||||
def get_custom_node_services(
|
||||
|
|
|
@ -29,7 +29,7 @@ class StatusBar(ttk.Frame):
|
|||
|
||||
def draw(self):
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.columnconfigure(1, weight=7)
|
||||
self.columnconfigure(1, weight=5)
|
||||
self.columnconfigure(2, weight=1)
|
||||
self.columnconfigure(3, weight=1)
|
||||
self.columnconfigure(4, weight=1)
|
||||
|
|
|
@ -2,6 +2,8 @@ import logging
|
|||
import threading
|
||||
from typing import Any, Callable
|
||||
|
||||
from core.gui.errors import show_grpc_response_exceptions
|
||||
|
||||
|
||||
class BackgroundTask:
|
||||
def __init__(self, master: Any, task: Callable, callback: Callable = None, args=()):
|
||||
|
@ -19,6 +21,19 @@ class BackgroundTask:
|
|||
def run(self):
|
||||
result = self.task(*self.args)
|
||||
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 result is None:
|
||||
args = ()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import font, ttk
|
||||
|
||||
THEME_DARK = "black"
|
||||
PADX = (0, 5)
|
||||
|
@ -176,25 +176,35 @@ def style_listbox(widget: tk.Widget):
|
|||
|
||||
def theme_change(event: tk.Event):
|
||||
style = ttk.Style()
|
||||
style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal"))
|
||||
style.configure(Styles.picker_button, font="TkSmallCaptionFont")
|
||||
style.configure(
|
||||
Styles.green_alert,
|
||||
background="green",
|
||||
padding=0,
|
||||
relief=tk.NONE,
|
||||
font=("TkDefaultFont", 8, "normal"),
|
||||
font="TkDefaultFont",
|
||||
)
|
||||
style.configure(
|
||||
Styles.yellow_alert,
|
||||
background="yellow",
|
||||
padding=0,
|
||||
relief=tk.NONE,
|
||||
font=("TkDefaultFont", 8, "normal"),
|
||||
font="TkDefaultFont",
|
||||
)
|
||||
style.configure(
|
||||
Styles.red_alert,
|
||||
background="red",
|
||||
padding=0,
|
||||
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 time
|
||||
import tkinter as tk
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
from tkinter import ttk
|
||||
from tkinter.font import Font
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
|
@ -25,6 +25,12 @@ TOOLBAR_SIZE = 32
|
|||
PICKER_SIZE = 24
|
||||
|
||||
|
||||
class NodeTypeEnum(Enum):
|
||||
NODE = 0
|
||||
NETWORK = 1
|
||||
OTHER = 2
|
||||
|
||||
|
||||
def icon(image_enum, width=TOOLBAR_SIZE):
|
||||
return Images.get(image_enum, width)
|
||||
|
||||
|
@ -43,10 +49,8 @@ class Toolbar(ttk.Frame):
|
|||
self.master = app.master
|
||||
self.time = None
|
||||
|
||||
# picker data
|
||||
self.picker_font = Font(size=8)
|
||||
|
||||
# design buttons
|
||||
self.play_button = None
|
||||
self.select_button = None
|
||||
self.link_button = None
|
||||
self.node_button = None
|
||||
|
@ -71,9 +75,18 @@ class Toolbar(ttk.Frame):
|
|||
# dialog
|
||||
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
|
||||
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):
|
||||
self.columnconfigure(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.grid(row=0, column=0, sticky="nsew")
|
||||
self.design_frame.columnconfigure(0, weight=1)
|
||||
self.create_button(
|
||||
self.play_button = self.create_button(
|
||||
self.design_frame,
|
||||
icon(ImageEnum.START),
|
||||
self.get_icon(ImageEnum.START),
|
||||
self.click_start,
|
||||
"start the session",
|
||||
)
|
||||
self.select_button = self.create_button(
|
||||
self.design_frame,
|
||||
icon(ImageEnum.SELECT),
|
||||
self.get_icon(ImageEnum.SELECT),
|
||||
self.click_selection,
|
||||
"selection tool",
|
||||
)
|
||||
self.link_button = self.create_button(
|
||||
self.design_frame, icon(ImageEnum.LINK), self.click_link, "link tool"
|
||||
self.design_frame,
|
||||
self.get_icon(ImageEnum.LINK),
|
||||
self.click_link,
|
||||
"link tool",
|
||||
)
|
||||
self.create_node_button()
|
||||
self.create_network_button()
|
||||
|
@ -130,18 +146,21 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
self.stop_button = self.create_button(
|
||||
self.runtime_frame,
|
||||
icon(ImageEnum.STOP),
|
||||
self.get_icon(ImageEnum.STOP),
|
||||
self.click_stop,
|
||||
"stop the session",
|
||||
)
|
||||
self.runtime_select_button = self.create_button(
|
||||
self.runtime_frame,
|
||||
icon(ImageEnum.SELECT),
|
||||
self.get_icon(ImageEnum.SELECT),
|
||||
self.click_runtime_selection,
|
||||
"selection tool",
|
||||
)
|
||||
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_frame,
|
||||
|
@ -164,23 +183,38 @@ class Toolbar(ttk.Frame):
|
|||
self.node_picker = ttk.Frame(self.master)
|
||||
# draw default nodes
|
||||
for node_draw in NodeUtils.NODES:
|
||||
toolbar_image = icon(node_draw.image_enum)
|
||||
image = icon(node_draw.image_enum, PICKER_SIZE)
|
||||
toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE)
|
||||
image = self.get_icon(node_draw.image_enum, PICKER_SIZE)
|
||||
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)
|
||||
# draw custom nodes
|
||||
for name in sorted(self.app.core.custom_nodes):
|
||||
node_draw = self.app.core.custom_nodes[name]
|
||||
toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE)
|
||||
image = Images.get_custom(node_draw.image_file, PICKER_SIZE)
|
||||
toolbar_image = Images.get_custom(
|
||||
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(
|
||||
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)
|
||||
# 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(
|
||||
image, self.click_edit_node, self.node_picker, "Custom"
|
||||
)
|
||||
|
@ -246,6 +280,7 @@ class Toolbar(ttk.Frame):
|
|||
server.
|
||||
"""
|
||||
self.app.canvas.hide_context()
|
||||
self.app.menubar.change_menubar_item_state(is_runtime=True)
|
||||
self.app.statusbar.progress_bar.start(5)
|
||||
self.app.canvas.mode = GraphMode.SELECT
|
||||
self.time = time.perf_counter()
|
||||
|
@ -281,13 +316,24 @@ class Toolbar(ttk.Frame):
|
|||
dialog = CustomNodesDialog(self.app, self.app)
|
||||
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)
|
||||
self.hide_pickers()
|
||||
button.configure(image=image)
|
||||
button.image = image
|
||||
self.app.canvas.mode = GraphMode.NODE
|
||||
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):
|
||||
logging.debug("hiding pickers")
|
||||
|
@ -305,13 +351,14 @@ class Toolbar(ttk.Frame):
|
|||
"""
|
||||
Create network layer button
|
||||
"""
|
||||
image = icon(ImageEnum.ROUTER)
|
||||
image = self.get_icon(ImageEnum.ROUTER, TOOLBAR_SIZE)
|
||||
self.node_button = ttk.Button(
|
||||
self.design_frame, image=image, command=self.draw_node_picker
|
||||
)
|
||||
self.node_button.image = image
|
||||
self.node_button.grid(sticky="ew")
|
||||
Tooltip(self.node_button, "Network-layer virtual nodes")
|
||||
self.node_enum = ImageEnum.ROUTER
|
||||
|
||||
def draw_network_picker(self):
|
||||
"""
|
||||
|
@ -320,12 +367,17 @@ class Toolbar(ttk.Frame):
|
|||
self.hide_pickers()
|
||||
self.network_picker = ttk.Frame(self.master)
|
||||
for node_draw in NodeUtils.NETWORK_NODES:
|
||||
toolbar_image = icon(node_draw.image_enum)
|
||||
image = icon(node_draw.image_enum, PICKER_SIZE)
|
||||
toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE)
|
||||
image = self.get_icon(node_draw.image_enum, PICKER_SIZE)
|
||||
self.create_picker_button(
|
||||
image,
|
||||
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,
|
||||
node_draw.label,
|
||||
|
@ -340,13 +392,14 @@ class Toolbar(ttk.Frame):
|
|||
Create link-layer node button and the options that represent different
|
||||
link-layer node types.
|
||||
"""
|
||||
image = icon(ImageEnum.HUB)
|
||||
image = self.get_icon(ImageEnum.HUB, TOOLBAR_SIZE)
|
||||
self.network_button = ttk.Button(
|
||||
self.design_frame, image=image, command=self.draw_network_picker
|
||||
)
|
||||
self.network_button.image = image
|
||||
self.network_button.grid(sticky="ew")
|
||||
Tooltip(self.network_button, "link-layer nodes")
|
||||
self.network_enum = ImageEnum.HUB
|
||||
|
||||
def draw_annotation_picker(self):
|
||||
"""
|
||||
|
@ -361,11 +414,11 @@ class Toolbar(ttk.Frame):
|
|||
(ImageEnum.TEXT, ShapeType.TEXT),
|
||||
]
|
||||
for image_enum, shape_type in nodes:
|
||||
toolbar_image = icon(image_enum)
|
||||
image = icon(image_enum, PICKER_SIZE)
|
||||
toolbar_image = self.get_icon(image_enum, TOOLBAR_SIZE)
|
||||
image = self.get_icon(image_enum, PICKER_SIZE)
|
||||
self.create_picker_button(
|
||||
image,
|
||||
partial(self.update_annotation, toolbar_image, shape_type),
|
||||
partial(self.update_annotation, toolbar_image, shape_type, image_enum),
|
||||
self.annotation_picker,
|
||||
shape_type.value,
|
||||
)
|
||||
|
@ -378,13 +431,14 @@ class Toolbar(ttk.Frame):
|
|||
"""
|
||||
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.design_frame, image=image, command=self.draw_annotation_picker
|
||||
)
|
||||
self.annotation_button.image = image
|
||||
self.annotation_button.grid(sticky="ew")
|
||||
Tooltip(self.annotation_button, "background annotation tools")
|
||||
self.annotation_enum = ImageEnum.MARKER
|
||||
|
||||
def create_observe_button(self):
|
||||
menu_button = ttk.Menubutton(
|
||||
|
@ -416,6 +470,7 @@ class Toolbar(ttk.Frame):
|
|||
"""
|
||||
logging.info("Click stop button")
|
||||
self.app.canvas.hide_context()
|
||||
self.app.menubar.change_menubar_item_state(is_runtime=False)
|
||||
self.app.statusbar.progress_bar.start(5)
|
||||
self.time = time.perf_counter()
|
||||
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.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: ")
|
||||
self.hide_pickers()
|
||||
self.annotation_button.configure(image=image)
|
||||
self.annotation_button.image = image
|
||||
self.app.canvas.mode = GraphMode.ANNOTATION
|
||||
self.app.canvas.annotation_type = shape_type
|
||||
self.annotation_enum = image_enum
|
||||
if is_marker(shape_type):
|
||||
if self.marker_tool:
|
||||
self.marker_tool.destroy()
|
||||
|
@ -460,3 +518,24 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
def click_two_node_button(self):
|
||||
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:
|
||||
from core.gui.app import Application
|
||||
|
||||
SMALLEST_SCALE = 0.5
|
||||
LARGEST_SCALE = 5.0
|
||||
|
||||
|
||||
class InputValidation:
|
||||
def __init__(self, app: "Application"):
|
||||
self.master = app.master
|
||||
self.positive_int = None
|
||||
self.positive_float = None
|
||||
self.app_scale = None
|
||||
self.name = None
|
||||
self.ip4 = None
|
||||
self.rgb = None
|
||||
|
@ -26,6 +30,7 @@ class InputValidation:
|
|||
def register(self):
|
||||
self.positive_int = self.master.register(self.check_positive_int)
|
||||
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.ip4 = self.master.register(self.check_ip4)
|
||||
self.rgb = self.master.register(self.check_rbg)
|
||||
|
@ -105,6 +110,18 @@ class InputValidation:
|
|||
except ValueError:
|
||||
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
|
||||
def check_ip4(cls, s: str) -> bool:
|
||||
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
|
||||
|
||||
self.doupdateloop = True
|
||||
self.updatethread = threading.Thread(target=self.updateloop)
|
||||
self.updatethread.daemon = True
|
||||
self.updatethread = threading.Thread(target=self.updateloop, daemon=True)
|
||||
self.updatethread.start()
|
||||
|
||||
def stopupdateloop(self, wlan: "CoreNetwork") -> None:
|
||||
|
@ -1068,6 +1067,7 @@ class WlanNode(CoreNetwork):
|
|||
"""
|
||||
super().startup()
|
||||
self.net_client.disable_mac_learning(self.brname)
|
||||
ebq.ebchange(self)
|
||||
|
||||
def attach(self, netif: CoreInterface) -> None:
|
||||
"""
|
||||
|
|
|
@ -4,22 +4,15 @@ sdt.py: Scripted Display Tool (SDT3D) helper
|
|||
|
||||
import logging
|
||||
import socket
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from core import constants
|
||||
from core.api.tlv.coreapi import CoreLinkMessage, CoreMessage, CoreNodeMessage
|
||||
from core.constants import CORE_DATA_DIR
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import LinkData, NodeData
|
||||
from core.emulator.enumerations import (
|
||||
EventTypes,
|
||||
LinkTlvs,
|
||||
LinkTypes,
|
||||
MessageFlags,
|
||||
NodeTlvs,
|
||||
NodeTypes,
|
||||
)
|
||||
from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
|
||||
from core.errors import CoreError
|
||||
from core.nodes.base import CoreNetworkBase, NodeBase
|
||||
from core.nodes.network import WlanNode
|
||||
|
@ -28,19 +21,11 @@ if TYPE_CHECKING:
|
|||
from core.emulator.session import Session
|
||||
|
||||
|
||||
# TODO: A named tuple may be more appropriate, than abusing a class dict like this
|
||||
class Bunch:
|
||||
"""
|
||||
Helper class for recording a collection of attributes.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
"""
|
||||
Create a Bunch instance.
|
||||
|
||||
:param kwargs: keyword arguments
|
||||
"""
|
||||
self.__dict__.update(kwargs)
|
||||
def link_data_params(link_data: LinkData) -> Tuple[int, int, bool]:
|
||||
node_one = link_data.node1_id
|
||||
node_two = link_data.node2_id
|
||||
is_wireless = link_data.link_type == LinkTypes.WIRELESS.value
|
||||
return node_one, node_two, is_wireless
|
||||
|
||||
|
||||
class Sdt:
|
||||
|
@ -74,60 +59,16 @@ class Sdt:
|
|||
:param session: session this manager is tied to
|
||||
"""
|
||||
self.session = session
|
||||
self.lock = threading.Lock()
|
||||
self.sock = None
|
||||
self.connected = False
|
||||
self.showerror = True
|
||||
self.url = self.DEFAULT_SDT_URL
|
||||
# node information for remote nodes not in session._objs
|
||||
# local nodes also appear here since their obj may not exist yet
|
||||
self.remotes = {}
|
||||
|
||||
# add handler for node updates
|
||||
self.address = None
|
||||
self.protocol = None
|
||||
self.session.node_handlers.append(self.handle_node_update)
|
||||
|
||||
# add handler for link updates
|
||||
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:
|
||||
"""
|
||||
Check for "enablesdt" session option. Return False by default if
|
||||
|
@ -144,9 +85,7 @@ class Sdt:
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
url = self.session.options.get_config("stdurl")
|
||||
if not url:
|
||||
url = self.DEFAULT_SDT_URL
|
||||
url = self.session.options.get_config("stdurl", default=self.DEFAULT_SDT_URL)
|
||||
self.url = urlparse(url)
|
||||
self.address = (self.url.hostname, self.url.port)
|
||||
self.protocol = self.url.scheme
|
||||
|
@ -185,7 +124,6 @@ class Sdt:
|
|||
# refresh all objects in SDT3D when connecting after session start
|
||||
if not flags & MessageFlags.ADD.value and not self.sendobjs():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def initialize(self) -> bool:
|
||||
|
@ -241,8 +179,10 @@ class Sdt:
|
|||
"""
|
||||
if self.sock is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
cmd = f"{cmdstr}\n".encode()
|
||||
logging.debug("sdt cmd: %s", cmd)
|
||||
self.sock.sendall(cmd)
|
||||
return True
|
||||
except IOError:
|
||||
|
@ -251,91 +191,6 @@ class Sdt:
|
|||
self.connected = 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:
|
||||
"""
|
||||
Session has already started, and the SDT3D GUI later connects.
|
||||
|
@ -352,171 +207,177 @@ class Sdt:
|
|||
nets.append(node)
|
||||
if not isinstance(node, NodeBase):
|
||||
continue
|
||||
(x, y, z) = node.getposition()
|
||||
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
|
||||
)
|
||||
self.add_node(node)
|
||||
|
||||
for net in nets:
|
||||
all_links = net.all_link_data(flags=MessageFlags.ADD.value)
|
||||
for link_data in all_links:
|
||||
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:
|
||||
continue
|
||||
params = link_data_params(link_data)
|
||||
self.add_link(*params)
|
||||
|
||||
self.updatelink(
|
||||
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:
|
||||
def get_node_position(self, node: NodeBase) -> Optional[str]:
|
||||
"""
|
||||
Broker handler for processing CORE API messages as they are
|
||||
received. This is used to snoop the Node messages and update
|
||||
node positions.
|
||||
Convenience to generate an SDT position string, given a node.
|
||||
|
||||
: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
|
||||
"""
|
||||
if isinstance(message, CoreLinkMessage):
|
||||
self.handlelinkmsg(message)
|
||||
elif isinstance(message, CoreNodeMessage):
|
||||
self.handlenodemsg(message)
|
||||
logging.debug("sdt add node: %s - %s", node.id, node.name)
|
||||
if not self.connect():
|
||||
return
|
||||
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
|
||||
the SDT display. Node properties are found in a session or
|
||||
self.remotes for remote nodes (or those not yet instantiated).
|
||||
Handle updating a node in SDT.
|
||||
|
||||
: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
|
||||
"""
|
||||
# for distributed sessions to work properly, the SDT option should be
|
||||
# enabled prior to starting the session
|
||||
if not self.is_enabled():
|
||||
logging.debug("sdt update node: %s - %s", node.id, node.name)
|
||||
if not self.connect():
|
||||
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)
|
||||
model = msg.get_tlv(NodeTlvs.MODEL.value)
|
||||
icon = msg.get_tlv(NodeTlvs.ICON.value)
|
||||
|
||||
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
|
||||
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.id} {pos}")
|
||||
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:
|
||||
node = self.session.get_node(nodenum)
|
||||
node = self.session.get_node(node_id)
|
||||
result = isinstance(node, (WlanNode, EmaneNet))
|
||||
except CoreError:
|
||||
node = None
|
||||
if node:
|
||||
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)
|
||||
pass
|
||||
return result
|
||||
|
||||
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.
|
||||
Links are recorded in the remotes[nodenum1].links set for updating
|
||||
the SDT display at a later time.
|
||||
Handle adding a link in SDT.
|
||||
|
||||
: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
|
||||
"""
|
||||
if not self.is_enabled():
|
||||
logging.debug("sdt add link: %s, %s, %s", node_one, node_two, is_wireless)
|
||||
if not self.connect():
|
||||
return
|
||||
nodenum1 = msg.get_tlv(LinkTlvs.N1_NUMBER.value)
|
||||
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):
|
||||
if self.wireless_net_check(node_one) or self.wireless_net_check(node_two):
|
||||
return
|
||||
wl = link_msg_type == LinkTypes.WIRELESS.value
|
||||
if nodenum1 in self.remotes:
|
||||
r = self.remotes[nodenum1]
|
||||
if msg.flags & MessageFlags.DELETE.value:
|
||||
if (nodenum2, wl) in r.links:
|
||||
r.links.remove((nodenum2, wl))
|
||||
else:
|
||||
r.links.add((nodenum2, wl))
|
||||
self.updatelink(nodenum1, nodenum2, msg.flags, wireless=wl)
|
||||
|
||||
def wlancheck(self, nodenum: int) -> bool:
|
||||
"""
|
||||
Helper returns True if a node number corresponds to a WLAN or EMANE node.
|
||||
|
||||
:param nodenum: node id to check
|
||||
:return: True if node is wlan or emane, False otherwise
|
||||
"""
|
||||
if nodenum in self.remotes:
|
||||
node_type = self.remotes[nodenum].type
|
||||
if node_type in ("wlan", "emane"):
|
||||
return True
|
||||
if is_wireless:
|
||||
attr = "green,2"
|
||||
else:
|
||||
try:
|
||||
n = self.session.get_node(nodenum)
|
||||
except CoreError:
|
||||
return False
|
||||
if isinstance(n, (WlanNode, EmaneNet)):
|
||||
return True
|
||||
return False
|
||||
attr = "red,2"
|
||||
self.cmd(f"link {node_one},{node_two} line {attr}")
|
||||
|
||||
def delete_link(self, node_one: int, node_two: int) -> None:
|
||||
"""
|
||||
Handle deleting a node in SDT.
|
||||
|
||||
:param node_one: node one id
|
||||
:param node_two: node two id
|
||||
:return: nothing
|
||||
"""
|
||||
logging.debug("sdt delete link: %s, %s", node_one, node_two)
|
||||
if not self.connect():
|
||||
return
|
||||
if self.wireless_net_check(node_one) or self.wireless_net_check(node_two):
|
||||
return
|
||||
self.cmd(f"delete link,{node_one},{node_two}")
|
||||
|
||||
def handle_link_update(self, link_data: LinkData) -> None:
|
||||
"""
|
||||
Handle link broadcast messages and push changes to SDT.
|
||||
|
||||
: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 random
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
from subprocess import PIPE, STDOUT, Popen
|
||||
from typing import (
|
||||
|
@ -151,16 +152,9 @@ def which(command: str, required: bool) -> str:
|
|||
:return: command location or None
|
||||
:raises ValueError: when not found and required
|
||||
"""
|
||||
found_path = None
|
||||
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
|
||||
|
||||
found_path = shutil.which(command)
|
||||
if found_path is None and required:
|
||||
raise ValueError(f"failed to find required executable({command}) in path")
|
||||
|
||||
return found_path
|
||||
|
||||
|
||||
|
@ -444,7 +438,7 @@ def random_mac() -> str:
|
|||
value = random.randint(0, 0xFFFFFF)
|
||||
value |= 0x00163E << 24
|
||||
mac = netaddr.EUI(value)
|
||||
mac.dialect = netaddr.mac_unix
|
||||
mac.dialect = netaddr.mac_unix_expanded
|
||||
return str(mac)
|
||||
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ from core.emulator.data import LinkData
|
|||
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
|
||||
from core.emulator.enumerations import NodeTypes
|
||||
from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase
|
||||
from core.nodes.docker import DockerNode
|
||||
from core.nodes.lxd import LxcNode
|
||||
from core.nodes.network import CtrlNet
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
|
@ -213,8 +215,21 @@ class DeviceElement(NodeElement):
|
|||
def __init__(self, session: "Session", node: NodeBase) -> None:
|
||||
super().__init__(session, node, "device")
|
||||
add_attribute(self.element, "type", node.type)
|
||||
self.add_class()
|
||||
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:
|
||||
service_elements = etree.Element("services")
|
||||
for service in self.node.services:
|
||||
|
@ -796,9 +811,17 @@ class CoreXmlReader:
|
|||
name = device_element.get("name")
|
||||
model = device_element.get("type")
|
||||
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
|
||||
|
||||
node_type = NodeTypes.DEFAULT
|
||||
if clazz == "docker":
|
||||
node_type = NodeTypes.DOCKER
|
||||
elif clazz == "lxc":
|
||||
node_type = NodeTypes.LXC
|
||||
|
||||
service_elements = device_element.find("services")
|
||||
if service_elements is not None:
|
||||
options.services = [x.get("name") for x in service_elements.iterchildren()]
|
||||
|
@ -823,7 +846,7 @@ class CoreXmlReader:
|
|||
options.set_location(lat, lon, alt)
|
||||
|
||||
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:
|
||||
node_id = get_int(network_element, "id")
|
||||
|
|
|
@ -22,6 +22,8 @@ service CoreApi {
|
|||
}
|
||||
rpc GetSession (GetSessionRequest) returns (GetSessionResponse) {
|
||||
}
|
||||
rpc CheckSession (CheckSessionRequest) returns (CheckSessionResponse) {
|
||||
}
|
||||
rpc GetSessionOptions (GetSessionOptionsRequest) returns (GetSessionOptionsResponse) {
|
||||
}
|
||||
rpc SetSessionOptions (SetSessionOptionsRequest) returns (SetSessionOptionsResponse) {
|
||||
|
@ -154,6 +156,8 @@ service CoreApi {
|
|||
}
|
||||
rpc EmaneLink (EmaneLinkRequest) returns (EmaneLinkResponse) {
|
||||
}
|
||||
rpc ExecuteScript (ExecuteScriptRequest) returns (ExecuteScriptResponse) {
|
||||
}
|
||||
}
|
||||
|
||||
// rpc request/response messages
|
||||
|
@ -210,6 +214,14 @@ message GetSessionsResponse {
|
|||
repeated SessionSummary sessions = 1;
|
||||
}
|
||||
|
||||
message CheckSessionRequest {
|
||||
int32 session_id = 1;
|
||||
}
|
||||
|
||||
message CheckSessionResponse {
|
||||
bool result = 1;
|
||||
}
|
||||
|
||||
message GetSessionRequest {
|
||||
int32 session_id = 1;
|
||||
}
|
||||
|
@ -406,6 +418,7 @@ message EditNodeRequest {
|
|||
Position position = 3;
|
||||
string icon = 4;
|
||||
string source = 5;
|
||||
Geo geo = 6;
|
||||
}
|
||||
|
||||
message EditNodeResponse {
|
||||
|
@ -759,6 +772,14 @@ message EmaneLinkResponse {
|
|||
bool result = 1;
|
||||
}
|
||||
|
||||
message ExecuteScriptRequest {
|
||||
string script = 1;
|
||||
}
|
||||
|
||||
message ExecuteScriptResponse {
|
||||
int32 session_id = 1;
|
||||
}
|
||||
|
||||
// data structures for messages below
|
||||
message WlanConfig {
|
||||
int32 node_id = 1;
|
||||
|
@ -967,6 +988,7 @@ message Node {
|
|||
string image = 10;
|
||||
string server = 11;
|
||||
repeated string config_services = 12;
|
||||
Geo geo = 13;
|
||||
}
|
||||
|
||||
message Link {
|
||||
|
@ -1019,7 +1041,10 @@ message Position {
|
|||
float x = 1;
|
||||
float y = 2;
|
||||
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
|
||||
cffi==1.13.2
|
||||
cffi==1.14.0
|
||||
cryptography==2.8
|
||||
fabric==2.5.0
|
||||
grpcio==1.26.0
|
||||
invoke==1.4.0
|
||||
lxml==4.4.2
|
||||
grpcio==1.27.2
|
||||
invoke==1.4.1
|
||||
lxml==4.5.0
|
||||
Mako==1.1.1
|
||||
MarkupSafe==1.1.1
|
||||
netaddr==0.7.19
|
||||
paramiko==2.7.1
|
||||
Pillow==7.0.0
|
||||
protobuf==3.11.2
|
||||
protobuf==3.11.3
|
||||
pycparser==2.19
|
||||
PyNaCl==1.3.0
|
||||
pyproj==2.5.0
|
||||
PyYAML==5.3
|
||||
six==1.14.0
|
||||
|
|
|
@ -19,7 +19,7 @@ PATH="/sbin:/bin:/usr/sbin:/usr/bin"
|
|||
export PATH
|
||||
|
||||
if [ "z$1" = "z-d" ]; then
|
||||
pypids=`pidof python python2`
|
||||
pypids=`pidof python3 python`
|
||||
for p in $pypids; do
|
||||
grep -q core-daemon /proc/$p/cmdline
|
||||
if [ $? = 0 ]; then
|
||||
|
|
|
@ -42,6 +42,7 @@ setup(
|
|||
"mako",
|
||||
"pillow",
|
||||
"protobuf",
|
||||
"pyproj",
|
||||
"pyyaml",
|
||||
],
|
||||
tests_require=[
|
||||
|
|
|
@ -19,84 +19,43 @@ Current development focuses on the Python modules and daemon. Here is a brief de
|
|||
|
||||
## Getting started
|
||||
|
||||
Overview for setting up the pipenv environment, building core, installing the GUI and netns, then running
|
||||
the core-daemon for development based on Ubuntu 18.04.
|
||||
To setup CORE for develop we will leverage to automated install script.
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```shell
|
||||
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
|
||||
## Clone CORE Repo
|
||||
|
||||
```shell
|
||||
cd ~/Documents
|
||||
git clone https://github.com/coreemu/core.git
|
||||
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
|
||||
./bootstrap.sh
|
||||
./configure
|
||||
make -j8
|
||||
# default dev install using python3.6
|
||||
./install.sh -d
|
||||
|
||||
# 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.
|
||||
|
||||
```shell
|
||||
# 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
|
||||
```
|
||||
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.
|
||||
These changes are currently mandated as part of the current CI, so add the changes and commit again.
|
||||
|
||||
### Adding EMANE to Pipenv
|
||||
|
||||
|
@ -121,9 +80,9 @@ make -j8
|
|||
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
|
||||
# runs for daemon
|
||||
|
|
167
docs/install.md
167
docs/install.md
|
@ -3,30 +3,30 @@
|
|||
* Table of Contents
|
||||
{:toc}
|
||||
|
||||
# Overview
|
||||
## Overview
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
**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.**
|
||||
|
||||
# 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
|
||||
-------------|------------
|
||||
|
@ -43,27 +43,35 @@ Install Path | Description
|
|||
/etc/init.d/core-daemon|SysV 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
|
||||
needed you can run the following to install python and pip.
|
||||
Python 3.6 is the minimum required python version. Newer versions can be used if available.
|
||||
These steps are needed, since the system packages can not provide all the
|
||||
dependencies needed by CORE.
|
||||
|
||||
### Ubuntu
|
||||
|
||||
```shell
|
||||
sudo apt install python3.6
|
||||
sudo apt install python3-pip
|
||||
```
|
||||
|
||||
# Pre-Req Python Requirements
|
||||
|
||||
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).
|
||||
### CentOS
|
||||
|
||||
```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
|
||||
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
|
||||
(and the MDR service) requires this variant of Quagga.
|
||||
|
||||
## Ubuntu <= 16.04 and Fedora/CentOS
|
||||
|
||||
There is a built package which can be used.
|
||||
### Ubuntu
|
||||
|
||||
```shell
|
||||
wget https://github.com/USNavalResearchLaboratory/ospf-mdr/releases/download/v0.99.21mr2.2/quagga-mr_0.99.21mr2.2_amd64.deb
|
||||
sudo dpkg -i quagga-mr_0.99.21mr2.2_amd64.deb
|
||||
sudo apt install libtool gawk libreadline-dev
|
||||
```
|
||||
|
||||
## Ubuntu >= 18.04
|
||||
|
||||
Requires building from source, from the latest nightly snapshot.
|
||||
### CentOS
|
||||
|
||||
```shell
|
||||
# packages needed beyond what's normally required to build core on ubuntu
|
||||
sudo apt install libtool libreadline-dev autoconf gawk
|
||||
sudo yum install libtool gawk readline-devel
|
||||
```
|
||||
|
||||
### Build and Install
|
||||
|
||||
```shell
|
||||
git clone https://github.com/USNavalResearchLaboratory/ospf-mdr
|
||||
cd ospf-mdr
|
||||
./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.
|
||||
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -127,16 +133,7 @@ Ubuntu package defaults to using systemd for running as a service.
|
|||
sudo apt install ./core_$VERSION_amd64.deb
|
||||
```
|
||||
|
||||
Run the CORE GUI as a normal user:
|
||||
|
||||
```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
|
||||
### CentOS
|
||||
|
||||
**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**
|
||||
|
@ -153,10 +150,6 @@ SELINUX=disabled
|
|||
|
||||
# add the following to the kernel line in /etc/grub.conf
|
||||
selinux=0
|
||||
|
||||
# Fedora 15 and newer, disable sandboxd
|
||||
# reboot in order for this change to take effect
|
||||
chkconfig sandbox off
|
||||
```
|
||||
|
||||
Turn off firewalls:
|
||||
|
@ -176,63 +169,46 @@ iptables -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
|
||||
# systemd
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl start core-daemon
|
||||
|
||||
# sysv
|
||||
sudo service core-daemon start
|
||||
sudo apt install git automake pkg-config gcc libev-dev ebtables iproute2 \
|
||||
python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool autoconf
|
||||
```
|
||||
|
||||
Run the CORE GUI as a normal user:
|
||||
#### CentOS
|
||||
|
||||
```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
|
||||
|
||||
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.
|
||||
Clone the CORE repository for building from source.
|
||||
|
||||
```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
|
||||
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
|
||||
|
||||
```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
|
||||
### Build and Install
|
||||
|
||||
```shell
|
||||
./bootstrap.sh
|
||||
|
@ -241,7 +217,7 @@ make
|
|||
sudo make install
|
||||
```
|
||||
|
||||
# Building Documentation
|
||||
## Building Documentation
|
||||
|
||||
Building documentation requires python-sphinx not noted above.
|
||||
|
||||
|
@ -254,7 +230,7 @@ sudo yum install python3-sphinx
|
|||
make doc
|
||||
```
|
||||
|
||||
# Building Packages
|
||||
## Building Packages
|
||||
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**
|
||||
|
@ -270,3 +246,26 @@ make fpm DESTDIR=/tmp/core-build
|
|||
```
|
||||
|
||||
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
|
||||
set -e
|
||||
|
||||
ubuntu_py=3.6
|
||||
centos_py=36
|
||||
|
||||
function install_python_depencencies() {
|
||||
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() {
|
||||
rm -rf /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
|
||||
cd -
|
||||
cd daemon
|
||||
pipenv install --dev
|
||||
cd -
|
||||
}
|
||||
|
||||
# detect os/ver for install type
|
||||
|
@ -54,13 +51,18 @@ if [[ -f /etc/os-release ]]; then
|
|||
fi
|
||||
|
||||
# parse arguments
|
||||
while getopts ":d" opt; do
|
||||
while getopts "dv:" opt; do
|
||||
case ${opt} in
|
||||
d)
|
||||
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
|
||||
done
|
||||
|
@ -70,8 +72,12 @@ shift $((OPTIND - 1))
|
|||
case ${os} in
|
||||
"ubuntu")
|
||||
echo "Installing CORE for Ubuntu"
|
||||
sudo apt install -y automake pkg-config gcc libev-dev ebtables gawk iproute2 \
|
||||
python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf
|
||||
echo "installing core system dependencies"
|
||||
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
|
||||
if [[ -z ${dev} ]]; then
|
||||
echo "normal install"
|
||||
|
@ -80,23 +86,35 @@ case ${os} in
|
|||
install_core
|
||||
else
|
||||
echo "dev install"
|
||||
install_python_dev_dependencies
|
||||
python3 -m pip install pipenv
|
||||
build_core
|
||||
install_dev_core
|
||||
python3 -m pipenv sync --dev
|
||||
python3 -m pipenv run pre-commit install
|
||||
fi
|
||||
;;
|
||||
"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 \
|
||||
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
|
||||
if [[ -z ${dev} ]]; then
|
||||
echo "normal install"
|
||||
install_python_depencencies
|
||||
build_core --prefix=/usr
|
||||
install_core
|
||||
else
|
||||
install_python_dev_dependencies
|
||||
echo "dev install"
|
||||
sudo python3 -m pip install pipenv
|
||||
build_core --prefix=/usr
|
||||
install_dev_core
|
||||
sudo python3 -m pipenv sync --dev
|
||||
python3 -m pipenv sync --dev
|
||||
python3 -m pipenv run pre-commit install
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
|
|
Loading…
Reference in a new issue