Merge pull request #407 from coreemu/develop

Merge 6.2.0
This commit is contained in:
bharnden 2020-03-16 11:37:08 -07:00 committed by GitHub
commit 3be162b0b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1934 additions and 1597 deletions

View file

@ -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
View file

@ -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"
}
}
}

View file

@ -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.

View file

@ -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:

View file

@ -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)

View file

@ -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(

View file

@ -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),
],

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View 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()

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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))

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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):

View file

@ -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))

View file

@ -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)

View file

@ -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,
]

View file

@ -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)

View file

@ -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

View file

@ -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")

View file

@ -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(

View file

@ -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)

View file

@ -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 = ()

View file

@ -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))

View file

@ -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)

View file

@ -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:

View file

@ -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
View 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

View file

@ -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

View file

@ -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:
"""

View file

@ -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])

View file

@ -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)

View file

@ -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")

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -42,6 +42,7 @@ setup(
"mako",
"pillow",
"protobuf",
"pyproj",
"pyyaml",
],
tests_require=[

View file

@ -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

View file

@ -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
```

View file

@ -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
;;
*)